cdk-local 0.149.3 → 0.149.4

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.
@@ -1699,6 +1699,393 @@ function effectiveAssumeRoleArn(logicalId, opt) {
1699
1699
  return opt.perLambda?.[logicalId] ?? opt.globalArn;
1700
1700
  }
1701
1701
 
1702
+ //#endregion
1703
+ //#region src/utils/assembly-path.ts
1704
+ /**
1705
+ * `true` when `candidate` names something strictly beneath `base`, both given
1706
+ * as already-resolved absolute paths.
1707
+ *
1708
+ * The `..` test is SEPARATOR-AWARE rather than a bare `startsWith('..')`,
1709
+ * which would also reject a legitimate sibling named `..foo`. The empty-string
1710
+ * case is `base` itself; callers decide whether that counts as an escape,
1711
+ * because an asset path legitimately names a DIRECTORY while a template path
1712
+ * does not.
1713
+ */
1714
+ function isInside(base, candidate) {
1715
+ const rel = relative(base, candidate);
1716
+ if (rel === "" || rel === "..") return false;
1717
+ if (rel.startsWith(`..${sep}`)) return false;
1718
+ return !isAbsolute(rel);
1719
+ }
1720
+ /**
1721
+ * `realpath(3)`, or `undefined` for a path that does not fully resolve.
1722
+ *
1723
+ * `.native` is load-bearing and must not be simplified to `fs.realpathSync`.
1724
+ * The plain form is a JavaScript walker that folds `..` LEXICALLY, so it
1725
+ * disagrees with the kernel on a target carrying a `..` after a symlinked
1726
+ * component — answering `ENOENT` for a path the kernel resolves, and on a
1727
+ * case-insensitive filesystem (APFS by default) it can spin on the same shape.
1728
+ * `.native` is libuv's `realpath(3)` and throws the same
1729
+ * `ENOENT` / `ELOOP` / `EACCES`.
1730
+ */
1731
+ function tryRealpath(p) {
1732
+ try {
1733
+ return realpathSync.native(p);
1734
+ } catch {
1735
+ return;
1736
+ }
1737
+ }
1738
+ /** `fs.readlinkSync`, or `undefined` when `p` is not a symbolic link. */
1739
+ function tryReadlink(p) {
1740
+ try {
1741
+ return readlinkSync(p);
1742
+ } catch {
1743
+ return;
1744
+ }
1745
+ }
1746
+ /**
1747
+ * Link follows before the walk gives up and reports the path as unresolvable.
1748
+ * It does NOT refuse: an exhausted budget answers `undefined`, which leaves
1749
+ * the symlink arm silent and the lexical verdict standing. Safe because the OS
1750
+ * gives up first (macOS caps at 32, `ELOOP`), so a chain that reaches this cap
1751
+ * is one nothing can open anyway.
1752
+ */
1753
+ const MAX_LINK_HOPS = 40;
1754
+ /**
1755
+ * Unresolvable path COMPONENTS the climb walks before giving up. Bounds the
1756
+ * recursion below, which is one frame per component, so a pathological value
1757
+ * cannot raise a `RangeError` from inside {@link tryRealpath}'s own `try` (its
1758
+ * `catch` would swallow the crash into a silent `undefined`).
1759
+ *
1760
+ * KNOW THAT EXHAUSTING THIS FAILS OPEN, and why that is accepted rather than
1761
+ * unnoticed. Unlike {@link MAX_LINK_HOPS}, which has the OS's own `ELOOP` cap
1762
+ * behind it, nothing else stops a path of 1 001 absent components: the climb
1763
+ * gives up, the symlink arm goes silent, and a value that would land outside
1764
+ * reads as CONTAINED on the lexical verdict alone. It is not reachable in
1765
+ * effect — such a path cannot exist, so `cdkl invoke`'s `existsSync` refuses it
1766
+ * and `start-api`'s `docker run` fails to mount it — but that safety lives in
1767
+ * the CONSUMERS, not here. A consumer that neither checks existence nor mounts
1768
+ * would need its own answer.
1769
+ */
1770
+ const MAX_PATH_COMPONENTS = 1e3;
1771
+ /**
1772
+ * Where `target` REALLY points, for a path that may not exist yet.
1773
+ *
1774
+ * `realpathSync` answers only for a path that fully resolves, and it throws
1775
+ * `ENOENT` for a DANGLING symbolic link exactly as it does for an absent file.
1776
+ * `cdkl start-api` resolves an asset path without an existence check, so an
1777
+ * absent or dangling component must not silence the symlink arm.
1778
+ *
1779
+ * Each unresolvable component is therefore handled by hand: climb to the
1780
+ * deepest ancestor that DOES resolve, then re-apply the remaining components,
1781
+ * following any symbolic link with `readlink` and re-resolving its target.
1782
+ * `undefined` means the walk could not resolve the path at all.
1783
+ *
1784
+ * For a path that FULLY RESOLVES the answer is the kernel's and is exact. For
1785
+ * one that does not exist yet this is a best-effort MODEL of kernel
1786
+ * resolution; the known edge is a `..` INSIDE an unresolvable link's target,
1787
+ * folded lexically here while the kernel folds it only after following each
1788
+ * preceding component. That edge is benign for THESE callers rather than in
1789
+ * general: it needs the target to be absent, and both consumers resolve the
1790
+ * path and then open or mount it, so the divergence is between this model and
1791
+ * a path nothing can reach. A caller that CREATED a file through such a path
1792
+ * would need an `lstat` of its own.
1793
+ *
1794
+ * `EACCES` is folded into "does not resolve" along with `ENOENT`, so a link
1795
+ * under a directory this process cannot traverse reads as contained. Same
1796
+ * bound: the consumer's own `existsSync` / mount runs as the same user and
1797
+ * fails identically.
1798
+ */
1799
+ function resolveThroughLinks(target, hops = 0, climbs = 0) {
1800
+ const direct = tryRealpath(target);
1801
+ if (direct !== void 0) return direct;
1802
+ if (hops >= MAX_LINK_HOPS) return void 0;
1803
+ if (climbs >= MAX_PATH_COMPONENTS) return void 0;
1804
+ const parent = dirname(target);
1805
+ if (parent === target) return void 0;
1806
+ const realParent = resolveThroughLinks(parent, hops, climbs + 1);
1807
+ if (realParent === void 0) return void 0;
1808
+ const link = tryReadlink(target);
1809
+ if (link === void 0) return join(realParent, basename(target));
1810
+ return resolveThroughLinks(resolve(realParent, link), hops + 1, climbs);
1811
+ }
1812
+ /**
1813
+ * Resolve an assembly-supplied `candidate` against `dir` and report whether
1814
+ * the result stays inside it.
1815
+ *
1816
+ * The lexical arm joins exactly the way the call sites used to (`path.join`,
1817
+ * NOT `path.resolve`), so the verdict is about the path the caller will
1818
+ * actually use. `join`'s handling of an absolute candidate makes this arm
1819
+ * strictly more permissive than a `resolve`-based one would be; an absolute
1820
+ * value is therefore answered by {@link absoluteAssemblyPathEscape} instead,
1821
+ * because this function structurally cannot.
1822
+ *
1823
+ * The SYMLINK arm exists because the lexical arm alone leaves an equivalent
1824
+ * hole: `cdk.out/link -> /etc` plus a candidate of `link/passwd` is lexically
1825
+ * contained and still reaches `/etc/passwd`. Both sides go through
1826
+ * {@link resolveThroughLinks}, so an assembly directory REACHED through a link
1827
+ * (macOS spells `/tmp` as `/private/tmp`) is unaffected.
1828
+ *
1829
+ * The verdict is about the assembly AS IT SITS ON DISK. It is NOT a defence
1830
+ * against a process rewriting that directory concurrently — the caller uses
1831
+ * the path after this returns.
1832
+ */
1833
+ function resolveAssemblyPath(dir, candidate, options) {
1834
+ const base = resolve(dir);
1835
+ const bound = options?.containWithin === void 0 ? base : resolve(options.containWithin);
1836
+ const joined = resolve(join(base, candidate));
1837
+ if (!isInside(bound, joined)) return {
1838
+ contained: false,
1839
+ escape: "lexical",
1840
+ path: joined
1841
+ };
1842
+ const realBound = resolveThroughLinks(bound);
1843
+ if (realBound !== void 0) {
1844
+ const realTarget = resolveThroughLinks(joined);
1845
+ if (realTarget !== void 0 && !isInside(realBound, realTarget)) return {
1846
+ contained: false,
1847
+ escape: "symlink",
1848
+ path: joined,
1849
+ realPath: realTarget
1850
+ };
1851
+ }
1852
+ return {
1853
+ contained: true,
1854
+ path: joined
1855
+ };
1856
+ }
1857
+ /**
1858
+ * Whether an ALREADY-ABSOLUTE assembly-supplied path lies outside `bound`.
1859
+ *
1860
+ * {@link resolveAssemblyPath} cannot answer this, and the reason is structural
1861
+ * rather than an oversight: its lexical arm joins with `path.join`, which does
1862
+ * NOT honour a leading separator, so an absolute candidate is folded INTO the
1863
+ * directory and the verdict would describe a path no caller opens. A site that
1864
+ * HONOURS an absolute value needs the verdict about the value itself.
1865
+ *
1866
+ * The callers are `cdkl invoke`'s and `cdkl start-api`'s
1867
+ * `Metadata['aws:asset:path']` resolution. Those honour an absolute path
1868
+ * because `cdk synth --no-staging` emits one — CDK writes the asset's absolute
1869
+ * SOURCE directory under `aws:cdk:disable-asset-staging`, usually outside the
1870
+ * outdir — and they WARN rather than refuse when it leaves the bound, so this
1871
+ * returns a verdict rather than throwing. The asset-MANIFEST readers that
1872
+ * honour an absolute value take the same verdict: `'honour-warn'` (the
1873
+ * start-cloudfront origin) warns when the value is a folder inside the user's
1874
+ * project not under a credential or version-control directory, and refuses
1875
+ * otherwise, `'honour'` (the soft-reload
1876
+ * sources) refuses (#745).
1877
+ *
1878
+ * It lives HERE so the containment rule has one spelling: it reuses this
1879
+ * module's own {@link isInside} and {@link resolveThroughLinks}, symlink arm
1880
+ * included, rather than letting a caller re-spell `path.relative` and drift.
1881
+ *
1882
+ * `bound` itself is NOT an escape, unlike in {@link resolveAssemblyPath},
1883
+ * where an empty `path.relative` means "names the directory rather than a file
1884
+ * inside it". An asset path legitimately names a DIRECTORY, so a value equal
1885
+ * to the bound is inside it and reporting it as outside would be false.
1886
+ *
1887
+ * THE REAL PATHS DECIDE IN BOTH DIRECTIONS, which is the one place this
1888
+ * deliberately does more than {@link resolveAssemblyPath}'s lexical-first
1889
+ * ordering. The lexical verdict here is about a SPELLING, and two spellings of
1890
+ * the same directory are common rather than exotic: macOS resolves `/tmp` to
1891
+ * `/private/tmp` and `/var` to `/private/var`, and a user may symlink `cdk.out`
1892
+ * itself. With an outdir of `/tmp/cdk.out` and a `--no-staging` value of
1893
+ * `/private/tmp/cdk.out/src`, a lexical-only verdict cries "pointing outside
1894
+ * the assembly ... treat this assembly as untrusted" about an asset that is
1895
+ * plainly inside it — and this arm exists to make an UNEXPECTED path visible,
1896
+ * so a false alarm is the failure that costs it its meaning.
1897
+ *
1898
+ * Exonerating requires the kernel to answer for BOTH operands: an unresolvable
1899
+ * one leaves the lexical verdict standing, so the arm stays loud when it cannot
1900
+ * see. (`resolveAssemblyPath` cannot take the same shape: its verdict is about
1901
+ * a path built with `path.join`, and letting a real path overrule the lexical
1902
+ * `..` there would answer about a location the caller never opens.)
1903
+ */
1904
+ function absoluteAssemblyPathEscape(bound, absolutePath) {
1905
+ const resolvedBound = resolve(bound);
1906
+ const target = resolve(absolutePath);
1907
+ const lexicallyOutside = !isInside(resolvedBound, target) && target !== resolvedBound;
1908
+ const realBound = resolveThroughLinks(resolvedBound);
1909
+ const realTarget = resolveThroughLinks(target);
1910
+ const reallyOutside = realBound === void 0 || realTarget === void 0 ? void 0 : !isInside(realBound, realTarget) && realTarget !== realBound;
1911
+ if (lexicallyOutside) {
1912
+ if (reallyOutside === false) return void 0;
1913
+ return {
1914
+ contained: false,
1915
+ escape: "lexical",
1916
+ path: target
1917
+ };
1918
+ }
1919
+ if (reallyOutside === true) return {
1920
+ contained: false,
1921
+ escape: "symlink",
1922
+ path: target,
1923
+ realPath: realTarget
1924
+ };
1925
+ }
1926
+ /**
1927
+ * Whether `candidate` names the SAME directory as `bound` — lexically, or
1928
+ * through a symbolic link on either side.
1929
+ *
1930
+ * An asset source legitimately names a DIRECTORY, so a value that resolves
1931
+ * onto the output directory itself is not an escape; but it hands the WHOLE
1932
+ * assembly to the sink, which is worth a line. A caller that re-spells this as
1933
+ * `resolve(a) === resolve(b)` misses every second spelling of the bound
1934
+ * (macOS `/tmp` vs `/private/tmp`, a symlinked `cdk.out`), and
1935
+ * {@link absoluteAssemblyPathEscape} exonerates exactly those spellings as
1936
+ * inside — so the equality beside it would stay silent for them.
1937
+ *
1938
+ * Conservative on failure: an unresolvable side answers from the lexical
1939
+ * comparison alone, so it can only say "not the same", never wrongly claim
1940
+ * identity.
1941
+ */
1942
+ function namesTheSameDirectory(bound, candidate) {
1943
+ const resolvedBound = resolve(bound);
1944
+ const target = resolve(candidate);
1945
+ if (target === resolvedBound) return true;
1946
+ const realBound = resolveThroughLinks(resolvedBound);
1947
+ const realTarget = resolveThroughLinks(target);
1948
+ return realBound !== void 0 && realTarget !== void 0 && realBound === realTarget;
1949
+ }
1950
+ /**
1951
+ * The escape verdict for an assembly-supplied path WITHOUT throwing, taking
1952
+ * whichever arm the value's own shape calls for: {@link absoluteAssemblyPathEscape}
1953
+ * for an absolute value, {@link resolveAssemblyPath} for a relative one.
1954
+ *
1955
+ * For a caller that only WARNS and has many values to judge — the BuildKit
1956
+ * passthroughs in `src/assets/buildkit-passthrough-warnings.ts`. `base` is what
1957
+ * a relative value resolves against, `bound` what it must stay inside.
1958
+ * Returns `undefined` when the value is fine, including when it names `bound`
1959
+ * itself ({@link namesTheSameDirectory}).
1960
+ */
1961
+ function assemblyPathEscape(base, bound, candidate) {
1962
+ if (isAbsolute(candidate)) return absoluteAssemblyPathEscape(bound, candidate);
1963
+ const resolved = resolveAssemblyPath(base, candidate, { containWithin: bound });
1964
+ if (resolved.contained || namesTheSameDirectory(bound, resolved.path)) return void 0;
1965
+ return resolved;
1966
+ }
1967
+ /**
1968
+ * The shared tail of a containment refusal: what the value resolved to, what
1969
+ * it escaped, and why that means the assembly is not CDK-generated. The call
1970
+ * site supplies its own subject ("Lambda 'X' has ... which ") and its own
1971
+ * error class. `action` completes "Refusing to ..." and is REQUIRED rather
1972
+ * than defaulted: a default is a branch no caller takes, so it can neither be
1973
+ * fenced nor be right for the next caller.
1974
+ *
1975
+ * `provenanceOverride` replaces that whole closing sentence for a caller that
1976
+ * does NOT refuse — the BuildKit passthrough warnings, where "Refusing to ..."
1977
+ * would be a false statement about a value that is forwarded anyway.
1978
+ */
1979
+ function renderAssemblyPathEscape(escape, dir, action, provenanceOverride) {
1980
+ const provenance = provenanceOverride ?? `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}.`;
1981
+ const base = resolve(dir);
1982
+ const realBase = resolveThroughLinks(base) ?? base;
1983
+ const shownPath = displayUntrustedValue(escape.path);
1984
+ const shownBase = displayUntrustedValue(base);
1985
+ if (escape.escape === "symlink") {
1986
+ if (escape.realPath === realBase) return `resolves to ${shownPath}, a symbolic link to the directory ${shownBase} itself rather than to a path inside it. ${provenance}`;
1987
+ return `resolves to ${shownPath}, which leads through a symbolic link to ${displayUntrustedValue(escape.realPath)}, outside ${shownBase}. ${provenance}`;
1988
+ }
1989
+ if (escape.path === base) return `names the directory ${shownBase} itself rather than a path inside it. ${provenance}`;
1990
+ return `resolves to ${shownPath}, outside ${shownBase}. ${provenance}`;
1991
+ }
1992
+ /**
1993
+ * A letter or digit that neither draws as a blank nor reads as a quote.
1994
+ * `\p{L}` alone admits both, so two classes are carved out: the
1995
+ * default-ignorables, which hold letters that draw as a blank (the Hangul
1996
+ * fillers U+3164 and U+FFA0), and the quote-shaped letters — the Spacing
1997
+ * Modifier Letters block (U+02BA reads as `"`, U+02BC as `'`) plus the ones
1998
+ * outside it (U+0374, U+0559, U+07F4-U+07F5, U+A78B-U+A78C, and the halfwidth
1999
+ * sound marks U+FF9E-U+FF9F). Not all of `\p{Lm}`: U+30FC is in it, and it is
2000
+ * an ordinary character of a Japanese directory name.
2001
+ */
2002
+ const VISIBLE_LETTER = new RegExp(String.raw`^(?![\p{Default_Ignorable_Code_Point}\u02b0-\u02ff\u0374\u0559\u07f4\u07f5\ua78b\ua78c\uff9e\uff9f])[\p{L}\p{N}]$`, "u");
2003
+ /**
2004
+ * A combining mark, which counts only DIRECTLY after a visible letter (or a
2005
+ * mark that itself counted): on a space or on punctuation it draws on its own,
2006
+ * and U+030B / U+030E there look like a quote.
2007
+ */
2008
+ const COMBINING_MARK = /^\p{M}$/u;
2009
+ /**
2010
+ * A default-ignorable code point, which draws as NOTHING. Tested before the
2011
+ * mark rule, because some are combining marks (U+034F, U+17B4, U+180B, the
2012
+ * variation selectors U+FE00-U+FE0F and U+E0100-U+E01EF): after a letter they
2013
+ * would otherwise pass as bare, and `/home/me/.ss\u034fh` would print exactly
2014
+ * like `.ssh` while naming a different path. The host's classifier
2015
+ * (go-to-k/cdkd#3509) does not carve these out; this one does.
2016
+ */
2017
+ const DEFAULT_IGNORABLE = /^\p{Default_Ignorable_Code_Point}$/u;
2018
+ /**
2019
+ * The ASCII a bare value may carry besides letters and digits. Everything else
2020
+ * — any whitespace, any quote, any other symbol — takes the boundary, which
2021
+ * costs a legitimate path nothing but a pair of quotes.
2022
+ */
2023
+ const BARE_PUNCTUATION = /^[/\\._~+@:=-]$/;
2024
+ /**
2025
+ * Classify each code point: `bare` (allowed in a bare value), `shown` (shown
2026
+ * as itself inside the boundary), or `escaped`. An ALLOWLIST, because a
2027
+ * denylist has to enumerate every character that draws as a blank or reads as
2028
+ * a quote.
2029
+ *
2030
+ * Each test is ONE character and this walks the value — never `^(...)+$` over
2031
+ * the whole of it: alternatives that overlap under a quantifier backtrack
2032
+ * exponentially on a long value that fails near its end.
2033
+ */
2034
+ function classify(chars) {
2035
+ let afterLetter = false;
2036
+ return chars.map((ch) => {
2037
+ if (DEFAULT_IGNORABLE.test(ch)) {
2038
+ afterLetter = false;
2039
+ return "escaped";
2040
+ }
2041
+ if (COMBINING_MARK.test(ch)) return afterLetter ? "bare" : "escaped";
2042
+ afterLetter = VISIBLE_LETTER.test(ch);
2043
+ if (afterLetter || BARE_PUNCTUATION.test(ch)) return "bare";
2044
+ return ch >= " " && ch <= "~" ? "shown" : "escaped";
2045
+ });
2046
+ }
2047
+ /**
2048
+ * Render an untrusted value — a filesystem path, identifier or command an
2049
+ * assembly chose, or a key a request supplied — into cdk-local's own prose
2050
+ * (go-to-k/cdk-local#758; the host's `displayAssemblyPath`, go-to-k/cdkd#3509).
2051
+ * The caller writes NO quotes around the result.
2052
+ *
2053
+ * `sanitizeServiceExceptionMessage` alone is not enough inside quotes of ours:
2054
+ * it flattens control characters and passes `'`, so a value carrying one
2055
+ * closed the quote and wrote a clause of its own into the message. This keeps
2056
+ * a plain value bare and puts any other one inside a JSON string literal.
2057
+ * Inside it, `"` and `\` are escaped as JSON escapes them, and so is every
2058
+ * character that is neither printable ASCII nor a visible letter — a curly or
2059
+ * fullwidth quote that could pass for the boundary's own closing `"`, and a
2060
+ * blank that could pass for a space. The result stays valid JSON: `JSON.parse`
2061
+ * returns the sanitized value.
2062
+ *
2063
+ * A legitimate value with a space or any symbol outside `/ \ . _ ~ + @ : = -`
2064
+ * (`/Users/me/My Project/cdk.out`) renders quoted; a non-ASCII letter is
2065
+ * shown as itself, and a non-letter non-ASCII character (an emoji, `©`) as
2066
+ * its `\u` escape. A value the sanitizer ALTERED (a control character
2067
+ * flattened, an over-long value truncated) did not arrive plain, so it always
2068
+ * takes the boundary. The empty string renders as `""`.
2069
+ *
2070
+ * The sanitizer also CAPS the value at 512 code points, so a longer path
2071
+ * prints truncated (inside the boundary, with its true length named). That
2072
+ * is deliberate: an unbounded assembly-chosen value would otherwise make the
2073
+ * log line as long as the attacker likes, and a real path is far shorter.
2074
+ */
2075
+ function displayUntrustedValue(value) {
2076
+ const clean = sanitizeServiceExceptionMessage(value);
2077
+ const chars = Array.from(clean);
2078
+ const kinds = classify(chars);
2079
+ if (clean === value && chars.length > 0 && kinds.every((k) => k === "bare")) return clean;
2080
+ let body = "";
2081
+ chars.forEach((ch, i) => {
2082
+ if (ch === "\"" || ch === "\\") body += `\\${ch}`;
2083
+ else if (kinds[i] !== "escaped") body += ch;
2084
+ else for (let u = 0; u < ch.length; u++) body += `\\u${ch.charCodeAt(u).toString(16).padStart(4, "0")}`;
2085
+ });
2086
+ return `"${body}"`;
2087
+ }
2088
+
1702
2089
  //#endregion
