cdk-local 0.147.19 → 0.147.21

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.
@@ -42,124 +42,6 @@ import { CloudFrontClient, paginateListKeyValueStores } from "@aws-sdk/client-cl
42
42
  import { CloudFrontKeyValueStoreClient, GetKeyCommand, ResourceNotFoundException } from "@aws-sdk/client-cloudfront-keyvaluestore";
43
43
  import { EventEmitter } from "node:events";
44
44
 
45
- //#region src/cli/options.ts
46
- /**
47
- * Parse context key=value pairs from CLI arguments into a Record.
48
- */
49
- function parseContextOptions(contextArgs) {
50
- const context = {};
51
- if (contextArgs) for (const arg of contextArgs) {
52
- const eqIndex = arg.indexOf("=");
53
- if (eqIndex > 0) context[arg.substring(0, eqIndex)] = arg.substring(eqIndex + 1);
54
- }
55
- return context;
56
- }
57
- /**
58
- * Options shared across every cdk-local command. Built per-call (not a
59
- * module-level const) so the `--role-arn` env-var hint reflects the active
60
- * embed config, which the command factory installs before calling this.
61
- *
62
- * `--region` is intentionally NOT in `commonOptions` — it is registered
63
- * separately via {@link regionOption} so a host CLI (cdkd) can swap the
64
- * shared option block once without losing the `--region` flag.
65
- */
66
- function commonOptions() {
67
- const { envPrefix } = getEmbedConfig();
68
- return [
69
- new Option("--verbose", "Enable verbose logging").default(false),
70
- new Option("--profile <profile>", "AWS profile"),
71
- new Option("--role-arn <arn>", `IAM role ARN to assume for AWS API calls (env: ${envPrefix}_ROLE_ARN)`),
72
- new Option("-y, --yes", "Automatically answer interactive prompts with the recommended response").default(false)
73
- ];
74
- }
75
- /**
76
- * `--region` option attached to every command. The value drives both
77
- * host-side AWS SDK calls (STS `GetCallerIdentity` for
78
- * `${AWS::AccountId}` resolution, `AssumeRole` for `--assume-role`, etc.)
79
- * and the container's `AWS_REGION` env var. When omitted, region
80
- * precedence falls back to `AWS_REGION` / `AWS_DEFAULT_REGION` env,
81
- * then the synthesized stack region, then the `--profile`'s configured
82
- * region — same shape as the AWS CLI's `--region` flag (issue #245).
83
- *
84
- * Kept as a standalone export rather than baked into `commonOptions`
85
- * so a host CLI (cdkd) can swap the shared option block once without
86
- * losing this flag.
87
- */
88
- const regionOption = new Option("--region <region>", "AWS region for SDK calls; defaults to AWS_REGION env, the synthesized stack region, or the resolved profile region");
89
- /**
90
- * App options. Built per-call (not a module-level const) so the `--app`
91
- * env-var hint reflects the active embed config.
92
- *
93
- * `--app` is optional: falls back to `${envPrefix}_APP` env var, then
94
- * `cdk.json` `app` field. Accepts either a shell command (e.g.
95
- * `"node app.ts"`) or a path to a pre-synthesized cloud assembly directory
96
- * (e.g. `"cdk.out"`).
97
- */
98
- function appOptions() {
99
- const { envPrefix } = getEmbedConfig();
100
- return [new Option("-a, --app <command>", `CDK app command (e.g., "node app.ts") or path to a pre-synthesized cloud assembly directory. Falls back to cdk.json or ${envPrefix}_APP env`), new Option("--output <path>", "Output directory for synthesis").default("cdk.out")];
101
- }
102
- /**
103
- * Context options.
104
- */
105
- const contextOptions = [new Option("-c, --context <key=value...>", "Set context values (can be specified multiple times)")];
106
- const IAM_ROLE_ARN_REGEX = /^arn:[^:]+:iam::\d+:role\//;
107
- function parseAssumeRoleToken(raw, previous) {
108
- const acc = previous ?? { perLambda: {} };
109
- if (!acc.perLambda) acc.perLambda = {};
110
- const eqIndex = raw.indexOf("=");
111
- if (eqIndex === -1) {
112
- if (!IAM_ROLE_ARN_REGEX.test(raw)) throw new Error(`Invalid --assume-role value "${raw}": expected an IAM role ARN like arn:aws:iam::123456789012:role/MyRole, or LogicalId=<arn>.`);
113
- acc.globalArn = raw;
114
- return acc;
115
- }
116
- const logicalId = raw.substring(0, eqIndex).trim();
117
- const arn = raw.substring(eqIndex + 1).trim();
118
- if (!/^[A-Za-z][A-Za-z0-9]*$/.test(logicalId)) throw new Error(`Invalid --assume-role value "${raw}": left-hand side "${logicalId}" must be a CloudFormation logical ID (alphanumeric, leading letter).`);
119
- if (!IAM_ROLE_ARN_REGEX.test(arn)) throw new Error(`Invalid --assume-role value "${raw}": right-hand side "${arn}" must be an IAM role ARN like arn:aws:iam::123456789012:role/MyRole.`);
120
- acc.perLambda[logicalId] = arn;
121
- return acc;
122
- }
123
- /**
124
- * Compose `--assume-role` (value-form accumulator) and the separate
125
- * `--assume-role-auto` boolean flag into the in-process
126
- * representation `localStartApiCommand` consumes downstream.
127
- *
128
- * - both unset -> `undefined` (pass dev creds through)
129
- * - only `--assume-role-auto` -> `{ perLambda: {}, bareAutoResolve: true }`
130
- * (per-Lambda auto-resolve for every routed Lambda)
131
- * - only `--assume-role` -> the value-form accumulator (global ARN
132
- * and/or per-Lambda map) as-is
133
- * - both -> accumulator with `bareAutoResolve = true` overlaid;
134
- * the per-Lambda map still wins for named Lambdas, auto-resolve
135
- * fills in for every Lambda the map does NOT name
136
- *
137
- * Enforces the mutual-exclusion guard: `--assume-role-auto` and a
138
- * global ARN (`--assume-role <arn>`) both occupy the "default for
139
- * every other Lambda" slot, so the combination is ambiguous and
140
- * rejected at boot with a clear error. The per-Lambda map IS
141
- * compatible with either side.
142
- */
143
- function normalizeStartApiAssumeRole(raw, autoResolve) {
144
- if (raw === void 0) return autoResolve ? {
145
- perLambda: {},
146
- bareAutoResolve: true
147
- } : void 0;
148
- if (autoResolve && raw.globalArn) throw new Error(`--assume-role-auto auto-resolves EACH routed Lambda's own execution role, but --assume-role ${raw.globalArn} also names a single global default. These are mutually exclusive on the global slot. Either drop the global ARN to keep --assume-role-auto for every Lambda, or drop --assume-role-auto to keep the global default. Per-Lambda overrides (--assume-role <LogicalId>=<arn>) are compatible with either side.`);
149
- if (autoResolve) raw.bareAutoResolve = true;
150
- return raw;
151
- }
152
- /**
153
- * Resolve the effective IAM role ARN for a given Lambda. Per-Lambda
154
- * override wins; otherwise the global default; otherwise `undefined`
155
- * (no role to assume — pass developer creds through).
156
- */
157
- function effectiveAssumeRoleArn(logicalId, opt) {
158
- if (!opt) return void 0;
159
- return opt.perLambda?.[logicalId] ?? opt.globalArn;
160
- }
161
-
162
- //#endregion
163
45
  //#region src/utils/profile-resolver.ts
