cdk-local 0.147.18 → 0.147.19

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.
@@ -245,15 +245,84 @@ function buildStsClientConfig(args) {
245
245
  * SCOPE, stated so a later sweep finds a decision rather than an oversight:
246
246
  * this module governs those nine plus sigv4-verify's one, and its
247
247
  * {@link flattenToOneLine} additionally covers every other wire-derived value
248
- * printed on those lines (the role ARN, on the failure AND success paths). It
249
- * does NOT yet govern the AWS SDK error relays elsewhere under
250
- * `src/local/**` notably
251
- * `formatAwsErrorForWarn` (`cfn-local-state-provider.ts`, six warn sites and
252
- * one non-warn caller)
253
- * and `formatSsmError` (`ssm-parameter-resolver.ts`), which still print an
254
- * UNCLAMPED wire-derived `err.name`. Those are enumerated and tracked in
255
- * issue #579; they were left out here so a cross-cutting refactor of eight
256
- * more files would not share a review with the policy that justifies it.
248
+ * printed on those lines (the role ARN, on the failure AND success paths).
249
+ *
250
+ * Issue #579 extended it to the AWS SDK error relays elsewhere under
251
+ * `src/local/**` (plus `local-studio.ts`'s image-context warn), so the policy
252
+ * is no longer scoped to `src/cli/commands/**`:
253
+ * `cfn-local-state-provider.ts` (`formatAwsErrorForWarn`, six callers),
254
+ * `ssm-parameter-resolver.ts` (whose `formatSsmError` was DELETED rather than
255
+ * fixed it was a second spelling of the same forging vector),
256
+ * `state-resolver.ts`, `cloudfront-kvs-client.ts`, `cloudfront-s3-origin.ts`,
257
+ * `layer-arn-materializer.ts`, `ecr-puller.ts`, `ecs-secrets-resolver.ts` and
258
+ * `httpv2-service-integration.ts`. Each was decided on the SAME two axes below
259
+ * rather than rewritten mechanically, and a `catch` around a purely LOCAL
260
+ * operation was left alone: `agentcore-s3-bundle.ts`'s unzip and
261
+ * `layer-arn-materializer.ts`'s presigned-URL download / unzip see no
262
+ * credential chain and no service response, so there is nothing here for them.
263
+ *
264
+ * The test is what a `catch` CAN SEE, and applying it by LOCATION instead is
265
+ * how the sweep first got `agentcore-s3-bundle.ts` wrong. Its `try` does wrap
266
+ * `unzipSync` and nothing else — but that made the S3 `GetObject` above it
267
+ * UNCOVERED, not out of scope: it had no `catch` at all, so the raw SDK error
268
+ * propagated to `withErrorHandling` -> `formatError`, which prints
269
+ * `${error.name}: ${error.message}` unflattened and unclamped at DEFAULT
270
+ * level, and the default credential chain is what a run without
271
+ * `--assume-role` uses. "Outside that `catch`" is not "outside the sweep";
272
+ * ask which populations reach the site, and if the answer is "nothing catches
273
+ * it here", follow it to the frame that does.
274
+ *
275
+ * #579 also widened what LEVEL means, and the widening is the useful part:
276
+ * `httpv2-service-integration.ts`'s relay is not a log line at all, it is a
277
+ * served HTTP RESPONSE BODY — the widest reader in the sweep, since it needs
278
+ * neither `--verbose` nor access to the terminal, and the studio capture proxy
279
+ * records it onto the timeline besides. The axis is about READERS, not about
280
+ * logs.
281
+ *
282
+ * TWO CALLING CONVENTIONS this imposes, both found by #579 rather than
283
+ * anticipated here:
284
+ *
285
+ * 1. Render each failure ONCE. {@link describeAwsFailureForWarn} EMITS the
286
+ * `debug` line, so calling it twice for one error prints that line twice
287
+ * (`cfn-local-state-provider.ts`'s `load` did, having rendered the same
288
+ * failure for its warn and for `lastLoadError`).
289
+ * 2. Re-raise cdk-local's OWN throws ABOVE the relay, with an identifiable
290
+ * class. The discriminator is positive, so a `catch` that also catches
291
+ * the caller's own `throw new Error('...')` withholds text cdk-local
292
+ * wrote itself — which is the diagnostic loss this file's
293
+ * {@link describeCredentialLoadFailure} note calls "a real diagnostic
294
+ * loss, accepted". #579 stopped accepting it where it was cheap not to:
295
+ * `layer-arn-materializer.ts`'s ARN-shape and missing-`Content.Location`
296
+ * guards became `LayerMaterializationError`s and are re-raised, and
297
+ * `httpv2-service-integration.ts`'s missing-RequestParameter 400 became a
298
+ * `ServiceIntegrationRequestError` so an actionable 400 body did not
299
+ * degrade into a class name and a character count.
300
+ *
301
+ * TWO CATCH-ALL relays remain outside it, and both are UNCOVERED. An earlier
302
+ * revision of this note gave them opposite verdicts in adjacent paragraphs —
303
+ * clearing one and rejecting the other on the same evidence — which was simply
304
+ * wrong about the first, so both now read the same way:
305
+ *
306
+ * - `cloudfront-server.ts:129`'s `Request handling failed: ${err.message}`.
307
+ * It is what makes `cloudfront-kvs-client.ts`'s throw a DEFAULT-level
308
+ * line, and for THAT sub-path the text arrives pre-sanitized — but it is a
309
+ * catch-all over the whole request pipeline, so an origin fetch, an
310
+ * edge-function invoke or an SDK error raised anywhere below it still
311
+ * reaches the line RAW.
312
+ * - `ecs-service-emulator.ts`'s `logger.error` at three sites (`:909`,
313
+ * `:1508`, `:1599`). One of them does carry
314
+ * `ecs-secrets-resolver.ts`'s `EcsSecretsResolutionError`, which this
315
+ * module sanitized on the way in — and reasoning from that to "the relay
316
+ * is safe" is the SAME generalisation the bullet above rejects. All three
317
+ * are catch-alls over a whole reload or replica roll, relaying
318
+ * `err instanceof Error ? err.message : String(err)`, and what they
319
+ * usually carry is docker CLI stderr, which is multi-line.
320
+ *
321
+ * So: one sub-path arriving pre-sanitized never clears a catch-all. Both need
322
+ * the same per-occurrence call as everything else in this list. Neither is
323
+ * done here — `cloudfront-server.ts` is out of this change's scope, and the
324
+ * emulator's three are a different population (docker output, not SDK errors)
325
+ * that wants the flattening half rather than the withholding half.
257
326
  *
258
327
  * # The two axes that decide it
259
328
  *
@@ -547,13 +616,27 @@ function sanitizeServiceExceptionMessage(message) {
547
616
  * echo, plus the clamped {@link clampErrorCode} when the throw carries one.
548
617
  *
549
618
  * Withholding is NOT free, and the cost lands on cdk-local's own text as well
550
- * as on the SDK's: `role-arn.ts`'s
551
- * `AssumeRole(<arn>) returned no usable credentials.` is a plain `Error` with
552
- * no `$fault`, so it is withheld like any other. That is a real diagnostic
553
- * loss, accepted because the alternative — an allow-list of messages that may
554
- * print — is the deny-list this design rejects, wearing the other sign. The
555
- * fix is to make cdk-local's own throws identifiable rather than to guess at
556
- * their text; it is not done here, and is noted in issue #579.
619
+ * as on the SDK's. The worked example used to be `role-arn.ts`'s
620
+ * `AssumeRole(<arn>) returned no usable credentials.`: a plain `Error` with no
621
+ * `$fault`, so it was withheld like any other, and the note recorded that as a
622
+ * real diagnostic loss accepted because the alternative — an allow-list of
623
+ * messages that may print — is the deny-list this design rejects, wearing the
624
+ * other sign. It also named the right fix: make cdk-local's own throws
625
+ * IDENTIFIABLE rather than guess at their text.
626
+ *
627
+ * Issue #579 did that, for exactly this example. `role-arn.ts` now throws
628
+ * `AssumeRoleFailure`, carrying a `detail` its relaying callers print verbatim
629
+ * — and the same pattern closed the identical loss in
630
+ * `layer-arn-materializer.ts`, `httpv2-service-integration.ts`,
631
+ * `ecr-puller.ts`, `local-run-task.ts` and `ecs-service-emulator.ts`. The
632
+ * general point stands and is what a new site should apply: the policy cannot
633
+ * tell cdk-local's own sentence from a hostile endpoint's, so a `catch` that
634
+ * can see both must make its own throws recognisable BEFORE the relay.
635
+ *
636
+ * The remaining ARN-length question — an ARN is structured, so the bound
637
+ * belongs at the three resolution points rather than at the log line — is
638
+ * tracked separately at
639
+ * https://github.com/go-to-k/cdk-local/issues/607.
557
640
  *
558
641
  * The LENGTH is reported, unlike in `ecs-secrets-resolver.ts`, and the
559
642
  * difference is deliberate: there the user already knows which secret it is
@@ -622,6 +705,54 @@ function describeAwsFailureForWarn(err, operation) {
622
705
  //#endregion
623
706
  //#region src/utils/role-arn.ts
624
707
  /**
708
+ * {@link assumeRoleCredentials} failed, and the failure is cdk-local's OWN
709
+ * rendering of it rather than a raw SDK error.
710
+ *
711
+ * Exists because this helper has TWO kinds of caller and they need opposite
712
+ * things (issue #579 review round 4, found by #570's own harness going red):
713
+ *
714
+ * - THREE paths are unguarded all the way to `formatError`
715
+ * (`local-start-api`'s `assumeLambdaExecutionRole`, and `assumeTaskRole` in
716
+ * both `local-run-task` and `ecs-service-emulator`). They need this helper
717
+ * to render the failure, which is why it no longer propagates unwrapped.
718
+ * - ONE path -- `local-invoke.ts`'s `resolveLambdaContainerEnv` -- ALREADY
719
+ * renders it, guarded by the #570 lane. Wrapping for the first group broke
720
+ * that one: the outer `describeAwsFailureForWarn` saw a plain `Error`
721
+ * (no `$fault`, because it is cdk-local's own), withheld the
722
+ * already-sanitized text, and turned `ExpiredTokenException: The security
723
+ * token ... is expired` into `Error; 138-character message withheld`. The
724
+ * policy withholding its OWN output is the worst of both branches.
725
+ *
726
+ * So `message` carries the framed sentence for the first group, and `detail`
727
+ * carries the bare half for a caller that supplies its own framing. A relay
728
+ * that re-renders `detail` would double-withhold it again; print it verbatim.
729
+ *
730
+ * BOTH of this helper's throws use it (issue #579 review round 5). The
731
+ * no-usable-credentials sibling was left a bare `Error` in round 4 and carried
732
+ * the identical defect one throw over: at `local-invoke.ts` it missed the
733
+ * `instanceof` test and rendered as `Error; 47-character message withheld`.
734
+ * That is the exact loss `credential-error.ts` documents as accepted and
735
+ * points here to fix, so it is fixed here rather than described again.
736
+ *
737
+ * NOT thrown when `makeError` is supplied -- that caller asked for its own
738
+ * class, and every `makeError` path today is in the unguarded group.
739
+ */
740
+ var AssumeRoleFailure = class AssumeRoleFailure extends Error {
741
+ /**
742
+ * The bare half, with no `AssumeRole(<arn>) ...` framing, safe to print
743
+ * VERBATIM on a line that adds its own. Already policy-rendered when the
744
+ * cause was an SDK error; cdk-local's own literal otherwise. Never re-render
745
+ * it -- that is what double-withholds.
746
+ */
747
+ detail;
748
+ constructor(message, detail) {
749
+ super(message);
750
+ this.name = "AssumeRoleFailure";
751
+ this.detail = detail;
752
+ Object.setPrototypeOf(this, AssumeRoleFailure.prototype);
753
+ }
754
+ };
755
+ /**
625
756
  * Issue an STS AssumeRole for `roleArn` and return the temporary
626
757
  * credentials (issue #509).
627
758
  *
@@ -635,33 +766,65 @@ function describeAwsFailureForWarn(err, operation) {
635
766
  *
636
767
  * `sessionNameSuffix` distinguishes the callers in CloudTrail
637
768
  * (`<resourceNamePrefix>-<suffix>-<epochMs>`). `makeError` lets a
638
- * caller surface the no-usable-credentials failure as its own error
639
- * class (the ECS emulator throws `LocalStartServiceError`); STS
640
- * transport errors always propagate unwrapped.
769
+ * caller surface the failure as its own error class (the ECS emulator
770
+ * throws `LocalStartServiceError`).
771
+ *
772
+ * CHANGED by issue #579 review round 4: "STS transport errors always
773
+ * propagate unwrapped" is no longer true, and it was the last instance of the
774
+ * `try { send } finally { destroy }` with no `catch` that the derived
775
+ * population found. Propagating unwrapped meant the raw SDK error reached
776
+ * `withErrorHandling` -> `formatError`, which prints
777
+ * `${error.name}: ${error.message}` UNFLATTENED and UNCLAMPED at DEFAULT
778
+ * level — so a `CredentialsProviderError` carrying a `credential_process`
779
+ * command line landed there verbatim, from every `--assume-role` /
780
+ * `--assume-task-role` path in the CLI. This is the file `/review-pr`'s
781
+ * `UP_PATHS` names as credential-material surface, which is exactly why it is
782
+ * the wrong place to leave the relay open.
783
+ *
784
+ * The no-usable-credentials check sits OUTSIDE the `try` rather than being
785
+ * re-raised through an `instanceof` guard, because `makeError` mints the
786
+ * CALLER's class and there is no sentinel this function could test for.
787
+ * `sts.destroy()` still runs first — the `finally` fires on the way out of the
788
+ * `try`, and the response has already been read by then.
789
+ *
790
+ * What that buys is narrower than an earlier revision of this note claimed,
791
+ * and the overclaim is worth correcting rather than deleting: being outside the
792
+ * `try` means this function never routes its own text through the withholding
793
+ * policy, which is what the THREE `formatError` paths need — they print
794
+ * `message` directly. It does NOT help a caller that RE-RENDERS the error,
795
+ * because such a caller tests a class rather than a line number. That is what
796
+ * {@link AssumeRoleFailure} is for, and why BOTH throws below use it.
641
797
  */
642
798
  async function assumeRoleCredentials(opts) {
643
799
  const sts = new STSClient(buildStsClientConfig({
644
800
  region: opts.region,
645
801
  profile: opts.profile
646
802
  }));
803
+ const shownArn = flattenToOneLine(opts.roleArn);
804
+ let response;
647
805
  try {
648
- const creds = (await sts.send(new AssumeRoleCommand({
806
+ response = await sts.send(new AssumeRoleCommand({
649
807
  RoleArn: opts.roleArn,
650
808
  RoleSessionName: `${getEmbedConfig().resourceNamePrefix}-${opts.sessionNameSuffix}-${Date.now()}`,
651
809
  DurationSeconds: 3600
652
- }))).Credentials;
653
- if (!creds?.AccessKeyId || !creds.SecretAccessKey || !creds.SessionToken) {
654
- const message = `AssumeRole(${opts.roleArn}) returned no usable credentials.`;
655
- throw opts.makeError ? opts.makeError(message) : new Error(message);
656
- }
657
- return {
658
- accessKeyId: creds.AccessKeyId,
659
- secretAccessKey: creds.SecretAccessKey,
660
- sessionToken: creds.SessionToken
661
- };
810
+ }));
811
+ } catch (err) {
812
+ const detail = describeAwsFailureForWarn(err, "STS AssumeRole");
813
+ const message = `AssumeRole(${shownArn}) failed: ${detail}`;
814
+ throw opts.makeError ? opts.makeError(message) : new AssumeRoleFailure(message, detail);
662
815
  } finally {
663
816
  sts.destroy();
664
817
  }
818
+ const creds = response.Credentials;
819
+ if (!creds?.AccessKeyId || !creds.SecretAccessKey || !creds.SessionToken) {
820
+ const message = `AssumeRole(${shownArn}) returned no usable credentials.`;
821
+ throw opts.makeError ? opts.makeError(message) : new AssumeRoleFailure(message, "the response carried no usable credentials");
822
+ }
823
+ return {
824
+ accessKeyId: creds.AccessKeyId,
825
+ secretAccessKey: creds.SecretAccessKey,
826
+ sessionToken: creds.SessionToken
827
+ };
665
828
  }
666
829
  /**
667
830
  * Resolve the role-arn argument (CLI flag or `CDKL_ROLE_ARN` env var) and,
@@ -692,27 +855,31 @@ async function applyRoleArnIfSet(opts) {
692
855
  const roleArn = opts.roleArn || process.env[`${getEmbedConfig().envPrefix}_ROLE_ARN`];
693
856
  if (!roleArn) return;
694
857
  const logger = getLogger().child("role-arn");
695
- logger.debug(`Assuming role ${roleArn}...`);
858
+ const shownArn = flattenToOneLine(roleArn);
859
+ logger.debug(`Assuming role ${shownArn}...`);
696
860
  const sts = new STSClient(buildStsClientConfig({
697
861
  region: opts.region,
698
862
  profile: opts.profile
699
863
  }));
864
+ let response;
700
865
  try {
701
- const response = await sts.send(new AssumeRoleCommand({
866
+ response = await sts.send(new AssumeRoleCommand({
702
867
  RoleArn: roleArn,
703
868
  RoleSessionName: `${getEmbedConfig().binaryName}-${Date.now()}`,
704
869
  DurationSeconds: 3600
705
870
  }));
706
- if (!response.Credentials) throw new Error(`AssumeRole returned no credentials for role ${roleArn}`);
707
- const { AccessKeyId, SecretAccessKey, SessionToken, Expiration } = response.Credentials;
708
- if (!AccessKeyId || !SecretAccessKey || !SessionToken) throw new Error(`AssumeRole response missing credentials fields for role ${roleArn}`);
709
- process.env["AWS_ACCESS_KEY_ID"] = AccessKeyId;
710
- process.env["AWS_SECRET_ACCESS_KEY"] = SecretAccessKey;
711
- process.env["AWS_SESSION_TOKEN"] = SessionToken;
712
- logger.info(`Assumed role ${roleArn} (session expires ${Expiration?.toISOString() ?? "unknown"})`);
871
+ } catch (err) {
872
+ throw new Error(`AssumeRole(${shownArn}) failed: ${describeAwsFailureForWarn(err, "STS AssumeRole")}`);
713
873
  } finally {
714
874
  sts.destroy();
715
875
  }
876
+ if (!response.Credentials) throw new Error(`AssumeRole returned no credentials for role ${shownArn}`);
877
+ const { AccessKeyId, SecretAccessKey, SessionToken, Expiration } = response.Credentials;
878
+ if (!AccessKeyId || !SecretAccessKey || !SessionToken) throw new Error(`AssumeRole response missing credentials fields for role ${shownArn}`);
879
+ process.env["AWS_ACCESS_KEY_ID"] = AccessKeyId;
880
+ process.env["AWS_SECRET_ACCESS_KEY"] = SecretAccessKey;
881
+ process.env["AWS_SESSION_TOKEN"] = SessionToken;
882
+ logger.info(`Assumed role ${shownArn} (session expires ${Expiration?.toISOString() ?? "unknown"})`);
716
883
  }
717
884
 
718
885
  //#endregion
@@ -2674,13 +2841,35 @@ async function dispatchSfnStopExecution(params, region) {
2674
2841
  async function dispatchAppConfigGetConfiguration(params, region) {
2675
2842
  return errorResponse(501, `AppConfig-GetConfiguration is recognized but ${getEmbedConfig().productName} does not yet bundle @aws-sdk/client-appconfig. Use the deployed API for this subtype, or open an issue if you need local emulation.`);
2676
2843
  }
2844
+ /**
2845
+ * A REQUEST-shape rejection raised by cdk-local itself, before any AWS call.
2846
+ *
2847
+ * Issue #579 — it needs its own class so {@link translateSdkError} can tell it
2848
+ * apart from a relayed SDK failure. Its message is cdk-local's own literal and
2849
+ * names the missing parameter, so it is the diagnosis and must reach the
2850
+ * client verbatim; the credential-error policy withholds every non-service
2851
+ * exception, which would otherwise have turned this 400 into a class name and
2852
+ * a character count. `credential-error.ts` names exactly this remedy: make
2853
+ * cdk-local's own throws identifiable rather than guess at their text.
2854
+ *
2855
+ * The ONE remaining cdk-local-authored throw inside the guarded `try` is
2856
+ * `getClient`'s `unknown service '<x>'`, and it is left withheld deliberately:
2857
+ * its `switch` is exhaustive over the dispatched subtypes, so the arm is dead
2858
+ * code guarding against a future subtype added without a client case. If it
2859
+ * ever does fire, the developer who added that subtype reads the full text on
2860
+ * the `debug` line the helper emits.
2861
+ */
2862
+ var ServiceIntegrationRequestError = class ServiceIntegrationRequestError extends Error {
2863
+ statusCode = 400;
2864
+ constructor(message) {
2865
+ super(message);
2866
+ this.name = "ServiceIntegrationRequestError";
2867
+ Object.setPrototypeOf(this, ServiceIntegrationRequestError.prototype);
2868
+ }
2869
+ };
2677
2870
  function requireParams(params, required) {
2678
2871
  const missing = required.filter((k) => !params[k] || params[k].trim() === "");
2679
- if (missing.length > 0) {
2680
- const err = /* @__PURE__ */ new Error(`missing required RequestParameter(s): ${missing.join(", ")}`);
2681
- err.statusCode = 400;
2682
- throw err;
2683
- }
2872
+ if (missing.length > 0) throw new ServiceIntegrationRequestError(`missing required RequestParameter(s): ${missing.join(", ")}`);
2684
2873
  }
2685
2874
  function splitCsv(value) {
2686
2875
  return value.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
@@ -2726,14 +2915,38 @@ function errorResponse(statusCode, message) {
2726
2915
  * Translate an AWS SDK error to an HTTP response. AWS SDK v3 surfaces
2727
2916
  * errors as instances carrying `$metadata.httpStatusCode` + `name`;
2728
2917
  * we honor the status code when present, default to 500.
2729
- */
2918
+ *
2919
+ * Issue #579 routed the two relayed fields through the credential-error
2920
+ * policy. LEVEL: this one is NOT a log line — it is a RESPONSE BODY served on
2921
+ * the local API port, which makes it the widest reader of the set: the studio
2922
+ * capture proxy records a served response body onto the timeline, so the text
2923
+ * reaches the same browser the log ring does, and any HTTP client reaches it
2924
+ * without `--verbose`. RECONSTRUCTION: the `catch` wraps the per-subtype
2925
+ * dispatch, i.e. one `client.send(...)`, so a `CredentialsProviderError`
2926
+ * carrying a `credential_process` command line would have been JSON-encoded
2927
+ * into that body verbatim. A modeled service exception's message is what a
2928
+ * real API Gateway service integration surfaces and is the diagnosis, so it
2929
+ * keeps printing; `code` is `err.name`, wire-derived, and is clamped.
2930
+ */
2931
+ /** A value usable as an HTTP status line without raising `ERR_HTTP_INVALID_STATUS_CODE`. */
2932
+ function isHttpStatus(value) {
2933
+ return typeof value === "number" && Number.isInteger(value) && value >= 100 && value < 600;
2934
+ }
2730
2935
  function translateSdkError(subtype, err) {
2936
+ if (err instanceof ServiceIntegrationRequestError) return {
2937
+ statusCode: err.statusCode,
2938
+ body: JSON.stringify({
2939
+ message: err.message,
2940
+ code: err.name
2941
+ }),
2942
+ headers: { "content-type": "application/json" }
2943
+ };
2731
2944
  if (err && typeof err === "object") {
2732
2945
  const e = err;
2733
- const status = typeof e.statusCode === "number" && e.statusCode >= 100 && e.statusCode < 600 ? e.statusCode : e.$metadata?.httpStatusCode ?? 500;
2946
+ const status = isHttpStatus(e.statusCode) ? e.statusCode : isHttpStatus(e.$metadata?.httpStatusCode) ? e.$metadata.httpStatusCode : 500;
2734
2947
  const body = {
2735
- message: e.message ?? "AWS SDK call failed",
2736
- code: e.name ?? "UnknownError"
2948
+ message: isAwsServiceException(err) ? sanitizeServiceExceptionMessage(typeof e.message === "string" ? e.message : stringifyThrown(err)) : describeAwsFailureForWarn(err, `${subtype} service integration`),
2949
+ code: clampErrorName(err)
2737
2950
  };
2738
2951
  logger.debug(`[${subtype}] SDK error (${status}): ${stringifyValue(body)}`);
2739
2952
  return {
@@ -2742,7 +2955,7 @@ function translateSdkError(subtype, err) {
2742
2955
  headers: { "content-type": "application/json" }
2743
2956
  };
2744
2957
  }
2745
- return errorResponse(500, `Unexpected error invoking ${subtype}: ${String(err)}`);
2958
+ return errorResponse(500, `Unexpected error invoking ${subtype}: ${describeAwsFailureForWarn(err, `${subtype} service integration`)}`);
2746
2959
  }
2747
2960
  /**
2748
2961
  * Apply HTTP API v2 `ResponseParameters` mapping (per AWS docs:
@@ -4709,7 +4922,7 @@ async function resolveSsmParameters(client, refs, label) {
4709
4922
  const invalid = resp.InvalidParameters ?? [];
4710
4923
  if (invalid.length > 0) logger.warn(`${label}: SSM GetParameters reported invalid parameter name(s): ${invalid.join(", ")}. Ref to the matching CloudFormation parameter(s) will warn-and-drop (was the SSM parameter created?).`);
4711
4924
  } catch (err) {
4712
- logger.warn(`${label}: SSM GetParameters(${names.join(", ")}) failed: ${formatSsmError(err)}. Ref to the matching CloudFormation parameter(s) will warn-and-drop (grant ssm:GetParameters or override via --env-vars).`);
4925
+ logger.warn(`${label}: SSM GetParameters(${flattenToOneLine(names.join(", "))}) failed: ${describeAwsFailureForWarn(err, "SSM GetParameters")}. Ref to the matching CloudFormation parameter(s) will warn-and-drop (grant ssm:GetParameters or override via --env-vars).`);
4713
4926
  continue;
4714
4927
  }
4715
4928
  for (const p of resolved) {
@@ -4725,17 +4938,6 @@ async function resolveSsmParameters(client, refs, label) {
4725
4938
  }
4726
4939
  return out;
4727
4940
  }
4728
- /**
4729
- * Format an SSM SDK error as `<name>: <message>` so the warn names the
4730
- * error class (e.g. `AccessDeniedException`, `ThrottlingException`).
4731
- * Mirrors `formatAwsErrorForWarn` in `cfn-local-state-provider.ts`.
4732
- * Exported for unit testing.
4733
- */
4734
- function formatSsmError(err) {
4735
- if (!(err instanceof Error)) return String(err);
4736
- const name = err.name && err.name !== "Error" ? err.name : void 0;
4737
- return name !== void 0 ? `${name}: ${err.message}` : err.message;
4738
- }
4739
4941
 
4740
4942
  //#endregion
4741
4943
  //#region src/local/cfn-local-state-provider.ts
@@ -4890,7 +5092,7 @@ var CfnLocalStateProvider = class {
4890
5092
  try {
4891
5093
  return (await client.send(new GetFunctionConfigurationCommand({ FunctionName: functionPhysicalId }))).Environment?.Variables ?? {};
4892
5094
  } catch (err) {
4893
- logger.warn(`${this.label}: GetFunctionConfiguration(${functionPhysicalId}) failed: ${formatAwsErrorForWarn(err)}. Intrinsic-valued env vars that need the deployed value will warn-and-drop (grant lambda:GetFunctionConfiguration or override via --env-vars).`);
5095
+ logger.warn(`${this.label}: GetFunctionConfiguration(${flattenToOneLine(functionPhysicalId)}) failed: ${formatAwsErrorForWarn(err, "Lambda GetFunctionConfiguration")}. Intrinsic-valued env vars that need the deployed value will warn-and-drop (grant lambda:GetFunctionConfiguration or override via --env-vars).`);
4894
5096
  return;
4895
5097
  }
4896
5098
  }
@@ -4913,7 +5115,7 @@ var CfnLocalStateProvider = class {
4913
5115
  if (typeof resp.Role === "string" && resp.Role.startsWith("arn:")) return resp.Role;
4914
5116
  return;
4915
5117
  } catch (err) {
4916
- logger.warn(`${this.label}: GetFunctionConfiguration(${functionPhysicalId}) for --assume-role auto-resolve failed: ${formatAwsErrorForWarn(err)}. Pass the ARN explicitly: --assume-role <arn>.`);
5118
+ logger.warn(`${this.label}: GetFunctionConfiguration(${flattenToOneLine(functionPhysicalId)}) for --assume-role auto-resolve failed: ${formatAwsErrorForWarn(err, "Lambda GetFunctionConfiguration (--assume-role auto-resolve)")}. Pass the ARN explicitly: --assume-role <arn>.`);
4917
5119
  return;
4918
5120
  }
4919
5121
  }
@@ -4936,7 +5138,7 @@ var CfnLocalStateProvider = class {
4936
5138
  if (typeof resp.roleArn === "string" && resp.roleArn.startsWith("arn:")) return resp.roleArn;
4937
5139
  return;
4938
5140
  } catch (err) {
4939
- logger.warn(`${this.label}: GetAgentRuntime(${runtimePhysicalId}) for --assume-role auto-resolve failed: ${formatAwsErrorForWarn(err)}. Pass the ARN explicitly: --assume-role <arn>.`);
5141
+ logger.warn(`${this.label}: GetAgentRuntime(${flattenToOneLine(runtimePhysicalId)}) for --assume-role auto-resolve failed: ${formatAwsErrorForWarn(err, "bedrock-agentcore-control GetAgentRuntime (--assume-role auto-resolve)")}. Pass the ARN explicitly: --assume-role <arn>.`);
4940
5142
  return;
4941
5143
  }
4942
5144
  }
@@ -4961,8 +5163,9 @@ var CfnLocalStateProvider = class {
4961
5163
  try {
4962
5164
  resourceMap = buildResourceStateMap(await fetchAllStackResources(client, this.cfnStackName));
4963
5165
  } catch (err) {
4964
- this.lastLoadError = `ListStackResources(${this.cfnStackName}) failed: ${formatAwsErrorForWarn(err)} (region='${this.region}')`;
4965
- logger.warn(`${this.label}: ListStackResources(${this.cfnStackName}) failed: ${formatAwsErrorForWarn(err)}. Was the stack deployed in region '${this.region}'? Falling back.`);
5166
+ const detail = formatAwsErrorForWarn(err, "CloudFormation ListStackResources");
5167
+ this.lastLoadError = `ListStackResources(${this.cfnStackName}) failed: ${detail} (region='${this.region}')`;
5168
+ logger.warn(`${this.label}: ListStackResources(${this.cfnStackName}) failed: ${detail}. Was the stack deployed in region '${this.region}'? Falling back.`);
4966
5169
  return;
4967
5170
  }
4968
5171
  let outputs;
@@ -4973,7 +5176,7 @@ var CfnLocalStateProvider = class {
4973
5176
  outputs = {};
4974
5177
  } else outputs = buildOutputsMap(stack.Outputs ?? []);
4975
5178
  } catch (err) {
4976
- logger.warn(`${this.label}: DescribeStacks(${this.cfnStackName}) failed: ${formatAwsErrorForWarn(err)}. Outputs will be empty (Fn::GetStackOutput cannot resolve).`);
5179
+ logger.warn(`${this.label}: DescribeStacks(${this.cfnStackName}) failed: ${formatAwsErrorForWarn(err, "CloudFormation DescribeStacks")}. Outputs will be empty (Fn::GetStackOutput cannot resolve).`);
4977
5180
  outputs = {};
4978
5181
  }
4979
5182
  return {
@@ -5023,7 +5226,7 @@ var CfnLocalStateProvider = class {
5023
5226
  const ensureExports = () => {
5024
5227
  if (exportsPromise) return exportsPromise;
5025
5228
  exportsPromise = fetchAllExports(client).catch((err) => {
5026
- logger.warn(`${label}: ListExports (${region}) failed: ${formatAwsErrorForWarn(err)}. Fn::ImportValue intrinsics will warn-and-drop.`);
5229
+ logger.warn(`${label}: ListExports (${region}) failed: ${formatAwsErrorForWarn(err, "CloudFormation ListExports")}. Fn::ImportValue intrinsics will warn-and-drop.`);
5027
5230
  });
5028
5231
  return exportsPromise;
5029
5232
  };
@@ -5163,24 +5366,36 @@ async function fetchAllExports(client) {
5163
5366
  * with bucket / region context), so the CFn provider extracts the
5164
5367
  * pieces directly here.
5165
5368
  *
5166
- * Issue #578 the result is FLATTENED TO ONE LINE via
5167
- * {@link flattenToOneLine}, the same helper `credential-error` applies to
5168
- * every other wire-derived value that lands on a log line, rather than a
5169
- * second spelling of it. Both `err.name` and `err.message` come off the wire,
5170
- * and this warn is relayed onto a `cdkl studio` serve child's stdout, where
5171
- * `studio-serve-manager` splits on `\n`: an embedded newline puts line 2 on
5172
- * the stream with no `WARN: ` prefix, so it clears the diagnostic bound and
5173
- * matches a ready pattern at `^`. One line in, one line out.
5174
- */
5175
- function formatAwsErrorForWarn(err) {
5176
- if (!(err instanceof Error)) return flattenToOneLine(String(err));
5177
- const name = err.name && err.name !== "Error" ? err.name : void 0;
5178
- const status = err.$metadata?.httpStatusCode;
5179
- const prefixParts = [];
5180
- if (name !== void 0) prefixParts.push(name);
5181
- if (status !== void 0) prefixParts.push(`HTTP ${status}`);
5182
- if (prefixParts.length === 0) return flattenToOneLine(err.message);
5183
- return flattenToOneLine(`${prefixParts.join(" ")}: ${err.message}`);
5369
+ * Issue #578 flattened the result to ONE LINE, because this warn is relayed
5370
+ * onto a `cdkl studio` serve child's stdout where `studio-serve-manager`
5371
+ * splits on `\n`: an embedded newline puts line 2 on the stream with no
5372
+ * `WARN: ` prefix, so it clears the diagnostic bound and matches a ready
5373
+ * pattern at `^`.
5374
+ *
5375
+ * Issue #579 finished the job. Flattening closed the forged-LINE half while
5376
+ * leaving the OTHER two open: the wire-derived `err.name` was interpolated
5377
+ * UNCLAMPED (`@aws-sdk/core` builds it from `x-amzn-errortype` with no length
5378
+ * cap and no character class), and `err.message` printed verbatim whatever
5379
+ * population it came from. LEVEL: every caller below is a DEFAULT-level warn.
5380
+ * RECONSTRUCTION: each `catch` wraps a `client.send(...)` CloudFormation,
5381
+ * Lambda, or bedrock-agentcore-control — which resolves the credential chain
5382
+ * before the request goes out, so the `catch` sees both a
5383
+ * `CredentialsProviderError` (whose message can be a `credential_process`
5384
+ * command line) and a modeled service exception whose message IS the
5385
+ * diagnosis. That is exactly {@link describeAwsFailureForWarn}'s split, so
5386
+ * this is now a thin decoration over it rather than a second spelling of the
5387
+ * policy: `operation` names the call for the `debug` line the helper emits on
5388
+ * the withheld branch.
5389
+ *
5390
+ * The HTTP status is KEPT and moved to a suffix. Unlike the name it is not
5391
+ * body-derived — the SDK reads it off the HTTP response line — but it is
5392
+ * still range-guarded to an integer status, since `$metadata` is a plain
5393
+ * object a hostile shape could carry anything on.
5394
+ */
5395
+ function formatAwsErrorForWarn(err, operation) {
5396
+ const described = describeAwsFailureForWarn(err, operation);
5397
+ const status = err?.$metadata?.httpStatusCode;
5398
+ return typeof status === "number" && Number.isInteger(status) && status >= 100 && status < 600 ? `${described} (HTTP ${status})` : described;
5184
5399
  }
5185
5400
 
5186
5401
  //#endregion
@@ -5398,6 +5613,109 @@ function toFlagName(field) {
5398
5613
  //#endregion
5399
5614
  //#region src/local/state-resolver.ts
5400
5615
  /**
5616
+ * State-driven env-var resolution for `cdkl invoke --from-state`.
5617
+ *
5618
+ * The PR 1 env-resolver classifies any non-literal env-var value (a CFn
5619
+ * intrinsic like `Ref` / `Fn::GetAtt` / `Fn::Sub`) as "unresolved" and
5620
+ * drops it. That's correct when there's no source of truth for the
5621
+ * deployed value — which is also the SAM behavior — but it's wrong when
5622
+ * cdk-local has already deployed the stack and the AWS-current physical IDs
5623
+ * sit in the cdk-local state file.
5624
+ *
5625
+ * `--from-state` closes that gap: it loads the host's S3 state for the target
5626
+ * stack and substitutes `Ref` / `Fn::GetAtt` / `Fn::Sub` placeholders in
5627
+ * the function's `Properties.Environment.Variables` with the deployed
5628
+ * values. The result feeds back into the existing
5629
+ * `resolveEnvVars(...)` pipeline so `--env-vars` overrides still take
5630
+ * precedence — that ordering matters because users routinely override a
5631
+ * single variable while using `--from-state` to recover the rest.
5632
+ *
5633
+ * Scope:
5634
+ *
5635
+ * - `Ref: <LogicalId>` — substituted with `state.resources[id].physicalId`.
5636
+ * - `Fn::GetAtt: [<LogicalId>, <attr>]` (and the `"LogicalId.attr"`
5637
+ * string form) — substituted with
5638
+ * `state.resources[id].attributes[attr]`. We deliberately do NOT
5639
+ * synthesize attributes the provider would normally compute (e.g.
5640
+ * IAM role ARNs derived from physicalId + accountId) — `--from-state`
5641
+ * surfaces only what cdk-local recorded at deploy time, which is what the
5642
+ * deployed Lambda's env actually saw.
5643
+ * - `Fn::Sub: '<template>'` (and the two-argument `[template, vars]`
5644
+ * form) — `${LogicalId}` / `${LogicalId.attr}` placeholders are
5645
+ * substituted in place; pseudo parameters (`${AWS::AccountId}` /
5646
+ * `${AWS::Region}` / `${AWS::Partition}` / `${AWS::URLSuffix}`) are
5647
+ * substituted from the optional `pseudoParameters` bag; unrelated
5648
+ * placeholders (mapping references, parameters) are left untouched
5649
+ * and the value is treated as unresolved.
5650
+ * - `Fn::Join: [<delimiter>, [<elements>...]]` — every element is
5651
+ * recursively resolved via this same module and joined with the
5652
+ * delimiter. Closes Gap 1 of issue #286 (the SSM Parameter
5653
+ * `ecs.Secret.fromSsmParameter` shape CDK synthesizes is
5654
+ * `Fn::Join` with pseudo-parameter `Ref`s + a `Ref` to the
5655
+ * parameter; without Fn::Join support the secret silently drops).
5656
+ * - `Fn::Select: [<index>, <list>]` — picks the indexed element of an
5657
+ * array. Index may be a number or a numeric string (CFn templates
5658
+ * often carry the string form). The list argument may be a literal
5659
+ * array of intrinsics OR an intrinsic that resolves to a
5660
+ * `string[]` (today: only `Fn::Split`). Out-of-bounds / negative /
5661
+ * non-finite index reports unresolved. Closes issue #636 — the
5662
+ * `Fn.select(N, Fn.split(':', secret.secretArn))` shape CDK
5663
+ * synthesizes for parsing secret ARN segments.
5664
+ * - `Fn::Split: [<delimiter>, <string>]` — splits a string into a
5665
+ * `string[]`. **Only valid INSIDE `Fn::Select`** — at the env-var
5666
+ * top level the result is an array which can't be an env-var
5667
+ * value, so the top-level dispatcher reports unresolved with a
5668
+ * clear reason.
5669
+ * - `Ref: AWS::AccountId` / `AWS::Region` / `AWS::Partition` /
5670
+ * `AWS::URLSuffix` — substituted from the optional
5671
+ * `pseudoParameters` bag the caller supplies. When the bag is
5672
+ * missing (or the specific key isn't set), the placeholder reports
5673
+ * unresolved — same warn-and-drop policy as every other miss.
5674
+ *
5675
+ * Cross-stack intrinsics (when the caller supplies a `crossStackResolver`
5676
+ * on the `SubstitutionContext` via the async API):
5677
+ *
5678
+ * - `Fn::ImportValue: '<exportName>'` (or an intrinsic-valued argument
5679
+ * that resolves to a string against state + pseudo parameters) —
5680
+ * looked up via `crossStackResolver.resolveImport(exportName)`. The
5681
+ * resolver typically reads the host's persistent exports index,
5682
+ * falling back to a per-stack state.json scan on index miss
5683
+ * (closes issue #454).
5684
+ * - `Fn::GetStackOutput: { StackName, OutputName, Region? }` — looked
5685
+ * up via `crossStackResolver.resolveGetStackOutput(stackName, region,
5686
+ * outputName)`. The resolver typically reads the producer stack's
5687
+ * state.json from S3 directly. `Region` defaults to the consumer's
5688
+ * deploy region when omitted.
5689
+ *
5690
+ * Both resolvers return `string | undefined`; an `undefined` value
5691
+ * reports unresolved per the standard warn-and-drop policy. Cross-account
5692
+ * `Fn::GetStackOutput.RoleArn` is rejected at the resolver layer (cdk-local
5693
+ * uses S3 state, not CloudFormation; cross-account would require
5694
+ * assuming the role and reading the producer account's separate state
5695
+ * bucket — tracked under #449).
5696
+ *
5697
+ * Only the async API (`substituteAgainstStateAsync` /
5698
+ * `substituteEnvVarsFromStateAsync`) handles these. The legacy sync API
5699
+ * surfaces them as `unresolved` ("unsupported intrinsic") so existing
5700
+ * callers (e.g. `ecs-task-resolver.ts`'s sync container parsing) stay
5701
+ * unchanged and the per-key warn-and-drop UX still fires.
5702
+ *
5703
+ * Out of scope (deferred):
5704
+ *
5705
+ * - Cross-region `Fn::ImportValue` (tracked under #451).
5706
+ * - Cross-account `Fn::GetStackOutput.RoleArn` (tracked under #449).
5707
+ * - Other intrinsics (`Fn::If`, `Fn::FindInMap`, etc.). Anything
5708
+ * beyond the supported set is reported as unresolved and the env
5709
+ * var is dropped, matching PR 1's "warn-and-drop" semantics.
5710
+ *
5711
+ * Failure mode: per-key best-effort. When a substitution can't be
5712
+ * produced (state missing for the referenced logical ID, attribute not
5713
+ * captured at deploy time, unsupported intrinsic in `Fn::Sub`), the key
5714
+ * is reported as unresolved and the caller drops it from the env block
5715
+ * with a warn. We never throw out of substitution — a bad reference in
5716
+ * one env var must not abort the whole `cdkl invoke` call.
5717
+ */
5718
+ /**
5401
5719
  * Substitute a single env-var / secret-ValueFrom value (which may be a
5402
5720
  * CFn intrinsic) against the provided state-recorded resources map and
5403
5721
  * optional pseudo-parameter bag.
@@ -5842,7 +6160,7 @@ async function resolveImportValueAsync(arg, context) {
5842
6160
  } catch (err) {
5843
6161
  return {
5844
6162
  kind: "unresolved",
5845
- reason: `Fn::ImportValue '${exportName}': lookup failed: ${err instanceof Error ? err.message : String(err)}`
6163
+ reason: `Fn::ImportValue '${flattenToOneLine(exportName)}': lookup failed: ${describeAwsFailureForWarn(err, "CrossStackResolver.resolveImport (Fn::ImportValue)")}`
5846
6164
  };
5847
6165
  }
5848
6166
  if (resolved === void 0) return {
@@ -5917,7 +6235,7 @@ async function resolveGetStackOutputAsync(arg, context) {
5917
6235
  } catch (err) {
5918
6236
  return {
5919
6237
  kind: "unresolved",
5920
- reason: `Fn::GetStackOutput '${stackName}.${outputName}' (${region}): lookup failed: ${err instanceof Error ? err.message : String(err)}`
6238
+ reason: `Fn::GetStackOutput '${flattenToOneLine(stackName)}.${flattenToOneLine(outputName)}' (${flattenToOneLine(region)}): lookup failed: ${describeAwsFailureForWarn(err, "CrossStackResolver.resolveGetStackOutput (Fn::GetStackOutput)")}`
5921
6239
  };
5922
6240
  }
5923
6241
  if (resolved === void 0) return {
@@ -7406,6 +7724,9 @@ async function pullEcrImage(imageUri, options) {
7406
7724
  if (!identity.Account) throw new LocalInvokeBuildError("STS GetCallerIdentity returned no Account. Verify your AWS credentials.");
7407
7725
  callerAccount = identity.Account;
7408
7726
  CALLER_IDENTITY_CACHE.set(callerIdentityKey, callerAccount);
7727
+ } catch (err) {
7728
+ if (err instanceof LocalInvokeBuildError) throw err;
7729
+ throw new LocalInvokeBuildError(`STS GetCallerIdentity failed while preparing the ECR pull: ${describeAwsFailureForWarn(err, "STS GetCallerIdentity (ECR pull)")}. Verify your AWS credentials.`);
7409
7730
  } finally {
7410
7731
  sts.destroy();
7411
7732
  }
@@ -7471,7 +7792,8 @@ async function assumeRoleForEcr(roleArn, callerRegion, profile, logger) {
7471
7792
  };
7472
7793
  } catch (err) {
7473
7794
  if (err instanceof LocalInvokeBuildError) throw err;
7474
- throw new LocalInvokeBuildError(`Failed to assume role ${roleArn} for ECR pull: ${err instanceof Error ? err.message : String(err)}. Verify the role exists and its trust policy permits the caller's identity to assume it.`);
7795
+ const reason = describeAwsFailureForWarn(err, "STS AssumeRole (ECR pull)");
7796
+ throw new LocalInvokeBuildError(`Failed to assume role ${flattenToOneLine(roleArn)} for ECR pull: ${reason}. Verify the role exists and its trust policy permits the caller's identity to assume it.`);
7475
7797
  } finally {
7476
7798
  sts.destroy();
7477
7799
  }
@@ -7483,7 +7805,9 @@ async function assumeRoleForEcr(roleArn, callerRegion, profile, logger) {
7483
7805
  */
7484
7806
  async function ecrLogin(client, accountId, region) {
7485
7807
  getLogger().child("ecr-puller").debug(`ECR login (account=${accountId}, region=${region})`);
7486
- const authData = (await client.send(new GetAuthorizationTokenCommand({}))).authorizationData?.[0];
7808
+ const authData = (await client.send(new GetAuthorizationTokenCommand({})).catch((err) => {
7809
+ throw new LocalInvokeBuildError(`ECR GetAuthorizationToken failed (account=${accountId}, region=${region}): ${describeAwsFailureForWarn(err, "ECR GetAuthorizationToken")}. Verify the credentials can call ecr:GetAuthorizationToken.`);
7810
+ })).authorizationData?.[0];
7487
7811
  if (!authData?.authorizationToken) throw new LocalInvokeBuildError("Failed to get ECR authorization token");
7488
7812
  const [username, password] = Buffer.from(authData.authorizationToken, "base64").toString().split(":");
7489
7813
  if (!username || password === void 0) throw new LocalInvokeBuildError("ECR authorization token has unexpected shape (missing username/password)");
@@ -15942,6 +16266,45 @@ var LayerMaterializationError = class LayerMaterializationError extends Error {
15942
16266
  Object.setPrototypeOf(this, LayerMaterializationError.prototype);
15943
16267
  }
15944
16268
  };
16269
+ /**
16270
+ * cdk-local's OWN rejection raised INSIDE a guarded region, still needing the
16271
+ * call site's framing (issue #579, review round 2).
16272
+ *
16273
+ * Two classes are needed, not one, because the guarded regions contain BOTH
16274
+ * kinds of cdk-local throw and they want opposite handling:
16275
+ *
16276
+ * - ALREADY FRAMED — the ARN-shape guard raises
16277
+ * `Layer <arn>: not a layer-version ARN ...`, which names the layer and the
16278
+ * problem and fires before any AWS call. Re-raising it INTACT is right;
16279
+ * wrapping it would double the `Layer <arn>:` prefix and, worse, prepend
16280
+ * `GetLayerVersion failed` to a failure where no call was ever made.
16281
+ * - UNFRAMED — `AssumeRole returned no Credentials` and
16282
+ * `GetLayerVersion response did not include Content.Location` are bare
16283
+ * sentences that mean nothing without the site's `Layer <arn>: <call>
16284
+ * failed: ... <remedy>` envelope.
16285
+ *
16286
+ * A first pass at #579 made both `LayerMaterializationError` and re-raised
16287
+ * both, which silently DROPPED the envelope (and the `looksLikeAccessDenied`
16288
+ * hint) from the second group. The existing tests only substring-match the
16289
+ * inner sentence, so nothing went red. Hence a distinct type rather than a
16290
+ * shared one: the catch keeps the message verbatim — it is cdk-local's own
16291
+ * text, never anything off the wire, so the credential-error policy has
16292
+ * nothing to decide about it — and still applies the framing.
16293
+ */
16294
+ var UnframedLayerError = class UnframedLayerError extends Error {
16295
+ constructor(message) {
16296
+ super(message);
16297
+ this.name = "UnframedLayerError";
16298
+ Object.setPrototypeOf(this, UnframedLayerError.prototype);
16299
+ }
16300
+ };
16301
+ /**
16302
+ * The detail half of a layer failure message: cdk-local's own text kept
16303
+ * verbatim, anything else rendered by the shared credential-error policy.
16304
+ */
16305
+ function layerFailureDetail(err, operation) {
16306
+ return err instanceof UnframedLayerError ? err.message : describeAwsFailureForWarn(err, operation);
16307
+ }
15945
16308
  async function materializeLayerFromArn(layer, options = {}) {
15946
16309
  const logger = getLogger();
15947
16310
  let credentials;
@@ -15949,14 +16312,16 @@ async function materializeLayerFromArn(layer, options = {}) {
15949
16312
  credentials = await assumeRoleForLayer(options.roleArn, layer.region, options);
15950
16313
  logger.debug(`Layer ${layer.arn}: assumed role ${options.roleArn} for GetLayerVersion`);
15951
16314
  } catch (err) {
15952
- throw new LayerMaterializationError(`Layer ${layer.arn}: STS AssumeRole(${options.roleArn}) failed: ${errMsg(err)}. Check the role trust policy permits your principal and sts:AssumeRole is allowed.`);
16315
+ if (err instanceof LayerMaterializationError) throw err;
16316
+ throw new LayerMaterializationError(`Layer ${layer.arn}: STS AssumeRole(${flattenToOneLine(options.roleArn)}) failed: ${layerFailureDetail(err, "STS AssumeRole (--layer-role-arn)")}. Check the role trust policy permits your principal and sts:AssumeRole is allowed.`);
15953
16317
  }
15954
16318
  let presignedUrl;
15955
16319
  try {
15956
16320
  presignedUrl = await fetchLayerContentUrl(layer, credentials, options);
15957
16321
  } catch (err) {
16322
+ if (err instanceof LayerMaterializationError) throw err;
15958
16323
  const hint = looksLikeAccessDenied(err) ? " GetLayerVersion access denied; check the credentials / role can read the layer (grant lambda:GetLayerVersion on the layer ARN, or pass --layer-role-arn <arn> to assume a role in the layer account)." : "";
15959
- throw new LayerMaterializationError(`Layer ${layer.arn}: GetLayerVersion failed in region ${layer.region}: ${errMsg(err)}.${hint}`);
16324
+ throw new LayerMaterializationError(`Layer ${layer.arn}: GetLayerVersion failed in region ${layer.region}: ${layerFailureDetail(err, "Lambda GetLayerVersion")}.${hint}`);
15960
16325
  }
15961
16326
  let zipBytes;
15962
16327
  try {
@@ -15986,7 +16351,7 @@ async function fetchLayerContentUrl(layer, credentials, options) {
15986
16351
  const versionLessArn = versionSuffix[1];
15987
16352
  const command = await buildGetLayerVersionCommand(versionLessArn, Number(layer.version));
15988
16353
  const url = (await client.send(command))?.Content?.Location;
15989
- if (!url || typeof url !== "string") throw new Error("GetLayerVersion response did not include Content.Location (presigned ZIP URL)");
16354
+ if (!url || typeof url !== "string") throw new UnframedLayerError("GetLayerVersion response did not include Content.Location (presigned ZIP URL)");
15990
16355
  return url;
15991
16356
  } finally {
15992
16357
  client.destroy?.();
@@ -15997,7 +16362,7 @@ async function assumeRoleForLayer(roleArn, region, options) {
15997
16362
  try {
15998
16363
  const command = await buildAssumeRoleCommand(roleArn);
15999
16364
  const creds = (await client.send(command))?.Credentials;
16000
- if (!creds?.AccessKeyId || !creds.SecretAccessKey) throw new Error("AssumeRole returned no Credentials");
16365
+ if (!creds?.AccessKeyId || !creds.SecretAccessKey) throw new UnframedLayerError("AssumeRole returned no Credentials");
16001
16366
  return {
16002
16367
  accessKeyId: creds.AccessKeyId,
16003
16368
  secretAccessKey: creds.SecretAccessKey,
@@ -16040,7 +16405,7 @@ async function buildAssumeRoleCommand(roleArn) {
16040
16405
  async function downloadPresignedZip(presignedUrl, options) {
16041
16406
  if (options.fetchZip) return options.fetchZip(presignedUrl);
16042
16407
  const response = await fetch(presignedUrl);
16043
- if (!response.ok) throw new Error(`HTTP ${response.status} ${response.statusText} from layer Content.Location URL`);
16408
+ if (!response.ok) throw new Error(`HTTP ${response.status} ${flattenToOneLine(response.statusText)} from layer Content.Location URL`);
16044
16409
  const buf = await response.arrayBuffer();
16045
16410
  return new Uint8Array(buf);
16046
16411
  }
@@ -16114,6 +16479,27 @@ async function inflateRaw(data) {
16114
16479
  });
16115
16480
  });
16116
16481
  }
16482
+ /**
16483
+ * Message of a thrown value, for the two throws that are NOT AWS SDK relays.
16484
+ *
16485
+ * Issue #579 deliberately left these two on `errMsg`: the presigned-URL
16486
+ * download is a plain HTTPS fetch (the URL already carries its authorization,
16487
+ * so no credential chain is resolved and no modeled service exception is
16488
+ * parsed) and the unzip is a purely LOCAL operation. Neither `catch` can see a
16489
+ * `credential_process` command line, so there is nothing for the
16490
+ * credential-error policy to DECIDE about them. The two AWS calls in this
16491
+ * module — STS `AssumeRole` and `lambda:GetLayerVersion` — go through
16492
+ * `describeAwsFailureForWarn` instead.
16493
+ *
16494
+ * CORRECTED in review round 2: an earlier revision of this note claimed these
16495
+ * catches see nothing wire-derived at all, which was stronger than the code.
16496
+ * The download `catch` DOES see wire-derived text — `downloadPresignedZip`
16497
+ * raises `HTTP <status> <statusText> ...`, and the reason phrase is whatever
16498
+ * the presigned host sent. Nothing there needs WITHHOLDING (no credential
16499
+ * chain, no secret), but it does need FLATTENING, which is applied at that
16500
+ * throw rather than here, because `errMsg` is also used by the unzip `catch`
16501
+ * whose input is a local `fflate` error.
16502
+ */
16117
16503
  function errMsg(err) {
16118
16504
  return err instanceof Error ? err.message : String(err);
16119
16505
  }
@@ -18791,7 +19177,7 @@ async function resolveLambdaContainerEnv(lambda, options, profileCredentials, ex
18791
19177
  if (stsRegion) dockerEnv["AWS_REGION"] = stsRegion;
18792
19178
  assumeSucceeded = true;
18793
19179
  } catch (err) {
18794
- const reason = describeAwsFailureForWarn(err, "STS AssumeRole");
19180
+ const reason = err instanceof AssumeRoleFailure ? err.detail : describeAwsFailureForWarn(err, "STS AssumeRole");
18795
19181
  logger.warn(`--assume-role: STS AssumeRole(${flattenToOneLine(resolvedAssumeRoleArn)}) failed: ${reason}. Falling back to the developer's shell credentials.`);
18796
19182
  }
18797
19183
  }
@@ -19308,9 +19694,18 @@ async function downloadAndExtractS3Bundle(location, options = {}) {
19308
19694
  }).then(() => void 0)
19309
19695
  };
19310
19696
  }
19697
+ /**
19698
+ * `s3://<bucket>/<key>[?versionId=...]` for a log or error line.
19699
+ *
19700
+ * FLATTENED (issue #579 review round 3). Under `--from-cfn-stack` the bucket
19701
+ * and key are resolved from template intrinsics against DEPLOYED state — a
19702
+ * `Ref` / `Fn::ImportValue` answered by `ListStackResources` / `ListExports` —
19703
+ * so they are wire-derived on that path, and this string now lands on a
19704
+ * default-level error line as well as the `info` progress line it always fed.
19705
+ */
19311
19706
  function formatRef(location) {
19312
- const version = location.versionId ? `?versionId=${location.versionId}` : "";
19313
- return `s3://${location.bucket}/${location.key}${version}`;
19707
+ const version = location.versionId ? `?versionId=${flattenToOneLine(location.versionId)}` : "";
19708
+ return `s3://${flattenToOneLine(location.bucket)}/${flattenToOneLine(location.key)}${version}`;
19314
19709
  }
19315
19710
  /**
19316
19711
  * Guard against zip-slip: reject an entry whose normalized path escapes the
@@ -19342,6 +19737,9 @@ function defaultFetchObject$1(options) {
19342
19737
  }));
19343
19738
  if (!res.Body) throw new CdkLocalError(`S3 GetObject for ${formatRef(location)} returned an empty body.`, "LOCAL_INVOKE_AGENTCORE_S3_BUNDLE_EMPTY_BODY");
19344
19739
  return await res.Body.transformToByteArray();
19740
+ } catch (err) {
19741
+ if (err instanceof CdkLocalError) throw err;
19742
+ throw new CdkLocalError(`S3 GetObject for ${formatRef(location)} failed: ${describeAwsFailureForWarn(err, "S3 GetObject (fromS3 bundle)")}`, "LOCAL_INVOKE_AGENTCORE_S3_BUNDLE_FETCH_FAILED");
19345
19743
  } finally {
19346
19744
  client.destroy();
19347
19745
  }
@@ -20456,7 +20854,8 @@ async function resolveHostCredentialsForSigV4(options, resolved, loaded, region,
20456
20854
  if (assumeRoleArn) try {
20457
20855
  return await assumeAgentCoreExecutionRole(assumeRoleArn, region, options.profile);
20458
20856
  } catch (err) {
20459
- logger.warn(`--assume-role: STS AssumeRole(${flattenToOneLine(assumeRoleArn)}) failed for --sigv4 signing: ${describeAwsFailureForWarn(err, "STS AssumeRole (--sigv4 signing)")}. Falling back to ${options.profile ? `--profile ${options.profile}` : "shell credentials"}.`);
20857
+ const sigv4Detail = err instanceof AssumeRoleFailure ? err.detail : describeAwsFailureForWarn(err, "STS AssumeRole (--sigv4 signing)");
20858
+ logger.warn(`--assume-role: STS AssumeRole(${flattenToOneLine(assumeRoleArn)}) failed for --sigv4 signing: ${sigv4Detail}. Falling back to ${options.profile ? `--profile ${options.profile}` : "shell credentials"}.`);
20460
20859
  }
20461
20860
  if (options.profile) {
20462
20861
  const creds = await resolveProfileCredentials(options.profile);
@@ -20579,7 +20978,8 @@ async function resolveAgentCoreCodeImageFromS3(resolved, code, s3Source, options
20579
20978
  if (assumeRoleArn) try {
20580
20979
  credentials = await assumeAgentCoreExecutionRole(assumeRoleArn, region, options.profile);
20581
20980
  } catch (err) {
20582
- logger.warn(`--assume-role: STS AssumeRole(${flattenToOneLine(assumeRoleArn)}) failed for the fromS3 bundle download: ${describeAwsFailureForWarn(err, "STS AssumeRole (fromS3 bundle download)")}. Falling back to ${options.profile ? `--profile ${options.profile}` : "the default credentials"}.`);
20981
+ const bundleDetail = err instanceof AssumeRoleFailure ? err.detail : describeAwsFailureForWarn(err, "STS AssumeRole (fromS3 bundle download)");
20982
+ 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"}.`);
20583
20983
  }
20584
20984
  const bundle = await downloadAndExtractS3Bundle(location, {
20585
20985
  ...region !== void 0 && { region },
@@ -20777,7 +21177,8 @@ async function applyAgentCoreCredentialEnv(dockerEnv, args) {
20777
21177
  if (stsRegion) dockerEnv["AWS_REGION"] = stsRegion;
20778
21178
  assumeSucceeded = true;
20779
21179
  } catch (err) {
20780
- logger.warn(`--assume-role: STS AssumeRole(${flattenToOneLine(args.assumeRoleArn)}) failed: ${describeAwsFailureForWarn(err, "STS AssumeRole")}. Falling back to the developer's shell credentials.`);
21180
+ const assumeDetail = err instanceof AssumeRoleFailure ? err.detail : describeAwsFailureForWarn(err, "STS AssumeRole");
21181
+ logger.warn(`--assume-role: STS AssumeRole(${flattenToOneLine(args.assumeRoleArn)}) failed: ${assumeDetail}. Falling back to the developer's shell credentials.`);
20781
21182
  }
20782
21183
  }
20783
21184
  if (!assumeSucceeded) {
@@ -20937,7 +21338,7 @@ async function assumeAgentCoreExecutionRole(roleArn, region, profile) {
20937
21338
  RoleSessionName: `${getEmbedConfig().resourceNamePrefix}-invoke-agentcore-${Date.now()}`,
20938
21339
  DurationSeconds: 3600
20939
21340
  }))).Credentials;
20940
- if (!creds?.AccessKeyId || !creds.SecretAccessKey || !creds.SessionToken) throw new Error(`AssumeRole(${roleArn}) returned no usable credentials.`);
21341
+ if (!creds?.AccessKeyId || !creds.SecretAccessKey || !creds.SessionToken) throw new AssumeRoleFailure(`AssumeRole(${flattenToOneLine(roleArn)}) returned no usable credentials.`, "the response carried no usable credentials");
20941
21342
  return {
20942
21343
  accessKeyId: creds.AccessKeyId,
20943
21344
  secretAccessKey: creds.SecretAccessKey,
@@ -21914,7 +22315,7 @@ async function resolveSecretsManager(entry, shape, client) {
21914
22315
  try {
21915
22316
  secretString = (await client.send(new GetSecretValueCommand({ SecretId: shape.baseArn }))).SecretString;
21916
22317
  } catch (err) {
21917
- throw new EcsSecretsResolutionError(`Failed to resolve Secrets Manager secret for container '${entry.containerName}' / env '${entry.name}' (${shape.baseArn}): ${err instanceof Error ? err.message : String(err)}`);
22318
+ throw new EcsSecretsResolutionError(`Failed to resolve Secrets Manager secret for container '${entry.containerName}' / env '${entry.name}' (${shape.baseArn}): ${describeAwsFailureForWarn(err, "SecretsManager GetSecretValue")}`);
21918
22319
  }
21919
22320
  if (secretString === void 0) throw new EcsSecretsResolutionError(`Secrets Manager returned no SecretString for container '${entry.containerName}' / env '${entry.name}' (${shape.baseArn}). Binary secrets are not supported.`);
21920
22321
  if (shape.jsonKey === void 0) return secretString;
@@ -21940,7 +22341,7 @@ async function resolveSsm(entry, shape, client) {
21940
22341
  return value;
21941
22342
  } catch (err) {
21942
22343
  if (err instanceof EcsSecretsResolutionError) throw err;
21943
- throw new EcsSecretsResolutionError(`Failed to resolve SSM parameter for container '${entry.containerName}' / env '${entry.name}' (${shape.name}): ${err instanceof Error ? err.message : String(err)}`);
22344
+ throw new EcsSecretsResolutionError(`Failed to resolve SSM parameter for container '${entry.containerName}' / env '${entry.name}' (${shape.name}): ${describeAwsFailureForWarn(err, "SSM GetParameter (ECS secret)")}`);
21944
22345
  }
21945
22346
  }
21946
22347
 
@@ -24053,6 +24454,22 @@ const defaultReadContainerLogsImpl = async (containerId) => {
24053
24454
  * `read` is injectable so the unit test can assert the formatting without
24054
24455
  * a real container; production callers use the default `docker logs`
24055
24456
  * reader.
24457
+ *
24458
+ * Issue #579 — ONE `logger.warn` PER TAIL LINE, not one warn carrying an
24459
+ * embedded `\n`. LEVEL: a default-level warn, and under `cdkl studio` the
24460
+ * serve child's stdout is mirrored into a log ring served over HTTP. The
24461
+ * studio serve manager reads that stdout with `streamLines`, which splits on
24462
+ * `\n`, and it declines to pattern-match a line carrying cdk-local's own
24463
+ * `WARN: ` prefix (issue #578). A multi-line warn defeats that bound from the
24464
+ * inside: the logger prefixes the MESSAGE, so line 2 onward arrives
24465
+ * prefix-less and is matched at `^` — and the content is the CONTAINER's own
24466
+ * stdout, i.e. whatever the application printed. An app that logs
24467
+ * `Server listening on http://10.0.0.9:3000` (which is exactly what a web
24468
+ * framework prints, and this tail fires when such an app has just crashed)
24469
+ * would re-point the capture proxy. RECONSTRUCTION does not arise: this is
24470
+ * container output, not an AWS SDK failure, so nothing is withheld — the whole
24471
+ * tail still prints, one prefixed line at a time. Each line is additionally
24472
+ * flattened, so a `\r` or U+2028 inside one cannot re-split it downstream.
24056
24473
  */
24057
24474
  async function printExitedContainerLogs(replicaIndex, containerId, logger, read = defaultReadContainerLogsImpl) {
24058
24475
  let raw;
@@ -24064,7 +24481,12 @@ async function printExitedContainerLogs(replicaIndex, containerId, logger, read
24064
24481
  }
24065
24482
  const tail = raw.trimEnd();
24066
24483
  if (tail.length === 0) return;
24067
- logger.warn(`Replica ${replicaIndex} essential container logs (last ${EXIT_LOG_TAIL_LINES} lines):\n${tail}`);
24484
+ logger.warn(`Replica ${replicaIndex} essential container logs (last ${EXIT_LOG_TAIL_LINES} lines):`);
24485
+ for (const line of tail.split(/\r?\n/)) {
24486
+ const flat = flattenToOneLine(line).trimEnd();
24487
+ if (flat.trim().length === 0) continue;
24488
+ logger.warn(`Replica ${replicaIndex} | ${flat}`);
24489
+ }
24068
24490
  }
24069
24491
  const defaultSleepImpl = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
24070
24492
  let sleepImpl = defaultSleepImpl;
@@ -27742,6 +28164,9 @@ async function resolvePlaceholderAccount$1(arn, region, profile) {
27742
28164
  const account = (await sts.send(new GetCallerIdentityCommand({}))).Account;
27743
28165
  if (!account) throw new LocalStartServiceError(`--assume-task-role: GetCallerIdentity returned no Account; cannot resolve placeholder ARN '${arn}'.`);
27744
28166
  return arn.split(TASK_ROLE_ACCOUNT_PLACEHOLDER).join(account);
28167
+ } catch (err) {
28168
+ if (err instanceof LocalStartServiceError) throw err;
28169
+ 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>`);
27745
28170
  } finally {
27746
28171
  sts.destroy();
27747
28172
  }
@@ -28289,6 +28714,18 @@ async function localRunTaskCommand(target, options, extraStateProviders) {
28289
28714
  }
28290
28715
  }
28291
28716
  /**
28717
+ * cdk-local's own rejection from {@link resolvePlaceholderAccount}, so the
28718
+ * relay below can tell it apart from a wire-derived SDK failure and re-raise it
28719
+ * intact (issue #579).
28720
+ */
28721
+ var PlaceholderAccountError = class PlaceholderAccountError extends Error {
28722
+ constructor(message) {
28723
+ super(message);
28724
+ this.name = "PlaceholderAccountError";
28725
+ Object.setPrototypeOf(this, PlaceholderAccountError.prototype);
28726
+ }
28727
+ };
28728
+ /**
28292
28729
  * If `arn` contains the `${AWS::AccountId}` placeholder emitted by the
28293
28730
  * resolver for inline same-stack IAM Roles, substitute the live caller
28294
28731
  * account via STS `GetCallerIdentity`. Otherwise pass through unchanged.
@@ -28302,8 +28739,13 @@ async function resolvePlaceholderAccount(arn, region, profile) {
28302
28739
  }));
28303
28740
  try {
28304
28741
  const account = (await sts.send(new GetCallerIdentityCommand({}))).Account;
28305
- if (!account) throw new Error(`--assume-task-role: GetCallerIdentity returned no Account; cannot resolve placeholder ARN '${arn}'. Pass the ARN explicitly: --assume-task-role <arn>`);
28742
+ 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>`);
28306
28743
  return arn.split(TASK_ROLE_ACCOUNT_PLACEHOLDER).join(account);
28744
+ } catch (err) {
28745
+ if (err instanceof PlaceholderAccountError) throw err;
28746
+ const shownArn = flattenToOneLine(arn);
28747
+ const detail = describeAwsFailureForWarn(err, "STS GetCallerIdentity (task-role placeholder)");
28748
+ throw new PlaceholderAccountError(`--assume-task-role: STS GetCallerIdentity failed while resolving placeholder ARN '${shownArn}': ${detail}. Pass the ARN explicitly: --assume-task-role <arn>`);
28307
28749
  } finally {
28308
28750
  sts.destroy();
28309
28751
  }
@@ -31155,9 +31597,9 @@ function createS3OriginReader(bucketName, options = {}) {
31155
31597
  };
31156
31598
  if (direct.kind === "denied" && !deniedWarned) {
31157
31599
  deniedWarned = true;
31158
- getLogger().warn(`S3 denied reading '${key}' from bucket '${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.`);
31600
+ 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.`);
31159
31601
  }
31160
- if (direct.kind === "error") getLogger().warn(`S3 read of '${key}' from bucket '${bucketName}' failed: ${direct.message}`);
31602
+ if (direct.kind === "error") getLogger().warn(`S3 read of '${flattenToOneLine(key)}' from bucket '${flattenToOneLine(bucketName)}' failed: ${direct.message}`);
31161
31603
  }
31162
31604
  for (const candidate of resolveErrorResponseCandidates(input.customErrorResponses)) {
31163
31605
  const page = await fetchObject(candidate.errorKey);
@@ -31166,7 +31608,7 @@ function createS3OriginReader(bucketName, options = {}) {
31166
31608
  headers: { "content-type": contentTypeForKey(candidate.errorKey) },
31167
31609
  body: page.body
31168
31610
  };
31169
- if (page.kind === "denied" || page.kind === "error") getLogger().warn(`S3 could not read custom-error page '${candidate.errorKey}' from bucket '${bucketName}' (${page.kind}); falling through.`);
31611
+ 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.`);
31170
31612
  }
31171
31613
  return {
31172
31614
  statusCode: 404,
@@ -31234,6 +31676,18 @@ function defaultFetchObject(bucketName, options) {
31234
31676
  * Classify an S3 SDK error: a missing key (`NoSuchKey` / 404) is `not-found`
31235
31677
  * (try the SPA fallback), an `AccessDenied` / 403 is `denied` (a credential /
31236
31678
  * OAC config problem), anything else is `error`.
31679
+ *
31680
+ * Issue #579 — the `error` branch's `message` is the only one that reaches a
31681
+ * log line (the reader prints it in a DEFAULT-level `S3 read of ... failed:`
31682
+ * warn), so it is rendered by {@link describeAwsFailureForWarn}. LEVEL: a
31683
+ * default-level warn, mirrored into the studio log ring. RECONSTRUCTION: the
31684
+ * `catch` this classifies for wraps `client.send(GetObjectCommand)`, which
31685
+ * resolves the credential chain before the request goes out — a
31686
+ * `CredentialsProviderError` here is COMMON, since the whole point of this
31687
+ * origin is reading a deployed bucket with the developer's own credentials —
31688
+ * alongside a modeled S3 exception whose message is the diagnosis. The
31689
+ * `not-found` / `denied` branches carry no message at all and are untouched:
31690
+ * their names are structural discriminators, not text that gets printed.
31237
31691
  */
31238
31692
  function classifyS3Error(err) {
31239
31693
  const e = err;
@@ -31243,7 +31697,7 @@ function classifyS3Error(err) {
31243
31697
  if (status === 403 || name === "AccessDenied" || name === "Forbidden") return { kind: "denied" };
31244
31698
  return {
31245
31699
  kind: "error",
31246
- message: err instanceof Error ? err.message : String(err)
31700
+ message: describeAwsFailureForWarn(err, "S3 GetObject")
31247
31701
  };
31248
31702
  }
31249
31703
 
@@ -31305,7 +31759,7 @@ function createDeployedKvsDataSource(options) {
31305
31759
  }))).Value;
31306
31760
  } catch (err) {
31307
31761
  if (isKeyNotFound(err)) return void 0;
31308
- throw new Error(`cf.kvs().get('${key}') against ${options.kvsArn} failed: ${err instanceof Error ? err.message : String(err)}`);
31762
+ throw new Error(`cf.kvs().get('${key}') against ${flattenToOneLine(options.kvsArn)} failed: ${describeAwsFailureForWarn(err, "CloudFront KeyValueStore GetKey")}`);
31309
31763
  }
31310
31764
  }
31311
31765
  };
@@ -31332,7 +31786,7 @@ async function resolveDeployedKvsArnByName(name, options = {}) {
31332
31786
  ...item.Id !== void 0 && { id: item.Id }
31333
31787
  };
31334
31788
  } catch (err) {
31335
- getLogger().debug(`ListKeyValueStores lookup for '${name}' failed: ${err instanceof Error ? err.message : String(err)}`);
31789
+ getLogger().debug(`ListKeyValueStores lookup for '${name}' failed: ${flattenToOneLine(stringifyThrown(err))}`);
31336
31790
  }
31337
31791
  }