1703
2090
  //#region src/utils/error-handler.ts
1704
2091
  /**
@@ -2383,297 +2770,6 @@ function stringifyValue(value) {
2383
2770
  }
2384
2771
  }
2385
2772
 
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. The asset-MANIFEST readers that
2556
- * honour an absolute value take the same verdict: `'honour-warn'` (the
2557
- * start-cloudfront origin) warns when the value is a folder inside the user's
2558
- * project not under a credential or version-control directory, and refuses
2559
- * otherwise, `'honour'` (the soft-reload
2560
- * sources) refuses (#745).
2561
- *
2562
- * It lives HERE so the containment rule has one spelling: it reuses this
2563
- * module's own {@link isInside} and {@link resolveThroughLinks}, symlink arm
2564
- * included, rather than letting a caller re-spell `path.relative` and drift.
2565
- *
2566
- * `bound` itself is NOT an escape, unlike in {@link resolveAssemblyPath},
2567
- * where an empty `path.relative` means "names the directory rather than a file
2568
- * inside it". An asset path legitimately names a DIRECTORY, so a value equal
2569
- * to the bound is inside it and reporting it as outside would be false.
2570
- *
2571
- * THE REAL PATHS DECIDE IN BOTH DIRECTIONS, which is the one place this
2572
- * deliberately does more than {@link resolveAssemblyPath}'s lexical-first
2573
- * ordering. The lexical verdict here is about a SPELLING, and two spellings of
2574
- * the same directory are common rather than exotic: macOS resolves `/tmp` to
2575
- * `/private/tmp` and `/var` to `/private/var`, and a user may symlink `cdk.out`
2576
- * itself. With an outdir of `/tmp/cdk.out` and a `--no-staging` value of
2577
- * `/private/tmp/cdk.out/src`, a lexical-only verdict cries "pointing outside
2578
- * the assembly ... treat this assembly as untrusted" about an asset that is
2579
- * plainly inside it — and this arm exists to make an UNEXPECTED path visible,
2580
- * so a false alarm is the failure that costs it its meaning.
2581
- *
2582
- * Exonerating requires the kernel to answer for BOTH operands: an unresolvable
2583
- * one leaves the lexical verdict standing, so the arm stays loud when it cannot
2584
- * see. (`resolveAssemblyPath` cannot take the same shape: its verdict is about
2585
- * a path built with `path.join`, and letting a real path overrule the lexical
2586
- * `..` there would answer about a location the caller never opens.)
2587
- */
2588
- function absoluteAssemblyPathEscape(bound, absolutePath) {
2589
- const resolvedBound = resolve(bound);
2590
- const target = resolve(absolutePath);
2591
- const lexicallyOutside = !isInside(resolvedBound, target) && target !== resolvedBound;
2592
- const realBound = resolveThroughLinks(resolvedBound);
2593
- const realTarget = resolveThroughLinks(target);
2594
- const reallyOutside = realBound === void 0 || realTarget === void 0 ? void 0 : !isInside(realBound, realTarget) && realTarget !== realBound;
2595
- if (lexicallyOutside) {
2596
- if (reallyOutside === false) return void 0;
2597
- return {
2598
- contained: false,
2599
- escape: "lexical",
2600
- path: target
2601
- };
2602
- }
2603
- if (reallyOutside === true) return {
2604
- contained: false,
2605
- escape: "symlink",
2606
- path: target,
2607
- realPath: realTarget
2608
- };
2609
- }
2610
- /**
2611
- * Whether `candidate` names the SAME directory as `bound` — lexically, or
2612
- * through a symbolic link on either side.
2613
- *
2614
- * An asset source legitimately names a DIRECTORY, so a value that resolves
2615
- * onto the output directory itself is not an escape; but it hands the WHOLE
2616
- * assembly to the sink, which is worth a line. A caller that re-spells this as
2617
- * `resolve(a) === resolve(b)` misses every second spelling of the bound
2618
- * (macOS `/tmp` vs `/private/tmp`, a symlinked `cdk.out`), and
2619
- * {@link absoluteAssemblyPathEscape} exonerates exactly those spellings as
2620
- * inside — so the equality beside it would stay silent for them.
2621
- *
2622
- * Conservative on failure: an unresolvable side answers from the lexical
2623
- * comparison alone, so it can only say "not the same", never wrongly claim
2624
- * identity.
2625
- */
2626
- function namesTheSameDirectory(bound, candidate) {
2627
- const resolvedBound = resolve(bound);
2628
- const target = resolve(candidate);
2629
- if (target === resolvedBound) return true;
2630
- const realBound = resolveThroughLinks(resolvedBound);
2631
- const realTarget = resolveThroughLinks(target);
2632
- return realBound !== void 0 && realTarget !== void 0 && realBound === realTarget;
2633
- }
2634
- /**
2635
- * The escape verdict for an assembly-supplied path WITHOUT throwing, taking
2636
- * whichever arm the value's own shape calls for: {@link absoluteAssemblyPathEscape}
2637
- * for an absolute value, {@link resolveAssemblyPath} for a relative one.
2638
- *
2639
- * For a caller that only WARNS and has many values to judge — the BuildKit
2640
- * passthroughs in `src/assets/buildkit-passthrough-warnings.ts`. `base` is what
2641
- * a relative value resolves against, `bound` what it must stay inside.
2642
- * Returns `undefined` when the value is fine, including when it names `bound`
2643
- * itself ({@link namesTheSameDirectory}).
2644
- */
2645
- function assemblyPathEscape(base, bound, candidate) {
2646
- if (isAbsolute(candidate)) return absoluteAssemblyPathEscape(bound, candidate);
2647
- const resolved = resolveAssemblyPath(base, candidate, { containWithin: bound });
2648
- if (resolved.contained || namesTheSameDirectory(bound, resolved.path)) return void 0;
2649
- return resolved;
2650
- }
2651
- /**
2652
- * The shared tail of a containment refusal: what the value resolved to, what
2653
- * it escaped, and why that means the assembly is not CDK-generated. The call
2654
- * site supplies its own subject ("Lambda 'X' has ... which ") and its own
2655
- * error class. `action` completes "Refusing to ..." and is REQUIRED rather
2656
- * than defaulted: a default is a branch no caller takes, so it can neither be
2657
- * fenced nor be right for the next caller.
2658
- *
2659
- * `provenanceOverride` replaces that whole closing sentence for a caller that
2660
- * does NOT refuse — the BuildKit passthrough warnings, where "Refusing to ..."
2661
- * would be a false statement about a value that is forwarded anyway.
2662
- */
2663
- function renderAssemblyPathEscape(escape, dir, action, provenanceOverride) {
2664
- const provenance = provenanceOverride ?? `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}.`;
2665
- const base = resolve(dir);
2666
- const realBase = resolveThroughLinks(base) ?? base;
2667
- const shownPath = sanitizeServiceExceptionMessage(escape.path);
2668
- const shownBase = sanitizeServiceExceptionMessage(base);
2669
- if (escape.escape === "symlink") {
2670
- if (escape.realPath === realBase) return `resolves to '${shownPath}', a symbolic link to the directory '${shownBase}' itself rather than to a path inside it. ${provenance}`;
2671
- return `resolves to '${shownPath}', which leads through a symbolic link to '${sanitizeServiceExceptionMessage(escape.realPath)}', outside '${shownBase}'. ${provenance}`;
2672
- }
2673
- if (escape.path === base) return `names the directory '${shownBase}' itself rather than a path inside it. ${provenance}`;
2674
- return `resolves to '${shownPath}', outside '${shownBase}'. ${provenance}`;
2675
- }
2676
-
2677
2773
  //#endregion