164
46
  /**
165
47
  * Resolve `--profile <p>` to a concrete credential set AND the profile's
@@ -705,6 +587,118 @@ function describeAwsFailureForWarn(err, operation) {
705
587
  //#endregion
706
588
  //#region src/utils/role-arn.ts
707
589
  /**
590
+ * Upper bound on a value this module is willing to treat as a role ARN.
591
+ *
592
+ * NOT invented here: it is the length constraint the receiving API itself
593
+ * documents for the field this value ends up in — STS `AssumeRole`'s
594
+ * `RoleArn` (AWS STS API Reference: "Length Constraints: Minimum length of
595
+ * 20. Maximum length of 2048."). Bounding at the receiver's own limit is the
596
+ * defensible number because anything above it is a value STS would reject
597
+ * anyway, so the cap can never turn a working configuration into a broken one.
598
+ *
599
+ * It is deliberately far above what a real role ARN needs — IAM's own entity
600
+ * limits put the ceiling near 600 characters (`arn:<partition>:iam::<12
601
+ * digits>:role/` plus a path of at most 512 and a name of at most 64) — because
602
+ * the job here is stopping an UNBOUNDED wire value from being sent and printed,
603
+ * not re-deriving IAM's schema. A tighter guess would be this module's own
604
+ * opinion; 2048 is the receiver's.
605
+ *
606
+ * The MINIMUM (20) is deliberately NOT enforced. The grammar below already
607
+ * implies a floor one character under it (`arn:a:iam::1:role/X` is 19), so
608
+ * enforcing it would reject a synthetic-but-well-shaped value for no security
609
+ * gain — length amplification is the half that matters here.
610
+ *
611
+ * The comparison is on UTF-16 units, which is exact for every value the
612
+ * grammar can ACCEPT (it admits printable ASCII only, where units, code points
613
+ * and bytes coincide) and a safe over-approximation for the rest.
614
+ */
615
+ const IAM_ROLE_ARN_MAX_LENGTH = 2048;
616
+ /**
617
+ * The shape an IAM role ARN must have.
618
+ *
619
+ * Extracted from `src/cli/options.ts`, which validated `--assume-role` with
620
+ * `/^arn:[^:]+:iam::\d+:role\//` while three WIRE resolution points asked the
621
+ * same question with `startsWith('arn:')` (issue #607). Two spellings of one
622
+ * question regenerate: this is the single site that owns it, and every other
623
+ * site calls {@link isIamRoleArn} rather than paraphrasing it.
624
+ *
625
+ * Deliberate strictness decisions, in both directions:
626
+ *
627
+ * - The PARTITION is `[A-Za-z0-9-]+`, so `aws`, `aws-cn`, `aws-us-gov` and
628
+ * the iso partitions all pass. Tighter than the extracted `[^:]+` (which
629
+ * admitted spaces and slashes), and still open-ended enough that a
630
+ * partition AWS has not launched yet is not rejected by name.
631
+ * - The ACCOUNT stays `[0-9]+` rather than `[0-9]{12}`. Real account ids are
632
+ * 12 digits, but this is a SHAPE bound, not a schema validator — AWS
633
+ * rejects a wrong account id far better than cdk-local can — and this
634
+ * repo's own fixtures use short ids (`arn:aws:iam::111:role/...`).
635
+ * - The role tail is `[!-~]+`: at least one character, printable ASCII, no
636
+ * spaces. That accepts every path-shaped role name
637
+ * (`role/service-role/Foo`, `role/aws-service-role/x.amazonaws.com/Bar`)
638
+ * because IAM's own path grammar is exactly printable-ASCII segments.
639
+ * - The pattern is ANCHORED AT BOTH ENDS, which the extracted one was not.
640
+ * That is the half that matters for a hostile endpoint: an unanchored
641
+ * `role\/` accepts a value carrying a newline and a forged second line
642
+ * after it, and it is the value's PRINTED half that the flatten at the log
643
+ * sites was left holding on its own.
644
+ *
645
+ * LINEARITY: every quantified class is disjoint from the literal that follows
646
+ * it (`[A-Za-z0-9-]+` then `:`, `[0-9]+` then `:`), and the one class that is
647
+ * not (`[!-~]+`) is the last element before `$`. There is no alternation and
648
+ * no nested quantifier, so there is no input on which this backtracks
649
+ * super-linearly — which matters because it runs on values off the wire.
650
+ * {@link isIamRoleArn} also applies the length bound BEFORE the match, so the
651
+ * pattern never sees an unbounded string at all.
652
+ */
653
+ const IAM_ROLE_ARN_PATTERN = /^arn:[A-Za-z0-9-]+:iam::[0-9]+:role\/[!-~]+$/;
654
+ function isIamRoleArn(value) {
655
+ if (typeof value !== "string") return false;
656
+ if (value.length > 2048) return false;
657
+ return IAM_ROLE_ARN_PATTERN.test(value);
658
+ }
659
+ /**
660
+ * Longest prefix of a rejected value {@link describeRejectedRoleArn} will show.
661
+ *
662
+ * 128 rather than 64: `arn:aws:iam::123456789012:role/` is 31 characters on
663
+ * its own, so 64 left only 33 for the part that actually distinguishes one
664
+ * role from another — a CDK-generated name
665
+ * (`Stack-HandlerServiceRole1234ABCD-XXXXXXXXXXXX`) does not fit in that, and
666
+ * a preview that elides exactly the informative half is not a diagnosis. 128
667
+ * still leaves the rendered line well inside the 400-character bound its
668
+ * callers' tests assert.
669
+ */
670
+ const REJECTED_ARN_PREVIEW_MAX = 128;
671
+ /**
672
+ * Render a value {@link isIamRoleArn} REJECTED so it can go on a warn line.
673
+ *
674
+ * A rejection has two very different causes and the log line has to serve
675
+ * both: an ordinary misconfiguration (a role NAME where an ARN was expected,
676
+ * an ARN of the wrong resource type), where seeing the value is the whole
677
+ * diagnosis; and a hostile or broken endpoint returning something unbounded,
678
+ * where printing the value is the thing being prevented. So the value is
679
+ * shown, flattened and clamped, with its true length named — the same shape
680
+ * `sanitizeServiceExceptionMessage` uses, and for the same reason: a clamped
681
+ * preview must never be mistakable for the whole value.
682
+ *
683
+ * The cut is on CODE POINTS so it cannot emit half of a surrogate pair, and
684
+ * the reported count is the code-point count of the FLATTENED string, which is
685
+ * the string the preview is a prefix of.
686
+ */
687
+ function describeRejectedRoleArn(value) {
688
+ if (typeof value !== "string") return `a non-string ${typeof value} value`;
689
+ const points = [...flattenToOneLine(value)];
690
+ return `"${points.length <= REJECTED_ARN_PREVIEW_MAX ? points.join("") : `${points.slice(0, REJECTED_ARN_PREVIEW_MAX).join("")}...`}" (${points.length} characters)`;
691
+ }
692
+ /**
693
+ * The sentence a guarded send throws when a value is refused before it leaves
694
+ * the process. One spelling, exported, so every guarded send words the refusal
695
+ * identically — including `local-invoke-agentcore.ts`'s own inline
696
+ * `AssumeRoleCommand`, which is not routed through this module.
697
+ */
698
+ function refusedRoleArnMessage(value) {
699
+ return `AssumeRole refused: the role ARN is not a well-formed IAM role ARN (expected arn:<partition>:iam::<account>:role/<name>, at most ${IAM_ROLE_ARN_MAX_LENGTH} characters): ${describeRejectedRoleArn(value)}. Nothing was sent to STS.`;
700
+ }
701
+ /**
708
702
  * {@link assumeRoleCredentials} failed, and the failure is cdk-local's OWN
709
703
  * rendering of it rather than a raw SDK error.
710
704
  *
@@ -796,6 +790,11 @@ var AssumeRoleFailure = class AssumeRoleFailure extends Error {
796
790
  * {@link AssumeRoleFailure} is for, and why BOTH throws below use it.
797
791
  */