31338
31792
  /** True when a GetKey error means the key (or store) was not found, not a real failure. */
@@ -38520,7 +38974,7 @@ async function prepareEcsImageContexts(args) {
38520
38974
  const ctx = await buildEcsImageResolutionContext(stack, stateProvider, options);
38521
38975
  contextByStack.set(stack.stackName, ctx);
38522
38976
  } catch (err) {
38523
- logger.warn(`studio: could not build deployed-state image context for stack '${stack.stackName}'; ECS services in it resolve against the synthed template only. ${err instanceof Error ? err.message : String(err)}`);
38977
+ logger.warn(`studio: could not build deployed-state image context for stack '${stack.stackName}'; ECS services in it resolve against the synthed template only. ${describeAwsFailureForWarn(err, "deployed-state image context (CloudFormation + SSM)")}`);
38524
38978
  contextByStack.set(stack.stackName, void 0);
38525
38979
  } finally {
38526
38980
  stateProvider?.dispose();
@@ -38998,4 +39452,4 @@ function addStudioSpecificOptions(cmd) {
38998
39452
 
38999
39453
  //#endregion
39000
39454
  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, 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, 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 };
39001
- //# sourceMappingURL=local-studio-BIHN-Erp.js.map
39455
+ //# sourceMappingURL=local-studio-DOdzcjkE.js.map