2678
2774
  //#region src/local/lambda-resolver.ts
2679
2775
  /**
@@ -2999,7 +3095,7 @@ function resolveAssetCodeDirectory(manifestDir, assetPath, wrapError, assetOutdi
2999
3095
  if (isAbsolute(assetPath)) {
3000
3096
  const absolute = resolve(assetPath);
3001
3097
  const escape = absoluteAssemblyPathEscape(assetOutdir, absolute);
3002
- 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.`);
3098
+ if (escape !== void 0) getLogger().warn(`Lambda ${displayUntrustedValue(logicalId)} has an absolute Metadata['aws:asset:path'] pointing outside the assembly: ${displayUntrustedValue(absolute)}` + (escape.escape === "symlink" ? ` (through a symbolic link to ${displayUntrustedValue(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.`);
3003
3099
  warnOutsideNamedDirectory(assetOutdir, absolute);
3004
3100
  return absolute;
3005
3101
  }
@@ -3011,7 +3107,7 @@ function resolveAssetCodeDirectory(manifestDir, assetPath, wrapError, assetOutdi
3011
3107
  if (!resolved.contained) {
3012
3108
  const climbsOut = assetPath === ".." || assetPath.startsWith(`..${sep}`) || assetPath.startsWith("../");
3013
3109
  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." : "";
3014
- throw wrapError(`Lambda '${sanitizeServiceExceptionMessage(logicalId)}' has Metadata['aws:asset:path']='${sanitizeServiceExceptionMessage(assetPath)}' which ${renderAssemblyPathEscape(resolved, assetOutdir, "mount it")}${stageHint}`);
3110
+ throw wrapError(`Lambda ${displayUntrustedValue(logicalId)} has Metadata['aws:asset:path']=${displayUntrustedValue(assetPath)} which ${renderAssemblyPathEscape(resolved, assetOutdir, "mount it")}${stageHint}`);
3015
3111
  }
3016
3112
  warnOutsideNamedDirectory(assetOutdir, resolved.path);
3017
3113
  return resolved.path;
@@ -3265,7 +3361,7 @@ function warnOutsideNamedDirectory(assetOutdir, resolved) {
3265
3361
  if (absoluteAssemblyPathEscape(named, resolved) === void 0) return;
3266
3362
  if (warnedOutsideNamed.has(resolved)) return;
3267
3363
  warnedOutsideNamed.add(resolved);
3268
- 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.`);
3364
+ getLogger().warn(`${displayUntrustedValue(resolved)} is outside ${displayUntrustedValue(named)}, the directory --app named. It is inside the assembly root derived from it, so ${getEmbedConfig().productName} is using it.`);
3269
3365
  }
3270
3366
  /**
3271
3367
  * Say that the assembly root was DERIVED rather than given.
@@ -3278,7 +3374,7 @@ function warnOutsideNamedDirectory(assetOutdir, resolved) {
3278
3374
  function warnDerivedAssemblyRoot(named, derived) {
3279
3375
  if (warnedDerivedRoots.has(derived)) return;
3280
3376
  warnedDerivedRoots.add(derived);
3281
- 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.`);
3377
+ getLogger().warn(`${displayUntrustedValue(named)} is a cdk.Stage sub-assembly, so ${getEmbedConfig().productName} is treating its parent ${displayUntrustedValue(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.`);
3282
3378
  }
3283
3379
  /**
3284
3380
  * Resolve the local directory that corresponds to a function's deployed
@@ -3297,14 +3393,14 @@ function warnDerivedAssemblyRoot(named, derived) {
3297
3393
  */
