@go-to-k/cdkd 0.98.2 → 0.99.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -448,13 +448,15 @@ cdkd local start-api --port 3000 # pin the first server's port
448
448
  cdkd local start-api MyHttpApi # filter to one API (logical id, single-stack apps)
449
449
  cdkd local start-api MyStack/MyHttpApi # OR: CDK Construct path
450
450
  cdkd local start-api --warm --watch # pre-start + hot reload
451
+ cdkd local start-api --from-state # substitute deployed env vars in Lambda Environment
451
452
  ```
452
453
 
453
454
  One server per discovered API — authorizers, CORS configs, and stage
454
455
  variables stay scoped to the owning API. Supports REST v1 + HTTP API +
455
456
  Function URL with AWS_PROXY integrations; Lambda TOKEN / REQUEST,
456
457
  Cognito User Pool, and HTTP v2 JWT authorizers (JWKS-verified); CORS
457
- preflight; hot reload via `--watch`.
458
+ preflight; hot reload via `--watch`; deploy-state-backed env var
459
+ substitution via `--from-state`.
458
460
 
459
461
  ### `local run-task`
460
462
 
package/dist/cli.js CHANGED
@@ -4217,23 +4217,83 @@ var S3BucketPolicyProvider = class {
4217
4217
  /**
4218
4218
  * Adopt an existing S3 bucket policy into cdkd state.
4219
4219
  *
4220
- * **Explicit override only.** An `S3::BucketPolicy` is a policy document
4221
- * attached to a bucket via `PutBucketPolicy` it has no standalone
4222
- * identity and is not independently taggable. There is no `aws:cdk:path`
4223
- * tag to look up by; only the bucket itself is taggable.
4220
+ * The operational identifier for an `S3::BucketPolicy` is the **bucket
4221
+ * name** every AWS SDK call (`PutBucketPolicy` / `GetBucketPolicy` /
4222
+ * `DeleteBucketPolicy`) takes the bucket name via the `Bucket`
4223
+ * parameter, and cdkd's `create()` records `properties.Bucket` as the
4224
+ * resource's `physicalId` so subsequent `update()` / `delete()` /
4225
+ * `readCurrentState()` calls hit the right bucket. A `BucketPolicy`
4226
+ * has no standalone identity, no taggable ARN, and no `aws:cdk:path`
4227
+ * lookup — only the bucket itself is taggable.
4228
+ *
4229
+ * Resolution order (closes [#356](https://github.com/go-to-k/cdkd/issues/356)):
4230
+ *
4231
+ * 1. **`knownPhysicalId` if it matches an S3 bucket name shape.**
4232
+ * Preserves the `cdkd import --resource <logicalId>=<bucketName>`
4233
+ * path that has always worked.
4234
+ * 2. **`properties.Bucket` if it is a literal bucket name.** Closes
4235
+ * the `--migrate-from-cloudformation` case: AWS CloudFormation's
4236
+ * `DescribeStackResources` returns the CFn-generated policy NAME
4237
+ * for `AWS::S3::BucketPolicy` (e.g.
4238
+ * `MyStack-MyBucketPolicy-XXXXXXXXXX`), which is NOT a valid S3
4239
+ * bucket name. The first time cdkd touches the imported state with
4240
+ * that name, `readCurrentState` → `GetBucketPolicy` rejects it.
4241
+ * 3. **Hard error** when neither path resolves a bucket name. This
4242
+ * covers (a) `--migrate-from-cloudformation` against a CFn stack
4243
+ * whose template carries `Bucket: {Ref: <MyBucket>}` (the typical
4244
+ * CDK shape) when the referenced bucket is NOT in the importable
4245
+ * set (or hasn't been imported yet in the current run), and (b)
4246
+ * explicit `--resource <logicalId>=<non-bucket-name>` typos.
4247
+ * Pointing the user at `--resource <logicalId>=<bucketName>` is
4248
+ * the recovery path that always works.
4224
4249
  *
4225
- * Users adopting an existing bucket policy should pass
4226
- * `--resource <logicalId>=<bucketName>` (matching the physical id
4227
- * format returned by `create()`).
4250
+ * Intrinsic-valued `Bucket` (e.g. `{Ref: <MyBucket>}`) falls into
4251
+ * branch 3 here even when the referenced sibling has been imported in
4252
+ * the same run `import()` is called BEFORE
4253
+ * `resolveImportedProperties` runs the synth template's Properties
4254
+ * through the intrinsic resolver, so the raw intrinsic object is what
4255
+ * we see. The recovery message names `--resource` as the explicit
4256
+ * escape hatch.
4228
4257
  */
4229
4258
  async import(input) {
4230
- if (input.knownPhysicalId) return {
4259
+ if (input.knownPhysicalId && isS3BucketName(input.knownPhysicalId)) return {
4231
4260
  physicalId: input.knownPhysicalId,
4232
4261
  attributes: {}
4233
4262
  };
4234
- return null;
4263
+ const bucket = input.properties["Bucket"];
4264
+ if (typeof bucket === "string" && isS3BucketName(bucket)) return {
4265
+ physicalId: bucket,
4266
+ attributes: {}
4267
+ };
4268
+ const knownNote = input.knownPhysicalId ? ` Got knownPhysicalId='${input.knownPhysicalId}' (not a valid S3 bucket name; CloudFormation returns the policy resource NAME for AWS::S3::BucketPolicy, which is not the operational identifier).` : "";
4269
+ const bucketNote = bucket !== void 0 ? ` Properties.Bucket=${JSON.stringify(bucket)} did not resolve to a literal bucket name (intrinsic-valued entries like {Ref: <Bucket>} are not resolved at import time).` : " Properties.Bucket is missing.";
4270
+ throw new Error(`Cannot determine bucket name for ${input.resourceType} '${input.logicalId}'.${knownNote}${bucketNote} Re-run with --resource ${input.logicalId}=<bucketName> (e.g. my-bucket-12345) to point cdkd at the bucket this policy is attached to.`);
4235
4271
  }
4236
4272
  };
4273
+ /**
4274
+ * Recognize an S3 bucket name. AWS rules
4275
+ * (https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucketnamingrules.html):
4276
+ * - 3-63 characters
4277
+ * - lowercase letters, digits, hyphens, and dots
4278
+ * - must start and end with a letter or digit
4279
+ * - no consecutive dots
4280
+ * - no `xn--` prefix (reserved for IDN bucket names)
4281
+ * - no `-s3alias` suffix (reserved for S3 Access Point aliases)
4282
+ * - no `--ol-s3` suffix (reserved for S3 on Outposts)
4283
+ *
4284
+ * A practical pattern that excludes the obvious CFn-generated names like
4285
+ * `MyStack-MyBucketPolicy-XXXXXXXXXX` (which contain uppercase letters
4286
+ * and exceed 63 chars in common cases) while accepting every normal CDK
4287
+ * auto-generated and user-declared bucket name.
4288
+ */
4289
+ function isS3BucketName(value) {
4290
+ if (value.length < 3 || value.length > 63) return false;
4291
+ if (!/^[a-z0-9][a-z0-9.-]*[a-z0-9]$/.test(value)) return false;
4292
+ if (value.includes("..")) return false;
4293
+ if (value.startsWith("xn--")) return false;
4294
+ if (value.endsWith("-s3alias") || value.endsWith("--ol-s3")) return false;
4295
+ return true;
4296
+ }
4237
4297
 
4238
4298
  //#endregion
4239
4299
  //#region src/provisioning/providers/sqs-queue-provider.ts
@@ -5593,22 +5653,60 @@ var SNSTopicPolicyProvider = class {
5593
5653
  /**
5594
5654
  * Adopt an existing SNS topic policy into cdkd state.
5595
5655
  *
5596
- * **Explicit override only.** A `TopicPolicy` is an attachment to one or
5597
- * more SNS topics applied via `SetTopicAttributes(AttributeName=Policy)`
5598
- * it has no standalone identity and is not independently taggable. There
5599
- * is no `aws:cdk:path` tag to look up by, and the policy has no name/ARN
5600
- * of its own.
5656
+ * The operational identifier for a `TopicPolicy` is the **comma-joined
5657
+ * list of SNS topic ARNs** the policy is attached to every AWS SDK
5658
+ * call (`SetTopicAttributes` / `GetTopicAttributes`) takes a topic ARN
5659
+ * via the `TopicArn` parameter, and cdkd's `create()` records
5660
+ * `topics.join(',')` as the resource's `physicalId` so subsequent
5661
+ * `update()` / `delete()` / `readCurrentState()` calls hit the right
5662
+ * topic(s). A `TopicPolicy` has no standalone identity, no taggable
5663
+ * ARN, and no `aws:cdk:path` lookup — only the parent topics are
5664
+ * taggable.
5665
+ *
5666
+ * Resolution order (closes [#356](https://github.com/go-to-k/cdkd/issues/356)):
5667
+ *
5668
+ * 1. **`knownPhysicalId` if it is a comma-joined list of SNS topic ARNs.**
5669
+ * Preserves the `cdkd import --resource <logicalId>=<topic-arns>`
5670
+ * path that has always worked.
5671
+ * 2. **`properties.Topics.join(',')` if every entry is a literal topic
5672
+ * ARN.** Closes the `--migrate-from-cloudformation` case: AWS
5673
+ * CloudFormation's `DescribeStackResources` returns the CFn-generated
5674
+ * policy NAME for `AWS::SNS::TopicPolicy` (e.g.
5675
+ * `MyStack-MyTopicPolicy-XXXXXXXXXX`), which is NOT a valid topic
5676
+ * ARN. The first time cdkd touches the imported state with that
5677
+ * name, `readCurrentState` → `GetTopicAttributes` rejects it.
5678
+ * 3. **Hard error** when neither path resolves a topic-ARN list. This
5679
+ * covers (a) `--migrate-from-cloudformation` against a CFn stack
5680
+ * whose template carries `Topics: [{Ref: <MyTopic>}]` (the typical
5681
+ * CDK shape) when the referenced topic is NOT in the importable
5682
+ * set (or hasn't been imported yet in the current run), and (b)
5683
+ * explicit `--resource <logicalId>=<non-arn>` typos. Pointing the
5684
+ * user at `--resource <logicalId>=<topic-arns>` is the recovery
5685
+ * path that always works.
5601
5686
  *
5602
- * Users adopting an existing topic policy should pass
5603
- * `--resource <logicalId>=<comma-joined-topic-ARNs>` (matching the
5604
- * physical id format returned by `create()`).
5687
+ * Intrinsic-valued `Topics` entries (e.g. `{Ref: <MyTopic>}`) fall into
5688
+ * branch 3 here even when the referenced sibling has been imported in
5689
+ * the same run `import()` is called BEFORE
5690
+ * `resolveImportedProperties` runs the synth template's Properties
5691
+ * through the intrinsic resolver, so the raw intrinsic object is what
5692
+ * we see. The recovery message names `--resource` as the explicit
5693
+ * escape hatch.
5605
5694
  */
5606
5695
  async import(input) {
5607
- if (input.knownPhysicalId) return {
5696
+ if (input.knownPhysicalId && isSnsTopicArnList(input.knownPhysicalId)) return {
5608
5697
  physicalId: input.knownPhysicalId,
5609
5698
  attributes: {}
5610
5699
  };
5611
- return null;
5700
+ const topics = input.properties["Topics"];
5701
+ if (Array.isArray(topics) && topics.length > 0) {
5702
+ if (topics.every((t) => typeof t === "string" && isSnsTopicArn(t))) return {
5703
+ physicalId: topics.join(","),
5704
+ attributes: {}
5705
+ };
5706
+ }
5707
+ const knownNote = input.knownPhysicalId ? ` Got knownPhysicalId='${input.knownPhysicalId}' (not a comma-joined list of SNS topic ARNs; CloudFormation returns the policy resource NAME for AWS::SNS::TopicPolicy, which is not the operational identifier).` : "";
5708
+ const topicsNote = Array.isArray(topics) && topics.length > 0 ? ` Properties.Topics=${JSON.stringify(topics)} did not resolve to a list of literal topic ARNs (intrinsic-valued entries like {Ref: <Topic>} are not resolved at import time).` : " Properties.Topics is missing or empty.";
5709
+ throw new Error(`Cannot determine topic ARNs for ${input.resourceType} '${input.logicalId}'.${knownNote}${topicsNote} Re-run with --resource ${input.logicalId}=<comma-joined-topic-ARNs> (e.g. arn:aws:sns:${input.region}:<account>:<topic-name>) to point cdkd at the topic(s) this policy is attached to.`);
5612
5710
  }
5613
5711
  /**
5614
5712
  * Set the policy on a single SNS topic
@@ -5621,6 +5719,31 @@ var SNSTopicPolicyProvider = class {
5621
5719
  }));
5622
5720
  }
5623
5721
  };
5722
+ /**
5723
+ * Recognize a single SNS topic ARN. AWS standard form is
5724
+ * `arn:<partition>:sns:<region>:<account>:<name>`; FIFO topics end in
5725
+ * `.fifo`. Accepts every partition (`aws` / `aws-cn` / `aws-us-gov` /
5726
+ * `aws-iso` / etc.) via the broader `arn:<partition>:sns:` prefix shape.
5727
+ */
5728
+ function isSnsTopicArn(value) {
5729
+ return /^arn:[a-z0-9-]+:sns:[a-z0-9-]+:\d{12}:[\w.-]+$/.test(value);
5730
+ }
5731
+ /**
5732
+ * Recognize a comma-joined list of SNS topic ARNs. cdkd's `create()`
5733
+ * records `topics.join(',')` as the `physicalId`, so a single ARN
5734
+ * (`arn:aws:sns:us-east-1:123456789012:my-topic`) is also accepted.
5735
+ * Every comma-separated segment must be a valid SNS topic ARN — a CFn
5736
+ * generated name like `MyStack-MyTopicPolicy-XXX` is correctly rejected
5737
+ * because it does not match the ARN prefix, and a partially-valid
5738
+ * mixture (one literal ARN + one CFn name) is also rejected so we fall
5739
+ * back to the properties-based resolution rather than baking a half-bad
5740
+ * list into state.
5741
+ */
5742
+ function isSnsTopicArnList(value) {
5743
+ const segments = value.split(",");
5744
+ if (segments.length === 0) return false;
5745
+ return segments.every((s) => isSnsTopicArn(s));
5746
+ }
5624
5747
 
5625
5748
  //#endregion
5626
5749
  //#region src/provisioning/providers/lambda-function-provider.ts
@@ -38841,6 +38964,7 @@ async function localStartApiCommand(target, options) {
38841
38964
  const m = buildCorsConfigByApiId(stack.template);
38842
38965
  for (const [k, v] of m) corsConfigByApiId.set(k, v);
38843
38966
  }
38967
+ const stateByStack = options.fromState ? await loadStateForRoutedStacks(targetStacks, routes, routesWithAuth, options) : /* @__PURE__ */ new Map();
38844
38968
  const lambdaIds = uniqueLambdaIds(routes, routesWithAuth);
38845
38969
  const specs = /* @__PURE__ */ new Map();
38846
38970
  for (let i = 0; i < lambdaIds.length; i++) {
@@ -38854,7 +38978,8 @@ async function localStartApiCommand(target, options) {
38854
38978
  ...debugPortBase !== void 0 && { debugPort: debugPortBase + i },
38855
38979
  stsRegion: options.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"],
38856
38980
  inlineTmpDirs,
38857
- layerTmpDirs
38981
+ layerTmpDirs,
38982
+ stateByStack
38858
38983
  });
38859
38984
  specs.set(logicalId, spec);
38860
38985
  }
@@ -39133,12 +39258,27 @@ function warnVpcConfigLambdas(routesWithAuth, stacks) {
39133
39258
  * missing, runtime not supported).
39134
39259
  */
39135
39260
  async function buildContainerSpec(args) {
39136
- const { logicalId, stacks, overrides, assumeRole, containerHost, debugPort, stsRegion, inlineTmpDirs, layerTmpDirs } = args;
39261
+ const { logicalId, stacks, overrides, assumeRole, containerHost, debugPort, stsRegion, inlineTmpDirs, layerTmpDirs, stateByStack } = args;
39137
39262
  const lambda = resolveLambdaByLogicalId(logicalId, stacks);
39138
39263
  const codeDir = lambda.codePath ?? materializeInlineCode$1(lambda.handler, lambda.inlineCode ?? "", resolveRuntimeFileExtension(lambda.runtime), inlineTmpDirs);
39139
39264
  const optDir = materializeLambdaLayers$1(lambda.layers, layerTmpDirs);
39140
- const envResult = resolveEnvVars(logicalId, getTemplateEnv$1(lambda.resource), overrides);
39141
- for (const key of envResult.unresolved) getLogger().warn(`Lambda ${logicalId}: env var ${key} contains a CloudFormation intrinsic and was dropped. Override it with --env-vars (e.g. {"${logicalId}":{"${key}":"<literal>"}}) to surface a literal value.`);
39265
+ let templateEnv = getTemplateEnv$1(lambda.resource);
39266
+ const stateBundle = stateByStack.get(lambda.stack.stackName);
39267
+ let stateAudit;
39268
+ if (stateBundle) {
39269
+ const context = { resources: stateBundle.state.resources };
39270
+ if (stateBundle.pseudoParameters) context.pseudoParameters = stateBundle.pseudoParameters;
39271
+ const { env, audit } = substituteEnvVarsFromState(templateEnv, context);
39272
+ templateEnv = env;
39273
+ stateAudit = audit;
39274
+ for (const key of audit.resolvedKeys) getLogger().debug(`Lambda ${logicalId}: --from-state substituted env var ${key}`);
39275
+ for (const { key, reason } of audit.unresolved) getLogger().warn(`Lambda ${logicalId}: --from-state could not substitute env var ${key} (${reason}). Override it via --env-vars or it will be dropped.`);
39276
+ }
39277
+ const envResult = resolveEnvVars(logicalId, templateEnv, overrides);
39278
+ for (const key of envResult.unresolved) {
39279
+ if (stateAudit && stateAudit.unresolved.some((u) => u.key === key)) continue;
39280
+ getLogger().warn(`Lambda ${logicalId}: env var ${key} contains a CloudFormation intrinsic and was dropped. Override it with --env-vars (e.g. {"${logicalId}":{"${key}":"<literal>"}}) or pass --from-state to recover deployed values.`);
39281
+ }
39142
39282
  const dockerEnv = {
39143
39283
  AWS_LAMBDA_FUNCTION_NAME: logicalId,
39144
39284
  AWS_LAMBDA_FUNCTION_MEMORY_SIZE: String(lambda.memoryMb),
@@ -39446,6 +39586,117 @@ async function reloadAllServers(args) {
39446
39586
  if (watcher) watcher.update([output, ...lastAssetPaths.value]);
39447
39587
  printPerServerRouteTables(servers);
39448
39588
  }
39589
+ /**
39590
+ * Returns true when any value in the function's template env map is a
39591
+ * CFn intrinsic (non-primitive). Used to gate the pseudo-parameter STS
39592
+ * hop inside the `--from-state` flow: literal-only env maps don't need
39593
+ * the pseudo-parameter bag and shouldn't pay for an STS call. Mirrors
39594
+ * the same gating in `local-invoke.ts` (`envHasIntrinsicValue`) and
39595
+ * `ecs-task-resolver.ts` (`containerHasIntrinsicEnvOrSecret`).
39596
+ */
39597
+ function envHasIntrinsicValue$1(templateEnv) {
39598
+ if (!templateEnv) return false;
39599
+ for (const v of Object.values(templateEnv)) {
39600
+ if (v === void 0 || v === null) continue;
39601
+ if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") continue;
39602
+ return true;
39603
+ }
39604
+ return false;
39605
+ }
39606
+ /**
39607
+ * Load cdkd's S3 state for every stack that owns a routed Lambda. Once
39608
+ * per `synthesizeAndBuild` pass (initial boot + every reload), so a
39609
+ * Lambda's per-spec build does not pay one round-trip per Lambda. Per-
39610
+ * stack failures (no state, ambiguous region, bucket resolution error)
39611
+ * degrade to warn-and-fall-back via {@link loadStateForStack} — the
39612
+ * affected stack's reachable Lambdas behave as if `--from-state` were
39613
+ * not set, while sibling stacks with loadable state still substitute.
39614
+ *
39615
+ * Pseudo parameters are resolved per stack and only when at least one
39616
+ * reachable Lambda in that stack has an intrinsic-valued env entry
39617
+ * (gated via {@link envHasIntrinsicValue}). STS failures degrade to
39618
+ * warn and leave `pseudoParameters: undefined` — substitution still
39619
+ * runs for non-`AWS::*` refs.
39620
+ */
39621
+ async function loadStateForRoutedStacks(stacks, routes, routesWithAuth, options) {
39622
+ const logger = getLogger();
39623
+ const out = /* @__PURE__ */ new Map();
39624
+ const lambdaIds = uniqueLambdaIds(routes, routesWithAuth);
39625
+ const reachableStackNames = /* @__PURE__ */ new Set();
39626
+ for (const logicalId of lambdaIds) for (const stack of stacks) {
39627
+ const resource = stack.template.Resources?.[logicalId];
39628
+ if (resource && resource.Type === "AWS::Lambda::Function") {
39629
+ reachableStackNames.add(stack.stackName);
39630
+ break;
39631
+ }
39632
+ }
39633
+ const stackHasIntrinsicEnv = (stackName) => {
39634
+ for (const logicalId of lambdaIds) for (const stack of stacks) {
39635
+ if (stack.stackName !== stackName) continue;
39636
+ const resource = stack.template.Resources?.[logicalId];
39637
+ if (!resource || resource.Type !== "AWS::Lambda::Function") continue;
39638
+ if (envHasIntrinsicValue$1(getTemplateEnv$1(resource))) return true;
39639
+ }
39640
+ return false;
39641
+ };
39642
+ for (const stackName of reachableStackNames) {
39643
+ const stack = stacks.find((s) => s.stackName === stackName);
39644
+ if (!stack) continue;
39645
+ const loaded = await loadStateForStack(stack.stackName, stack.region, {
39646
+ ...options.stackRegion !== void 0 && { stackRegion: options.stackRegion },
39647
+ ...options.stateBucket !== void 0 && { stateBucket: options.stateBucket },
39648
+ statePrefix: options.statePrefix,
39649
+ ...options.region !== void 0 && { region: options.region },
39650
+ ...options.profile !== void 0 && { profile: options.profile }
39651
+ });
39652
+ if (!loaded) continue;
39653
+ const bundle = { state: loaded.state };
39654
+ if (stackHasIntrinsicEnv(stackName)) {
39655
+ const pseudo = await resolvePseudoParametersForStartApi(loaded.region, options);
39656
+ if (pseudo) bundle.pseudoParameters = pseudo;
39657
+ }
39658
+ out.set(stackName, bundle);
39659
+ logger.debug(`--from-state: loaded state for ${stackName} (${loaded.region})`);
39660
+ }
39661
+ return out;
39662
+ }
39663
+ /**
39664
+ * Build the AWS pseudo-parameter bag for `--from-state` env-var
39665
+ * substitution. Mirrors `resolvePseudoParametersForInvoke` in
39666
+ * `local-invoke.ts` byte-for-byte — kept inlined here rather than
39667
+ * extracted into a shared helper because the two call sites differ in
39668
+ * region precedence (this one is per-stack so the resolved state
39669
+ * region takes priority).
39670
+ *
39671
+ * Region precedence: `--region` > `AWS_REGION` > `AWS_DEFAULT_REGION` >
39672
+ * the state record's region (returned by `loadStateForStack`).
39673
+ */
39674
+ async function resolvePseudoParametersForStartApi(stateRegion, options) {
39675
+ const logger = getLogger();
39676
+ const region = options.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"] ?? stateRegion;
39677
+ let accountId;
39678
+ try {
39679
+ const { STSClient, GetCallerIdentityCommand } = await import("@aws-sdk/client-sts");
39680
+ const sts = new STSClient({ ...region && { region } });
39681
+ try {
39682
+ accountId = (await sts.send(new GetCallerIdentityCommand({}))).Account;
39683
+ } finally {
39684
+ sts.destroy();
39685
+ }
39686
+ } catch (err) {
39687
+ logger.warn(`--from-state: resolver needs \${AWS::AccountId} but STS GetCallerIdentity failed: ${err instanceof Error ? err.message : String(err)}. Substitution will be skipped for AWS::AccountId; affected env entries will be dropped with per-key warnings.`);
39688
+ }
39689
+ const partitionAndSuffix = region ? derivePartitionAndUrlSuffix(region) : void 0;
39690
+ const bag = {
39691
+ ...accountId !== void 0 && { accountId },
39692
+ ...region !== void 0 && { region },
39693
+ ...partitionAndSuffix && {
39694
+ partition: partitionAndSuffix.partition,
39695
+ urlSuffix: partitionAndSuffix.urlSuffix
39696
+ }
39697
+ };
39698
+ return Object.keys(bag).length === 0 ? void 0 : bag;
39699
+ }
39449
39700
  /** Validate `--debug-port-base`. */
39450
39701
  function parseDebugPort(raw) {
39451
39702
  const parsed = parseInt(raw, 10);
@@ -39456,11 +39707,12 @@ function parseDebugPort(raw) {
39456
39707
  * Builder for the `start-api` subcommand. Wired up by `local.ts`.
39457
39708
  */
39458
39709
  function createLocalStartApiCommand() {
39459
- const startApi = new Command("start-api").description("Run a long-running local HTTP server that maps API Gateway routes (REST v1, HTTP API, Function URL) to Lambda invocations against the AWS Lambda Runtime Interface Emulator (Docker required). Supports Lambda TOKEN/REQUEST authorizers and Cognito User Pool / HTTP v2 JWT authorizers; when JWKS is unreachable, JWT authorizers fall back to pass-through (every token accepted) with a warn line — local dev fallback. VPC-config Lambdas run locally and surface a warn line at startup; their containers do NOT get attached to the deployed VPC subnets, so calls to private RDS / ElastiCache will fail.").argument("[target]", "Optional API filter. Accepts the bare CDK logical id ('MyHttpApi'; single-stack apps only), stack-qualified logical id ('MyStack:MyHttpApi'), full CDK Construct path ('MyStack/MyHttpApi/Resource'), or an ancestor Construct path that prefix-matches ('MyStack/MyHttpApi'). When omitted, every discovered API gets its own server. Mirrors `cdkd local invoke` / `cdkd local run-task` target syntax.").addOption(new Option("--port <port>", "HTTP server port (default: auto-allocate)").default("0")).addOption(new Option("--host <host>", "Bind address").default("127.0.0.1")).addOption(new Option("--stack <name>", "Stack to start (single-stack apps auto-detect)")).addOption(new Option("--warm", "Pre-start one container per Lambda at server boot").default(false)).addOption(new Option("--per-lambda-concurrency <n>", "Pool size cap per Lambda (default 2, max 4)").default("2")).addOption(new Option("--no-pull", "Skip docker pull (cached image)")).addOption(new Option("--container-host <host>", "IP the host uses to bind/probe the RIE port (must be a numeric IP — `docker run -p <ip>:<port>:8080` rejects hostnames). Defaults to 127.0.0.1.").default("127.0.0.1")).addOption(new Option("--debug-port-base <port>", "Reserve a contiguous --debug-port range (one per Lambda)")).addOption(new Option("--env-vars <file>", "JSON env-var overrides (SAM-compatible: {\"LogicalId\":{\"KEY\":\"VALUE\"}, \"Parameters\": {...}})")).addOption(new Option("--assume-role <arn-or-pair>", "Assume the Lambda's execution role and forward STS-issued temp creds. Bare <arn> = global default; <LogicalId>=<arn> = per-Lambda override (repeatable). Per-Lambda > global > unset (developer creds passed through).").argParser((raw, prev) => parseAssumeRoleToken(raw, prev))).addOption(new Option("--watch", "Hot-reload: re-synth + re-discover routes when cdk.out/ or asset directories change. Off by default; the server keeps the previous version serving when synth fails mid-reload.").default(false)).addOption(new Option("--stage <name>", "Select an API Gateway Stage by its 'StageName'. Default: the first Stage attached to each API. Drives event.stageVariables for both REST v1 and HTTP API v2. NOTE: For HTTP API v2 routes, requestContext.stage is always '$default' regardless of this flag (AWS-side limitation — HTTP API only exposes one stage to the integration event); only event.stageVariables is affected for v2 routes. For REST v1 routes the selected StageName is also threaded into requestContext.stage.")).addOption(new Option("--api <id>", "DEPRECATED — use the positional <target> argument instead. Same accepted forms (bare logical id, stack-qualified, Construct path, ancestor prefix). Will be removed in a future major release.")).action(withErrorHandling(localStartApiCommand));
39710
+ const startApi = new Command("start-api").description("Run a long-running local HTTP server that maps API Gateway routes (REST v1, HTTP API, Function URL) to Lambda invocations against the AWS Lambda Runtime Interface Emulator (Docker required). Supports Lambda TOKEN/REQUEST authorizers and Cognito User Pool / HTTP v2 JWT authorizers; when JWKS is unreachable, JWT authorizers fall back to pass-through (every token accepted) with a warn line — local dev fallback. VPC-config Lambdas run locally and surface a warn line at startup; their containers do NOT get attached to the deployed VPC subnets, so calls to private RDS / ElastiCache will fail.").argument("[target]", "Optional API filter. Accepts the bare CDK logical id ('MyHttpApi'; single-stack apps only), stack-qualified logical id ('MyStack:MyHttpApi'), full CDK Construct path ('MyStack/MyHttpApi/Resource'), or an ancestor Construct path that prefix-matches ('MyStack/MyHttpApi'). When omitted, every discovered API gets its own server. Mirrors `cdkd local invoke` / `cdkd local run-task` target syntax.").addOption(new Option("--port <port>", "HTTP server port (default: auto-allocate)").default("0")).addOption(new Option("--host <host>", "Bind address").default("127.0.0.1")).addOption(new Option("--stack <name>", "Stack to start (single-stack apps auto-detect)")).addOption(new Option("--warm", "Pre-start one container per Lambda at server boot").default(false)).addOption(new Option("--per-lambda-concurrency <n>", "Pool size cap per Lambda (default 2, max 4)").default("2")).addOption(new Option("--no-pull", "Skip docker pull (cached image)")).addOption(new Option("--container-host <host>", "IP the host uses to bind/probe the RIE port (must be a numeric IP — `docker run -p <ip>:<port>:8080` rejects hostnames). Defaults to 127.0.0.1.").default("127.0.0.1")).addOption(new Option("--debug-port-base <port>", "Reserve a contiguous --debug-port range (one per Lambda)")).addOption(new Option("--env-vars <file>", "JSON env-var overrides (SAM-compatible: {\"LogicalId\":{\"KEY\":\"VALUE\"}, \"Parameters\": {...}})")).addOption(new Option("--assume-role <arn-or-pair>", "Assume the Lambda's execution role and forward STS-issued temp creds. Bare <arn> = global default; <LogicalId>=<arn> = per-Lambda override (repeatable). Per-Lambda > global > unset (developer creds passed through).").argParser((raw, prev) => parseAssumeRoleToken(raw, prev))).addOption(new Option("--watch", "Hot-reload: re-synth + re-discover routes when cdk.out/ or asset directories change. Off by default; the server keeps the previous version serving when synth fails mid-reload.").default(false)).addOption(new Option("--stage <name>", "Select an API Gateway Stage by its 'StageName'. Default: the first Stage attached to each API. Drives event.stageVariables for both REST v1 and HTTP API v2. NOTE: For HTTP API v2 routes, requestContext.stage is always '$default' regardless of this flag (AWS-side limitation — HTTP API only exposes one stage to the integration event); only event.stageVariables is affected for v2 routes. For REST v1 routes the selected StageName is also threaded into requestContext.stage.")).addOption(new Option("--api <id>", "DEPRECATED — use the positional <target> argument instead. Same accepted forms (bare logical id, stack-qualified, Construct path, ancestor prefix). Will be removed in a future major release.")).addOption(new Option("--from-state", "Read cdkd S3 state for every routed stack and substitute Ref / Fn::GetAtt / Fn::Sub / Fn::Join (and AWS pseudo parameters) in Lambda env vars with the deployed physical IDs / attributes. Off by default — pre-PR warn-and-drop semantics are preserved. Turn on for stacks already deployed via cdkd deploy. Mirrors `cdkd local invoke --from-state` / `cdkd local run-task --from-state`. Re-runs against fresh state on every hot-reload firing (--watch).").default(false)).addOption(new Option("--stack-region <region>", "Region of the cdkd state record to read (used with --from-state when the same stack name has state in multiple regions).")).action(withErrorHandling(localStartApiCommand));
39460
39711
  [
39461
39712
  ...commonOptions,
39462
39713
  ...appOptions,
39463
- ...contextOptions
39714
+ ...contextOptions,
39715
+ ...stateOptions
39464
39716
  ].forEach((opt) => startApi.addOption(opt));
39465
39717
  startApi.addOption(deprecatedRegionOption);
39466
39718
  return startApi;
@@ -42295,7 +42547,7 @@ function reorderArgs(argv) {
42295
42547
  */
42296
42548
  async function main() {
42297
42549
  const program = new Command();
42298
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.98.1");
42550
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.99.0");
42299
42551
  program.addCommand(createBootstrapCommand());
42300
42552
  program.addCommand(createSynthCommand());
42301
42553
  program.addCommand(createListCommand());