cdk-local 0.148.4 → 0.149.1
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 +1 -1
- package/dist/cli.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/internal.d.ts +1 -1
- package/dist/internal.js +1 -1
- package/dist/{local-studio-9bZLFQqv.d.ts → local-studio-D-j3TZWX.d.ts} +2 -1
- package/dist/local-studio-D-j3TZWX.d.ts.map +1 -0
- package/dist/{local-studio-BziOGWc5.js → local-studio-D8cHeCcp.js} +803 -29
- package/dist/local-studio-D8cHeCcp.js.map +1 -0
- package/package.json +6 -10
- package/dist/local-studio-9bZLFQqv.d.ts.map +0 -1
- package/dist/local-studio-BziOGWc5.js.map +0 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { a as runDockerStreaming, c as resolveConfiguredLogLevel, d as setEmbedConfig, i as runDockerForeground, l as getEmbedConfig, n as formatDockerLoginError, o as spawnStreaming, r as getDockerCmd, s as getLogger } from "./docker-cmd-DLX1TqVw.js";
|
|
2
|
-
import { chmodSync, cpSync, createWriteStream, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { chmodSync, cpSync, createWriteStream, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, readlinkSync, realpathSync, rmSync, statSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { homedir, tmpdir } from "node:os";
|
|
4
4
|
import * as path$1 from "node:path";
|
|
5
|
-
import path, { dirname, isAbsolute, join, normalize, relative, resolve, sep } from "node:path";
|
|
5
|
+
import path, { basename, dirname, isAbsolute, join, normalize, relative, resolve, sep } from "node:path";
|
|
6
6
|
import { Command, Option } from "commander";
|
|
7
7
|
import { AssumeRoleCommand, GetCallerIdentityCommand, STSClient } from "@aws-sdk/client-sts";
|
|
8
8
|
import { Agent, createServer, request } from "node:http";
|
|
@@ -2383,6 +2383,247 @@ function stringifyValue(value) {
|
|
|
2383
2383
|
}
|
|
2384
2384
|
}
|
|
2385
2385
|
|
|
2386
|
+
//#endregion
|
|
2387
|
+
//#region src/utils/assembly-path.ts
|
|
2388
|
+
/**
|
|
2389
|
+
* `true` when `candidate` names something strictly beneath `base`, both given
|
|
2390
|
+
* as already-resolved absolute paths.
|
|
2391
|
+
*
|
|
2392
|
+
* The `..` test is SEPARATOR-AWARE rather than a bare `startsWith('..')`,
|
|
2393
|
+
* which would also reject a legitimate sibling named `..foo`. The empty-string
|
|
2394
|
+
* case is `base` itself; callers decide whether that counts as an escape,
|
|
2395
|
+
* because an asset path legitimately names a DIRECTORY while a template path
|
|
2396
|
+
* does not.
|
|
2397
|
+
*/
|
|
2398
|
+
function isInside(base, candidate) {
|
|
2399
|
+
const rel = relative(base, candidate);
|
|
2400
|
+
if (rel === "" || rel === "..") return false;
|
|
2401
|
+
if (rel.startsWith(`..${sep}`)) return false;
|
|
2402
|
+
return !isAbsolute(rel);
|
|
2403
|
+
}
|
|
2404
|
+
/**
|
|
2405
|
+
* `realpath(3)`, or `undefined` for a path that does not fully resolve.
|
|
2406
|
+
*
|
|
2407
|
+
* `.native` is load-bearing and must not be simplified to `fs.realpathSync`.
|
|
2408
|
+
* The plain form is a JavaScript walker that folds `..` LEXICALLY, so it
|
|
2409
|
+
* disagrees with the kernel on a target carrying a `..` after a symlinked
|
|
2410
|
+
* component — answering `ENOENT` for a path the kernel resolves, and on a
|
|
2411
|
+
* case-insensitive filesystem (APFS by default) it can spin on the same shape.
|
|
2412
|
+
* `.native` is libuv's `realpath(3)` and throws the same
|
|
2413
|
+
* `ENOENT` / `ELOOP` / `EACCES`.
|
|
2414
|
+
*/
|
|
2415
|
+
function tryRealpath(p) {
|
|
2416
|
+
try {
|
|
2417
|
+
return realpathSync.native(p);
|
|
2418
|
+
} catch {
|
|
2419
|
+
return;
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
/** `fs.readlinkSync`, or `undefined` when `p` is not a symbolic link. */
|
|
2423
|
+
function tryReadlink(p) {
|
|
2424
|
+
try {
|
|
2425
|
+
return readlinkSync(p);
|
|
2426
|
+
} catch {
|
|
2427
|
+
return;
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
/**
|
|
2431
|
+
* Link follows before the walk gives up and reports the path as unresolvable.
|
|
2432
|
+
* It does NOT refuse: an exhausted budget answers `undefined`, which leaves
|
|
2433
|
+
* the symlink arm silent and the lexical verdict standing. Safe because the OS
|
|
2434
|
+
* gives up first (macOS caps at 32, `ELOOP`), so a chain that reaches this cap
|
|
2435
|
+
* is one nothing can open anyway.
|
|
2436
|
+
*/
|
|
2437
|
+
const MAX_LINK_HOPS = 40;
|
|
2438
|
+
/**
|
|
2439
|
+
* Unresolvable path COMPONENTS the climb walks before giving up. Bounds the
|
|
2440
|
+
* recursion below, which is one frame per component, so a pathological value
|
|
2441
|
+
* cannot raise a `RangeError` from inside {@link tryRealpath}'s own `try` (its
|
|
2442
|
+
* `catch` would swallow the crash into a silent `undefined`).
|
|
2443
|
+
*
|
|
2444
|
+
* KNOW THAT EXHAUSTING THIS FAILS OPEN, and why that is accepted rather than
|
|
2445
|
+
* unnoticed. Unlike {@link MAX_LINK_HOPS}, which has the OS's own `ELOOP` cap
|
|
2446
|
+
* behind it, nothing else stops a path of 1 001 absent components: the climb
|
|
2447
|
+
* gives up, the symlink arm goes silent, and a value that would land outside
|
|
2448
|
+
* reads as CONTAINED on the lexical verdict alone. It is not reachable in
|
|
2449
|
+
* effect — such a path cannot exist, so `cdkl invoke`'s `existsSync` refuses it
|
|
2450
|
+
* and `start-api`'s `docker run` fails to mount it — but that safety lives in
|
|
2451
|
+
* the CONSUMERS, not here. A consumer that neither checks existence nor mounts
|
|
2452
|
+
* would need its own answer.
|
|
2453
|
+
*/
|
|
2454
|
+
const MAX_PATH_COMPONENTS = 1e3;
|
|
2455
|
+
/**
|
|
2456
|
+
* Where `target` REALLY points, for a path that may not exist yet.
|
|
2457
|
+
*
|
|
2458
|
+
* `realpathSync` answers only for a path that fully resolves, and it throws
|
|
2459
|
+
* `ENOENT` for a DANGLING symbolic link exactly as it does for an absent file.
|
|
2460
|
+
* `cdkl start-api` resolves an asset path without an existence check, so an
|
|
2461
|
+
* absent or dangling component must not silence the symlink arm.
|
|
2462
|
+
*
|
|
2463
|
+
* Each unresolvable component is therefore handled by hand: climb to the
|
|
2464
|
+
* deepest ancestor that DOES resolve, then re-apply the remaining components,
|
|
2465
|
+
* following any symbolic link with `readlink` and re-resolving its target.
|
|
2466
|
+
* `undefined` means the walk could not resolve the path at all.
|
|
2467
|
+
*
|
|
2468
|
+
* For a path that FULLY RESOLVES the answer is the kernel's and is exact. For
|
|
2469
|
+
* one that does not exist yet this is a best-effort MODEL of kernel
|
|
2470
|
+
* resolution; the known edge is a `..` INSIDE an unresolvable link's target,
|
|
2471
|
+
* folded lexically here while the kernel folds it only after following each
|
|
2472
|
+
* preceding component. That edge is benign for THESE callers rather than in
|
|
2473
|
+
* general: it needs the target to be absent, and both consumers resolve the
|
|
2474
|
+
* path and then open or mount it, so the divergence is between this model and
|
|
2475
|
+
* a path nothing can reach. A caller that CREATED a file through such a path
|
|
2476
|
+
* would need an `lstat` of its own.
|
|
2477
|
+
*
|
|
2478
|
+
* `EACCES` is folded into "does not resolve" along with `ENOENT`, so a link
|
|
2479
|
+
* under a directory this process cannot traverse reads as contained. Same
|
|
2480
|
+
* bound: the consumer's own `existsSync` / mount runs as the same user and
|
|
2481
|
+
* fails identically.
|
|
2482
|
+
*/
|
|
2483
|
+
function resolveThroughLinks(target, hops = 0, climbs = 0) {
|
|
2484
|
+
const direct = tryRealpath(target);
|
|
2485
|
+
if (direct !== void 0) return direct;
|
|
2486
|
+
if (hops >= MAX_LINK_HOPS) return void 0;
|
|
2487
|
+
if (climbs >= MAX_PATH_COMPONENTS) return void 0;
|
|
2488
|
+
const parent = dirname(target);
|
|
2489
|
+
if (parent === target) return void 0;
|
|
2490
|
+
const realParent = resolveThroughLinks(parent, hops, climbs + 1);
|
|
2491
|
+
if (realParent === void 0) return void 0;
|
|
2492
|
+
const link = tryReadlink(target);
|
|
2493
|
+
if (link === void 0) return join(realParent, basename(target));
|
|
2494
|
+
return resolveThroughLinks(resolve(realParent, link), hops + 1, climbs);
|
|
2495
|
+
}
|
|
2496
|
+
/**
|
|
2497
|
+
* Resolve an assembly-supplied `candidate` against `dir` and report whether
|
|
2498
|
+
* the result stays inside it.
|
|
2499
|
+
*
|
|
2500
|
+
* The lexical arm joins exactly the way the call sites used to (`path.join`,
|
|
2501
|
+
* NOT `path.resolve`), so the verdict is about the path the caller will
|
|
2502
|
+
* actually use. `join`'s handling of an absolute candidate makes this arm
|
|
2503
|
+
* strictly more permissive than a `resolve`-based one would be; an absolute
|
|
2504
|
+
* value is therefore answered by {@link absoluteAssemblyPathEscape} instead,
|
|
2505
|
+
* because this function structurally cannot.
|
|
2506
|
+
*
|
|
2507
|
+
* The SYMLINK arm exists because the lexical arm alone leaves an equivalent
|
|
2508
|
+
* hole: `cdk.out/link -> /etc` plus a candidate of `link/passwd` is lexically
|
|
2509
|
+
* contained and still reaches `/etc/passwd`. Both sides go through
|
|
2510
|
+
* {@link resolveThroughLinks}, so an assembly directory REACHED through a link
|
|
2511
|
+
* (macOS spells `/tmp` as `/private/tmp`) is unaffected.
|
|
2512
|
+
*
|
|
2513
|
+
* The verdict is about the assembly AS IT SITS ON DISK. It is NOT a defence
|
|
2514
|
+
* against a process rewriting that directory concurrently — the caller uses
|
|
2515
|
+
* the path after this returns.
|
|
2516
|
+
*/
|
|
2517
|
+
function resolveAssemblyPath(dir, candidate, options) {
|
|
2518
|
+
const base = resolve(dir);
|
|
2519
|
+
const bound = options?.containWithin === void 0 ? base : resolve(options.containWithin);
|
|
2520
|
+
const joined = resolve(join(base, candidate));
|
|
2521
|
+
if (!isInside(bound, joined)) return {
|
|
2522
|
+
contained: false,
|
|
2523
|
+
escape: "lexical",
|
|
2524
|
+
path: joined
|
|
2525
|
+
};
|
|
2526
|
+
const realBound = resolveThroughLinks(bound);
|
|
2527
|
+
if (realBound !== void 0) {
|
|
2528
|
+
const realTarget = resolveThroughLinks(joined);
|
|
2529
|
+
if (realTarget !== void 0 && !isInside(realBound, realTarget)) return {
|
|
2530
|
+
contained: false,
|
|
2531
|
+
escape: "symlink",
|
|
2532
|
+
path: joined,
|
|
2533
|
+
realPath: realTarget
|
|
2534
|
+
};
|
|
2535
|
+
}
|
|
2536
|
+
return {
|
|
2537
|
+
contained: true,
|
|
2538
|
+
path: joined
|
|
2539
|
+
};
|
|
2540
|
+
}
|
|
2541
|
+
/**
|
|
2542
|
+
* Whether an ALREADY-ABSOLUTE assembly-supplied path lies outside `bound`.
|
|
2543
|
+
*
|
|
2544
|
+
* {@link resolveAssemblyPath} cannot answer this, and the reason is structural
|
|
2545
|
+
* rather than an oversight: its lexical arm joins with `path.join`, which does
|
|
2546
|
+
* NOT honour a leading separator, so an absolute candidate is folded INTO the
|
|
2547
|
+
* directory and the verdict would describe a path no caller opens. A site that
|
|
2548
|
+
* HONOURS an absolute value needs the verdict about the value itself.
|
|
2549
|
+
*
|
|
2550
|
+
* The callers are `cdkl invoke`'s and `cdkl start-api`'s
|
|
2551
|
+
* `Metadata['aws:asset:path']` resolution. Those honour an absolute path
|
|
2552
|
+
* because `cdk synth --no-staging` emits one — CDK writes the asset's absolute
|
|
2553
|
+
* SOURCE directory under `aws:cdk:disable-asset-staging`, usually outside the
|
|
2554
|
+
* outdir — and they WARN rather than refuse when it leaves the bound, so this
|
|
2555
|
+
* returns a verdict rather than throwing.
|
|
2556
|
+
*
|
|
2557
|
+
* It lives HERE so the containment rule has one spelling: it reuses this
|
|
2558
|
+
* module's own {@link isInside} and {@link resolveThroughLinks}, symlink arm
|
|
2559
|
+
* included, rather than letting a caller re-spell `path.relative` and drift.
|
|
2560
|
+
*
|
|
2561
|
+
* `bound` itself is NOT an escape, unlike in {@link resolveAssemblyPath},
|
|
2562
|
+
* where an empty `path.relative` means "names the directory rather than a file
|
|
2563
|
+
* inside it". An asset path legitimately names a DIRECTORY, so a value equal
|
|
2564
|
+
* to the bound is inside it and reporting it as outside would be false.
|
|
2565
|
+
*
|
|
2566
|
+
* THE REAL PATHS DECIDE IN BOTH DIRECTIONS, which is the one place this
|
|
2567
|
+
* deliberately does more than {@link resolveAssemblyPath}'s lexical-first
|
|
2568
|
+
* ordering. The lexical verdict here is about a SPELLING, and two spellings of
|
|
2569
|
+
* the same directory are common rather than exotic: macOS resolves `/tmp` to
|
|
2570
|
+
* `/private/tmp` and `/var` to `/private/var`, and a user may symlink `cdk.out`
|
|
2571
|
+
* itself. With an outdir of `/tmp/cdk.out` and a `--no-staging` value of
|
|
2572
|
+
* `/private/tmp/cdk.out/src`, a lexical-only verdict cries "pointing outside
|
|
2573
|
+
* the assembly ... treat this assembly as untrusted" about an asset that is
|
|
2574
|
+
* plainly inside it — and this arm exists to make an UNEXPECTED path visible,
|
|
2575
|
+
* so a false alarm is the failure that costs it its meaning.
|
|
2576
|
+
*
|
|
2577
|
+
* Exonerating requires the kernel to answer for BOTH operands: an unresolvable
|
|
2578
|
+
* one leaves the lexical verdict standing, so the arm stays loud when it cannot
|
|
2579
|
+
* see. (`resolveAssemblyPath` cannot take the same shape: its verdict is about
|
|
2580
|
+
* a path built with `path.join`, and letting a real path overrule the lexical
|
|
2581
|
+
* `..` there would answer about a location the caller never opens.)
|
|
2582
|
+
*/
|
|
2583
|
+
function absoluteAssemblyPathEscape(bound, absolutePath) {
|
|
2584
|
+
const resolvedBound = resolve(bound);
|
|
2585
|
+
const target = resolve(absolutePath);
|
|
2586
|
+
const lexicallyOutside = !isInside(resolvedBound, target) && target !== resolvedBound;
|
|
2587
|
+
const realBound = resolveThroughLinks(resolvedBound);
|
|
2588
|
+
const realTarget = resolveThroughLinks(target);
|
|
2589
|
+
const reallyOutside = realBound === void 0 || realTarget === void 0 ? void 0 : !isInside(realBound, realTarget) && realTarget !== realBound;
|
|
2590
|
+
if (lexicallyOutside) {
|
|
2591
|
+
if (reallyOutside === false) return void 0;
|
|
2592
|
+
return {
|
|
2593
|
+
contained: false,
|
|
2594
|
+
escape: "lexical",
|
|
2595
|
+
path: target
|
|
2596
|
+
};
|
|
2597
|
+
}
|
|
2598
|
+
if (reallyOutside === true) return {
|
|
2599
|
+
contained: false,
|
|
2600
|
+
escape: "symlink",
|
|
2601
|
+
path: target,
|
|
2602
|
+
realPath: realTarget
|
|
2603
|
+
};
|
|
2604
|
+
}
|
|
2605
|
+
/**
|
|
2606
|
+
* The shared tail of a containment refusal: what the value resolved to, what
|
|
2607
|
+
* it escaped, and why that means the assembly is not CDK-generated. The call
|
|
2608
|
+
* site supplies its own subject ("Lambda 'X' has ... which ") and its own
|
|
2609
|
+
* error class. `action` completes "Refusing to ..." and is REQUIRED rather
|
|
2610
|
+
* than defaulted: a default is a branch no caller takes, so it can neither be
|
|
2611
|
+
* fenced nor be right for the next caller.
|
|
2612
|
+
*/
|
|
2613
|
+
function renderAssemblyPathEscape(escape, dir, action) {
|
|
2614
|
+
const provenance = `CDK emits assembly paths that stay inside the assembly directory; one that leaves it indicates the synth output was hand-modified or generated by a non-CDK toolchain. Refusing to ${action}.`;
|
|
2615
|
+
const base = resolve(dir);
|
|
2616
|
+
const realBase = resolveThroughLinks(base) ?? base;
|
|
2617
|
+
const shownPath = sanitizeServiceExceptionMessage(escape.path);
|
|
2618
|
+
const shownBase = sanitizeServiceExceptionMessage(base);
|
|
2619
|
+
if (escape.escape === "symlink") {
|
|
2620
|
+
if (escape.realPath === realBase) return `resolves to '${shownPath}', a symbolic link to the directory '${shownBase}' itself rather than to a path inside it. ${provenance}`;
|
|
2621
|
+
return `resolves to '${shownPath}', which leads through a symbolic link to '${sanitizeServiceExceptionMessage(escape.realPath)}', outside '${shownBase}'. ${provenance}`;
|
|
2622
|
+
}
|
|
2623
|
+
if (escape.path === base) return `names the directory '${shownBase}' itself rather than a path inside it. ${provenance}`;
|
|
2624
|
+
return `resolves to '${shownPath}', outside '${shownBase}'. ${provenance}`;
|
|
2625
|
+
}
|
|
2626
|
+
|
|
2386
2627
|
//#endregion
|
|
2387
2628
|
//#region src/local/lambda-resolver.ts
|
|
2388
2629
|
/**
|
|
@@ -2647,6 +2888,325 @@ function extractImageLambdaProperties(args) {
|
|
|
2647
2888
|
};
|
|
2648
2889
|
}
|
|
2649
2890
|
/**
|
|
2891
|
+
* Where a Lambda's `Metadata['aws:asset:path']` really lives on this host
|
|
2892
|
+
* (go-to-k/cdkd#3534, applying the decision taken in go-to-k/cdkd#3494).
|
|
2893
|
+
*
|
|
2894
|
+
* THE one spelling shared by `cdkl invoke`'s resolver and `cdkl start-api`'s,
|
|
2895
|
+
* which each carried their own copy of `isAbsolute(p) ? p : resolve(dir, p)`.
|
|
2896
|
+
* A second hand-written copy is how a guard on one twin becomes a guard on
|
|
2897
|
+
* neither. It throws through the CALLER's `wrapError` so each site keeps its
|
|
2898
|
+
* own error class and names its own command.
|
|
2899
|
+
*
|
|
2900
|
+
* TWO VALUE SHAPES, ANSWERED DIFFERENTLY, and the asymmetry is the decision
|
|
2901
|
+
* rather than an accident:
|
|
2902
|
+
*
|
|
2903
|
+
* - RELATIVE, escaping. REFUSED. `path.resolve` folds `..` exactly as
|
|
2904
|
+
* `path.join` does, so `../../../home/<user>/.aws` leaves the assembly, and
|
|
2905
|
+
* nothing a real `cdk synth` emits has that shape.
|
|
2906
|
+
* - ABSOLUTE. ACCEPTED, with a WARNING naming the path when it leaves the
|
|
2907
|
+
* asset outdir, and SILENCE when it does not.
|
|
2908
|
+
*
|
|
2909
|
+
* **Why absolute is accepted, when the security axis argued for refusing it.**
|
|
2910
|
+
* `cdk synth --no-staging` (context flag `aws:cdk:disable-asset-staging`) makes
|
|
2911
|
+
* upstream `AssetStaging.relativeStagedPath` return the staged path verbatim
|
|
2912
|
+
* instead of relativising it, so `aws:asset:path` is the asset's absolute
|
|
2913
|
+
* SOURCE directory, normally outside the outdir. Refusing it therefore rejects
|
|
2914
|
+
* the output of a documented CDK CLI flag, and the user's view is simply that
|
|
2915
|
+
* cdk-local will not read what `cdk` just wrote. Record both halves of the
|
|
2916
|
+
* trade, because a later reader must not "restore" the refusal as an
|
|
2917
|
+
* oversight:
|
|
2918
|
+
*
|
|
2919
|
+
* - The security cost is genuine. This value is BIND-MOUNTED read-only at
|
|
2920
|
+
* `/var/task` (or `/opt` for a layer) into a container running handler code
|
|
2921
|
+
* the SAME assembly supplied, and the run may carry the caller's
|
|
2922
|
+
* credentials, so an absolute path a hostile assembly chose reaches the host
|
|
2923
|
+
* filesystem. Nothing here can distinguish a `--no-staging` value from a
|
|
2924
|
+
* planted one: both are an absolute directory the assembly named.
|
|
2925
|
+
* - What bounds it: this is a LOCAL developer command run against an assembly
|
|
2926
|
+
* the user pointed at, and the warning names the directory so an unexpected
|
|
2927
|
+
* one is visible rather than silent. **Do not count the read-only mount as
|
|
2928
|
+
* the bound.** It stops writes to the tree, not the capability: a read-only
|
|
2929
|
+
* bind of a directory holding a unix socket (`/var/run`, `/run`) still
|
|
2930
|
+
* permits `connect(2)` on, say, `docker.sock`. The warning is the
|
|
2931
|
+
* mitigation.
|
|
2932
|
+
*
|
|
2933
|
+
* The `..` containment is NOT relaxed with it, but be precise about what it
|
|
2934
|
+
* buys. Against an ADVERSARY it stops nothing: they write the ABSOLUTE
|
|
2935
|
+
* spelling and reach the same place with a warning instead of a refusal. What
|
|
2936
|
+
* it still catches is an ACCIDENTAL or legacy `..`, and it costs nothing,
|
|
2937
|
+
* which is why the arm stays. There is no containment boundary here any more;
|
|
2938
|
+
* the warning is the whole signal.
|
|
2939
|
+
*
|
|
2940
|
+
* CONTAIN WITHIN `assetOutdir`, NOT the manifest's directory. A Lambda inside
|
|
2941
|
+
* a `cdk.Stage` legitimately carries `../asset.<hash>`, because `cdk synth`
|
|
2942
|
+
* stages a Stage's assets into the APP's outdir while the Stage's manifest
|
|
2943
|
+
* sits in `cdk.out/assembly-<Stage>/`. Binding to the manifest directory
|
|
2944
|
+
* refuses every Stage asset as "hand-modified".
|
|
2945
|
+
*
|
|
2946
|
+
* Exported for unit testing and for `local-start-api.ts`'s copy of the caller.
|
|
2947
|
+
*/
|
|
2948
|
+
function resolveAssetCodeDirectory(manifestDir, assetPath, wrapError, assetOutdir, logicalId) {
|
|
2949
|
+
if (isAbsolute(assetPath)) {
|
|
2950
|
+
const absolute = resolve(assetPath);
|
|
2951
|
+
const escape = absoluteAssemblyPathEscape(assetOutdir, absolute);
|
|
2952
|
+
if (escape !== void 0) getLogger().warn(`Lambda '${sanitizeServiceExceptionMessage(logicalId)}' has an absolute Metadata['aws:asset:path'] pointing outside the assembly: '${sanitizeServiceExceptionMessage(absolute)}'` + (escape.escape === "symlink" ? ` (through a symbolic link to '${sanitizeServiceExceptionMessage(escape.realPath)}')` : "") + `. ${getEmbedConfig().productName} will read that directory and expose its contents to the container — bind-mounted read-only, or copied when the asset is a .zip or one of several merged layers — where the code in this assembly can read it. This is what cdk synth --no-staging emits, and is expected for it; if you did not synthesize with that flag, treat this assembly as untrusted.`);
|
|
2953
|
+
warnOutsideNamedDirectory(assetOutdir, absolute);
|
|
2954
|
+
return absolute;
|
|
2955
|
+
}
|
|
2956
|
+
const resolved = resolveAssemblyPath(manifestDir, assetPath, { containWithin: assetOutdir });
|
|
2957
|
+
if (!resolved.contained && resolved.escape === "lexical" && resolved.path === resolve(assetOutdir)) {
|
|
2958
|
+
warnOutsideNamedDirectory(assetOutdir, resolved.path);
|
|
2959
|
+
return resolved.path;
|
|
2960
|
+
}
|
|
2961
|
+
if (!resolved.contained) {
|
|
2962
|
+
const climbsOut = assetPath === ".." || assetPath.startsWith(`..${sep}`) || assetPath.startsWith("../");
|
|
2963
|
+
const stageHint = basename(assetOutdir).startsWith("assembly-") && climbsOut ? " This directory is named like a cdk.Stage sub-assembly, but its parent is not an assembly (no manifest.json), so it was treated as the assembly root. If you meant to point --app at a Stage inside an app's output directory, point it at that output directory." : "";
|
|
2964
|
+
throw wrapError(`Lambda '${sanitizeServiceExceptionMessage(logicalId)}' has Metadata['aws:asset:path']='${sanitizeServiceExceptionMessage(assetPath)}' which ${renderAssemblyPathEscape(resolved, assetOutdir, "mount it")}${stageHint}`);
|
|
2965
|
+
}
|
|
2966
|
+
warnOutsideNamedDirectory(assetOutdir, resolved.path);
|
|
2967
|
+
return resolved.path;
|
|
2968
|
+
}
|
|
2969
|
+
/**
|
|
2970
|
+
* The two directories an asset path is judged against — THE one spelling both
|
|
2971
|
+
* resolvers derive them with, for the same reason
|
|
2972
|
+
* {@link resolveAssetCodeDirectory} is one function: two sites disagreeing
|
|
2973
|
+
* about the BOUND is a defect no refusal test can see.
|
|
2974
|
+
*
|
|
2975
|
+
* Asset paths are relative to the manifest's own directory: the stack's
|
|
2976
|
+
* `assetManifestPath` is `<cdk.out>/<stack>.assets.json`, so stripping the
|
|
2977
|
+
* filename gives the base.
|
|
2978
|
+
*
|
|
2979
|
+
* `assetOutdir` is the app's outdir and is the CONTAINMENT bound. An ABSENT
|
|
2980
|
+
* one falls back to the base, which is correct for a top-level stack and
|
|
2981
|
+
* NARROWS for a Stage — it never opens past the base, so a hand-built
|
|
2982
|
+
* `StackInfo` carrying neither field is refused rather than admitted.
|
|
2983
|
+
* `AssemblyReader` always sets it.
|
|
2984
|
+
*
|
|
2985
|
+
* The two fallbacks are asymmetric, and the base's prefers `assetOutdir` over
|
|
2986
|
+
* `process.cwd()` deliberately: `AssemblyReader` always sets `assetOutdir` but
|
|
2987
|
+
* may leave `assetManifestPath` undefined, and taking `process.cwd()` there
|
|
2988
|
+
* produces a base DISJOINT from the bound — nothing under the cwd is inside
|
|
2989
|
+
* `cdk.out` — so every asset path is refused with a message blaming the
|
|
2990
|
+
* assembly. Fail-closed, so never a hole, but a wrong diagnosis.
|
|
2991
|
+
* `process.cwd()` survives only for a `StackInfo` carrying NEITHER field,
|
|
2992
|
+
* where base and bound coincide again.
|
|
2993
|
+
*
|
|
2994
|
+
* A PRESENT `assetOutdir` is never replaced BY THE BASE. It may be replaced by
|
|
2995
|
+
* its own ANCESTOR, and only in one shape: {@link assemblyRootOf} climbs when
|
|
2996
|
+
* `--app` names a `cdk.Stage` sub-assembly. That is a real widening, decided
|
|
2997
|
+
* partly by a file inside the tree being examined, and every instance of it is
|
|
2998
|
+
* warned — the reasoning is on `assemblyRootOf` and must be read with this
|
|
2999
|
+
* paragraph, not instead of it. **Do not restate the stronger claim here.** An
|
|
3000
|
+
* earlier revision of this block said the bound is used "AS GIVEN" after the
|
|
3001
|
+
* climb had already made that false, which is the same failure this PR
|
|
3002
|
+
* repaired in `StackInfo.assetOutdir`'s own doc one commit earlier.
|
|
3003
|
+
*
|
|
3004
|
+
* Note also that this helper is no longer pure: the climb reads the filesystem
|
|
3005
|
+
* and can emit a log line.
|
|
3006
|
+
*
|
|
3007
|
+
* The base/bound asymmetry is a trust boundary rather than a style choice:
|
|
3008
|
+
* `assetOutdir` comes from the user's own `--app` / `--output`, while
|
|
3009
|
+
* `manifestDir` is derived from `AssetManifestArtifact.file`, which cx-api
|
|
3010
|
+
* resolves out of the assembly's OWN `manifest.json` — so the base is
|
|
3011
|
+
* assembly-CONTROLLED and the bound is not. An earlier revision dropped a
|
|
3012
|
+
* present bound whenever it was not an ancestor of the base, meaning to
|
|
3013
|
+
* improve the diagnosis for a host that supplied a nonsense bound; measured,
|
|
3014
|
+
* it let a planted `"file": "../../../../x.assets.json"` push `manifestDir` to
|
|
3015
|
+
* `/` and carry the bound with it, so a plain relative `etc/passwd` resolved
|
|
3016
|
+
* CONTAINED. A disjoint host-supplied bound refusing everything is a wrong
|
|
3017
|
+
* DIAGNOSIS; a widened bound is the vulnerability this module exists to stop.
|
|
3018
|
+
*
|
|
3019
|
+
* State the property precisely, because the obvious stronger version is FALSE:
|
|
3020
|
+
* it is NOT that "the base left the bound, so everything resolved against it is
|
|
3021
|
+
* outside" — a candidate can climb back in (`app/cdk.out/asset.9f1` from a base
|
|
3022
|
+
* of `/Users/dev`). What holds is that THE BOUND IS ENFORCED INDEPENDENTLY OF
|
|
3023
|
+
* THE BASE, so a planted `file` can steer where a relative value resolves FROM
|
|
3024
|
+
* and can still only reach inside the BOUND — the user's own outdir, or the
|
|
3025
|
+
* root `assemblyRootOf` derived from it, which is the one case where a planted
|
|
3026
|
+
* tree can influence the bound and is warned for exactly that reason. A later reader leaning on the stronger sentence would think a
|
|
3027
|
+
* separate base check is redundant.
|
|
3028
|
+
*
|
|
3029
|
+
* An EMPTY string is treated as ABSENT rather than as a bound, because
|
|
3030
|
+
* `path.resolve('')` is the cwd — a directory the host never named. It takes
|
|
3031
|
+
* the same fallback as `undefined`.
|
|
3032
|
+
*
|
|
3033
|
+
* KNOW WHAT THE ABSENT CASE BUYS, which is NOT "a narrower bound": the
|
|
3034
|
+
* fallback is `manifestDir`, and that is assembly-derived, so a `StackInfo`
|
|
3035
|
+
* carrying no `assetOutdir` gets NO containment rather than a tighter one — a
|
|
3036
|
+
* planted `file` moves base and bound together. It is unreachable through
|
|
3037
|
+
* `AssemblyReader`, which always sets `assetOutdir` from `cloudAssembly.directory`,
|
|
3038
|
+
* and it is what cdkd does; a library host that builds `StackInfo` by hand and
|
|
3039
|
+
* wants the guard must supply the field.
|
|
3040
|
+
*
|
|
3041
|
+
* Exported for `local-start-api.ts`'s copy of the caller, and for unit testing.
|
|
3042
|
+
*/
|
|
3043
|
+
function assetPathDirs(stack) {
|
|
3044
|
+
const bound = stack.assetOutdir === "" ? void 0 : stack.assetOutdir;
|
|
3045
|
+
const manifestDir = stack.assetManifestPath ? dirname(stack.assetManifestPath) : bound ?? process.cwd();
|
|
3046
|
+
return {
|
|
3047
|
+
manifestDir,
|
|
3048
|
+
assetOutdir: bound === void 0 ? manifestDir : assemblyRootOf(bound)
|
|
3049
|
+
};
|
|
3050
|
+
}
|
|
3051
|
+
/**
|
|
3052
|
+
* The real assembly ROOT for a `--app` that names a `cdk.Stage`
|
|
3053
|
+
* SUB-assembly.
|
|
3054
|
+
*
|
|
3055
|
+
* When a user points `--app` at `cdk.out/assembly-MyStage`, the assembly they
|
|
3056
|
+
* are working with is rooted at `cdk.out` — `cdk synth` writes the Stage's
|
|
3057
|
+
* manifest into the sub-directory and stages its ASSETS one level above, so
|
|
3058
|
+
* CDK's own `../asset.<hash>` is a within-assembly reference. Bounding at the
|
|
3059
|
+
* named directory made every one of those look like an escape, and the
|
|
3060
|
+
* refusal had to append a paragraph explaining that the layout "is not a
|
|
3061
|
+
* tamper" — a guard that has to talk you out of its own verdict is computing
|
|
3062
|
+
* the wrong thing.
|
|
3063
|
+
*
|
|
3064
|
+
* **It is a CONVENIENCE, not a safety property, and an earlier revision of
|
|
3065
|
+
* this comment claimed the opposite.** That revision argued the climb is safe
|
|
3066
|
+
* because it runs on a USER-supplied value. That is true of the STRING and
|
|
3067
|
+
* false of the DECISION: the predicate is a directory NAME and the presence of
|
|
3068
|
+
* a FILE, both inside the tree being examined — which under this module's own
|
|
3069
|
+
* threat model is the attacker's. An archive unpacking as `manifest.json` +
|
|
3070
|
+
* `assembly-X/` into a user's home, run as `--app ~/assembly-X`, moved the
|
|
3071
|
+
* bound to `~`, after which `../.aws` resolved CONTAINED and was mounted with
|
|
3072
|
+
* no refusal and no warning. It had been refused before the climb existed.
|
|
3073
|
+
*
|
|
3074
|
+
* So the climb is bounded by two things that are not arguments:
|
|
3075
|
+
*
|
|
3076
|
+
* 1. **It WARNS every time it fires** (see {@link warnDerivedAssemblyRoot}),
|
|
3077
|
+
* naming the directory the user passed and the one derived from it. The
|
|
3078
|
+
* module's doctrine for an absolute path applies here unchanged — a
|
|
3079
|
+
* directory the user did not name must be visible rather than silent — and
|
|
3080
|
+
* it costs one line on the legitimate Stage path.
|
|
3081
|
+
* 2. **The parent must DECLARE this child**, not merely sit above it: its
|
|
3082
|
+
* `manifest.json` must parse and carry a `cdk:cloud-assembly` artifact
|
|
3083
|
+
* whose `properties.directoryName` is this directory's basename, which is
|
|
3084
|
+
* cx-api's own invariant. This removes the accidental collision entirely
|
|
3085
|
+
* (`manifest.json` is not a CDK-exclusive filename) and makes the hostile
|
|
3086
|
+
* case require a purpose-built manifest. It does NOT make the climb safe
|
|
3087
|
+
* against someone who ships the whole tree — they can write that manifest
|
|
3088
|
+
* too. Point 1 is what covers that, which is why it is first.
|
|
3089
|
+
*
|
|
3090
|
+
* `manifestDir` never climbs: it stays `dirname(assetManifestPath)`, the
|
|
3091
|
+
* assembly-derived value, and is still judged against whatever this returns.
|
|
3092
|
+
*
|
|
3093
|
+
* Returns the caller's own spelling UNCHANGED when no climb happens, because
|
|
3094
|
+
* the bound is used as given elsewhere and normalising it would make a
|
|
3095
|
+
* relative `--output cdk.out` come back absolute for every ordinary app.
|
|
3096
|
+
*/
|
|
3097
|
+
function assemblyRootOf(outdir) {
|
|
3098
|
+
const cached = derivedRootCache.get(outdir);
|
|
3099
|
+
if (cached !== void 0) return cached;
|
|
3100
|
+
const root = climbToAssemblyRoot(outdir);
|
|
3101
|
+
derivedRootCache.set(outdir, root);
|
|
3102
|
+
return root;
|
|
3103
|
+
}
|
|
3104
|
+
/** The climb itself; {@link assemblyRootOf} holds the reasoning and memoizes. */
|
|
3105
|
+
function climbToAssemblyRoot(outdir) {
|
|
3106
|
+
let dir = resolve(outdir);
|
|
3107
|
+
let climbed = false;
|
|
3108
|
+
for (;;) {
|
|
3109
|
+
if (!basename(dir).startsWith("assembly-")) break;
|
|
3110
|
+
const parent = dirname(dir);
|
|
3111
|
+
if (parent === dir || !parentDeclaresNestedAssembly(parent, basename(dir))) break;
|
|
3112
|
+
dir = parent;
|
|
3113
|
+
climbed = true;
|
|
3114
|
+
}
|
|
3115
|
+
if (!climbed) return outdir;
|
|
3116
|
+
derivedRootOrigins.set(dir, outdir);
|
|
3117
|
+
warnDerivedAssemblyRoot(outdir, dir);
|
|
3118
|
+
return dir;
|
|
3119
|
+
}
|
|
3120
|
+
/**
|
|
3121
|
+
* Whether `parent`'s own `manifest.json` declares `child` as a nested
|
|
3122
|
+
* assembly, which is what cx-api writes for a `cdk.Stage`.
|
|
3123
|
+
*
|
|
3124
|
+
* Tolerant by construction: an unreadable or unparseable manifest, or one with
|
|
3125
|
+
* no matching artifact, answers `false` and the climb stops — the conservative
|
|
3126
|
+
* direction, since not climbing only restores the previous refusal.
|
|
3127
|
+
*/
|
|
3128
|
+
function parentDeclaresNestedAssembly(parent, child) {
|
|
3129
|
+
try {
|
|
3130
|
+
const raw = readFileSync(join(parent, "manifest.json"), "utf-8");
|
|
3131
|
+
const artifacts = JSON.parse(raw).artifacts;
|
|
3132
|
+
if (artifacts === null || typeof artifacts !== "object") return false;
|
|
3133
|
+
return Object.values(artifacts).some((a) => {
|
|
3134
|
+
const art = a;
|
|
3135
|
+
return art?.type === "cdk:cloud-assembly" && art.properties?.directoryName === child;
|
|
3136
|
+
});
|
|
3137
|
+
} catch {
|
|
3138
|
+
return false;
|
|
3139
|
+
}
|
|
3140
|
+
}
|
|
3141
|
+
/**
|
|
3142
|
+
* Per-process state for the climb. `assetPathDirs` runs once per Lambda AND
|
|
3143
|
+
* once per layer, so without the cache a real `cdk.out/manifest.json` —
|
|
3144
|
+
* routinely multi-MB — is re-read and re-parsed for every one of them, and
|
|
3145
|
+
* `--watch` repeats that per firing. It is also the amplification bound on an
|
|
3146
|
+
* attacker-sized manifest.
|
|
3147
|
+
*
|
|
3148
|
+
* **A cached root can outlive its own evidence, and only in the already-warned
|
|
3149
|
+
* direction.** If a `--watch` re-synth rewrites the parent so it no longer
|
|
3150
|
+
* declares the child, the cache keeps answering the WIDE root while a fresh
|
|
3151
|
+
* derivation would decline and narrow. That grants nothing new: the root
|
|
3152
|
+
* warning fired for that root when it was first derived, and re-deriving can
|
|
3153
|
+
* only narrow. The reverse — cached narrow, a declaring manifest appears
|
|
3154
|
+
* later — stays narrow, which is fail-closed. So the cache cannot introduce an
|
|
3155
|
+
* UNWARNED widening in either direction.
|
|
3156
|
+
*
|
|
3157
|
+
* Keyed by the raw outdir STRING and never evicted. Sound because the same
|
|
3158
|
+
* string names the same directory: a second assembly reusing an outdir is the
|
|
3159
|
+
* same tree, and a changed tree under it is case A above. Growth is one entry
|
|
3160
|
+
* per distinct `--app`, and a climb only happens under `readFromDirectory`
|
|
3161
|
+
* where that argument is constant for the process.
|
|
3162
|
+
*/
|
|
3163
|
+
const derivedRootCache = /* @__PURE__ */ new Map();
|
|
3164
|
+
/**
|
|
3165
|
+
* Derived root -> the directory the user actually named. LAST WRITER WINS, and
|
|
3166
|
+
* the consequence is always a WRONG NAME, never a missing warning: two sibling
|
|
3167
|
+
* Stages climbing to one root make the per-path line name the other sibling,
|
|
3168
|
+
* and a long-lived host that processes assembly A (climbed to `/x/cdk.out`)
|
|
3169
|
+
* and then assembly B whose `--app` IS `/x/cdk.out` names A's Stage directory
|
|
3170
|
+
* throughout run B. The root and the offending path stay correct in both.
|
|
3171
|
+
* Both are a wrong NAME in a line that still fires with the right root and the
|
|
3172
|
+
* right path, which is why neither buys an eviction policy.
|
|
3173
|
+
*/
|
|
3174
|
+
const derivedRootOrigins = /* @__PURE__ */ new Map();
|
|
3175
|
+
const warnedDerivedRoots = /* @__PURE__ */ new Set();
|
|
3176
|
+
const warnedOutsideNamed = /* @__PURE__ */ new Set();
|
|
3177
|
+
/**
|
|
3178
|
+
* Warn that an accepted path leaves the directory the user NAMED, when the
|
|
3179
|
+
* bound was widened by a climb.
|
|
3180
|
+
*
|
|
3181
|
+
* **This is what makes the climb's signal as loud as the absolute arm's.**
|
|
3182
|
+
* Without it the two diverge in the attacker's favour: an absolute path warns
|
|
3183
|
+
* PER LAMBDA naming the exact directory, while a climb warned once at startup
|
|
3184
|
+
* about the root and then mounted each individual path in silence — so
|
|
3185
|
+
* whoever gets the climb converts a per-mount warning into a one-line notice.
|
|
3186
|
+
* Now both arms say something per path that leaves what the user asked for.
|
|
3187
|
+
*/
|
|
3188
|
+
function warnOutsideNamedDirectory(assetOutdir, resolved) {
|
|
3189
|
+
const named = derivedRootOrigins.get(assetOutdir);
|
|
3190
|
+
if (named === void 0) return;
|
|
3191
|
+
if (absoluteAssemblyPathEscape(named, resolved) === void 0) return;
|
|
3192
|
+
if (warnedOutsideNamed.has(resolved)) return;
|
|
3193
|
+
warnedOutsideNamed.add(resolved);
|
|
3194
|
+
getLogger().warn(`'${sanitizeServiceExceptionMessage(resolved)}' is outside '${sanitizeServiceExceptionMessage(named)}', the directory --app named. It is inside the assembly root derived from it, so ${getEmbedConfig().productName} is using it.`);
|
|
3195
|
+
}
|
|
3196
|
+
/**
|
|
3197
|
+
* Say that the assembly root was DERIVED rather than given.
|
|
3198
|
+
*
|
|
3199
|
+
* The user named one directory and the containment bound is another, wider
|
|
3200
|
+
* one. On the legitimate Stage path that is exactly what they wanted and the
|
|
3201
|
+
* line is informative; on a hostile tree it is the only signal that a sibling
|
|
3202
|
+
* of the directory they named is now inside the bound.
|
|
3203
|
+
*/
|
|
3204
|
+
function warnDerivedAssemblyRoot(named, derived) {
|
|
3205
|
+
if (warnedDerivedRoots.has(derived)) return;
|
|
3206
|
+
warnedDerivedRoots.add(derived);
|
|
3207
|
+
getLogger().warn(`'${sanitizeServiceExceptionMessage(named)}' is a cdk.Stage sub-assembly, so ${getEmbedConfig().productName} is treating its parent '${sanitizeServiceExceptionMessage(derived)}' as the assembly root — that is where cdk synth stages a Stage's assets. Everything under that parent is now inside the containment bound, including siblings of the directory you named. If you did not expect the wider directory, point --app at the app's own output directory instead.`);
|
|
3208
|
+
}
|
|
3209
|
+
/**
|
|
2650
3210
|
* Resolve the local directory that corresponds to a function's deployed
|
|
2651
3211
|
* asset, using the CDK-blessed `Metadata['aws:asset:path']` hint (D2). The
|
|
2652
3212
|
* value is a directory path relative to `cdk.out` (e.g. `asset.abc123def`)
|
|
@@ -2656,18 +3216,21 @@ function extractImageLambdaProperties(args) {
|
|
|
2656
3216
|
* Falls back to a clear error when the metadata is missing OR the resolved
|
|
2657
3217
|
* directory does not exist (CDK should always emit it for asset-backed
|
|
2658
3218
|
* Lambdas; absence usually means the user pre-synthesized with a different
|
|
2659
|
-
* cdk.out and pointed `--output` at a stale one).
|
|
3219
|
+
* cdk.out and pointed `--output` at a stale one). Through
|
|
3220
|
+
* {@link resolveAssetCodeDirectory} it REFUSES an escaping RELATIVE value and
|
|
3221
|
+
* WARNS on an ABSOLUTE one that leaves the asset outdir — see that function
|
|
3222
|
+
* for why the two differ.
|
|
2660
3223
|
*/
|
|
2661
3224
|
function resolveAssetCodePath$1(stack, logicalId, resource, options = {}) {
|
|
2662
3225
|
const assetPath = resource.Metadata?.["aws:asset:path"];
|
|
2663
|
-
if (typeof assetPath !== "string" || assetPath.length === 0) throw new LocalInvokeResolutionError(`Lambda '${logicalId}' has no Metadata['aws:asset:path']. ${getEmbedConfig().cliName} invoke needs this hint to find the local asset directory. Re-synthesize the app (without \`--output <stale-dir>\`) and retry.`);
|
|
2664
|
-
const
|
|
2665
|
-
const abs =
|
|
2666
|
-
if (!existsSync(abs)) throw new LocalInvokeResolutionError(`Lambda '${logicalId}' asset path '${abs}' does not exist. Re-synthesize the app and retry.`);
|
|
3226
|
+
if (typeof assetPath !== "string" || assetPath.length === 0) throw new LocalInvokeResolutionError(`Lambda '${sanitizeServiceExceptionMessage(logicalId)}' has no Metadata['aws:asset:path']. ${getEmbedConfig().cliName} invoke needs this hint to find the local asset directory. Re-synthesize the app (without \`--output <stale-dir>\`) and retry.`);
|
|
3227
|
+
const { manifestDir, assetOutdir } = assetPathDirs(stack);
|
|
3228
|
+
const abs = resolveAssetCodeDirectory(manifestDir, assetPath, (message) => new LocalInvokeResolutionError(message), assetOutdir, logicalId);
|
|
3229
|
+
if (!existsSync(abs)) throw new LocalInvokeResolutionError(`Lambda '${sanitizeServiceExceptionMessage(logicalId)}' asset path '${sanitizeServiceExceptionMessage(abs)}' does not exist. Re-synthesize the app and retry.`);
|
|
2667
3230
|
const stat = statSync(abs);
|
|
2668
3231
|
if (stat.isDirectory()) return abs;
|
|
2669
3232
|
if (options.allowZip && stat.isFile() && abs.toLowerCase().endsWith(".zip")) return abs;
|
|
2670
|
-
throw new LocalInvokeResolutionError(`Lambda '${logicalId}' asset path '${abs}' is not a directory` + (options.allowZip ? " or a .zip archive" : "") + ". Re-synthesize the app and retry.");
|
|
3233
|
+
throw new LocalInvokeResolutionError(`Lambda '${sanitizeServiceExceptionMessage(logicalId)}' asset path '${sanitizeServiceExceptionMessage(abs)}' is not a directory` + (options.allowZip ? " or a .zip archive" : "") + ". Re-synthesize the app and retry.");
|
|
2671
3234
|
}
|
|
2672
3235
|
/**
|
|
2673
3236
|
* Turn a resolved function-code asset path into a directory ready to
|
|
@@ -2686,14 +3249,14 @@ function resolveAssetCodePath$1(stack, logicalId, resource, options = {}) {
|
|
|
2686
3249
|
* existing tmpdir cleanup.
|
|
2687
3250
|
*/
|
|
2688
3251
|
function materializeAssetCodeDir(codePath) {
|
|
2689
|
-
if (!existsSync(codePath)) throw new LocalInvokeResolutionError(`Lambda asset path '${codePath}' does not exist. Re-synthesize the app and retry.`);
|
|
3252
|
+
if (!existsSync(codePath)) throw new LocalInvokeResolutionError(`Lambda asset path '${sanitizeServiceExceptionMessage(codePath)}' does not exist. Re-synthesize the app and retry.`);
|
|
2690
3253
|
if (statSync(codePath).isDirectory()) return { dir: codePath };
|
|
2691
3254
|
const zipBytes = readFileSync(codePath);
|
|
2692
3255
|
let files;
|
|
2693
3256
|
try {
|
|
2694
3257
|
files = unzipSync(zipBytes);
|
|
2695
3258
|
} catch (err) {
|
|
2696
|
-
throw new LocalInvokeResolutionError(`Lambda asset '${codePath}' is a file but could not be read as a ZIP archive: ${err instanceof Error ? err.message : String(err)}. Re-synthesize the app and retry.`);
|
|
3259
|
+
throw new LocalInvokeResolutionError(`Lambda asset '${sanitizeServiceExceptionMessage(codePath)}' is a file but could not be read as a ZIP archive: ${err instanceof Error ? err.message : String(err)}. Re-synthesize the app and retry.`);
|
|
2697
3260
|
}
|
|
2698
3261
|
const modes = parseZipUnixModes(zipBytes);
|
|
2699
3262
|
const S_IFMT = 61440;
|
|
@@ -2780,7 +3343,7 @@ function resolveSafeZipEntryPath(root, entry) {
|
|
|
2780
3343
|
* **Order is preserved**: `Properties.Layers` is iterated left-to-right
|
|
2781
3344
|
* and the resulting `ResolvedLambdaLayer[]` carries the same order. The
|
|
2782
3345
|
* caller (`local-invoke.ts`'s `materializeLambdaLayers` and
|
|
2783
|
-
* `local-start-api.ts`'s server-boot pre-merge)
|
|
3346
|
+
* `local-start-api.ts`'s server-boot pre-merge) merges every
|
|
2784
3347
|
* entry into one host tmpdir in template order to honor AWS's
|
|
2785
3348
|
* "last-layer-wins" file-collision semantics — Docker rejects multiple
|
|
2786
3349
|
* bind mounts at the same target so cdk-local cannot rely on overlay
|
|
@@ -5391,7 +5954,7 @@ var AssemblyReader = class {
|
|
|
5391
5954
|
});
|
|
5392
5955
|
const cached = await toolkit.synth(source);
|
|
5393
5956
|
try {
|
|
5394
|
-
return cached.cloudAssembly
|
|
5957
|
+
return collectStacks(cached.cloudAssembly);
|
|
5395
5958
|
} finally {
|
|
5396
5959
|
await cached.dispose();
|
|
5397
5960
|
}
|
|
@@ -5421,19 +5984,54 @@ var AssemblyReader = class {
|
|
|
5421
5984
|
const source = await toolkit.fromAssemblyDirectory(assemblyDir, { failOnMissingContext: false });
|
|
5422
5985
|
const cached = await toolkit.synth(source);
|
|
5423
5986
|
try {
|
|
5424
|
-
return cached.cloudAssembly
|
|
5987
|
+
return collectStacks(cached.cloudAssembly);
|
|
5425
5988
|
} finally {
|
|
5426
5989
|
await cached.dispose();
|
|
5427
5990
|
}
|
|
5428
5991
|
}
|
|
5429
5992
|
};
|
|
5430
|
-
|
|
5993
|
+
/**
|
|
5994
|
+
* Every stack this assembly enumerates, each bounded by the assembly ROOT.
|
|
5995
|
+
*
|
|
5996
|
+
* NOT `stacksRecursively`, and the omission is a decision rather than an
|
|
5997
|
+
* oversight. `CloudAssembly.stacks` lists only this assembly's own artifacts,
|
|
5998
|
+
* and a `cdk.Stage` is a `NestedCloudAssemblyArtifact`, so a Stage's stacks are
|
|
5999
|
+
* not enumerated at all — a pre-existing gap tracked as
|
|
6000
|
+
* [#746](https://github.com/go-to-k/cdk-local/issues/746). Recursing here would
|
|
6001
|
+
* close it and is tempting because it also makes `assetOutdir` differ from the
|
|
6002
|
+
* manifest directory, which is the shape this module's bound exists for. It is
|
|
6003
|
+
* NOT done here because the recursion changes STACK ENUMERATION for every
|
|
6004
|
+
* command: eleven call sites read `stacks.length === 1` to offer the
|
|
6005
|
+
* "single-stack apps may omit the stack prefix" convenience, `matchStacks`
|
|
6006
|
+
* dedupes by `stackName`, and a Stage's clones carry identical logical IDs. That
|
|
6007
|
+
* is a feature with its own design and its own tests, not a side effect of a
|
|
6008
|
+
* containment fix.
|
|
6009
|
+
*
|
|
6010
|
+
* The consequence to know: a Stage's stacks are reachable only by pointing
|
|
6011
|
+
* `--app` at `cdk.out/assembly-<Stage>/`, which makes that directory the
|
|
6012
|
+
* assembly root, so the Stage's own assets — staged into the APP's outdir by
|
|
6013
|
+
* `cdk synth` — fall outside it and are refused. `resolveAssetCodeDirectory`'s
|
|
6014
|
+
* refusal says so in as many words rather than leaving the user with the
|
|
6015
|
+
* generic hand-modified-assembly diagnosis.
|
|
6016
|
+
*/
|
|
6017
|
+
function collectStacks(assembly) {
|
|
6018
|
+
return assembly.stacks.map((stack) => mapStackArtifact(stack, assembly.directory));
|
|
6019
|
+
}
|
|
6020
|
+
/**
|
|
6021
|
+
* `assetOutdir` is threaded in from the ROOT assembly rather than read off the
|
|
6022
|
+
* artifact's own `assembly.directory`: for a stack under a `cdk.Stage` the
|
|
6023
|
+
* latter is `cdk.out/assembly-<Stage>/`, which is the manifest directory, not
|
|
6024
|
+
* the directory the Stage's assets were staged into. See
|
|
6025
|
+
* `StackInfo.assetOutdir`.
|
|
6026
|
+
*/
|
|
6027
|
+
function mapStackArtifact(stack, assetOutdir) {
|
|
5431
6028
|
const info = {
|
|
5432
6029
|
stackName: stack.stackName,
|
|
5433
6030
|
displayName: stack.displayName ?? stack.id,
|
|
5434
6031
|
artifactId: stack.id,
|
|
5435
6032
|
template: stack.template,
|
|
5436
|
-
dependencyNames: stack.dependencies.map((d) => d.id)
|
|
6033
|
+
dependencyNames: stack.dependencies.map((d) => d.id),
|
|
6034
|
+
assetOutdir
|
|
5437
6035
|
};
|
|
5438
6036
|
if (stack.environment.region) info.region = stack.environment.region;
|
|
5439
6037
|
if (stack.environment.account) info.account = stack.environment.account;
|
|
@@ -8372,6 +8970,61 @@ function resolveRuntimeCodeMountPath(runtime) {
|
|
|
8372
8970
|
//#endregion
|
|
8373
8971
|
//#region src/assets/docker-build.ts
|
|
8374
8972
|
/**
|
|
8973
|
+
* Announce that a Docker asset's `source.executable` is about to run.
|
|
8974
|
+
*
|
|
8975
|
+
* The command line comes from the asset manifest, which is chosen by whoever
|
|
8976
|
+
* wrote the assembly, and cdk-local SPAWNS it — so a `cdkl` command against a
|
|
8977
|
+
* pre-synthesized assembly executes code the CloudFormation template does not
|
|
8978
|
+
* show. It is RUN, never refused: CDK CLI's `buildExternalAsset` does the same,
|
|
8979
|
+
* and an assembly is trusted input. What was missing is the line saying so.
|
|
8980
|
+
*
|
|
8981
|
+
* **It names `executable[0]` and an argument COUNT, deliberately, and the full
|
|
8982
|
+
* argv must stay at `debug`.** Rendering every argument at warn level is a new
|
|
8983
|
+
* leak introduced by a warning about leaks: a build script is not `docker
|
|
8984
|
+
* build`, so its own `--token ghp_…` or `--password=…` matches no argv masker's
|
|
8985
|
+
* flag shapes, and the warning would promote it from `--verbose`-only to a line
|
|
8986
|
+
* in every CI log of every run. That was a live defect in the host's copy of
|
|
8987
|
+
* this warning (go-to-k/cdkd#3497); do not "improve" this by expanding it.
|
|
8988
|
+
*
|
|
8989
|
+
* Placed in `buildDockerImage` rather than at a call site because this is the
|
|
8990
|
+
* ONE spawn point: `docker-image-builder.ts` (`invoke` / `start-api` /
|
|
8991
|
+
* `invoke-agentcore` / `start-agentcore`, the last two through
|
|
8992
|
+
* `resolveAgentCoreImage`) and `ecs-task-runner.ts` (`run-task` /
|
|
8993
|
+
* `start-service` / `start-alb`) both reach it, and `studio` inherits it by
|
|
8994
|
+
* spawning those as children. The two ECS server commands were the gap
|
|
8995
|
+
* (go-to-k/cdkd#3540) — the host cannot warn for them, since it re-exports
|
|
8996
|
+
* `runEcsServiceEmulator` as a bare passthrough and the asset build happens
|
|
8997
|
+
* several layers inside this engine.
|
|
8998
|
+
*
|
|
8999
|
+
* **Announced ONCE per distinct command line per process, not once per
|
|
9000
|
+
* spawn.** The spawn genuinely repeats — `ecs-service-runner.ts` boots every
|
|
9001
|
+
* replica through `bootReplica` and restarts a crashed one the same way, and
|
|
9002
|
+
* the agentcore commands re-resolve on reload — so a `DesiredCount: 3` service
|
|
9003
|
+
* would print this ~450-character paragraph three times at boot and again per
|
|
9004
|
+
* crash-loop restart. That is a storm, and a storm is not read. Repeats drop
|
|
9005
|
+
* to `debug`, where the argv already is. Deduping the LINE must never dedupe
|
|
9006
|
+
* the WORK: the spawn happens every time regardless.
|
|
9007
|
+
*
|
|
9008
|
+
* `sanitizeServiceExceptionMessage` for the same reason `assembly-path.ts`
|
|
9009
|
+
* uses it: the value is manifest-chosen, it keeps control characters, and an
|
|
9010
|
+
* unsanitized one lets the command forge its own log line. It is applied to
|
|
9011
|
+
* the DISPLAYED command only — the dedupe key is built from the raw argv and
|
|
9012
|
+
* cwd, so two commands differing only in control characters stay distinct
|
|
9013
|
+
* keys.
|
|
9014
|
+
*/
|
|
9015
|
+
const warnedExecutables = /* @__PURE__ */ new Set();
|
|
9016
|
+
function warnManifestExecutable(cmd, executable, cwd) {
|
|
9017
|
+
const logger = getLogger().child("docker-build");
|
|
9018
|
+
const key = JSON.stringify([cwd, executable]);
|
|
9019
|
+
if (warnedExecutables.has(key)) {
|
|
9020
|
+
logger.debug(`source.executable already announced this process: ${sanitizeServiceExceptionMessage(cmd)} (cwd=${cwd})`);
|
|
9021
|
+
return;
|
|
9022
|
+
}
|
|
9023
|
+
warnedExecutables.add(key);
|
|
9024
|
+
const argCount = executable.length - 1;
|
|
9025
|
+
logger.warn(`Docker asset source.executable runs a command this asset manifest chose, on this machine: '${sanitizeServiceExceptionMessage(cmd)}'` + (argCount > 0 ? ` (with ${argCount} argument(s); --verbose shows them)` : "") + ". cdk-local runs it, matching the CDK CLI — a pre-synthesized assembly is trusted input. Note that this means running a local command against a pre-synthesized assembly DOES execute code from it, which the CloudFormation template does not show.");
|
|
9026
|
+
}
|
|
9027
|
+
/**
|
|
8375
9028
|
* Build a Docker image from a CDK asset source. Returns the local image
|
|
8376
9029
|
* tag the caller should use for `docker tag` / `docker push` (publisher)
|
|
8377
9030
|
* or `docker run` (local-invoke).
|
|
@@ -8394,6 +9047,7 @@ async function buildDockerImage(asset, cdkOutDir, options) {
|
|
|
8394
9047
|
if (!cmd) throw options.wrapError("asset source.executable[] is empty");
|
|
8395
9048
|
const cwd = source.directory ? `${cdkOutDir}/${source.directory}` : cdkOutDir;
|
|
8396
9049
|
logger.debug(`Building Docker image via executable: ${source.executable.join(" ")} (cwd=${cwd})`);
|
|
9050
|
+
warnManifestExecutable(cmd, source.executable, cwd);
|
|
8397
9051
|
let result;
|
|
8398
9052
|
try {
|
|
8399
9053
|
result = await spawnStreaming(cmd, args, { cwd });
|
|
@@ -17254,6 +17908,110 @@ function looksLikeAccessDenied(err) {
|
|
|
17254
17908
|
return name === "AccessDeniedException" || code === "AccessDeniedException" || /access denied/i.test(message) || /not authorized/i.test(message);
|
|
17255
17909
|
}
|
|
17256
17910
|
|
|
17911
|
+
//#endregion
|
|
17912
|
+
//#region src/local/layer-tree-copy.ts
|
|
17913
|
+
/**
|
|
17914
|
+
* Merge one Lambda Layer's asset tree into the directory that will be
|
|
17915
|
+
* bind-mounted at `/opt`, with AWS's "last layer wins" semantic and with
|
|
17916
|
+
* symlinks kept VERBATIM (issue #727).
|
|
17917
|
+
*
|
|
17918
|
+
* Why this is a hand-written walk and not `cpSync`. The merge used to be
|
|
17919
|
+
* `cpSync(src, dest, { recursive: true, force: true })`, and every
|
|
17920
|
+
* `cpSync`-shaped repair of it was measured wrong in some direction:
|
|
17921
|
+
*
|
|
17922
|
+
* - Without `verbatimSymlinks: true` (the default), `cpSync` rewrites a
|
|
17923
|
+
* RELATIVE link target to the ABSOLUTE path of the source on the host —
|
|
17924
|
+
* `bin/rel-link -> real.sh` arrives as `-> <cdk.out>/asset.<hash>/bin/
|
|
17925
|
+
* real.sh`, which is dangling inside the container, where only `dest`
|
|
17926
|
+
* is mounted. Measured on Node 22.12 / 24.21.
|
|
17927
|
+
* - WITH `verbatimSymlinks: true`, Node's C++ `cpSync` (every release
|
|
17928
|
+
* after 22.12) throws `EEXIST` when a later layer carries a symlink at a
|
|
17929
|
+
* path an earlier layer already placed one — `nodejs/node_modules/.bin/
|
|
17930
|
+
* <tool>` in two layers built from the same dependency is the ordinary
|
|
17931
|
+
* case. Measured by the go-to-k/cdkd#3118 review (the cdkd twin) on 22.23 / 24.15 /
|
|
17932
|
+
* 24.21; the JS implementation on 22.12 does not throw, which is why
|
|
17933
|
+
* the floor alone did not show it.
|
|
17934
|
+
* - A `cpSync` + `readdirSync({ recursive: true })` pair was worse:
|
|
17935
|
+
* the recursive readdir DESCENDS INTO directory symlinks on every Node
|
|
17936
|
+
* after 22.12 (not on 22.12), so a cyclic link (`sub/up -> ..`, or a
|
|
17937
|
+
* pnpm-style `node_modules`) hung the merge, and an absolute link to a
|
|
17938
|
+
* host directory made the walk delete and write INSIDE that host
|
|
17939
|
+
* directory. And `cpSync`'s `force` writes THROUGH a destination that
|
|
17940
|
+
* is already a directory symlink, so a later layer's real directory
|
|
17941
|
+
* landed in the earlier layer's link target — on the host, when the
|
|
17942
|
+
* link was absolute. Same review, same versions.
|
|
17943
|
+
*
|
|
17944
|
+
* So the walk is explicit and recurses only on a real directory (`d_type`,
|
|
17945
|
+
* via `Dirent.isDirectory()`, which reports a directory symlink as a
|
|
17946
|
+
* symlink on 22.12 and 24.21 alike). Per entry, "last layer wins" is applied
|
|
17947
|
+
* the same way for every kind: whatever sits at the destination path that is
|
|
17948
|
+
* not of the same kind is removed first — a link, a file, or a whole
|
|
17949
|
+
* directory an earlier layer placed — then the entry lands. A symlink is
|
|
17950
|
+
* recreated with the SOURCE's own target string and never resolved, so an
|
|
17951
|
+
* absolute link stays absolute and a dangling one stays dangling, exactly as
|
|
17952
|
+
* AWS extracts the layer ZIP into `/opt`; nothing is ever written through a
|
|
17953
|
+
* link. A file is copied with `cpSync` (single file, `force`), which is what
|
|
17954
|
+
* preserves the mode bits, so a layer's `bin/<script>` keeps its `+x` — the
|
|
17955
|
+
* property the `local-invoke-layers` fixture executes through.
|
|
17956
|
+
*
|
|
17957
|
+
* The ROOT may itself be a symlink (an asset dir handed over through a link
|
|
17958
|
+
* passes `resolveAssetCodePath`'s `statSync`). `readdirSync` follows a root
|
|
17959
|
+
* link by itself, so the `realpathSync` is not what makes that case work,
|
|
17960
|
+
* and it does not move where a dangling or looping root fails either (the
|
|
17961
|
+
* first `readdirSync` raises the same `ENOENT` / `ELOOP` naming the asset
|
|
17962
|
+
* path); it only keeps every path the walk joins real.
|
|
17963
|
+
*
|
|
17964
|
+
* Bound, stated rather than handled: a FIFO or a socket inside a layer
|
|
17965
|
+
* makes the single-file `cpSync` throw (`ERR_FS_CP_FIFO_PIPE` /
|
|
17966
|
+
* `ERR_FS_CP_SOCKET`, immediately, never blocking) where the old recursive
|
|
17967
|
+
* copy skipped it in silence on some releases. CDK's own asset staging
|
|
17968
|
+
* cannot stage such an entry, so no real layer carries one.
|
|
17969
|
+
*
|
|
17970
|
+
* Both layer merges (`cdkl invoke` and `cdkl start-api`) call
|
|
17971
|
+
* this; keep them on the one helper rather than re-spelling copy options.
|
|
17972
|
+
*/
|
|
17973
|
+
function copyLayerTreeLastWins(src, dest) {
|
|
17974
|
+
mkdirSync(dest, { recursive: true });
|
|
17975
|
+
copyDirLastWins(realpathSync(src), dest);
|
|
17976
|
+
}
|
|
17977
|
+
function copyDirLastWins(fromDir, toDir) {
|
|
17978
|
+
for (const entry of readdirSync(fromDir, { withFileTypes: true })) {
|
|
17979
|
+
const from = join(fromDir, entry.name);
|
|
17980
|
+
const to = join(toDir, entry.name);
|
|
17981
|
+
if (entry.isSymbolicLink()) {
|
|
17982
|
+
removeUnless(to, "symlink");
|
|
17983
|
+
symlinkSync(readlinkSync(from), to);
|
|
17984
|
+
} else if (entry.isDirectory()) {
|
|
17985
|
+
removeUnless(to, "directory");
|
|
17986
|
+
mkdirSync(to, { recursive: true });
|
|
17987
|
+
copyDirLastWins(from, to);
|
|
17988
|
+
} else {
|
|
17989
|
+
removeUnless(to, "file");
|
|
17990
|
+
cpSync(from, to, { force: true });
|
|
17991
|
+
}
|
|
17992
|
+
}
|
|
17993
|
+
}
|
|
17994
|
+
/**
|
|
17995
|
+
* Clear the destination path unless what is there is already of `kind`. A
|
|
17996
|
+
* directory is kept so a later layer MERGES into it; a file is kept because
|
|
17997
|
+
* `cpSync` overwrites it in place; a symlink is always replaced (its target
|
|
17998
|
+
* string comes from the later layer).
|
|
17999
|
+
*/
|
|
18000
|
+
function removeUnless(to, kind) {
|
|
18001
|
+
let st;
|
|
18002
|
+
try {
|
|
18003
|
+
st = lstatSync(to);
|
|
18004
|
+
} catch {
|
|
18005
|
+
return;
|
|
18006
|
+
}
|
|
18007
|
+
if (kind === "directory" && st.isDirectory()) return;
|
|
18008
|
+
if (kind === "file" && st.isFile()) return;
|
|
18009
|
+
rmSync(to, {
|
|
18010
|
+
recursive: true,
|
|
18011
|
+
force: true
|
|
18012
|
+
});
|
|
18013
|
+
}
|
|
18014
|
+
|
|
17257
18015
|
//#endregion
|
|
17258
18016
|
//#region src/local/stage-resolver.ts
|
|
17259
18017
|
/**
|
|
@@ -18692,12 +19450,12 @@ async function resolveLocalBuildPlan$1(lambda) {
|
|
|
18692
19450
|
* when the entry is a same-stack asset. Literal-ARN entries always
|
|
18693
19451
|
* pre-materialize first.
|
|
18694
19452
|
* - 2+ layers → copy each into a fresh tmpdir IN ORDER (later
|
|
18695
|
-
* layers overwrite earlier files via `
|
|
19453
|
+
* layers overwrite earlier files via `copyLayerTreeLastWins`),
|
|
18696
19454
|
* bind-mount the tmpdir at `/opt`. Records the tmpdir in
|
|
18697
19455
|
* `layerTmpDirs` so `shutdown(...)` removes it.
|
|
18698
19456
|
*
|
|
18699
19457
|
* Issue #448: literal-ARN entries (`{kind: 'arn', ...}`) are downloaded
|
|
18700
|
-
* + unzipped via `lambda:GetLayerVersion` BEFORE the
|
|
19458
|
+
* + unzipped via `lambda:GetLayerVersion` BEFORE the merge
|
|
18701
19459
|
* branches run. Every per-ARN tmpdir is also recorded in `layerTmpDirs`
|
|
18702
19460
|
* so the same shutdown path cleans it up — even for the single-layer
|
|
18703
19461
|
* fast path that bind-mounts the dir directly.
|
|
@@ -18727,10 +19485,15 @@ async function materializeLambdaLayers$1(layers, layerTmpDirs, layerRoleArn) {
|
|
|
18727
19485
|
}
|
|
18728
19486
|
if (flat.length === 1) return flat[0].assetPath;
|
|
18729
19487
|
const dir = mkdtempSync(path$1.join(tmpdir(), `${getEmbedConfig().resourceNamePrefix}-start-api-layers-`));
|
|
18730
|
-
|
|
18731
|
-
|
|
18732
|
-
|
|
18733
|
-
|
|
19488
|
+
try {
|
|
19489
|
+
for (const layer of flat) copyLayerTreeLastWins(layer.assetPath, dir);
|
|
19490
|
+
} catch (error) {
|
|
19491
|
+
rmSync(dir, {
|
|
19492
|
+
recursive: true,
|
|
19493
|
+
force: true
|
|
19494
|
+
});
|
|
19495
|
+
throw error;
|
|
19496
|
+
}
|
|
18734
19497
|
layerTmpDirs.add(dir);
|
|
18735
19498
|
return dir;
|
|
18736
19499
|
}
|
|
@@ -18863,12 +19626,18 @@ function resolveImageLambda(args) {
|
|
|
18863
19626
|
* Locate the Lambda's local code directory using the CDK-blessed
|
|
18864
19627
|
* `Metadata['aws:asset:path']` hint. Bind-mounted directly at
|
|
18865
19628
|
* `/var/task` (read-only) by the docker-runner.
|
|
19629
|
+
*
|
|
19630
|
+
* The resolution itself is `lambda-resolver.ts`'s
|
|
19631
|
+
* {@link resolveAssetCodeDirectory} — THE one spelling, so this twin and
|
|
19632
|
+
* `cdkl invoke`'s cannot disagree about the containment bound. It REFUSES an
|
|
19633
|
+
* escaping RELATIVE value and WARNS on an ABSOLUTE one that leaves the asset
|
|
19634
|
+
* outdir; that function's header records why the two shapes differ.
|
|
18866
19635
|
*/
|
|
18867
19636
|
function resolveAssetCodePath(stack, logicalId, resource) {
|
|
18868
19637
|
const assetPath = resource.Metadata?.["aws:asset:path"];
|
|
18869
|
-
if (typeof assetPath !== "string" || assetPath.length === 0) throw new Error(`Lambda '${logicalId}' has no Metadata['aws:asset:path']. ${getEmbedConfig().cliName} start-api needs this hint to find the local asset directory. Re-synthesize the app and retry.`);
|
|
18870
|
-
const
|
|
18871
|
-
return
|
|
19638
|
+
if (typeof assetPath !== "string" || assetPath.length === 0) throw new Error(`Lambda '${sanitizeServiceExceptionMessage(logicalId)}' has no Metadata['aws:asset:path']. ${getEmbedConfig().cliName} start-api needs this hint to find the local asset directory. Re-synthesize the app and retry.`);
|
|
19639
|
+
const { manifestDir, assetOutdir } = assetPathDirs(stack);
|
|
19640
|
+
return resolveAssetCodeDirectory(manifestDir, assetPath, (message) => new Error(message), assetOutdir, logicalId);
|
|
18872
19641
|
}
|
|
18873
19642
|
/**
|
|
18874
19643
|
* Print the discovered route table to stdout. Format mirrors the spec
|
|
@@ -19668,10 +20437,15 @@ function materializeLambdaLayers(layers) {
|
|
|
19668
20437
|
readOnly: true
|
|
19669
20438
|
} };
|
|
19670
20439
|
const tmpDir = mkdtempSync(path$1.join(tmpdir(), `${getEmbedConfig().resourceNamePrefix}-invoke-layers-`));
|
|
19671
|
-
|
|
19672
|
-
|
|
19673
|
-
|
|
19674
|
-
|
|
20440
|
+
try {
|
|
20441
|
+
for (const layer of layers) copyLayerTreeLastWins(layer.assetPath, tmpDir);
|
|
20442
|
+
} catch (error) {
|
|
20443
|
+
rmSync(tmpDir, {
|
|
20444
|
+
recursive: true,
|
|
20445
|
+
force: true
|
|
20446
|
+
});
|
|
20447
|
+
throw error;
|
|
20448
|
+
}
|
|
19675
20449
|
return {
|
|
19676
20450
|
mount: {
|
|
19677
20451
|
hostPath: tmpDir,
|
|
@@ -40285,4 +41059,4 @@ function addStudioSpecificOptions(cmd) {
|
|
|
40285
41059
|
|
|
40286
41060
|
//#endregion
|
|
40287
41061
|
export { applyEdgeResponseResult as $, buildJwksUrlFromIssuer as $n, resolveCfnStackName as $r, buildCloudMapIndex as $t, startAgentCoreHttpServer as A, describeCredentialLoadFailure as Ai, classifySourceChange as An, ConnectionRegistry as Ar, addRunTaskSpecificOptions as At, idFromArn as B, buildStageMap as Bn, resolveRuntimeFileExtension as Br, resolveEcsAssumeRoleOption as Bt, addListSpecificOptions as C, resolveAgentCoreTarget as Ci, waitForAgentCorePing as Cn, tryParseStatus as Cr, parseLbPortOverrides as Ct, createLocalStartAgentCoreCommand as D, tryResolveImageFnJoin as Di, computeCodeImageTag as Dn, probeHostGatewaySupport as Dr, addStartServiceSpecificOptions as Dt, addStartAgentCoreSpecificOptions as E, substituteImagePlaceholders as Ei, buildAgentCoreCodeImage as En, HOST_GATEWAY_MIN_VERSION as Er, resolveAlbFrontDoor as Et, createLocalStartCloudFrontCommand as F, createWatchPredicates as Fn, buildDisconnectEvent as Fr, addImageOverrideOptions as Ft, classifyS3Error as G, filterRoutesByApiIdentifiers as Gn, substituteEnvVarsFromState as Gr, enforceImageOverrideOrphans as Gt, createDeployedKvsDataSource as H, resolveEnvVars$1 as Hn, EcsTaskResolutionError as Hr, runEcsServiceEmulator as Ht, normalizeKvsFileKeys as I, resolveApiTargetSubset as In, buildMessageEvent as Ir, buildEcsImageResolutionContext$1 as It, startCloudFrontServer as J, startApiServer as Jn, createLocalStateProvider as Jr, resolveImageOverrides as Jt, createS3OriginReader as K, groupRoutesByServer as Kn, substituteEnvVarsFromStateAsync as Kr, mergeForService as Kt, parseKvsFileOverrides as L, createAuthorizerCache as Ln, architectureToPlatform as Lr, ecsClusterOption as Lt, startAgentCoreWsBridge as M, resolveProfileCredentials as Mi, createLocalInvokeCommand as Mn, handleConnectionsRequest as Mr, MAX_TASKS_SUBNET_RANGE_CAP as Mt, LocalStartCloudFrontError as N, buildProxyClientConfig as Ni, addStartApiSpecificOptions as Nn, parseConnectionsPath as Nr, addCommonEcsServiceOptions as Nt, buildAgentCoreServeAuthCheck as O, LocalInvokeBuildError as Oi, renderCodeDockerfile as On, resolveHostGatewayExtraHosts as Or, createLocalStartServiceCommand as Ot, addStartCloudFrontSpecificOptions as P, isProxyEnvConfigured as Pi, createLocalStartApiCommand as Pn, buildConnectEvent as Pr, addEcsAssumeRoleOptions as Pt, applyEdgeRequestResult as Q, buildCognitoJwksUrl as Qn, resolveCfnRegion as Qr, listPinnedTargets as Qt, parseOriginOverrides as R, createFileWatcher as Rn, buildContainerImage as Rr, parseMaxTasks as Rt, StudioEventBus as S, pickAgentCoreCandidateStack as Si, waitForAgentCoreHttpReady as Sn, selectIntegrationResponse as Sr, createLocalStartAlbCommand as St, formatTargetListing as T, formatStateRemedy as Ti, SUPPORTED_CODE_RUNTIMES as Tn, HOST_DOCKER_INTERNAL_GATEWAY as Tr, isApplicationLoadBalancer as Tt, resolveDeployedKvsArnByName as U, availableApiIdentifiers as Un, substituteAgainstState as Ur, ImageOverrideError as Ut, resolveKvsModulesForDistribution as V, materializeLayerFromArn as Vn, resolveRuntimeImage as Vr, resolveSharedSidecarCredentials as Vt, resolveDeployedOriginBucket as W, filterRoutesByApiIdentifier as Wn, substituteAgainstStateAsync as Wr, buildImageOverrideTag as Wt, serveFromStaticOrigin as X, resolveServiceIntegrationParameters as Xn, rejectExplicitCfnStackWithMultipleStacks as Xr, describePinnedImageUri as Xt, resolveErrorResponseCandidates as Y, resolveSelectionExpression as Yn, isCfnFlagPresent as Yr, runImageOverrideBuilds as Yt, serveLambdaUrlOrigin as Z, defaultCredentialsLoader as Zn, resolveCfnFallbackRegion as Zr, isLocalCdkAssetImage as Zt, filterStudioTargetGroups as _, AGENTCORE_AGUI_PROTOCOL as _i, parseSseForJsonRpc as _n, applyAuthorizerOverlay as _r, createCloudFrontModule as _t, createLocalStudioCommand as a, countTargets as ai, attachContainerLogStreamer as an, computeRequestIdentityHash as ar, describeS3OriginDomain as at, renderStudioHtml as b, AGENTCORE_RUNTIME_TYPE as bi, AGENTCORE_SESSION_ID_HEADER as bn, evaluateResponseParameters as br, addAlbSpecificOptions as bt, startStudioProxy as c, discoverWebSocketApis as ci, bridgeAgentCoreWs as cn, invokeTokenAuthorizer as cr, pickFunctionUrlLogicalIdFromOrigin as ct, createStudioDispatcher as d, parseSelectionExpressionPath as di, A2A_PATH as dn, buildCorsConfigByApiId as dr, pickTargetFunctionLogicalId as dt, CfnLocalStateProvider as ei, CloudMapRegistry as en, createJwksCache as er, buildEdgeRequestEvent as et, filterStudioCustomResources as f, webSocketApiMatchesIdentifier as fi, a2aInvokeOnce as fn, buildCorsConfigFromCloudFrontChain as fr, resolveCloudFrontDistribution as ft, annotatePinnedEcsTargets as g, AGENTCORE_A2A_PROTOCOL as gi, mcpInvokeOnce as gn, translateLambdaResponse as gr, stripCloudFrontImport as gt, annotateEcsTaskPinnedTargets as h, resolveLambdaArnIntrinsic as hi, MCP_PROTOCOL_VERSION as hn, matchRoute as hr, runViewerResponse as ht, coerceStopRequest as i, resolveSingleTarget as ii, getContainerNetworkIp as in, buildMethodArn as ir, CLOUDFRONT_DISTRIBUTION_TYPE as it, attachAgentCoreWsBridge as j, buildStsClientConfig as ji, addInvokeSpecificOptions as jn, buildMgmtEndpointEnvUrl as jr, createLocalRunTaskCommand as jt, selectServeInboundAuth as k, describeAwsFailureForWarn as ki, toCmdArgv as kn, bufferToBody as kr, serviceStrategy as kt, relayServeRequest as l, discoverWebSocketApisOrThrow as li, invokeAgentCoreWs as ln, attachAuthorizers as lr, pickKvsLogicalIdFromArn as lt, annotateAlbPinnedBackingServices as m, pickRefLogicalId as mi, MCP_PATH as mn, matchPreflight as mr, runViewerRequest as mt, coerceRunRequest as n, resolveSsmParameters as ni, SOFT_RELOAD_COMPLETION_LOG_SUFFIX as nn, verifyJwtAuthorizer as nr, edgeHeadersToHttp as nt, resolveServeBaseUrl as o, listTargets as oi, addInvokeAgentCoreSpecificOptions as on, evaluateCachedLambdaPolicy as or, extractKvsAssociations as ot, isCustomResourceLambdaTarget as p, discoverRoutes as pi, MCP_CONTAINER_PORT as pn, isFunctionUrlOacFronted as pr, compileCloudFrontFunction as pt, matchBehavior as q, readMtlsMaterialsFromDisk as qn, LocalStateSourceError as qr, parseImageOverrideFlags as qt, coerceServeRequest as r, resolveWatchConfig as ri, setShadowReadyTimeoutMs as rn, verifyJwtViaDiscovery as rr, httpHeadersToEdge as rt, createStudioServeManager as s, availableWebSocketApiIdentifiers as si, createLocalInvokeAgentCoreCommand as sn, invokeRequestAuthorizer as sr, isCloudFrontDistribution as st, addStudioSpecificOptions as t, collectSsmParameterRefs as ti, DEFAULT_SHADOW_READY_TIMEOUT_MS as tn, verifyCognitoJwt as tr, buildEdgeResponseEvent as tt, reinvoke as u, filterWebSocketApisByIdentifiers as ui, A2A_CONTAINER_PORT as un, applyCorsResponseHeaders as ur, pickLambdaEdgeFunctionLogicalId as ut, startStudioServer as v, AGENTCORE_HTTP_PROTOCOL as vi, AGENTCORE_SIGV4_SERVICE as vn, buildHttpApiV2Event as vr, createLocalFileKvsDataSource as vt, createLocalListCommand as w, derivePseudoParametersFromRegion as wi, downloadAndExtractS3Bundle as wn, VtlEvaluationError as wr, resolveAlbTarget as wt, createStudioStore as x, AgentCoreResolutionError as xi, invokeAgentCore as xn, pickResponseTemplate as xr, albStrategy as xt, toStudioTargetGroups as y, AGENTCORE_MCP_PROTOCOL as yi, signAgentCoreInvocation as yn, buildRestV1Event as yr, createUnboundCloudFrontModule as yt, resolveCloudFrontTarget as z, attachStageContext as zn, resolveRuntimeCodeMountPath as zr, parseRestartPolicy as zt };
|
|
40288
|
-
//# sourceMappingURL=local-studio-
|
|
41062
|
+
//# sourceMappingURL=local-studio-D8cHeCcp.js.map
|