3298
3394
  function resolveAssetCodePath$1(stack, logicalId, resource, options = {}) {
3299
3395
  const assetPath = resource.Metadata?.["aws:asset:path"];
3300
- 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.`);
3396
+ if (typeof assetPath !== "string" || assetPath.length === 0) throw new LocalInvokeResolutionError(`Lambda ${displayUntrustedValue(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.`);
3301
3397
  const { manifestDir, assetOutdir } = assetPathDirs(stack);
3302
3398
  const abs = resolveAssetCodeDirectory(manifestDir, assetPath, (message) => new LocalInvokeResolutionError(message), assetOutdir, logicalId);
3303
- if (!existsSync(abs)) throw new LocalInvokeResolutionError(`Lambda '${sanitizeServiceExceptionMessage(logicalId)}' asset path '${sanitizeServiceExceptionMessage(abs)}' does not exist. Re-synthesize the app and retry.`);
3399
+ if (!existsSync(abs)) throw new LocalInvokeResolutionError(`Lambda ${displayUntrustedValue(logicalId)} asset path ${displayUntrustedValue(abs)} does not exist. Re-synthesize the app and retry.`);
3304
3400
  const stat = statSync(abs);
3305
3401
  if (stat.isDirectory()) return abs;
3306
3402
  if (options.allowZip && stat.isFile() && abs.toLowerCase().endsWith(".zip")) return abs;
3307
- 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.");
3403
+ throw new LocalInvokeResolutionError(`Lambda ${displayUntrustedValue(logicalId)} asset path ${displayUntrustedValue(abs)} is not a directory` + (options.allowZip ? " or a .zip archive" : "") + ". Re-synthesize the app and retry.");
3308
3404
  }
3309
3405
  /**
3310
3406
  * Turn a resolved function-code asset path into a directory ready to
@@ -3323,14 +3419,14 @@ function resolveAssetCodePath$1(stack, logicalId, resource, options = {}) {
3323
3419
  * existing tmpdir cleanup.
3324
3420
  */
3325
3421
  function materializeAssetCodeDir(codePath) {
3326
- if (!existsSync(codePath)) throw new LocalInvokeResolutionError(`Lambda asset path '${sanitizeServiceExceptionMessage(codePath)}' does not exist. Re-synthesize the app and retry.`);
3422
+ if (!existsSync(codePath)) throw new LocalInvokeResolutionError(`Lambda asset path ${displayUntrustedValue(codePath)} does not exist. Re-synthesize the app and retry.`);
3327
3423
  if (statSync(codePath).isDirectory()) return { dir: codePath };
3328
3424
  const zipBytes = readFileSync(codePath);
3329
3425
  let files;
3330
3426
  try {
3331
3427
  files = unzipSync(zipBytes);
3332
3428
  } catch (err) {
3333
- 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.`);
3429
+ throw new LocalInvokeResolutionError(`Lambda asset ${displayUntrustedValue(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.`);
3334
3430
  }
3335
3431
  const modes = parseZipUnixModes(zipBytes);
3336
3432
  const S_IFMT = 61440;
@@ -6750,7 +6846,7 @@ var CfnLocalStateProvider = class {
6750
6846
  return map.get(exportName);
6751
6847
  },
6752
6848
  async resolveGetStackOutput(producerStack, producerRegion, outputName) {
6753
- logger.warn(`${label}: Fn::GetStackOutput '${producerStack}.${outputName}' (${producerRegion}) has no CloudFormation equivalent and cannot be resolved when reading state from a CloudFormation stack. Use Fn::ImportValue against an exported output instead.`);
6849
+ logger.warn(`${label}: Fn::GetStackOutput ${displayUntrustedValue(producerStack)}.${displayUntrustedValue(outputName)} (${displayUntrustedValue(producerRegion)}) has no CloudFormation equivalent and cannot be resolved when reading state from a CloudFormation stack. Use Fn::ImportValue against an exported output instead.`);
6754
6850
  }
6755
6851
  };