798
792
  async function assumeRoleCredentials(opts) {
793
+ if (!isIamRoleArn(opts.roleArn)) {
794
+ const detail = `the role ARN is not well-formed: ${describeRejectedRoleArn(opts.roleArn)}`;
795
+ const message = refusedRoleArnMessage(opts.roleArn);
796
+ throw opts.makeError ? opts.makeError(message) : new AssumeRoleFailure(message, detail);
797
+ }
799
798
  const sts = new STSClient(buildStsClientConfig({
800
799
  region: opts.region,
801
800
  profile: opts.profile
@@ -854,6 +853,7 @@ async function assumeRoleCredentials(opts) {
854
853
  async function applyRoleArnIfSet(opts) {
855
854
  const roleArn = opts.roleArn || process.env[`${getEmbedConfig().envPrefix}_ROLE_ARN`];
856
855
  if (!roleArn) return;
856
+ if (!isIamRoleArn(roleArn)) throw new Error(refusedRoleArnMessage(roleArn));
857
857
  const logger = getLogger().child("role-arn");
858
858
  const shownArn = flattenToOneLine(roleArn);
859
859
  logger.debug(`Assuming role ${shownArn}...`);
@@ -882,6 +882,134 @@ async function applyRoleArnIfSet(opts) {
882
882
  logger.info(`Assumed role ${shownArn} (session expires ${Expiration?.toISOString() ?? "unknown"})`);
883
883
  }
884
884
 
885
+ //#endregion
886
+ //#region src/cli/options.ts
887
+ /**
888
+ * Parse context key=value pairs from CLI arguments into a Record.
889
+ */
890
+ function parseContextOptions(contextArgs) {
891
+ const context = {};
892
+ if (contextArgs) for (const arg of contextArgs) {
893
+ const eqIndex = arg.indexOf("=");
894
+ if (eqIndex > 0) context[arg.substring(0, eqIndex)] = arg.substring(eqIndex + 1);
895
+ }
896
+ return context;
897
+ }
898
+ /**
899
+ * Options shared across every cdk-local command. Built per-call (not a
900
+ * module-level const) so the `--role-arn` env-var hint reflects the active
901
+ * embed config, which the command factory installs before calling this.
902
+ *
903
+ * `--region` is intentionally NOT in `commonOptions` — it is registered
904
+ * separately via {@link regionOption} so a host CLI (cdkd) can swap the
905
+ * shared option block once without losing the `--region` flag.
906
+ */
907
+ function commonOptions() {
908
+ const { envPrefix } = getEmbedConfig();
909
+ return [
910
+ new Option("--verbose", "Enable verbose logging").default(false),
911
+ new Option("--profile <profile>", "AWS profile"),
912
+ new Option("--role-arn <arn>", `IAM role ARN to assume for AWS API calls (env: ${envPrefix}_ROLE_ARN)`),
913
+ new Option("-y, --yes", "Automatically answer interactive prompts with the recommended response").default(false)
914
+ ];
915
+ }
916
+ /**
917
+ * `--region` option attached to every command. The value drives both
918
+ * host-side AWS SDK calls (STS `GetCallerIdentity` for
919
+ * `${AWS::AccountId}` resolution, `AssumeRole` for `--assume-role`, etc.)
920
+ * and the container's `AWS_REGION` env var. When omitted, region
921
+ * precedence falls back to `AWS_REGION` / `AWS_DEFAULT_REGION` env,
922
+ * then the synthesized stack region, then the `--profile`'s configured
923
+ * region — same shape as the AWS CLI's `--region` flag (issue #245).
924
+ *
925
+ * Kept as a standalone export rather than baked into `commonOptions`
926
+ * so a host CLI (cdkd) can swap the shared option block once without
927
+ * losing this flag.
928
+ */
929
+ const regionOption = new Option("--region <region>", "AWS region for SDK calls; defaults to AWS_REGION env, the synthesized stack region, or the resolved profile region");
930
+ /**
931
+ * App options. Built per-call (not a module-level const) so the `--app`
932
+ * env-var hint reflects the active embed config.
933
+ *
934
+ * `--app` is optional: falls back to `${envPrefix}_APP` env var, then
935
+ * `cdk.json` `app` field. Accepts either a shell command (e.g.
936
+ * `"node app.ts"`) or a path to a pre-synthesized cloud assembly directory
937
+ * (e.g. `"cdk.out"`).
938
+ */
939
+ function appOptions() {
940
+ const { envPrefix } = getEmbedConfig();
941
+ return [new Option("-a, --app <command>", `CDK app command (e.g., "node app.ts") or path to a pre-synthesized cloud assembly directory. Falls back to cdk.json or ${envPrefix}_APP env`), new Option("--output <path>", "Output directory for synthesis").default("cdk.out")];
942
+ }
943
+ /**
944
+ * Context options.
945
+ */
946
+ const contextOptions = [new Option("-c, --context <key=value...>", "Set context values (can be specified multiple times)")];
947
+ /**
948
+ * Shape-checks both forms against {@link isIamRoleArn}.
949
+ *
950
+ * This file used to own a private `IAM_ROLE_ARN_REGEX`, while three WIRE
951
+ * resolution points asked the same question with `startsWith('arn:')` (issue
952
+ * #607) — so "is this a role ARN?" had two answers depending on whether the
953
+ * value came from the flag or off a `GetFunctionConfiguration` response. The
954
+ * predicate now lives in `src/utils/role-arn.ts`, beside the AssumeRole call
955
+ * that consumes it, and this site calls it rather than keeping a copy.
956
+ */
957
+ function parseAssumeRoleToken(raw, previous) {
958
+ const acc = previous ?? { perLambda: {} };
959
+ if (!acc.perLambda) acc.perLambda = {};
960
+ const eqIndex = raw.indexOf("=");
961
+ if (eqIndex === -1) {
962
+ const bare = raw.trim();
963
+ if (!isIamRoleArn(bare)) throw new Error(`Invalid --assume-role value "${raw}": expected an IAM role ARN like arn:aws:iam::123456789012:role/MyRole, or LogicalId=<arn>.`);
964
+ acc.globalArn = bare;
965
+ return acc;
966
+ }
967
+ const logicalId = raw.substring(0, eqIndex).trim();
968
+ const arn = raw.substring(eqIndex + 1).trim();
969
+ if (!/^[A-Za-z][A-Za-z0-9]*$/.test(logicalId)) throw new Error(`Invalid --assume-role value "${raw}": left-hand side "${logicalId}" must be a CloudFormation logical ID (alphanumeric, leading letter).`);
970
+ if (!isIamRoleArn(arn)) throw new Error(`Invalid --assume-role value "${raw}": right-hand side "${arn}" must be an IAM role ARN like arn:aws:iam::123456789012:role/MyRole.`);
971
+ acc.perLambda[logicalId] = arn;
972
+ return acc;
973
+ }
974
+ /**
975
+ * Compose `--assume-role` (value-form accumulator) and the separate
976
+ * `--assume-role-auto` boolean flag into the in-process
977
+ * representation `localStartApiCommand` consumes downstream.
978
+ *
979
+ * - both unset -> `undefined` (pass dev creds through)
980
+ * - only `--assume-role-auto` -> `{ perLambda: {}, bareAutoResolve: true }`
981
+ * (per-Lambda auto-resolve for every routed Lambda)
982
+ * - only `--assume-role` -> the value-form accumulator (global ARN
983
+ * and/or per-Lambda map) as-is
984
+ * - both -> accumulator with `bareAutoResolve = true` overlaid;
985
+ * the per-Lambda map still wins for named Lambdas, auto-resolve
986
+ * fills in for every Lambda the map does NOT name
987
+ *
988
+ * Enforces the mutual-exclusion guard: `--assume-role-auto` and a
989
+ * global ARN (`--assume-role <arn>`) both occupy the "default for
990
+ * every other Lambda" slot, so the combination is ambiguous and
991
+ * rejected at boot with a clear error. The per-Lambda map IS
992
+ * compatible with either side.
993
+ */
994
+ function normalizeStartApiAssumeRole(raw, autoResolve) {
995
+ if (raw === void 0) return autoResolve ? {
996
+ perLambda: {},
997
+ bareAutoResolve: true
998
+ } : void 0;
999
+ if (autoResolve && raw.globalArn) throw new Error(`--assume-role-auto auto-resolves EACH routed Lambda's own execution role, but --assume-role ${raw.globalArn} also names a single global default. These are mutually exclusive on the global slot. Either drop the global ARN to keep --assume-role-auto for every Lambda, or drop --assume-role-auto to keep the global default. Per-Lambda overrides (--assume-role <LogicalId>=<arn>) are compatible with either side.`);
1000
+ if (autoResolve) raw.bareAutoResolve = true;
1001
+ return raw;
1002
+ }
1003
+ /**
1004
+ * Resolve the effective IAM role ARN for a given Lambda. Per-Lambda
1005
+ * override wins; otherwise the global default; otherwise `undefined`
1006
+ * (no role to assume — pass developer creds through).
1007
+ */
1008
+ function effectiveAssumeRoleArn(logicalId, opt) {
1009
+ if (!opt) return void 0;
1010
+ return opt.perLambda?.[logicalId] ?? opt.globalArn;
1011
+ }
1012
+
885
1013
  //#endregion
886
1014
  //#region src/utils/error-handler.ts
887
1015
  /**
@@ -5112,7 +5240,8 @@ var CfnLocalStateProvider = class {
5112
5240
  const client = this.getLambdaClient();
5113
5241
  try {
5114
5242
  const resp = await client.send(new GetFunctionConfigurationCommand({ FunctionName: functionPhysicalId }));
5115
- if (typeof resp.Role === "string" && resp.Role.startsWith("arn:")) return resp.Role;
5243
+ if (isIamRoleArn(resp.Role)) return resp.Role;
5244
+ if (resp.Role !== void 0) logger.warn(`${this.label}: GetFunctionConfiguration(${flattenToOneLine(functionPhysicalId)}) returned a Role that is not a well-formed IAM role ARN: ${describeRejectedRoleArn(resp.Role)}. Ignoring it. Pass the ARN explicitly: --assume-role <arn>.`);
5116
5245
  return;
5117
5246
  } catch (err) {
5118
5247
  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>.`);
@@ -5135,7 +5264,8 @@ var CfnLocalStateProvider = class {
5135
5264
  const client = this.getAgentCoreControlClient();
5136
5265
  try {
5137
5266
  const resp = await client.send(new GetAgentRuntimeCommand({ agentRuntimeId: runtimePhysicalId }));
5138
- if (typeof resp.roleArn === "string" && resp.roleArn.startsWith("arn:")) return resp.roleArn;
5267
+ if (isIamRoleArn(resp.roleArn)) return resp.roleArn;
5268
+ if (resp.roleArn !== void 0) logger.warn(`${this.label}: GetAgentRuntime(${flattenToOneLine(runtimePhysicalId)}) returned a roleArn that is not a well-formed IAM role ARN: ${describeRejectedRoleArn(resp.roleArn)}. Ignoring it. Pass the ARN explicitly: --assume-role <arn>.`);
5139
5269
  return;
5140
5270
  } catch (err) {
5141
5271
  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>.`);
@@ -7772,6 +7902,7 @@ async function pullEcrImage(imageUri, options) {
7772
7902
  * mirrors the convention used by `src/utils/role-arn.ts`.
7773
7903
  */
7774
7904
  async function assumeRoleForEcr(roleArn, callerRegion, profile, logger) {
7905
+ if (!isIamRoleArn(roleArn)) throw new LocalInvokeBuildError(refusedRoleArnMessage(roleArn));
7775
7906
  logger.debug(`Assuming role ${roleArn} for ECR pull...`);
7776
7907
  const sts = new STSClient(buildStsClientConfig({
7777
7908
  region: callerRegion,
@@ -16358,6 +16489,7 @@ async function fetchLayerContentUrl(layer, credentials, options) {
16358
16489
  }
16359
16490
  }
16360
16491
  async function assumeRoleForLayer(roleArn, region, options) {
16492
+ if (!isIamRoleArn(roleArn)) throw new LayerMaterializationError(refusedRoleArnMessage(roleArn));
16361
16493
  const client = (options.stsClientFactory ?? await defaultStsClientFactory())(region);
16362
16494
  try {
16363
16495
  const command = await buildAssumeRoleCommand(roleArn);
@@ -17724,7 +17856,10 @@ function resolveStartApiAssumeRoleArn(args) {
17724
17856
  if (explicit) return explicit;
17725
17857
  if (!assumeRole.bareAutoResolve) return void 0;
17726
17858
  const roleProp = (lambdaResource.Properties ?? {})["Role"];
17727
- if (typeof roleProp === "string" && roleProp.startsWith("arn:")) return roleProp;
17859
+ if (typeof roleProp === "string") {
17860
+ if (isIamRoleArn(roleProp)) return roleProp;
17861
+ getLogger().warn(`--assume-role: the template Role for '${logicalId}' is not a well-formed IAM role ARN: ${describeRejectedRoleArn(roleProp)}. Ignoring it.`);
17862
+ }
17728
17863
  if (stateBundle) {
17729
17864
  const fromState = resolveExecutionRoleArnFromState(stateBundle.state, logicalId);
17730
17865
  if (fromState) {
@@ -19263,23 +19398,56 @@ async function resolveAssumeRoleArnForLambda(assumeRole, stateForRoleHint, state
19263
19398
  const fnPhysicalId = stateForRoleHint.resources[lambdaLogicalId]?.physicalId;
19264
19399
  if (stateProvider?.resolveLambdaExecutionRoleArn && fnPhysicalId) {
19265
19400
  const liveArn = await stateProvider.resolveLambdaExecutionRoleArn(fnPhysicalId);
19266
- if (liveArn) {
19401
+ if (liveArn !== void 0 && !isIamRoleArn(liveArn)) logger.warn(`--assume-role: GetFunctionConfiguration for '${lambdaLogicalId}' produced a value that is not a well-formed IAM role ARN: ${describeRejectedRoleArn(liveArn)}. Ignoring it.`);
19402
+ else if (liveArn) {
19267
19403
  logger.info(`--assume-role: auto-resolved execution role from GetFunctionConfiguration: ${flattenToOneLine(liveArn)}`);
19268
19404
  return liveArn;
19269
19405
  }
19270
19406
  }
19271
19407
  logger.warn(`--assume-role: could not resolve the execution role ARN for '${lambdaLogicalId}'. Pass the ARN explicitly: --assume-role <arn>. Falling back to the developer's shell credentials.`);
19272
19408
  }
19409
+ /**
19410
+ * Pull a resource's execution-role ARN out of deployed stack state.
19411
+ *
19412
+ * Both reads below are WIRE-derived — `properties` / `observedProperties` and
19413
+ * the referenced role's cached `Arn` attribute all come from a
19414
+ * CloudFormation-backed state record — and the value is handed to
19415
+ * `AssumeRoleCommand.RoleArn`, so each is shape-checked with
19416
+ * {@link isIamRoleArn} rather than the `startsWith('arn:')` it used to carry
19417
+ * (issue #607).
19418
+ *
19419
+ * A value that is PRESENT and rejected warns, because a caller that gets
19420
+ * `undefined` cannot distinguish "state names no role" from "state names
19421
+ * something that is not a role ARN", and the second is a real misconfiguration
19422
+ * a user needs to see. Rejection still returns `undefined`, so the existing
19423
+ * fallback order (state -> live `GetFunctionConfiguration` -> shell
19424
+ * credentials) is unchanged.
19425
+ *
19426
+ * The warns are worded WITHOUT a `--assume-role:` prefix on purpose. An
19427
+ * earlier revision carried one, on the belief that every caller ends in that
19428
+ * flag's "could not resolve the execution role ARN" line. It does not:
19429
+ * {@link suggestAssumeRoleFromState} calls this function on a plain
19430
+ * `cdkl invoke --from-cfn-stack` with NO role flag at all (its caller is
19431
+ * guarded on `options.assumeRole === undefined`) and is silent on a miss — so
19432
+ * the prefix named a flag the user had not passed, on the one path where the
19433
+ * whole point is to SUGGEST it.
19434
+ */
19273
19435
  function resolveExecutionRoleArnFromState(state, logicalId, roleProperty = "Role") {
19274
19436
  const lambda = state.resources[logicalId];
19275
19437
  if (!lambda) return void 0;
19438
+ const logger = getLogger();
19276
19439
  const roleRef = lambda.properties?.[roleProperty] ?? lambda.observedProperties?.[roleProperty];
19277
- if (typeof roleRef === "string" && roleRef.startsWith("arn:")) return roleRef;
19440
+ if (typeof roleRef === "string") {
19441
+ if (isIamRoleArn(roleRef)) return roleRef;
19442
+ logger.warn(`Deployed state for '${logicalId}' carries a ${roleProperty} that is not a well-formed IAM role ARN: ${describeRejectedRoleArn(roleRef)}. Ignoring it.`);
19443
+ return;
19444
+ }
19278
19445
  if (typeof roleRef === "object" && roleRef !== null) {
19279
19446
  const refLogicalId = pickReferencedLogicalId(roleRef);
19280
19447
  if (refLogicalId) {
19281
19448
  const cached = state.resources[refLogicalId]?.attributes?.["Arn"];
19282
- if (typeof cached === "string" && cached.startsWith("arn:")) return cached;
19449
+ if (isIamRoleArn(cached)) return cached;
19450
+ if (typeof cached === "string" && cached.length > 0) logger.warn(`The cached Arn attribute of '${refLogicalId}' is not a well-formed IAM role ARN: ${describeRejectedRoleArn(cached)}. Ignoring it.`);
19283
19451
  }
19284
19452
  }
19285
19453
  }
@@ -21203,9 +21371,15 @@ async function applyAgentCoreCredentialEnv(dockerEnv, args) {
21203
21371
  * when all of those miss.
21204
21372
  */
21205
21373
  async function resolveAssumeRoleArn(options, resolved, loaded, stateProvider) {
21206
- if (typeof options.assumeRole === "string") return options.assumeRole;
21374
+ if (typeof options.assumeRole === "string") {
21375
+ if (!isIamRoleArn(options.assumeRole)) throw new CdkLocalError(`Invalid --assume-role value: expected an IAM role ARN like arn:aws:iam::123456789012:role/MyRole, got ${describeRejectedRoleArn(options.assumeRole)}.`, "LOCAL_INVOKE_AGENTCORE_ASSUME_ROLE_INVALID");
21376
+ return options.assumeRole;
21377
+ }
21207
21378
  if (options.assumeRole !== true) return void 0;
21208
- if (resolved.roleArn) return resolved.roleArn;
21379
+ if (resolved.roleArn !== void 0) {
21380
+ if (isIamRoleArn(resolved.roleArn)) return resolved.roleArn;
21381
+ getLogger().warn(`--assume-role: the template RoleArn for '${resolved.logicalId}' is not a well-formed IAM role ARN: ${describeRejectedRoleArn(resolved.roleArn)}. Ignoring it.`);
21382
+ }
21209
21383
  if (loaded) {
21210
21384
  const fromState = resolveExecutionRoleArnFromState(loaded, resolved.logicalId, "RoleArn");
21211
21385
  if (fromState) {
@@ -21215,7 +21389,8 @@ async function resolveAssumeRoleArn(options, resolved, loaded, stateProvider) {
21215
21389
  const runtimePhysicalId = loaded.resources[resolved.logicalId]?.physicalId;
21216
21390
  if (stateProvider?.resolveAgentCoreRuntimeRoleArn && runtimePhysicalId) {
21217
21391
  const liveArn = await stateProvider.resolveAgentCoreRuntimeRoleArn(runtimePhysicalId);
21218
- if (liveArn) {
21392
+ if (liveArn !== void 0 && !isIamRoleArn(liveArn)) getLogger().warn(`--assume-role: GetAgentRuntime for '${resolved.logicalId}' produced a value that is not a well-formed IAM role ARN: ${describeRejectedRoleArn(liveArn)}. Ignoring it.`);
21393
+ else if (liveArn) {
21219
21394
  getLogger().info(`--assume-role: auto-resolved execution role from GetAgentRuntime: ${flattenToOneLine(liveArn)}`);
21220
21395
  return liveArn;
21221
21396
  }
@@ -21327,6 +21502,7 @@ function forwardAwsEnv$1(env) {
21327
21502
  }
21328
21503
  }
21329
21504
  async function assumeAgentCoreExecutionRole(roleArn, region, profile) {
21505
+ if (!isIamRoleArn(roleArn)) throw new AssumeRoleFailure(refusedRoleArnMessage(roleArn), `the role ARN is not well-formed: ${describeRejectedRoleArn(roleArn)}`);
21330
21506
  const { STSClient, AssumeRoleCommand } = await import("@aws-sdk/client-sts");
21331
21507
  const sts = new STSClient(buildStsClientConfig({
21332
21508
  region,
@@ -39452,4 +39628,4 @@ function addStudioSpecificOptions(cmd) {
39452
39628
 
39453
39629
  //#endregion
39454
39630
  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 };
39455
- //# sourceMappingURL=local-studio-DOdzcjkE.js.map
39631
+ //# sourceMappingURL=local-studio-J8BtocsI.js.map