cdk-local 0.148.3 → 0.149.0

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.
@@ -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";
@@ -203,6 +203,12 @@ function hostFromAuthority(authority) {
203
203
  * so `proxyAwareFetch()` is the second seam: same `NO_PROXY` decision, same
204
204
  * "empty when unconfigured" contract, fenced by
205
205
  * `tests/unit/utils/aws-proxy-fetch-audit.test.ts`.
206
+ *
207
+ * Both seams route through ONE routing decision,
208
+ * {@link resolveProxyForTarget} (issue go-to-k/cdk-local#663) -- which is
209
+ * also where a proxy scheme these agents cannot speak is turned into a
210
+ * warned DIRECT connection rather than into an HTTP agent pointed at, for
211
+ * example, a SOCKS port.
206
212
  */
207
213
  /**
208
214
  * The SDK applies these defaults only when it builds its OWN agents from a
@@ -227,6 +233,129 @@ function isProxyEnvConfigured() {
227
233
  return Boolean(env["https_proxy"] || env["HTTPS_PROXY"] || env["http_proxy"] || env["HTTP_PROXY"] || env["all_proxy"] || env["ALL_PROXY"]);
228
234
  }
229
235
  /**
236
+ * Proxy SCHEMES already warned about, so a decision taken per REQUEST does
237
+ * not print a line per AWS call. Keyed by scheme rather than by URL because
238
+ * the message names only the scheme — two SOCKS proxies would otherwise
239
+ * print the same sentence twice.
240
+ */
241
+ const warnedProxySchemes = /* @__PURE__ */ new Set();
242
+ /**
243
+ * The scheme of a proxy URL, lowercased and without its `://`.
244
+ *
245
+ * `getProxyForUrl` prepends the REQUEST's scheme to any value that does not
246
+ * already contain `://`, so the URL it returns normally carries one — even
247
+ * `HTTPS_PROXY=proxy.internal:3128`, which becomes `https://proxy.internal:3128`
248
+ * and therefore makes the connection TO THE PROXY ITSELF TLS. (Both spellings
249
+ * still tunnel with `CONNECT`; what changes is whether that tunnel is wrapped
250
+ * in TLS to the proxy. Read from `https-proxy-agent`: an `https:` proxy is
251
+ * reached with `tls.connect` and the `CONNECT` line is written over it.) A
252
+ * value containing `://` somewhere OTHER than the start is returned verbatim
253
+ * and has no leading scheme; pathological, but reachable, so it gets a NAME
254
+ * rather than an empty string that would read as a missing message.
255
+ *
256
+ * The `:\/\/` is load-bearing and NOT decoration. Matching a bare `token:`
257
+ * would name whatever precedes the first colon, and in a verbatim value that
258
+ * is routinely the PROXY USERNAME. Measured:
259
+ * `HTTPS_PROXY=corp-user:s3cr3t@http://proxy.corp:3128` contains `://`, so it
260
+ * comes back untouched — and a `^([a-z][a-z0-9+.-]*):` reading of it yields
261
+ * `corp-user`, which {@link warnUnspeakableProxy} would then print at default
262
+ * level into a log panel `cdkl studio` serves over HTTP. Requiring `://`
263
+ * sends that shape to `(unrecognized)`.
264
+ *
265
+ * It does NOT make "a username can never be printed" true, and the bound is
266
+ * stated rather than left to be rediscovered: `alice://s3cr3t@proxy:3128` is
267
+ * a syntactically valid URL whose scheme IS `alice`, so `alice` is named.
268
+ * That is the URL grammar answering correctly — nothing here can know the
269
+ * author meant it as a username — and it is not a working proxy
270
+ * configuration in any case. Pinned by a case so a future reader finds a
271
+ * decision instead of an oversight.
272
+ *
273
+ * The length bound is the other half: without it the scheme run is
274
+ * unbounded, so a proxy value of 100 kB of `a` followed by `://` becomes a
275
+ * 100 kB default-level warn. 32 is far past every real scheme, and it never
276
+ * affects ROUTING — {@link isSpeakableProxy} is a separate unbounded test, so
277
+ * a 40-character scheme is unspeakable either way.
278
+ */
279
+ function proxySchemeOf(proxyUrl) {
280
+ const match = /^([a-z][a-z0-9+.-]{0,31}):\/\//i.exec(proxyUrl);
281
+ return match ? match[1].toLowerCase() : "(unrecognized)";
282
+ }
283
+ /**
284
+ * Whether `proxyUrl` names a proxy these agents can actually SPEAK to.
285
+ *
286
+ * `getProxyForUrl` honours `ALL_PROXY`, and `ALL_PROXY=socks5://...` is an
287
+ * ordinary spelling — but `http-proxy-agent` / `https-proxy-agent` speak HTTP
288
+ * `CONNECT`, not SOCKS. Measured: `new HttpsProxyAgent('socks5://127.0.0.1:1080')`
289
+ * constructs happily and reports `proxy.protocol === 'socks5:'`, then talks
290
+ * HTTP at a SOCKS port, so every request through it fails.
291
+ */
292
+ function isSpeakableProxy(proxyUrl) {
293
+ return /^https?:\/\//i.test(proxyUrl);
294
+ }
295
+ /**
296
+ * Say ONCE per scheme that a proxy variable is being ignored.
297
+ *
298
+ * The URL is withheld and only the SCHEME is named: a proxy URL routinely
299
+ * carries `user:password@`, and this line is emitted at DEFAULT level
300
+ * somewhere a third party reads it — `cdkl studio` mirrors a serve child's
301
+ * output into the log panel it serves over HTTP. Same rule `parseHttpUrl`'s
302
+ * rejection follows, for the same reason.
303
+ */
304
+ function warnUnspeakableProxy(proxyUrl) {
305
+ const scheme = proxySchemeOf(proxyUrl);
306
+ if (warnedProxySchemes.has(scheme)) return;
307
+ try {
308
+ getLogger().child("aws-proxy").warn(`Unsupported proxy scheme "${scheme}" — cdk-local speaks HTTP CONNECT proxies only, so this request went DIRECT instead of through the proxy. Set HTTPS_PROXY / HTTP_PROXY / ALL_PROXY to an http:// or https:// proxy URL. (The proxy URL is withheld here: it can carry credentials.)`);
309
+ } catch {
310
+ return;
311
+ }
312
+ warnedProxySchemes.add(scheme);
313
+ }
314
+ /**
315
+ * The proxy to route `targetHref` through, or `''` for a DIRECT connection.
316
+ *
317
+ * ONE site owns the question, so the SDK-client half
318
+ * ({@link EnvRoutingProxyAgent}) and the fetch half ({@link proxyAwareFetch})
319
+ * cannot answer it differently for the same environment — they did until
320
+ * issue go-to-k/cdk-local#663: PR 656 taught the fetch half to fall back on
321
+ * an unspeakable proxy, while the agent PR 646 built kept constructing an
322
+ * HTTP agent pointed at a SOCKS port.
323
+ *
324
+ * Falling back to DIRECT rather than REFUSING is the deliberate call, and it
325
+ * is now the same call on both halves:
326
+ *
327
+ * - Before either seam existed every one of these requests went direct, so a
328
+ * developer who exports `ALL_PROXY=socks5://...` for an unrelated tunnel
329
+ * AND has working direct egress had cdk-local working; PR 646 broke that.
330
+ * Refusing keeps them broken with no configuration to reach — cdk-local
331
+ * speaks no SOCKS, so the only remedy would be unsetting the variable.
332
+ * - On the JWKS / OIDC discovery path a FAILED read does not deny: the
333
+ * verifier caches pass-through and accepts every bearer token for the
334
+ * failure TTL (`docs/cli-reference.md`, "JWKS / OIDC discovery
335
+ * unreachable"). Attempting and failing there is strictly worse than going
336
+ * direct.
337
+ * - The one argument for refusing was that a fallback is SILENT, and
338
+ * {@link warnUnspeakableProxy} removes it: a user whose direct egress is
339
+ * blocked still gets a line naming the cause, instead of an `ETIMEDOUT`
340
+ * that points at nothing.
341
+ *
342
+ * An unparsable proxy value whose scheme IS `http(s):` is a DIFFERENT
343
+ * question and still throws from the agent constructor, as before
344
+ * (`HTTPS_PROXY=http://[`): a typo has no working setup behind it, so going
345
+ * direct would hide it rather than restore anything. A value whose scheme is
346
+ * NEITHER — garbage that merely contains `://` — is refused here first and
347
+ * gets the warn instead of the throw. That is the same trade either way: the
348
+ * cause stays attached to the request, which is the property the throw
349
+ * existed for.
350
+ */
351
+ function resolveProxyForTarget(targetHref) {
352
+ const proxyUrl = getProxyForUrl(targetHref).trim();
353
+ if (proxyUrl === "") return "";
354
+ if (isSpeakableProxy(proxyUrl)) return proxyUrl;
355
+ warnUnspeakableProxy(proxyUrl);
356
+ return "";
357
+ }
358
+ /**
230
359
  * An `agent-base` Agent that decides PER REQUEST whether to tunnel through
231
360
  * the configured proxy or connect directly. `NodeHttpHandler` picks its agent
232
361
  * by protocol alone, and `https-proxy-agent` does not consult `NO_PROXY` —
@@ -254,7 +383,7 @@ var EnvRoutingProxyAgent = class extends Agent$2 {
254
383
  const secure = options.secureEndpoint;
255
384
  const host = options.host ?? "localhost";
256
385
  const port = options.port || (secure ? 443 : 80);
257
- const proxyUrl = getProxyForUrl(`${secure ? "https" : "http"}://${formatAuthority(host, port)}`);
386
+ const proxyUrl = resolveProxyForTarget(`${secure ? "https" : "http"}://${formatAuthority(host, port)}`);
258
387
  if (!proxyUrl) return secure ? this.directHttpsAgent : this.directHttpAgent;
259
388
  const key = `${secure ? "https" : "http"}|${proxyUrl}`;
260
389
  let agent = this.proxyAgents.get(key);
@@ -397,30 +526,6 @@ function isRedirectStatus(status) {
397
526
  * plausibly does reach those, so exempting them would be a guess rather
398
527
  * than an impossibility. `NO_PROXY` remains the control for that case.
399
528
  */
400
- /**
401
- * Whether `proxyUrl` names a proxy these agents can actually SPEAK to.
402
- *
403
- * `getProxyForUrl` honours `ALL_PROXY`, and `ALL_PROXY=socks5://...` is an
404
- * ordinary spelling — but `http-proxy-agent` / `https-proxy-agent` speak HTTP
405
- * `CONNECT`, not SOCKS. Measured: `new HttpsProxyAgent('socks5://127.0.0.1:1080')`
406
- * constructs happily and then talks HTTP at a SOCKS port, so every request
407
- * through it fails.
408
- *
409
- * Falling back to a DIRECT request is the right answer rather than a
410
- * best-effort attempt, for a reason specific to this seam: before issue #647
411
- * these reads went direct through undici and WORKED for a SOCKS user, and an
412
- * unreachable JWKS does not deny requests — it caches `passThrough` and
413
- * accepts every token for the failure TTL. Trying and failing would turn a
414
- * working setup into a silent auth downgrade, which is the outcome the
415
- * loopback rule below exists to prevent.
416
- *
417
- * The SDK-client half of this — `EnvRoutingProxyAgent`, which PR 646 built
418
- * with the same blind spot, and whose failures are at least loud — is tracked
419
- * in https://github.com/go-to-k/cdk-local/issues/663.
420
- */
421
- function isSpeakableProxy(proxyUrl) {
422
- return /^https?:\/\//i.test(proxyUrl);
423
- }
424
529
  function isLoopbackHost(hostname) {
425
530
  const host = hostname.trim().replace(/^\[|\]$/g, "").replace(/\.$/, "").toLowerCase();
426
531
  if (host === "localhost" || host.endsWith(".localhost")) return true;
@@ -605,7 +710,7 @@ async function proxyAwareFetch(url, opts = {}) {
605
710
  if (!isProxyEnvConfigured()) return globalThis.fetch(url);
606
711
  let target = parseHttpUrl(url);
607
712
  for (let hop = 0;; hop++) {
608
- if (isLoopbackHost(target.hostname) || !isSpeakableProxy(getProxyForUrl(target.href))) return globalThis.fetch(target.href);
713
+ if (isLoopbackHost(target.hostname) || resolveProxyForTarget(target.href) === "") return globalThis.fetch(target.href);
609
714
  const agent = new EnvRoutingProxyAgent();
610
715
  let raw;
611
716
  try {
@@ -2278,6 +2383,247 @@ function stringifyValue(value) {
2278
2383
  }
2279
2384
  }
2280
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
+
2281
2627
  //#endregion
2282
2628
  //#region src/local/lambda-resolver.ts
2283
2629
  /**
@@ -2542,6 +2888,325 @@ function extractImageLambdaProperties(args) {
2542
2888
  };
2543
2889
  }
2544
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
+ /**
2545
3210
  * Resolve the local directory that corresponds to a function's deployed
2546
3211
  * asset, using the CDK-blessed `Metadata['aws:asset:path']` hint (D2). The
2547
3212
  * value is a directory path relative to `cdk.out` (e.g. `asset.abc123def`)
@@ -2551,18 +3216,21 @@ function extractImageLambdaProperties(args) {
2551
3216
  * Falls back to a clear error when the metadata is missing OR the resolved
2552
3217
  * directory does not exist (CDK should always emit it for asset-backed
2553
3218
  * Lambdas; absence usually means the user pre-synthesized with a different
2554
- * 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.
2555
3223
  */
2556
3224
  function resolveAssetCodePath$1(stack, logicalId, resource, options = {}) {
2557
3225
  const assetPath = resource.Metadata?.["aws:asset:path"];
2558
- 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.`);
2559
- const cdkOutDir = stack.assetManifestPath ? dirname(stack.assetManifestPath) : process.cwd();
2560
- const abs = isAbsolute(assetPath) ? assetPath : resolve(cdkOutDir, assetPath);
2561
- 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.`);
2562
3230
  const stat = statSync(abs);
2563
3231
  if (stat.isDirectory()) return abs;
2564
3232
  if (options.allowZip && stat.isFile() && abs.toLowerCase().endsWith(".zip")) return abs;
2565
- 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.");
2566
3234
  }
2567
3235
  /**
2568
3236
  * Turn a resolved function-code asset path into a directory ready to
@@ -2581,14 +3249,14 @@ function resolveAssetCodePath$1(stack, logicalId, resource, options = {}) {
2581
3249
  * existing tmpdir cleanup.
2582
3250
  */
2583
3251
  function materializeAssetCodeDir(codePath) {
2584
- 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.`);
2585
3253
  if (statSync(codePath).isDirectory()) return { dir: codePath };
2586
3254
  const zipBytes = readFileSync(codePath);
2587
3255
  let files;
2588
3256
  try {
2589
3257
  files = unzipSync(zipBytes);
2590
3258
  } catch (err) {
2591
- 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.`);
2592
3260
  }
2593
3261
  const modes = parseZipUnixModes(zipBytes);
2594
3262
  const S_IFMT = 61440;
@@ -2675,7 +3343,7 @@ function resolveSafeZipEntryPath(root, entry) {
2675
3343
  * **Order is preserved**: `Properties.Layers` is iterated left-to-right
2676
3344
  * and the resulting `ResolvedLambdaLayer[]` carries the same order. The
2677
3345
  * caller (`local-invoke.ts`'s `materializeLambdaLayers` and
2678
- * `local-start-api.ts`'s server-boot pre-merge) `cpSync`-merges every
3346
+ * `local-start-api.ts`'s server-boot pre-merge) merges every
2679
3347
  * entry into one host tmpdir in template order to honor AWS's
2680
3348
  * "last-layer-wins" file-collision semantics — Docker rejects multiple
2681
3349
  * bind mounts at the same target so cdk-local cannot rely on overlay
@@ -5286,7 +5954,7 @@ var AssemblyReader = class {
5286
5954
  });
5287
5955
  const cached = await toolkit.synth(source);
5288
5956
  try {
5289
- return cached.cloudAssembly.stacks.map((stack) => mapStackArtifact(stack));
5957
+ return collectStacks(cached.cloudAssembly);
5290
5958
  } finally {
5291
5959
  await cached.dispose();
5292
5960
  }
@@ -5316,19 +5984,54 @@ var AssemblyReader = class {
5316
5984
  const source = await toolkit.fromAssemblyDirectory(assemblyDir, { failOnMissingContext: false });
5317
5985
  const cached = await toolkit.synth(source);
5318
5986
  try {
5319
- return cached.cloudAssembly.stacks.map((stack) => mapStackArtifact(stack));
5987
+ return collectStacks(cached.cloudAssembly);
5320
5988
  } finally {
5321
5989
  await cached.dispose();
5322
5990
  }
5323
5991
  }
5324
5992
  };
5325
- function mapStackArtifact(stack) {
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) {
5326
6028
  const info = {
5327
6029
  stackName: stack.stackName,
5328
6030
  displayName: stack.displayName ?? stack.id,
5329
6031
  artifactId: stack.id,
5330
6032
  template: stack.template,
5331
- dependencyNames: stack.dependencies.map((d) => d.id)
6033
+ dependencyNames: stack.dependencies.map((d) => d.id),
6034
+ assetOutdir
5332
6035
  };
5333
6036
  if (stack.environment.region) info.region = stack.environment.region;
5334
6037
  if (stack.environment.account) info.account = stack.environment.account;
@@ -17149,6 +17852,110 @@ function looksLikeAccessDenied(err) {
17149
17852
  return name === "AccessDeniedException" || code === "AccessDeniedException" || /access denied/i.test(message) || /not authorized/i.test(message);
17150
17853
  }
17151
17854
 
17855
+ //#endregion
17856
+ //#region src/local/layer-tree-copy.ts
17857
+ /**
17858
+ * Merge one Lambda Layer's asset tree into the directory that will be
17859
+ * bind-mounted at `/opt`, with AWS's "last layer wins" semantic and with
17860
+ * symlinks kept VERBATIM (issue #727).
17861
+ *
17862
+ * Why this is a hand-written walk and not `cpSync`. The merge used to be
17863
+ * `cpSync(src, dest, { recursive: true, force: true })`, and every
17864
+ * `cpSync`-shaped repair of it was measured wrong in some direction:
17865
+ *
17866
+ * - Without `verbatimSymlinks: true` (the default), `cpSync` rewrites a
17867
+ * RELATIVE link target to the ABSOLUTE path of the source on the host —
17868
+ * `bin/rel-link -> real.sh` arrives as `-> <cdk.out>/asset.<hash>/bin/
17869
+ * real.sh`, which is dangling inside the container, where only `dest`
17870
+ * is mounted. Measured on Node 22.12 / 24.21.
17871
+ * - WITH `verbatimSymlinks: true`, Node's C++ `cpSync` (every release
17872
+ * after 22.12) throws `EEXIST` when a later layer carries a symlink at a
17873
+ * path an earlier layer already placed one — `nodejs/node_modules/.bin/
17874
+ * <tool>` in two layers built from the same dependency is the ordinary
17875
+ * case. Measured by the go-to-k/cdkd#3118 review (the cdkd twin) on 22.23 / 24.15 /
17876
+ * 24.21; the JS implementation on 22.12 does not throw, which is why
17877
+ * the floor alone did not show it.
17878
+ * - A `cpSync` + `readdirSync({ recursive: true })` pair was worse:
17879
+ * the recursive readdir DESCENDS INTO directory symlinks on every Node
17880
+ * after 22.12 (not on 22.12), so a cyclic link (`sub/up -> ..`, or a
17881
+ * pnpm-style `node_modules`) hung the merge, and an absolute link to a
17882
+ * host directory made the walk delete and write INSIDE that host
17883
+ * directory. And `cpSync`'s `force` writes THROUGH a destination that
17884
+ * is already a directory symlink, so a later layer's real directory
17885
+ * landed in the earlier layer's link target — on the host, when the
17886
+ * link was absolute. Same review, same versions.
17887
+ *
17888
+ * So the walk is explicit and recurses only on a real directory (`d_type`,
17889
+ * via `Dirent.isDirectory()`, which reports a directory symlink as a
17890
+ * symlink on 22.12 and 24.21 alike). Per entry, "last layer wins" is applied
17891
+ * the same way for every kind: whatever sits at the destination path that is
17892
+ * not of the same kind is removed first — a link, a file, or a whole
17893
+ * directory an earlier layer placed — then the entry lands. A symlink is
17894
+ * recreated with the SOURCE's own target string and never resolved, so an
17895
+ * absolute link stays absolute and a dangling one stays dangling, exactly as
17896
+ * AWS extracts the layer ZIP into `/opt`; nothing is ever written through a
17897
+ * link. A file is copied with `cpSync` (single file, `force`), which is what
17898
+ * preserves the mode bits, so a layer's `bin/<script>` keeps its `+x` — the
17899
+ * property the `local-invoke-layers` fixture executes through.
17900
+ *
17901
+ * The ROOT may itself be a symlink (an asset dir handed over through a link
17902
+ * passes `resolveAssetCodePath`'s `statSync`). `readdirSync` follows a root
17903
+ * link by itself, so the `realpathSync` is not what makes that case work,
17904
+ * and it does not move where a dangling or looping root fails either (the
17905
+ * first `readdirSync` raises the same `ENOENT` / `ELOOP` naming the asset
17906
+ * path); it only keeps every path the walk joins real.
17907
+ *
17908
+ * Bound, stated rather than handled: a FIFO or a socket inside a layer
17909
+ * makes the single-file `cpSync` throw (`ERR_FS_CP_FIFO_PIPE` /
17910
+ * `ERR_FS_CP_SOCKET`, immediately, never blocking) where the old recursive
17911
+ * copy skipped it in silence on some releases. CDK's own asset staging
17912
+ * cannot stage such an entry, so no real layer carries one.
17913
+ *
17914
+ * Both layer merges (`cdkl invoke` and `cdkl start-api`) call
17915
+ * this; keep them on the one helper rather than re-spelling copy options.
17916
+ */
17917
+ function copyLayerTreeLastWins(src, dest) {
17918
+ mkdirSync(dest, { recursive: true });
17919
+ copyDirLastWins(realpathSync(src), dest);
17920
+ }
17921
+ function copyDirLastWins(fromDir, toDir) {
17922
+ for (const entry of readdirSync(fromDir, { withFileTypes: true })) {
17923
+ const from = join(fromDir, entry.name);
17924
+ const to = join(toDir, entry.name);
17925
+ if (entry.isSymbolicLink()) {
17926
+ removeUnless(to, "symlink");
17927
+ symlinkSync(readlinkSync(from), to);
17928
+ } else if (entry.isDirectory()) {
17929
+ removeUnless(to, "directory");
17930
+ mkdirSync(to, { recursive: true });
17931
+ copyDirLastWins(from, to);
17932
+ } else {
17933
+ removeUnless(to, "file");
17934
+ cpSync(from, to, { force: true });
17935
+ }
17936
+ }
17937
+ }
17938
+ /**
17939
+ * Clear the destination path unless what is there is already of `kind`. A
17940
+ * directory is kept so a later layer MERGES into it; a file is kept because
17941
+ * `cpSync` overwrites it in place; a symlink is always replaced (its target
17942
+ * string comes from the later layer).
17943
+ */
17944
+ function removeUnless(to, kind) {
17945
+ let st;
17946
+ try {
17947
+ st = lstatSync(to);
17948
+ } catch {
17949
+ return;
17950
+ }
17951
+ if (kind === "directory" && st.isDirectory()) return;
17952
+ if (kind === "file" && st.isFile()) return;
17953
+ rmSync(to, {
17954
+ recursive: true,
17955
+ force: true
17956
+ });
17957
+ }
17958
+
17152
17959
  //#endregion
17153
17960
  //#region src/local/stage-resolver.ts
17154
17961
  /**
@@ -18587,12 +19394,12 @@ async function resolveLocalBuildPlan$1(lambda) {
18587
19394
  * when the entry is a same-stack asset. Literal-ARN entries always
18588
19395
  * pre-materialize first.
18589
19396
  * - 2+ layers → copy each into a fresh tmpdir IN ORDER (later
18590
- * layers overwrite earlier files via `cpSync({force: true})`),
19397
+ * layers overwrite earlier files via `copyLayerTreeLastWins`),
18591
19398
  * bind-mount the tmpdir at `/opt`. Records the tmpdir in
18592
19399
  * `layerTmpDirs` so `shutdown(...)` removes it.
18593
19400
  *
18594
19401
  * Issue #448: literal-ARN entries (`{kind: 'arn', ...}`) are downloaded
18595
- * + unzipped via `lambda:GetLayerVersion` BEFORE the cpSync-merge
19402
+ * + unzipped via `lambda:GetLayerVersion` BEFORE the merge
18596
19403
  * branches run. Every per-ARN tmpdir is also recorded in `layerTmpDirs`
18597
19404
  * so the same shutdown path cleans it up — even for the single-layer
18598
19405
  * fast path that bind-mounts the dir directly.
@@ -18622,10 +19429,15 @@ async function materializeLambdaLayers$1(layers, layerTmpDirs, layerRoleArn) {
18622
19429
  }
18623
19430
  if (flat.length === 1) return flat[0].assetPath;
18624
19431
  const dir = mkdtempSync(path$1.join(tmpdir(), `${getEmbedConfig().resourceNamePrefix}-start-api-layers-`));
18625
- for (const layer of flat) cpSync(layer.assetPath, dir, {
18626
- recursive: true,
18627
- force: true
18628
- });
19432
+ try {
19433
+ for (const layer of flat) copyLayerTreeLastWins(layer.assetPath, dir);
19434
+ } catch (error) {
19435
+ rmSync(dir, {
19436
+ recursive: true,
19437
+ force: true
19438
+ });
19439
+ throw error;
19440
+ }
18629
19441
  layerTmpDirs.add(dir);
18630
19442
  return dir;
18631
19443
  }
@@ -18758,12 +19570,18 @@ function resolveImageLambda(args) {
18758
19570
  * Locate the Lambda's local code directory using the CDK-blessed
18759
19571
  * `Metadata['aws:asset:path']` hint. Bind-mounted directly at
18760
19572
  * `/var/task` (read-only) by the docker-runner.
19573
+ *
19574
+ * The resolution itself is `lambda-resolver.ts`'s
19575
+ * {@link resolveAssetCodeDirectory} — THE one spelling, so this twin and
19576
+ * `cdkl invoke`'s cannot disagree about the containment bound. It REFUSES an
19577
+ * escaping RELATIVE value and WARNS on an ABSOLUTE one that leaves the asset
19578
+ * outdir; that function's header records why the two shapes differ.
18761
19579
  */
18762
19580
  function resolveAssetCodePath(stack, logicalId, resource) {
18763
19581
  const assetPath = resource.Metadata?.["aws:asset:path"];
18764
- 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.`);
18765
- const cdkOutDir = stack.assetManifestPath ? path$1.dirname(stack.assetManifestPath) : process.cwd();
18766
- return path$1.isAbsolute(assetPath) ? assetPath : path$1.resolve(cdkOutDir, assetPath);
19582
+ 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.`);
19583
+ const { manifestDir, assetOutdir } = assetPathDirs(stack);
19584
+ return resolveAssetCodeDirectory(manifestDir, assetPath, (message) => new Error(message), assetOutdir, logicalId);
18767
19585
  }
18768
19586
  /**
18769
19587
  * Print the discovered route table to stdout. Format mirrors the spec
@@ -19484,6 +20302,44 @@ async function resolveZipImagePlan$1(lambda, options) {
19484
20302
  ...tmpfs !== void 0 && { tmpfs }
19485
20303
  };
19486
20304
  }
20305
+ /**
20306
+ * Render an STS AssumeRole failure into the one line a warn may print.
20307
+ *
20308
+ * The single home for the policy that used to be copy-pasted as a ternary at
20309
+ * four relay sites (one here, three in `local-invoke-agentcore.ts`). Both
20310
+ * branches matter and neither is the obvious one:
20311
+ *
20312
+ * - `AssumeRoleFailure` carries a detail `assumeRoleCredentials` ALREADY
20313
+ * rendered through the policy, so it is taken verbatim. Re-rendering it here
20314
+ * would hand `describeAwsFailureForWarn` a plain `Error` -- cdk-local's own,
20315
+ * so no `$fault` -- and WITHHOLD text that was already sanitized, which is
20316
+ * how `ExpiredTokenException: ...` briefly became
20317
+ * `Error; 138-character message withheld` (issue #579 review round 4).
20318
+ * - Anything else is a raw SDK error and goes through
20319
+ * `describeAwsFailureForWarn`, which keeps a modeled service exception's
20320
+ * message and withholds everything else -- credential-chain failures above
20321
+ * all, which can carry a `credential_process` command line.
20322
+ *
20323
+ * `op` is the label `describeAwsFailureForWarn` puts on the `debug` line it
20324
+ * emits when it withholds a message -- NOT on the returned string, which is the
20325
+ * clamped class plus a character count and carries no operation. Callers pass
20326
+ * `'STS AssumeRole'` or a qualified variant such as
20327
+ * `'STS AssumeRole (--sigv4 signing)'`, and that qualification is the whole
20328
+ * reason the label is a parameter rather than a constant in here.
20329
+ *
20330
+ * This is deliberately a ONE-EXPRESSION function, and that is a hard
20331
+ * constraint rather than a style preference:
20332
+ * `tests/unit/cli/sts-error-relay-source-fence.test.ts` only accepts a NAMED
20333
+ * RENDERER whose body carries the full policy shape, does not `warn` itself,
20334
+ * and closes within `RENDERER_BODY_MAX_LINES`. A longer body, or one that
20335
+ * warned, would stop being collected and every site delegating to it would be
20336
+ * reported as an unguarded relay. Before issue #644 the fence could not model
20337
+ * indirection at all, so these four sites had to stay inline; it can now, and
20338
+ * this is the refactor that was waiting on it.
20339
+ */
20340
+ function assumeRoleDetail(err, op) {
20341
+ return err instanceof AssumeRoleFailure ? err.detail : describeAwsFailureForWarn(err, op);
20342
+ }
19487
20343
  async function materializeLambdaLayersIncludingArns(layers, options) {
19488
20344
  const extraTmpDirs = [];
19489
20345
  const flat = [];
@@ -19525,10 +20381,15 @@ function materializeLambdaLayers(layers) {
19525
20381
  readOnly: true
19526
20382
  } };
19527
20383
  const tmpDir = mkdtempSync(path$1.join(tmpdir(), `${getEmbedConfig().resourceNamePrefix}-invoke-layers-`));
19528
- for (const layer of layers) cpSync(layer.assetPath, tmpDir, {
19529
- recursive: true,
19530
- force: true
19531
- });
20384
+ try {
20385
+ for (const layer of layers) copyLayerTreeLastWins(layer.assetPath, tmpDir);
20386
+ } catch (error) {
20387
+ rmSync(tmpDir, {
20388
+ recursive: true,
20389
+ force: true
20390
+ });
20391
+ throw error;
20392
+ }
19532
20393
  return {
19533
20394
  mount: {
19534
20395
  hostPath: tmpDir,
@@ -19818,7 +20679,7 @@ async function resolveLambdaContainerEnv(lambda, options, profileCredentials, ex
19818
20679
  if (stsRegion) dockerEnv["AWS_REGION"] = stsRegion;
19819
20680
  assumeSucceeded = true;
19820
20681
  } catch (err) {
19821
- const reason = err instanceof AssumeRoleFailure ? err.detail : describeAwsFailureForWarn(err, "STS AssumeRole");
20682
+ const reason = assumeRoleDetail(err, "STS AssumeRole");
19822
20683
  logger.warn(`--assume-role: STS AssumeRole(${flattenToOneLine(resolvedAssumeRoleArn)}) failed: ${reason}. Falling back to the developer's shell credentials.`);
19823
20684
  }
19824
20685
  }
@@ -21529,7 +22390,7 @@ async function resolveHostCredentialsForSigV4(options, resolved, loaded, region,
21529
22390
  if (assumeRoleArn) try {
21530
22391
  return await assumeAgentCoreExecutionRole(assumeRoleArn, region, options.profile);
21531
22392
  } catch (err) {
21532
- const sigv4Detail = err instanceof AssumeRoleFailure ? err.detail : describeAwsFailureForWarn(err, "STS AssumeRole (--sigv4 signing)");
22393
+ const sigv4Detail = assumeRoleDetail(err, "STS AssumeRole (--sigv4 signing)");
21533
22394
  logger.warn(`--assume-role: STS AssumeRole(${flattenToOneLine(assumeRoleArn)}) failed for --sigv4 signing: ${sigv4Detail}. Falling back to ${options.profile ? `--profile ${options.profile}` : "shell credentials"}.`);
21534
22395
  }
21535
22396
  if (options.profile) {
@@ -21653,7 +22514,7 @@ async function resolveAgentCoreCodeImageFromS3(resolved, code, s3Source, options
21653
22514
  if (assumeRoleArn) try {
21654
22515
  credentials = await assumeAgentCoreExecutionRole(assumeRoleArn, region, options.profile);
21655
22516
  } catch (err) {
21656
- const bundleDetail = err instanceof AssumeRoleFailure ? err.detail : describeAwsFailureForWarn(err, "STS AssumeRole (fromS3 bundle download)");
22517
+ const bundleDetail = assumeRoleDetail(err, "STS AssumeRole (fromS3 bundle download)");
21657
22518
  logger.warn(`--assume-role: STS AssumeRole(${flattenToOneLine(assumeRoleArn)}) failed for the fromS3 bundle download: ${bundleDetail}. Falling back to ${options.profile ? `--profile ${options.profile}` : "the default credentials"}.`);
21658
22519
  }
21659
22520
  const bundle = await downloadAndExtractS3Bundle(location, {
@@ -21852,7 +22713,7 @@ async function applyAgentCoreCredentialEnv(dockerEnv, args) {
21852
22713
  if (stsRegion) dockerEnv["AWS_REGION"] = stsRegion;
21853
22714
  assumeSucceeded = true;
21854
22715
  } catch (err) {
21855
- const assumeDetail = err instanceof AssumeRoleFailure ? err.detail : describeAwsFailureForWarn(err, "STS AssumeRole");
22716
+ const assumeDetail = assumeRoleDetail(err, "STS AssumeRole");
21856
22717
  logger.warn(`--assume-role: STS AssumeRole(${flattenToOneLine(args.assumeRoleArn)}) failed: ${assumeDetail}. Falling back to the developer's shell credentials.`);
21857
22718
  }
21858
22719
  }
@@ -40142,4 +41003,4 @@ function addStudioSpecificOptions(cmd) {
40142
41003
 
40143
41004
  //#endregion
40144
41005
  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 };
40145
- //# sourceMappingURL=local-studio-BVxRUfQV.js.map
41006
+ //# sourceMappingURL=local-studio-CP1PAUS4.js.map