6756
6852
  }
@@ -7665,7 +7761,7 @@ async function resolveImportValueAsync(arg, context) {
7665
7761
  const exportName = inner.value;
7666
7762
  if (!context.crossStackResolver) return {
7667
7763
  kind: "unresolved",
7668
- reason: `Fn::ImportValue '${exportName}': no cross-stack resolver supplied (pass a state-source flag, e.g. --from-cfn-stack, and ensure the producer stack was deployed)`
7764
+ reason: `Fn::ImportValue ${displayUntrustedValue(exportName)}: no cross-stack resolver supplied (pass a state-source flag, e.g. --from-cfn-stack, and ensure the producer stack was deployed)`
7669
7765
  };
7670
7766
  let resolved;
7671
7767
  try {
@@ -7673,12 +7769,12 @@ async function resolveImportValueAsync(arg, context) {
7673
7769
  } catch (err) {
7674
7770
  return {
7675
7771
  kind: "unresolved",
7676
- reason: `Fn::ImportValue '${flattenToOneLine(exportName)}': lookup failed: ${describeAwsFailureForWarn(err, "CrossStackResolver.resolveImport (Fn::ImportValue)")}`
7772
+ reason: `Fn::ImportValue ${displayUntrustedValue(exportName)}: lookup failed: ${describeAwsFailureForWarn(err, "CrossStackResolver.resolveImport (Fn::ImportValue)")}`
7677
7773
  };
7678
7774
  }
7679
7775
  if (resolved === void 0) return {
7680
7776
  kind: "unresolved",
7681
- reason: `Fn::ImportValue '${exportName}': export not found in any deployed stack in this region`
7777
+ reason: `Fn::ImportValue ${displayUntrustedValue(exportName)}: export not found in any deployed stack in this region`
7682
7778
  };
7683
7779
  return {
7684
7780
  kind: "literal",
@@ -7732,15 +7828,15 @@ async function resolveGetStackOutputAsync(arg, context) {
7732
7828
  } else region = context.consumerRegion ?? context.pseudoParameters?.region;
7733
7829
  if (!region) return {
7734
7830
  kind: "unresolved",
7735
- reason: `Fn::GetStackOutput '${stackName}.${outputName}': no Region supplied and consumer region is unknown (set --region, AWS_REGION, or env.region on the CDK stack)`
7831
+ reason: `Fn::GetStackOutput ${displayUntrustedValue(stackName)}.${displayUntrustedValue(outputName)}: no Region supplied and consumer region is unknown (set --region, AWS_REGION, or env.region on the CDK stack)`
7736
7832
  };
7737
7833
  if (args["RoleArn"] !== void 0 && args["RoleArn"] !== null) return {
7738
7834
  kind: "unresolved",
7739
- reason: `Fn::GetStackOutput '${stackName}.${outputName}': RoleArn (cross-account) is not yet supported by the state source — tracked under issue #449`
7835
+ reason: `Fn::GetStackOutput ${displayUntrustedValue(stackName)}.${displayUntrustedValue(outputName)}: RoleArn (cross-account) is not yet supported by the state source — tracked under issue #449`
7740
7836
  };
7741
7837
  if (!context.crossStackResolver) return {
7742
7838
  kind: "unresolved",
7743
- reason: `Fn::GetStackOutput '${stackName}.${outputName}': no cross-stack resolver supplied (pass a state-source flag, e.g. --from-cfn-stack, and ensure the producer stack was deployed)`
7839
+ reason: `Fn::GetStackOutput ${displayUntrustedValue(stackName)}.${displayUntrustedValue(outputName)}: no cross-stack resolver supplied (pass a state-source flag, e.g. --from-cfn-stack, and ensure the producer stack was deployed)`
7744
7840
  };
7745
7841
  let resolved;
7746
7842
  try {
@@ -7748,12 +7844,12 @@ async function resolveGetStackOutputAsync(arg, context) {
7748
7844
  } catch (err) {
7749
7845
  return {
7750
7846
  kind: "unresolved",
7751
- reason: `Fn::GetStackOutput '${flattenToOneLine(stackName)}.${flattenToOneLine(outputName)}' (${flattenToOneLine(region)}): lookup failed: ${describeAwsFailureForWarn(err, "CrossStackResolver.resolveGetStackOutput (Fn::GetStackOutput)")}`
7847
+ reason: `Fn::GetStackOutput ${displayUntrustedValue(stackName)}.${displayUntrustedValue(outputName)} (${displayUntrustedValue(region)}): lookup failed: ${describeAwsFailureForWarn(err, "CrossStackResolver.resolveGetStackOutput (Fn::GetStackOutput)")}`
7752
7848
  };
7753
7849
  }
7754
7850
  if (resolved === void 0) return {
7755
7851
  kind: "unresolved",
7756
- reason: `Fn::GetStackOutput '${stackName}.${outputName}' (${region}): output not found in producer stack state`
7852
+ reason: `Fn::GetStackOutput ${displayUntrustedValue(stackName)}.${displayUntrustedValue(outputName)} (${displayUntrustedValue(region)}): output not found in producer stack state`
7757
7853
  };
7758
7854
  return {
7759
7855
  kind: "literal",
@@ -9065,14 +9161,14 @@ function resolveRuntimeCodeMountPath(runtime) {
9065
9161
  */
9066
9162
  function resolveAssetSourcePath(opts) {
9067
9163
  const { manifestDir, value, assetOutdir, field, subject, action, wrapError } = opts;
9068
- const shownValue = sanitizeServiceExceptionMessage(value);
9164
+ const shownValue = displayUntrustedValue(value);
9069
9165
  if (isAbsolute(value) && opts.absolute !== "fold") {
9070
9166
  const absolute = resolve(value);
9071
9167
  const escape = absoluteAssemblyPathEscape(assetOutdir, absolute);
9072
- if (escape !== void 0 && opts.absolute === "honour") throw wrapError(`${subject} has an absolute ${field}='${shownValue}' which ` + renderAssemblyPathEscape(escape, assetOutdir, action));
9168
+ if (escape !== void 0 && opts.absolute === "honour") throw wrapError(`${subject} has an absolute ${field}=${shownValue} which ` + renderAssemblyPathEscape(escape, assetOutdir, action));
9073
9169
  if (escape !== void 0) {
9074
9170
  const refusal = originScopeRefusal(absolute, assetOutdir);
9075
- if (refusal !== void 0) throw wrapError(`${subject} has an absolute ${field}='${shownValue}' which ${refusal}. An absolute ${field} is accepted only as a cdk synth --no-staging source folder: a directory inside your project (the git work tree holding the output directory, or the current directory when it contains the output directory or is a git work-tree root), not under a credential or version-control directory. Refusing to ${action}.`);
9171
+ if (refusal !== void 0) throw wrapError(`${subject} has an absolute ${field}=${shownValue} which ${refusal}. An absolute ${field} is accepted only as a cdk synth --no-staging source folder: a directory inside your project (the git work tree holding the output directory, or the current directory when it contains the output directory or is a git work-tree root), not under a credential or version-control directory. Refusing to ${action}.`);
9076
9172
  warnAbsoluteOutsideAssembly(opts, absolute, escape);
9077
9173
  return absolute;
9078
9174
  }
@@ -9085,7 +9181,7 @@ function resolveAssetSourcePath(opts) {
9085
9181
  warnWholeAssemblyAsSource(opts, resolved.path);
9086
9182
  return resolved.path;
9087
9183
  }
9088
- throw wrapError(`${subject} has ${field}='${shownValue}' which ` + renderAssemblyPathEscape(resolved, assetOutdir, action));
9184
+ throw wrapError(`${subject} has ${field}=${shownValue} which ` + renderAssemblyPathEscape(resolved, assetOutdir, action));
9089
9185
  }
9090
9186
  /**
9091
9187
  * Why an accepted-absolute root is too broad to be a `--no-staging` source, or
@@ -9139,9 +9235,9 @@ function originScopeRefusal(absolute, assetOutdir) {
9139
9235
  hidden = dotted;
9140
9236
  }
9141
9237
  if (isProjectRoot) return "is your project root itself; name a folder inside it";
9142
- if (hidden !== void 0) return `passes through the credential / version-control directory '${sanitizeServiceExceptionMessage(hidden)}' inside your project`;
9143
- const shown = projects.map((p) => `'${sanitizeServiceExceptionMessage(p)}'`).join(" or ");
9144
- return projects.length === 0 ? "is outside any usable project root (the current directory and the git work tree are too broad to scope it)" : `resolves to '${sanitizeServiceExceptionMessage(root)}', outside your project (${shown})`;
9238
+ if (hidden !== void 0) return `passes through the credential / version-control directory ${displayUntrustedValue(hidden)} inside your project`;
9239
+ const shown = projects.map((p) => displayUntrustedValue(p)).join(" or ");
9240
+ return projects.length === 0 ? "is outside any usable project root (the current directory and the git work tree are too broad to scope it)" : `resolves to ${displayUntrustedValue(root)}, outside your project (${shown})`;
9145
9241
  }
9146
9242
  /**
9147
9243
  * Directory names no `--no-staging` site folder sits under: credential stores
@@ -9235,7 +9331,7 @@ function realPath(p, hops = 0) {
9235
9331
  const warnedAbsoluteOutside = /* @__PURE__ */ new Set();
9236
9332
  function warnAbsoluteOutsideAssembly(opts, absolute, escape) {
9237
9333
  const logger = getLogger().child("assets");
9238
- const line = `${opts.subject} has an absolute ${opts.field} pointing outside the assembly: '${sanitizeServiceExceptionMessage(absolute)}'` + (escape.escape === "symlink" ? ` (through a symbolic link to '${sanitizeServiceExceptionMessage(escape.realPath)}')` : "") + `. ${getEmbedConfig().productName} will ${opts.sink}. ` + (escape.escape === "lexical" ? "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." : "No CDK synth writes this, so treat this assembly as untrusted unless you made that link yourself.");
9334
+ const line = `${opts.subject} has an absolute ${opts.field} pointing outside the assembly: ` + displayUntrustedValue(absolute) + (escape.escape === "symlink" ? ` (through a symbolic link to ${displayUntrustedValue(escape.realPath)})` : "") + `. ${getEmbedConfig().productName} will ${opts.sink}. ` + (escape.escape === "lexical" ? "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." : "No CDK synth writes this, so treat this assembly as untrusted unless you made that link yourself.");
9239
9335
  if (warnedAbsoluteOutside.has(line)) {
9240
9336
  logger.debug(line);
9241
9337
  return;
@@ -9252,7 +9348,7 @@ function warnAbsoluteOutsideAssembly(opts, absolute, escape) {
9252
9348
  const warnedWholeAssembly = /* @__PURE__ */ new Set();
9253
9349
  function warnWholeAssemblyAsSource(opts, outdir) {
9254
9350
  const logger = getLogger().child("assets");
9255
- const line = `${opts.subject} has ${opts.field} naming the assembly's output directory ITSELF: '${sanitizeServiceExceptionMessage(outdir)}'. ${getEmbedConfig().productName} will ${opts.sink} — that is the WHOLE assembly, every template and every staged asset, not one asset directory. No CDK synth emits this, so treat this assembly as untrusted unless you wrote that path yourself.`;
9351
+ const line = `${opts.subject} has ${opts.field} naming the assembly's output directory ITSELF: ${displayUntrustedValue(outdir)}. ${getEmbedConfig().productName} will ${opts.sink} — that is the WHOLE assembly, every template and every staged asset, not one asset directory. No CDK synth emits this, so treat this assembly as untrusted unless you wrote that path yourself.`;
9256
9352
  if (warnedWholeAssembly.has(line)) {
9257
9353
  logger.debug(line);
9258
9354
  return;
@@ -9405,7 +9501,7 @@ function warnEscapingBuildKitPaths(source, base, bound) {
9405
9501
  const escape = assemblyPathEscape(base, bound, ref.path);
9406
9502
  if (escape === void 0) continue;
9407
9503
  const verb = ref.write ? "WRITE to" : "read";
9408
- const line = `Docker asset ${ref.field}` + (ref.where === ref.field ? "" : `['${sanitizeServiceExceptionMessage(ref.where)}']`) + ` names a host path outside the assembly, which ` + renderAssemblyPathEscape(escape, bound, "load", `${getEmbedConfig().productName} will ${verb} it during the image build, matching the CDK CLI — a pre-synthesized assembly is trusted input. If you did not produce this assembly, that is a host path it chose and the CloudFormation template does not show.`);
9504
+ const line = `Docker asset ${ref.field}` + (ref.where === ref.field ? "" : `[${displayUntrustedValue(ref.where)}]`) + ` names a host path outside the assembly, which ` + renderAssemblyPathEscape(escape, bound, "load", `${getEmbedConfig().productName} will ${verb} it during the image build, matching the CDK CLI — a pre-synthesized assembly is trusted input. If you did not produce this assembly, that is a host path it chose and the CloudFormation template does not show.`);
9409
9505
  if (warnedPassthroughs.has(line)) {
9410
9506
  logger.debug(line);
9411
9507
  continue;
@@ -9453,9 +9549,10 @@ function warnEscapingBuildKitPaths(source, base, bound) {
9453
9549
  * to `debug`, where the argv already is. Deduping the LINE must never dedupe
9454
9550
  * the WORK: the spawn happens every time regardless.
9455
9551
  *
9456
- * `sanitizeServiceExceptionMessage` for the same reason `assembly-path.ts`
9457
- * uses it: the value is manifest-chosen, it keeps control characters, and an
9458
- * unsanitized one lets the command forge its own log line. It is applied to
9552
+ * `displayUntrustedValue` for the same reason `assembly-path.ts` uses it: the
9553
+ * value is manifest-chosen, it keeps control characters and quotes, and an
9554
+ * unsanitized one lets the command forge its own log line or close a quote
9555
+ * of ours (go-to-k/cdk-local#758). It is applied to
9459
9556
  * the DISPLAYED command only — the dedupe key is built from the raw argv and
9460
9557
  * cwd, so two commands differing only in control characters stay distinct
9461
9558
  * keys.
@@ -9465,12 +9562,12 @@ function warnManifestExecutable(cmd, executable, cwd) {
9465
9562
  const logger = getLogger().child("docker-build");
9466
9563
  const key = JSON.stringify([cwd, executable]);
9467
9564
  if (warnedExecutables.has(key)) {
9468
- logger.debug(`source.executable already announced this process: ${sanitizeServiceExceptionMessage(cmd)} (cwd=${cwd})`);
9565
+ logger.debug(`source.executable already announced this process: ${displayUntrustedValue(cmd)} (cwd=${displayUntrustedValue(cwd)})`);
9469
9566
  return;
9470
9567
  }
9471
9568
  warnedExecutables.add(key);
9472
9569
  const argCount = executable.length - 1;
9473
- 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.");
9570
+ logger.warn(`Docker asset source.executable runs a command this asset manifest chose, on this machine: ${displayUntrustedValue(cmd)}` + (argCount > 0 ? ` (with ${argCount} argument(s); --verbose shows them)` : "") + `. ${getEmbedConfig().productName} 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.`);
9474
9571
  }
9475
9572
  /**
9476
9573
  * Build a Docker image from a CDK asset source. Returns the local image
@@ -9506,7 +9603,7 @@ async function buildDockerImage(asset, cdkOutDir, options) {
9506
9603
  const [cmd, ...args] = source.executable;
9507
9604
  if (!cmd) throw options.wrapError("asset source.executable[] is empty");
9508
9605
  const cwd = source.directory ? contextDirectory(source.directory, "run this asset's source.executable with that directory as its working directory") : cdkOutDir;
9509
- logger.debug(`Building Docker image via executable: ${sanitizeServiceExceptionMessage(source.executable.join(" "))} (cwd=${sanitizeServiceExceptionMessage(cwd)})`);
9606
+ logger.debug(`Building Docker image via executable: ${sanitizeServiceExceptionMessage(source.executable.join(" "))} (cwd=${displayUntrustedValue(cwd)})`);
9510
9607
  warnManifestExecutable(cmd, source.executable, cwd);
9511
9608
  let result;
9512
9609
  try {
@@ -10275,7 +10372,7 @@ var AssetManifestLoader = class {
10275
10372
  */
10276
10373
  async loadManifest(cdkOutputDir, stackName) {
10277
10374
  const resolved = resolveAssemblyPath(cdkOutputDir, `${stackName}.assets.json`);
10278
- if (!resolved.contained) throw new Error(`Refusing to read the asset manifest for stack '${sanitizeServiceExceptionMessage(stackName)}': it ` + renderAssemblyPathEscape(resolved, cdkOutputDir, "read it"));
10375
+ if (!resolved.contained) throw new Error(`Refusing to read the asset manifest for stack ${displayUntrustedValue(stackName)}: it ` + renderAssemblyPathEscape(resolved, cdkOutputDir, "read it"));
10279
10376
  const manifestPath = join$1(cdkOutputDir, `${stackName}.assets.json`);
10280
10377
  try {
10281
10378
  this.logger.debug(`Loading asset manifest from: ${manifestPath}`);
@@ -15053,7 +15150,7 @@ function parseHttpV2IdentitySources(raw) {
15053
15150
  */
15054
15151
  function parseCognitoUserPoolArn(arn, location) {
15055
15152
  const m = /^arn:aws[a-z0-9-]*:cognito-idp:([a-z0-9-]+):[0-9]+:userpool\/(.+)$/.exec(arn);
15056
- if (!m) throw new RouteDiscoveryError(`${location}: malformed Cognito User Pool ARN '${arn}'. Expected 'arn:aws:cognito-idp:<region>:<account>:userpool/<id>'.`);
15153
+ if (!m) throw new RouteDiscoveryError(`${location}: malformed Cognito User Pool ARN ${displayUntrustedValue(arn)}. Expected 'arn:aws:cognito-idp:<region>:<account>:userpool/<id>'.`);
15057
15154
  return {
15058
15155
  region: m[1],
15059
15156
  userPoolId: m[2]
@@ -20117,7 +20214,7 @@ function resolveImageLambda(args) {
20117
20214
  */
20118
20215
  function resolveAssetCodePath(stack, logicalId, resource) {
20119
20216
  const assetPath = resource.Metadata?.["aws:asset:path"];
20120
- 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.`);
20217
+ if (typeof assetPath !== "string" || assetPath.length === 0) throw new Error(`Lambda ${displayUntrustedValue(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.`);
20121
20218
  const { manifestDir, assetOutdir } = assetPathDirs(stack);
20122
20219
  return resolveAssetCodeDirectory(manifestDir, assetPath, (message) => new Error(message), assetOutdir, logicalId);
20123
20220
  }
@@ -21630,7 +21727,7 @@ async function buildAgentCoreCodeImage(options) {
21630
21727
  ], { progressLabel: `Building agent image (runtime=${options.runtime})` });
21631
21728
  } catch (err) {
21632
21729
  const stderr = err.stderr?.trim();
21633
- throw new LocalInvokeBuildError(`docker build failed for AgentCore code artifact (${flattenToOneLine(options.sourceDir)})${stderr ? `: ${stderr}` : ""}`);
21730
+ throw new LocalInvokeBuildError(`docker build failed for AgentCore code artifact (${displayUntrustedValue(options.sourceDir)})${stderr ? `: ${stderr}` : ""}`);
21634
21731
  } finally {
21635
21732
  await rm(buildDir, {
21636
21733
  recursive: true,
@@ -21675,14 +21772,14 @@ async function warnIfDependenciesNotVendored(sourceDir, runtime, isNode, logger)
21675
21772
  }
21676
21773
  const has = (name) => entries.includes(name);
21677
21774
  if (isNode) {
21678
- if (has("package.json") && !has("node_modules")) logger.warn(`AgentCore code bundle '${flattenToOneLine(sourceDir)}' declares package.json but does not vendor node_modules. The AgentCore managed runtime does NOT install dependencies at runtime, so the deployed agent will fail to resolve them. Vendor dependencies into the bundle (e.g. 'npm install --omit=dev' in the bundle dir) so the deploy artifact is self-contained. cdk-local runs the bundle as-is to match the deployed runtime.`);
21775
+ if (has("package.json") && !has("node_modules")) logger.warn(`AgentCore code bundle ${displayUntrustedValue(sourceDir)} declares package.json but does not vendor node_modules. The AgentCore managed runtime does NOT install dependencies at runtime, so the deployed agent will fail to resolve them. Vendor dependencies into the bundle (e.g. 'npm install --omit=dev' in the bundle dir) so the deploy artifact is self-contained. cdk-local runs the bundle as-is to match the deployed runtime.`);
21679
21776
  return;
21680
21777
  }
21681
21778
  const manifest = await pythonManifestDeclaringDeps(sourceDir, entries);
21682
21779
  const vendored = entries.some((e) => e.endsWith(".dist-info"));
21683
21780
  if (manifest && !vendored) {
21684
21781
  const pyVersion = runtime.replace("PYTHON_", "").replace("_", ".");
21685
- logger.warn(`AgentCore code bundle '${flattenToOneLine(sourceDir)}' declares ${manifest} but does not vendor its dependencies. The AgentCore managed runtime does NOT install dependencies at runtime, so the deployed agent will fail with ModuleNotFoundError. Vendor arm64 wheels into the bundle, e.g.:
21782
+ logger.warn(`AgentCore code bundle ${displayUntrustedValue(sourceDir)} declares ${manifest} but does not vendor its dependencies. The AgentCore managed runtime does NOT install dependencies at runtime, so the deployed agent will fail with ModuleNotFoundError. Vendor arm64 wheels into the bundle, e.g.:
21686
21783
  uv pip install --python-platform aarch64-manylinux2014 --python-version ${pyVersion} --target <bundle-dir> -r requirements.txt\ncdk-local runs the bundle as-is to match the deployed runtime.`);
21687
21784
  }
21688
21785
  }
@@ -23018,10 +23115,10 @@ async function resolveAgentCoreCodeImage(resolved, code, options, architecture,
23018
23115
  if (!asset) throw new CdkLocalError(`AgentCore Runtime '${resolved.logicalId}' code bundle (asset ${code.codeAssetHash}) was not found in the cdk.out asset manifest. ${getEmbedConfig().cliName} invoke-agentcore runs a local from-source build of a fromCodeAsset bundle — re-synthesize the app so the asset is staged in cdk.out and retry. (A fromS3 bundle is downloaded from S3 instead; this runtime has no literal Code.S3.Bucket.)`, "LOCAL_INVOKE_AGENTCORE_CODE_ASSET_NOT_FOUND");
23019
23116
  const sourceDir = loader.getAssetSourcePath(cdkOutDir, asset, {
23020
23117
  assetOutdir: assetPathDirs(resolved.stack).assetOutdir,
23021
- subject: `AgentCore Runtime '${flattenToOneLine(resolved.logicalId)}' code bundle`,
23118
+ subject: `AgentCore Runtime ${displayUntrustedValue(resolved.logicalId)} code bundle`,
23022
23119
  wrapError: (message) => new CdkLocalError(message, "LOCAL_INVOKE_AGENTCORE_CODE_SOURCE_ESCAPES_ASSEMBLY")
23023
23120
  });
23024
- if (!existsSync(sourceDir) || !statSync(sourceDir).isDirectory()) throw new CdkLocalError(`AgentCore Runtime '${flattenToOneLine(resolved.logicalId)}' code bundle source '${flattenToOneLine(sourceDir)}' does not exist or is not a directory. Re-synthesize the app and retry.`, "LOCAL_INVOKE_AGENTCORE_CODE_SOURCE_MISSING");
23121
+ if (!existsSync(sourceDir) || !statSync(sourceDir).isDirectory()) throw new CdkLocalError(`AgentCore Runtime ${displayUntrustedValue(resolved.logicalId)} code bundle source ${displayUntrustedValue(sourceDir)} does not exist or is not a directory. Re-synthesize the app and retry.`, "LOCAL_INVOKE_AGENTCORE_CODE_SOURCE_MISSING");
23025
23122
  return buildAgentCoreCodeImage({
23026
23123
  sourceDir,
23027
23124
  runtime: code.runtime,
@@ -23786,7 +23883,7 @@ async function loadAgentCoreAssetContext(args) {
23786
23883
  if (!asset) return void 0;
23787
23884
  const sourceDir = assetLoader.getAssetSourcePath(watchManifestDir(newCandidate, cdkOutDir), asset, {
23788
23885
  assetOutdir: outputAssetBound(newCandidate, cdkOutDir),
23789
- subject: `AgentCore Runtime '${flattenToOneLine(resolved.logicalId)}' code bundle`,
23886
+ subject: `AgentCore Runtime ${displayUntrustedValue(resolved.logicalId)} code bundle`,
23790
23887
  wrapError: (message) => new CdkLocalError(message, "LOCAL_INVOKE_AGENTCORE_CODE_SOURCE_ESCAPES_ASSEMBLY")
23791
23888
  });
23792
23889
  return {
@@ -23810,7 +23907,7 @@ async function loadAgentCoreAssetContext(args) {
23810
23907
  assetOutdir: outputAssetBound(newCandidate, cdkOutDir),
23811
23908
  absolute: "honour",
23812
23909
  field: "source.directory",
23813
- subject: `AgentCore Runtime '${flattenToOneLine(resolved.logicalId)}' image asset`,
23910
+ subject: `AgentCore Runtime ${displayUntrustedValue(resolved.logicalId)} image asset`,
23814
23911
  action: "copy from it",
23815
23912
  sink: "copy that directory into the running agent container on a soft reload",
23816
23913
  wrapError: (message) => new CdkLocalError(message, "LOCAL_INVOKE_AGENTCORE_CODE_SOURCE_ESCAPES_ASSEMBLY")
@@ -24388,7 +24485,7 @@ async function resolveOne(entry, secretsClient, ssmClient) {
24388
24485
  switch (shape.kind) {
24389
24486
  case "secrets-manager": return resolveSecretsManager(entry, shape, secretsClient);
24390
24487
  case "ssm": return resolveSsm(entry, shape, ssmClient);
24391
- case "unknown": throw new EcsSecretsResolutionError(`Container '${entry.containerName}' secret '${entry.name}' references an unsupported ValueFrom shape '${arn}'. Expected Secrets Manager ARN (optionally with :<json-key>::) or SSM Parameter ARN.`);
24488
+ case "unknown": throw new EcsSecretsResolutionError(`Container ${displayUntrustedValue(entry.containerName)} secret ${displayUntrustedValue(entry.name)} references an unsupported ValueFrom shape ${displayUntrustedValue(arn)}. Expected Secrets Manager ARN (optionally with :<json-key>::) or SSM Parameter ARN.`);
24392
24489
  }
24393
24490
  }
24394
24491
  /**
@@ -24880,7 +24977,7 @@ async function realizeDockerVolumes(volumes, state) {
24880
24977
  const out = /* @__PURE__ */ new Map();
24881
24978
  for (const v of volumes) {
24882
24979
  if (v.kind === "host") {
24883
- if (v.hostPath && !checkVolumeHostPath(v.hostPath)) logger.warn(`Volume '${v.name}': host path '${v.hostPath}' does not exist or is not a directory. Docker will create an anonymous bind mount; create the host path before run-task if you expected to bind-mount it.`);
24980
+ if (v.hostPath && !checkVolumeHostPath(v.hostPath)) logger.warn(`Volume ${displayUntrustedValue(v.name)}: host path ${displayUntrustedValue(v.hostPath)} does not exist or is not a directory. Docker will create an anonymous bind mount; create the host path before run-task if you expected to bind-mount it.`);
24884
24981
  out.set(v.name, v);
24885
24982
  continue;
24886
24983
  }
@@ -29764,7 +29861,7 @@ async function loadAssetContextForTarget(args) {
29764
29861
  assetOutdir: outputAssetBound(candidate, cdkOutDir),
29765
29862
  absolute: "honour",
29766
29863
  field: "source.directory",
29767
- subject: `Docker image asset '${sanitizeServiceExceptionMessage(newAssetHash)}' of stack '${sanitizeServiceExceptionMessage(candidate.stackName)}'`,
29864
+ subject: `Docker image asset ${displayUntrustedValue(newAssetHash)} of stack ${displayUntrustedValue(candidate.stackName)}`,
29768
29865
  action: "copy from it",
29769
29866
  sink: "copy that directory into the running replicas on a soft reload",
29770
29867
  wrapError: (message) => new Error(message)
@@ -30293,11 +30390,11 @@ async function resolvePlaceholderAccount$1(arn, region, profile) {
30293
30390
  }));
30294
30391
  try {
30295
30392
  const account = (await sts.send(new GetCallerIdentityCommand({}))).Account;
30296
- if (!account) throw new LocalStartServiceError(`--assume-task-role: GetCallerIdentity returned no Account; cannot resolve placeholder ARN '${arn}'.`);
30393
+ if (!account) throw new LocalStartServiceError(`--assume-task-role: GetCallerIdentity returned no Account; cannot resolve placeholder ARN ${displayUntrustedValue(arn)}.`);
30297
30394
  return arn.split(TASK_ROLE_ACCOUNT_PLACEHOLDER).join(account);
30298
30395
  } catch (err) {
30299
30396
  if (err instanceof LocalStartServiceError) throw err;
30300
- throw new LocalStartServiceError(`--assume-task-role: STS GetCallerIdentity failed while resolving placeholder ARN '${flattenToOneLine(arn)}': ${describeAwsFailureForWarn(err, "STS GetCallerIdentity (task-role placeholder)")}. Pass the ARN explicitly: --assume-task-role <arn>`);
30397
+ throw new LocalStartServiceError(`--assume-task-role: STS GetCallerIdentity failed while resolving placeholder ARN ${displayUntrustedValue(arn)}: ${describeAwsFailureForWarn(err, "STS GetCallerIdentity (task-role placeholder)")}. Pass the ARN explicitly: --assume-task-role <arn>`);
30301
30398
  } finally {
30302
30399
  sts.destroy();
30303
30400
  }
@@ -30870,13 +30967,13 @@ async function resolvePlaceholderAccount(arn, region, profile) {
30870
30967
  }));
30871
30968
  try {
30872
30969
  const account = (await sts.send(new GetCallerIdentityCommand({}))).Account;
30873
- if (!account) throw new PlaceholderAccountError(`--assume-task-role: GetCallerIdentity returned no Account; cannot resolve placeholder ARN '${arn}'. Pass the ARN explicitly: --assume-task-role <arn>`);
30970
+ if (!account) throw new PlaceholderAccountError(`--assume-task-role: GetCallerIdentity returned no Account; cannot resolve placeholder ARN ${displayUntrustedValue(arn)}. Pass the ARN explicitly: --assume-task-role <arn>`);
30874
30971
  return arn.split(TASK_ROLE_ACCOUNT_PLACEHOLDER).join(account);
30875
30972
  } catch (err) {
30876
30973
  if (err instanceof PlaceholderAccountError) throw err;
30877
- const shownArn = flattenToOneLine(arn);
30974
+ const shownArn = displayUntrustedValue(arn);
30878
30975
  const detail = describeAwsFailureForWarn(err, "STS GetCallerIdentity (task-role placeholder)");
30879
- throw new PlaceholderAccountError(`--assume-task-role: STS GetCallerIdentity failed while resolving placeholder ARN '${shownArn}': ${detail}. Pass the ARN explicitly: --assume-task-role <arn>`);
30976
+ throw new PlaceholderAccountError(`--assume-task-role: STS GetCallerIdentity failed while resolving placeholder ARN ${shownArn}: ${detail}. Pass the ARN explicitly: --assume-task-role <arn>`);
30880
30977
  } finally {
30881
30978
  sts.destroy();
30882
30979
  }
@@ -32744,7 +32841,7 @@ function resolveBucketDeploymentDirs(template, manifest, stack, bucketLogicalId)
32744
32841
  assetOutdir,
32745
32842
  absolute: "honour-warn",
32746
32843
  field: "source.path",
32747
- subject: `BucketDeployment source asset for bucket '${sanitizeServiceExceptionMessage(bucketLogicalId)}'`,
32844
+ subject: `BucketDeployment source asset for bucket ${displayUntrustedValue(bucketLogicalId)}`,
32748
32845
  action: "serve it",
32749
32846
  sink: "serve every file under that directory as this bucket's local S3 origin",
32750
32847
  wrapError: (message) => new Error(message)
@@ -33307,7 +33404,7 @@ function realPathInsideRoot(root, file) {
33307
33404
  if (rel === "" || rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel)) return realFile;
33308
33405
  if (!warnedEscapes.has(file)) {
33309
33406
  warnedEscapes.add(file);
33310
- getLogger().warn(`Not serving '${flattenToOneLine(file)}': it is a symbolic link to '${flattenToOneLine(realFile)}', outside the origin directory '${flattenToOneLine(root)}' the asset manifest named. Answering as if the key did not exist.`);
33407
+ getLogger().warn(`Not serving ${displayUntrustedValue(file)}: it is a symbolic link to ${displayUntrustedValue(realFile)}, outside the origin directory ${displayUntrustedValue(root)} the asset manifest named. Answering as if the key did not exist.`);
33311
33408
  }
33312
33409
  return false;
33313
33410
  }
@@ -33346,7 +33443,7 @@ function isExistingFile(p) {
33346
33443
  function warnHiddenKey(dir, key, resolved) {
33347
33444
  if (warnedHiddenKeys.has(resolved)) return;
33348
33445
  warnedHiddenKeys.add(resolved);
33349
- getLogger().warn(`Not serving '${flattenToOneLine(key)}' from '${flattenToOneLine(dir)}': it is a hidden entry in a cdk synth --no-staging source folder (your own tree, not a staged asset). Answering as if the key did not exist.`);
33446
+ getLogger().warn(`Not serving ${displayUntrustedValue(key)} from ${displayUntrustedValue(dir)}: it is a hidden entry in a cdk synth --no-staging source folder (your own tree, not a staged asset). Answering as if the key did not exist.`);
33350
33447
  }
33351
33448
  /** Hidden keys already warned about. */
33352
33449
  const warnedHiddenKeys = /* @__PURE__ */ new Set();
@@ -33824,9 +33921,9 @@ function createS3OriginReader(bucketName, options = {}) {
33824
33921
  };
33825
33922
  if (direct.kind === "denied" && !deniedWarned) {
33826
33923
  deniedWarned = true;
33827
- getLogger().warn(`S3 denied reading '${flattenToOneLine(key)}' from bucket '${flattenToOneLine(bucketName)}'. If this is an OAC-locked / private bucket your credentials cannot read, point the origin at a local directory with --origin <originId>=<dir> (or use credentials with s3:GetObject on the bucket). ${getEmbedConfig().cliName} start-cloudfront reads the origin from real S3.`);
33924
+ getLogger().warn(`S3 denied reading ${displayUntrustedValue(key)} from bucket ${displayUntrustedValue(bucketName)}. If this is an OAC-locked / private bucket your credentials cannot read, point the origin at a local directory with --origin <originId>=<dir> (or use credentials with s3:GetObject on the bucket). ${getEmbedConfig().cliName} start-cloudfront reads the origin from real S3.`);
33828
33925
  }
33829
- if (direct.kind === "error") getLogger().warn(`S3 read of '${flattenToOneLine(key)}' from bucket '${flattenToOneLine(bucketName)}' failed: ${direct.message}`);
33926
+ if (direct.kind === "error") getLogger().warn(`S3 read of ${displayUntrustedValue(key)} from bucket ${displayUntrustedValue(bucketName)} failed: ${direct.message}`);
33830
33927
  }
33831
33928
  for (const candidate of resolveErrorResponseCandidates(input.customErrorResponses)) {
33832
33929
  const page = await fetchObject(candidate.errorKey);
@@ -33835,7 +33932,7 @@ function createS3OriginReader(bucketName, options = {}) {
33835
33932
  headers: { "content-type": contentTypeForKey(candidate.errorKey) },
33836
33933
  body: page.body
33837
33934
  };
33838
- if (page.kind === "denied" || page.kind === "error") getLogger().warn(`S3 could not read custom-error page '${flattenToOneLine(candidate.errorKey)}' from bucket '${flattenToOneLine(bucketName)}' (${page.kind}); falling through.`);
33935
+ if (page.kind === "denied" || page.kind === "error") getLogger().warn(`S3 could not read custom-error page ${displayUntrustedValue(candidate.errorKey)} from bucket ${displayUntrustedValue(bucketName)} (${page.kind}); falling through.`);
33839
33936
  }
33840
33937
  return {
33841
33938
  statusCode: 404,
@@ -41684,4 +41781,4 @@ function addStudioSpecificOptions(cmd) {
41684
41781
 
41685
41782
  //#endregion
41686
41783
  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 };
41687
- //# sourceMappingURL=local-studio-BbLfdSy4.js.map
41784
+ //# sourceMappingURL=local-studio-Cdq8klwg.js.map