@go-to-k/cdkd 0.283.10 → 0.283.12

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/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { $ as coerceCfnBoolean, $t as resolveStateBucketWithDefaultAndSource, A as cyan, An as NestedStackChildDirectDestroyError, At as BOOTSTRAP_MARKER_PREFIX, B as findSilentDropProperties, Bt as runDockerForeground, C as makeCanonicalizePropertiesFn, Ct as stringifyValue, D as renderStatefulReason, Dn as LocalStartServiceError, Dt as loadPublishableAssetManifest, E as isStatefulRecreateTargetSync, En as LocalMigrateError, Et as createAssetRedirectResolver, F as IAMRoleProvider, Fn as StackHasActiveImportsError, Ft as validateContainerRepoName, G as IntrinsicFunctionResolver, Gn as isThrottlingError, Gt as synthesisStatusMessage, H as slowCcOperationTimeoutMs, Hn as withErrorHandling, Ht as AssetManifestLoader, I as collectInlinePolicyNamesManagedBySiblings, In as StackTerminationProtectionError, It as buildDenyExternalAccessPolicy, J as refStateLookupFromResource, Jt as resolveApp, K as cfnRefValueFromPhysicalId, Kn as markNonRetryable, Kt as getDefaultStateBucketName, L as clearOnUpdateRemoval, Lt as buildDockerImage, M as green, Mn as ProvisioningError, Mt as getBootstrapMarkerKey, N as red, Nn as ResourceTimeoutError, Nt as parseBootstrapMarker, O as formatResourceLine, Ot as rewriteTemplateAssetReferences, P as yellow, Pn as ResourceUpdateNotSupportedError, Pt as validateAssetBucketName, Q as assertRegionMatch, Qt as resolveStateBucketWithDefault, R as ProviderRegistry, Rt as formatDockerLoginError, S as unsupportedFinalSnapshotError, St as AssetPublisher, T as MULTI_REGION_RECREATE_BLOCKED_TYPES, Tn as LocalInvokeBuildError$1, Tt as buildAssetRedirectMap, U as disableInstanceApiTermination, Un as isMarkedNonRetryable, Ut as getDockerImageBySourceHash, V as CloudControlProvider, Vn as normalizeAwsError, Vt as runDockerStreaming, W as isTerminationProtectionPropagationError, Wn as isRetryableTransientError, Wt as Synthesizer, X as normalizeAwsTagsToCfn, Xt as resolveCaptureObservedState, Y as WAFv2WebACLProvider, Yt as resolveAutoAssetStorage, Z as resolveExplicitPhysicalId, Zt as resolveSkipPrefix, _ as buildFinalSnapshotIdentifier, _n as getAwsClients, _t as TemplateParser, a as DeploymentEventsStore, an as MIGRATE_TMP_PREFIX, at as requireConfigObject, b as isFinalSnapshotError, bt as rebuildClientForBucketRegion, c as replayFailedOperations, cn as expectedOwnerParam, ct as s3BucketDomainName, d as deleteSkipReason, dn as derivePartitionAndUrlSuffix, dt as s3BucketWebsiteUrl, en as resolveUseCdkBootstrapAssets, et as configBooleanRefusal, f as withResourceDeadline, fn as AssemblyReader, ft as applyRoleArnIfSet, g as PRE_DELETE_SNAPSHOT_TYPES, gn as AwsClients, gt as DagBuilder, h as ATOMIC_FINAL_SNAPSHOT_TYPES, hn as resolveBucketRegion, ht as withRetry, i as DeploymentEventsReader, in as CFN_TEMPLATE_URL_LIMIT, it as requireConfigArray, j as gray, jn as PartialFailureError, jt as ensureAssetStorage, k as bold, kn as MissingCdkCliError, kt as AssetModeResolver, l as replayRollback, ln as PARTITION_TABLE, lt as s3BucketDualStackDomainName, m as computeImplicitDeleteEdges, mt as describeTypeWithThrottleRetry, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as warnDeprecatedNoPrefixCliFlag, nt as readConfigString, o as planFailedOps, on as findLargeInlineResources, ot as requireConfigString, p as IMPLICIT_DELETE_DEPENDENCIES, pn as processStackMessages, pt as DiffCalculator, q as getAccountInfo, qt as getLegacyStateBucketName, r as DeployEngine, rn as CFN_TEMPLATE_BODY_LIMIT, rt as replayWarn, s as planRollback, sn as uploadCfnTemplate, st as s3BucketArn, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as stateBucketExistenceConfirmed, tt as configStringRefusal, u as UNSPECIFIED_SKIP_REASON, un as canonicalizeRegion, ut as s3BucketRegionalDomainName, v as ccRoutedFinalSnapshotError, vn as resetAwsClients, vt as LockManager, w as extractDeploymentEventError, wn as DeployCancelledError, wt as WorkGraph, x as refusesFinalSnapshot, xn as CdkdError, xt as shouldRetainResource, y as createPreDeleteFinalSnapshot, yn as setAwsClients, yt as S3StateBackend, z as findActionableSilentDrops, zt as getDockerCmd } from "./deploy-engine-Bvr31OQ0.js";
2
+ import { $ as coerceCfnBoolean, $t as resolveStateBucketWithDefault, A as cyan, An as MissingCdkCliError, At as AssetModeResolver, B as findSilentDropProperties, Bt as getDockerCmd, C as makeCanonicalizePropertiesFn, Ct as stringifyValue, D as renderStatefulReason, Dn as LocalMigrateError, Dt as loadPublishableAssetManifest, E as isStatefulRecreateTargetSync, En as LocalInvokeBuildError$1, Et as createAssetRedirectResolver, F as IAMRoleProvider, Fn as ResourceUpdateNotSupportedError, Ft as validateAssetBucketName, G as IntrinsicFunctionResolver, Gn as isRetryableTransientError, Gt as Synthesizer, H as slowCcOperationTimeoutMs, Hn as normalizeAwsError, Ht as runDockerStreaming, I as collectInlinePolicyNamesManagedBySiblings, In as StackHasActiveImportsError, It as validateContainerRepoName, J as refStateLookupFromResource, Jt as getLegacyStateBucketName, K as cfnRefValueFromPhysicalId, Kn as isThrottlingError, Kt as synthesisStatusMessage, L as clearOnUpdateRemoval, Ln as StackTerminationProtectionError, Lt as buildDenyExternalAccessPolicy, M as green, Mn as PartialFailureError, Mt as ensureAssetStorage, N as red, Nn as ProvisioningError, Nt as getBootstrapMarkerKey, O as formatResourceLine, On as LocalStartServiceError, Ot as rewriteTemplateAssetReferences, P as yellow, Pn as ResourceTimeoutError, Pt as parseBootstrapMarker, Q as assertRegionMatch, Qt as resolveSkipPrefix, R as ProviderRegistry, Rt as buildDockerImage, S as unsupportedFinalSnapshotError, Sn as CdkdError, St as AssetPublisher, T as MULTI_REGION_RECREATE_BLOCKED_TYPES, Tn as DeployCancelledError, Tt as buildAssetRedirectMap, U as disableInstanceApiTermination, Un as withErrorHandling, Ut as AssetManifestLoader, V as CloudControlProvider, Vt as runDockerForeground, W as isTerminationProtectionPropagationError, Wn as isMarkedNonRetryable, Wt as getDockerImageBySourceHash, X as normalizeAwsTagsToCfn, Xt as resolveAutoAssetStorage, Y as WAFv2WebACLProvider, Yt as resolveApp, Z as resolveExplicitPhysicalId, Zt as resolveCaptureObservedState, _ as buildFinalSnapshotIdentifier, _n as AwsClients, _t as TemplateParser, a as DeploymentEventsStore, an as CFN_TEMPLATE_URL_LIMIT, at as requireConfigObject, b as isFinalSnapshotError, bn as setAwsClients, bt as rebuildClientForBucketRegion, c as replayFailedOperations, cn as uploadCfnTemplate, ct as s3BucketDomainName, d as deleteSkipReason, dn as canonicalizeRegion, dt as s3BucketWebsiteUrl, en as resolveStateBucketWithDefaultAndSource, et as configBooleanRefusal, f as withResourceDeadline, fn as derivePartitionAndUrlSuffix, ft as applyRoleArnIfSet, g as PRE_DELETE_SNAPSHOT_TYPES, gn as resolveBucketRegion, gt as DagBuilder, h as ATOMIC_FINAL_SNAPSHOT_TYPES, ht as withRetry, i as DeploymentEventsReader, in as CFN_TEMPLATE_BODY_LIMIT, it as requireConfigArray, j as gray, jn as NestedStackChildDirectDestroyError, jt as BOOTSTRAP_MARKER_PREFIX, k as bold, kt as escapeRegExp, l as replayRollback, ln as expectedOwnerParam, lt as s3BucketDualStackDomainName, m as computeImplicitDeleteEdges, mn as processStackMessages, mt as describeTypeWithThrottleRetry, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as stateBucketExistenceConfirmed, nt as readConfigString, o as planFailedOps, on as MIGRATE_TMP_PREFIX, ot as requireConfigString, p as IMPLICIT_DELETE_DEPENDENCIES, pn as AssemblyReader, pt as DiffCalculator, q as getAccountInfo, qn as markNonRetryable, qt as getDefaultStateBucketName, r as DeployEngine, rn as warnDeprecatedNoPrefixCliFlag, rt as replayWarn, s as planRollback, sn as findLargeInlineResources, st as s3BucketArn, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as resolveUseCdkBootstrapAssets, tt as configStringRefusal, u as UNSPECIFIED_SKIP_REASON, un as PARTITION_TABLE, ut as s3BucketRegionalDomainName, v as ccRoutedFinalSnapshotError, vn as getAwsClients, vt as LockManager, w as extractDeploymentEventError, wt as WorkGraph, x as refusesFinalSnapshot, xt as shouldRetainResource, y as createPreDeleteFinalSnapshot, yn as resetAwsClients, yt as S3StateBackend, z as findActionableSilentDrops, zt as formatDockerLoginError } from "./deploy-engine-DHOpGcMS.js";
3
3
  import { a as getLiveRenderer, c as PATTERN_B_RESOURCE_TYPES, d as generateResourceNameWithFallback, h as withStackName, i as runStackBuffered, m as withSkipPrefix, n as getLogger, o as PATTERN_B_NAME_OPTIONS, s as PATTERN_B_NAME_PROPERTIES, u as generateResourceName } from "./logger-zRrlbaQt.js";
4
- import { t as ASGProvider } from "./asg-provider-CrBdHIgB.js";
4
+ import { t as ASGProvider } from "./asg-provider-BKR0cF_X.js";
5
5
  import { AsyncLocalStorage } from "node:async_hooks";
6
6
  import { createHash, randomBytes, randomUUID } from "node:crypto";
7
7
  import { CopyObjectCommand, CreateBucketCommand, DeleteBucketAnalyticsConfigurationCommand, DeleteBucketCommand, DeleteBucketCorsCommand, DeleteBucketEncryptionCommand, DeleteBucketIntelligentTieringConfigurationCommand, DeleteBucketInventoryConfigurationCommand, DeleteBucketLifecycleCommand, DeleteBucketMetricsConfigurationCommand, DeleteBucketOwnershipControlsCommand, DeleteBucketPolicyCommand, DeleteBucketReplicationCommand, DeleteBucketTaggingCommand, DeleteBucketWebsiteCommand, DeleteObjectsCommand, GetBucketAccelerateConfigurationCommand, GetBucketCorsCommand, GetBucketEncryptionCommand, GetBucketLifecycleConfigurationCommand, GetBucketLocationCommand, GetBucketLoggingCommand, GetBucketNotificationConfigurationCommand, GetBucketOwnershipControlsCommand, GetBucketPolicyCommand, GetBucketReplicationCommand, GetBucketTaggingCommand, GetBucketVersioningCommand, GetBucketWebsiteCommand, GetObjectCommand, GetObjectLockConfigurationCommand, GetPublicAccessBlockCommand, HeadBucketCommand, ListBucketAnalyticsConfigurationsCommand, ListBucketIntelligentTieringConfigurationsCommand, ListBucketInventoryConfigurationsCommand, ListBucketMetricsConfigurationsCommand, ListObjectVersionsCommand, ListObjectsV2Command, NoSuchBucket, PutBucketAccelerateConfigurationCommand, PutBucketAnalyticsConfigurationCommand, PutBucketCorsCommand, PutBucketEncryptionCommand, PutBucketIntelligentTieringConfigurationCommand, PutBucketInventoryConfigurationCommand, PutBucketLifecycleConfigurationCommand, PutBucketLoggingCommand, PutBucketMetricsConfigurationCommand, PutBucketNotificationConfigurationCommand, PutBucketOwnershipControlsCommand, PutBucketPolicyCommand, PutBucketReplicationCommand, PutBucketTaggingCommand, PutBucketVersioningCommand, PutBucketWebsiteCommand, PutObjectCommand, PutObjectLockConfigurationCommand, PutPublicAccessBlockCommand, S3Client, S3ServiceException } from "@aws-sdk/client-s3";
@@ -36881,7 +36881,7 @@ var Route53Provider = class {
36881
36881
  physicalId: zoneId,
36882
36882
  attributes: {
36883
36883
  Id: zoneId,
36884
- NameServers: nameServers.join(",")
36884
+ NameServers: nameServers
36885
36885
  }
36886
36886
  };
36887
36887
  } catch (error) {
@@ -36915,7 +36915,7 @@ var Route53Provider = class {
36915
36915
  wasReplaced: false,
36916
36916
  attributes: {
36917
36917
  Id: physicalId,
36918
- NameServers: nameServers.join(",")
36918
+ NameServers: nameServers
36919
36919
  }
36920
36920
  };
36921
36921
  } catch (error) {
@@ -37023,7 +37023,7 @@ var Route53Provider = class {
37023
37023
  async getHostedZoneAttribute(physicalId, attributeName) {
37024
37024
  switch (attributeName) {
37025
37025
  case "Id": return physicalId;
37026
- case "NameServers": return ((await this.getClient().send(new GetHostedZoneCommand({ Id: physicalId }))).DelegationSet?.NameServers ?? []).join(",");
37026
+ case "NameServers": return (await this.getClient().send(new GetHostedZoneCommand({ Id: physicalId }))).DelegationSet?.NameServers ?? [];
37027
37027
  default: return;
37028
37028
  }
37029
37029
  }
@@ -65786,6 +65786,363 @@ function createEventsPruneCommand() {
65786
65786
  })).option("--older-than <duration>", "Delete runs older than this duration (e.g. 24h, 90m)").option("--all", "Delete every recorded run and the index (full purge)", false).action(withErrorHandling((stack, _options, command) => eventsPruneCommand(stack, command.optsWithGlobals())));
65787
65787
  }
65788
65788
 
65789
+ //#endregion
65790
+ //#region src/utils/ecr-uri.ts
65791
+ /**
65792
+ * The ECR registry-host FORM TABLE — the ONE place the host forms AWS serves are
65793
+ * spelled (issue #1793).
65794
+ *
65795
+ * The forms used to be spelled TWICE, here and in `src/cli/commands/gc.ts`,
65796
+ * and the two already disagreed: gc carried the `-fips` and `on.aws` forms
65797
+ * while this module matched only the plain one, so a genuine FIPS or dual-stack
65798
+ * registry was not recognized here AT ALL and `cdkd local invoke` /
65799
+ * `run-task` classified it as a public image (anonymous pull, no
65800
+ * `docker login`).
65801
+ *
65802
+ * **What is shared is this TABLE, not the whole matcher** — read that precisely,
65803
+ * because the unification is deliberately PARTIAL. gc builds its pattern from
65804
+ * the table through {@link ecrRegistryHostPattern}; this module's own
65805
+ * {@link ECR_URI_HOST_REGEX} re-spells the alternation from the table's LABELS
65806
+ * column and cannot use that builder at all (its doc says why). So the shared
65807
+ * fact is which label runs AWS serves and which of them carry a FIXED suffix —
65808
+ * which is exactly what had drifted — while each matcher keeps its own
65809
+ * acceptance rule, deliberately: gc matches the suffix against the union over
65810
+ * every partition and {@link parseEcrRegistryHost} pairs it WITH the region (see
65811
+ * `gc.ts`'s `AWS_URL_SUFFIXES` doc for why the two strictnesses must not merge).
65812
+ *
65813
+ * A THIRD, looser spelling of "this is an ECR registry host" still lives outside
65814
+ * this table — `isCdkAssetImageUri`'s `host.includes('.dkr.ecr.')` in
65815
+ * `src/local/ecs-task-resolver.ts`, which recognizes only the plain form and is
65816
+ * case-SENSITIVE. It is tracked as issue #1846 rather than folded in here: it
65817
+ * must keep tolerating an UNRESOLVED host
65818
+ * (`${AWS::AccountId}.dkr.ecr.${AWS::Region}.${AWS::URLSuffix}`), so it cannot
65819
+ * simply become a call to either matcher.
65820
+ *
65821
+ * Every row is read off the AWS-published `ecr` endpoint list
65822
+ * (https://docs.aws.amazon.com/general/latest/gr/ecr.html) plus the docker
65823
+ * push / pull examples in the ECR user guide's "Making requests to Amazon ECR
65824
+ * registries", rather than inferred:
65825
+ *
65826
+ * - `<acct>.dkr.ecr.<region>.<urlSuffix>` — the IPv4-only registry endpoint,
65827
+ * every partition.
65828
+ * - `<acct>.dkr.ecr-fips.<region>.<urlSuffix>` — the FIPS IPv4 endpoint, served
65829
+ * in `us-east-1` / `us-east-2` / `us-west-1` / `us-west-2` /
65830
+ * `us-gov-east-1` / `us-gov-west-1` — i.e. only in partitions whose suffix is
65831
+ * `amazonaws.com`, which is why pairing it with the region's own partition
65832
+ * suffix is exactly right rather than merely convenient.
65833
+ * - `<acct>.dkr-ecr.<region>.on.aws` — the dual-stack (IPv4 + IPv6) endpoint.
65834
+ * - `<acct>.dkr-ecr-fips.<region>.on.aws` — the dual-stack FIPS endpoint, in the
65835
+ * same six regions as the IPv4 FIPS form. This one was missing from BOTH
65836
+ * copies of the grammar; unifying them is what surfaced it.
65837
+ *
65838
+ * The two dual-stack forms carry a FIXED suffix, so their check is that literal
65839
+ * rather than the region's partition suffix — the TIGHTEST available check for
65840
+ * them, not a relaxation: `on.aws` is an AWS-owned domain, so unlike a captured
65841
+ * suffix it cannot be substituted by a host someone else owns. They are
65842
+ * accepted for ANY region rather than only the partitions AWS documents them
65843
+ * in, deliberately: under-accepting is precisely the issue #1764 failure (a
65844
+ * cdkd table lagging AWS made a GENUINE registry classify as public), and the
65845
+ * suffix being AWS-owned means the widening cannot point a `docker login`
65846
+ * anywhere but AWS.
65847
+ */
65848
+ const ECR_REGISTRY_HOST_FORMS = [
65849
+ { labels: "dkr.ecr" },
65850
+ { labels: "dkr.ecr-fips" },
65851
+ {
65852
+ labels: "dkr-ecr",
65853
+ fixedUrlSuffix: "on.aws"
65854
+ },
65855
+ {
65856
+ labels: "dkr-ecr-fips",
65857
+ fixedUrlSuffix: "on.aws"
65858
+ }
65859
+ ];
65860
+ /**
65861
+ * {@link ECR_REGISTRY_HOST_FORMS} ordered LONGEST LABELS FIRST, so
65862
+ * `dkr.ecr-fips` is tried before its own `dkr.ecr` prefix instead of relying on
65863
+ * backtracking.
65864
+ */
65865
+ const ECR_HOST_FORMS_LONGEST_FIRST = [...ECR_REGISTRY_HOST_FORMS].sort((a, b) => b.labels.length - a.labels.length);
65866
+ /**
65867
+ * {@link ECR_REGISTRY_HOST_FORMS} keyed by its (already lower-case) labels, for
65868
+ * resolving which suffix rule the matched form wants.
65869
+ */
65870
+ const ECR_HOST_FORM_BY_LABELS = new Map(ECR_REGISTRY_HOST_FORMS.map((form) => [form.labels, form]));
65871
+ /**
65872
+ * The host half of an ECR image URI as a regex SOURCE string, built from
65873
+ * {@link ECR_REGISTRY_HOST_FORMS} (issue #1793).
65874
+ *
65875
+ * ONE consumer: `cdkd gc`, which passes loose non-capturing sub-patterns and
65876
+ * matches the suffix against the union over every partition.
65877
+ *
65878
+ * **This module does NOT call it**, and that is structural rather than an
65879
+ * oversight — {@link ECR_URI_HOST_REGEX} re-spells the alternation from the
65880
+ * table's LABELS column instead. The strict matcher needs the suffix CAPTURED,
65881
+ * at a FIXED group index, to pair it with the region; this builder inlines a
65882
+ * `fixedUrlSuffix` as an UNCAPTURED literal, so the capturing-group count would
65883
+ * differ per alternative and the group index of the suffix would depend on which
65884
+ * form matched. Making the fixed suffix capturing to even the count is not the
65885
+ * fix either: it would hand gc a group it does not want, ahead of the tag /
65886
+ * digest groups it reads BY POSITION. So the sharable unit is the table, not the
65887
+ * pattern — and the cross-matcher test in `tests/unit/cli/gc.test.ts` is driven
65888
+ * off the table, so a row added there must work on BOTH sides or the suite reds.
65889
+ *
65890
+ * Every group this function ADDS is non-capturing, and that is load-bearing
65891
+ * rather than stylistic, for the same by-position reason: `gc.ts` reads its ECR
65892
+ * tag / digest by GROUP INDEX, and a capturing group here would silently shift
65893
+ * them. The caller's own three sub-patterns are interpolated verbatim, so
65894
+ * whether THOSE capture is the caller's business.
65895
+ */
65896
+ function ecrRegistryHostPattern(segments) {
65897
+ const forms = ECR_HOST_FORMS_LONGEST_FIRST.map((form) => {
65898
+ const suffix = form.fixedUrlSuffix === void 0 ? segments.partitionUrlSuffix : escapeRegExp(form.fixedUrlSuffix);
65899
+ return `${escapeRegExp(form.labels)}\\.${segments.region}\\.${suffix}`;
65900
+ });
65901
+ return `${segments.accountId}\\.(?:${forms.join("|")})`;
65902
+ }
65903
+ /**
65904
+ * Matching the HOST half of an ECR image URI:
65905
+ * `<acct>.<labels>.<region>.<urlSuffix>/`.
65906
+ *
65907
+ * The suffix is CAPTURED rather than spelled out (issue #1758): the previous
65908
+ * pattern hardcoded `amazonaws.com` with an optional `.cn` tail, so a `us-iso*`
65909
+ * registry (`c2s.ic.gov` / `sc2s.sgov.gov`) never matched and the caller
65910
+ * silently classified a real ECR image as a user-managed one — skipping the
65911
+ * `docker login` it needs.
65912
+ *
65913
+ * The LABELS are captured too, because which suffix is expected depends on
65914
+ * WHICH form matched (issue #1793) — the dual-stack forms carry a fixed
65915
+ * `on.aws` rather than the region's partition suffix.
65916
+ *
65917
+ * Built from {@link ECR_REGISTRY_HOST_FORMS}'s LABELS column rather than through
65918
+ * {@link ecrRegistryHostPattern}: the builder cannot serve this matcher's fixed
65919
+ * capture-group layout (see its doc). The table is what the two matchers share.
65920
+ *
65921
+ * The `i` flag makes the labels case-INSENSITIVE (issue #1792). DNS is
65922
+ * case-insensitive, so `<acct>.DKR.ECR.<region>.<suffix>` names the same host,
65923
+ * but the labels were spelled as case-sensitive literals — and unlike the
65924
+ * issue #1786 defect one layer over, the host then did not match the SHAPE at
65925
+ * all, so `parseEcrRegistryHost` returned `undefined` AND
65926
+ * {@link looksLikeEcrHostWithForeignSuffix} returned `false`, i.e. not even the
65927
+ * issue #1764 diagnostic fired.
65928
+ *
65929
+ * Widening the shape is the right fix rather than a refusal because DOCKER
65930
+ * accepts an upper-cased registry host, so cdkd is not made to `docker login`
65931
+ * for a URI docker will then reject. `distribution/reference`'s grammar spells
65932
+ * `domain-component` as `([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])`
65933
+ * while the repository PATH is `alpha-numeric := [a-z0-9]+`, and
65934
+ * `ParseNormalizedNamed` raises `repository name must be lowercase` for the
65935
+ * REMOTE NAME only — `splitDockerDomain` performs no case folding on the
65936
+ * domain. This change never touches the repository path.
65937
+ *
65938
+ * What docker does NOT fold is its CREDENTIAL STORE, which is keyed on the
65939
+ * hostname verbatim (measured in issue #1801: a pull of an upper-cased host
65940
+ * sent no credentials at all, while the lower-cased spelling authenticated).
65941
+ * That is why the fix here is only the SHAPE match: the login / pull spelling
65942
+ * is reconciled one layer up by `ecr-puller.ts`'s `canonicalizeImageUriHost`,
65943
+ * which every docker-facing consumer already routes through. Issue #1817
65944
+ * tracks moving that fold in here.
65945
+ *
65946
+ * **Every verdict issues #1792 / #1793 changed, in full** — `p` is
65947
+ * {@link parseEcrRegistryHost}, `d` is
65948
+ * {@link looksLikeEcrHostWithForeignSuffix} (the issue #1764 diagnostic), `S`
65949
+ * is the region's own partition suffix and `F` a foreign one. The table is
65950
+ * spelled out because the DIAGNOSTIC-ONLY rows are the easy ones to omit: `p`
65951
+ * is unchanged there, so a summary that tracks only the parse verdict reads as
65952
+ * "no change" while a warning that never used to fire now does.
65953
+ *
65954
+ * ```text
65955
+ * host before after what changed
65956
+ * <a>.dkr.ecr.<r>.S p=ok d=f p=ok d=f nothing
65957
+ * <a>.dkr.ecr.<r>.F p=und d=TRUE p=und d=TRUE nothing
65958
+ * <a>.dkr.ecr.<r>.on.aws p=und d=TRUE p=und d=TRUE nothing (*)
65959
+ * <a>.DKR.ECR.<r>.S p=und d=f p=ok d=f #1792 PARSE
65960
+ * <a>.DKR.ECR.<r>.F p=und d=f p=und d=TRUE #1792 diag only
65961
+ * <a>.dkr.ecr-fips.<r>.S p=und d=f p=ok d=f #1793 PARSE
65962
+ * <a>.dkr.ecr-fips.<r>.F p=und d=f p=und d=TRUE #1793 diag only
65963
+ * <a>.dkr-ecr.<r>.on.aws p=und d=f p=ok d=f #1793 PARSE
65964
+ * <a>.dkr-ecr-fips.<r>.on.aws p=und d=f p=ok d=f #1793 PARSE
65965
+ * <a>.dkr-ecr.<r>.S p=und d=f p=und d=TRUE #1793 diag only
65966
+ * <a>.dkr-ecr-fips.<r>.S p=und d=f p=und d=TRUE #1793 diag only
65967
+ * <a>.dkr.ecr-fips.<r>.on.aws p=und d=f p=und d=TRUE #1793 diag only
65968
+ * ```
65969
+ *
65970
+ * (*) `dkr.ecr` + `on.aws` is the one mispairing that ALREADY reported `true`
65971
+ * before the change: the pre-#1793 pattern matched the plain labels against a
65972
+ * wildcard `([^/]+)` suffix, so `on.aws` was simply a suffix that is not
65973
+ * `amazonaws.com`. Every other row that flips `d` was previously refused at the
65974
+ * SHAPE, which is why both verdicts were quiet.
65975
+ *
65976
+ * The five `d`-only flips are an improvement in kind — a host AWS does not serve
65977
+ * now says so instead of passing silently as a public image — but the caller's
65978
+ * MESSAGE was written for the #1764 partition-gap case and now over-claims for a
65979
+ * form/suffix mispairing (`amazonaws.com` IS `us-east-1`'s partition suffix).
65980
+ * `src/local/ecs-task-resolver.ts` owns that wording; recorded on issue #1846.
65981
+ */
65982
+ const ECR_URI_HOST_REGEX = new RegExp(`^(\\d{12})\\.(${ECR_HOST_FORMS_LONGEST_FIRST.map((form) => escapeRegExp(form.labels)).join("|")})\\.([^.]+)\\.([^/]+)/`, "i");
65983
+ /**
65984
+ * A region segment cdkd is willing to treat as a region id.
65985
+ *
65986
+ * Case is not the only way the `startsWith` partition classification can be
65987
+ * side-stepped: any leading junk defeats it the same way, and the captured
65988
+ * segment does not stay inert — it becomes an `ECRClient({region})` and is
65989
+ * interpolated into the fallback `docker login` endpoint. Measured before this
65990
+ * guard: `…dkr.ecr. us-iso-east-1.amazonaws.com/…` (leading space) and a
65991
+ * combining-mark form both parsed, yielding regions `" us-iso-east-1"` and
65992
+ * `"us-i̇so-east-1"`. Neither leaks credentials — an ACCEPTED suffix is always
65993
+ * an AWS-owned domain — but a region id is ASCII alphanumeric plus `-`, so
65994
+ * anything else is a malformed host and refusing it keeps the classification
65995
+ * honest.
65996
+ *
65997
+ * TESTED AGAINST THE RAW SEGMENT, BEFORE `toLowerCase()`, and that order is the
65998
+ * whole point: `String.prototype.toLowerCase` performs full Unicode case
65999
+ * folding, so the Kelvin sign U+212A folds to a plain ASCII `k`. Checking the
66000
+ * FOLDED segment therefore accepts `us-eKst-1` and yields the region
66001
+ * `us-ekst-1` — a region the host does not name — which is precisely the
66002
+ * class of substitution this guard exists to refuse. Matching `[A-Za-z0-9-]`
66003
+ * on the raw capture keeps case-insensitivity (the point of issue #1786) while
66004
+ * admitting only characters that fold to themselves.
66005
+ *
66006
+ * ONE DELIBERATE SIDE EFFECT, called out because it is a withdrawal rather than
66007
+ * an addition: a malformed-region host is now classified as an ordinary public
66008
+ * image with NO cdkd-side signal at all — neither the ECR verdict nor the #1764
66009
+ * foreign-suffix diagnostic. Both arms change, not just the foreign one:
66010
+ * `<acct>.dkr.ecr.us_east_1.example.com/…` used to log the partition-gap
66011
+ * warning and now logs nothing, and `<acct>.dkr.ecr.us_east_1.amazonaws.com/…`
66012
+ * used to be ACCEPTED as ECR and is now refused. That is the correct verdict
66013
+ * rather than a regression — the diagnostic's whole subject is "this suffix does
66014
+ * not belong to its region's PARTITION", and a segment that is not a region id
66015
+ * has no partition to belong to, so reporting it would send the reader hunting
66016
+ * for a table entry that could never exist — but it does mean a typo'd region
66017
+ * gets no cdkd-side hint, only docker's own pull failure. Both arms are pinned
66018
+ * by tests so a future widening of the guard cannot flip them silently.
66019
+ */
66020
+ const CANONICAL_REGION_SEGMENT = /^[A-Za-z0-9][A-Za-z0-9-]*$/;
66021
+ /**
66022
+ * A URL suffix cdkd is willing to compare against the partition table.
66023
+ *
66024
+ * The SAME raw-capture rule as the region, and for the same reason one layer
66025
+ * over: the suffix is folded before the comparison, so a code point folding
66026
+ * into ASCII can impersonate a partition's suffix. This became reachable the
66027
+ * moment issue #1764 landed `aws-isoe`'s `cloud.adc-e.uk` — that suffix
66028
+ * contains a `k`, so `…dkr.ecr.eu-isoe-west-1.cloud.adc-e.uK/…` (Kelvin
66029
+ * sign) folded onto it and was ACCEPTED. Not a credential leak, since UTS-46
66030
+ * maps U+212A to `k` too and the host resolves to the real AWS domain, but it
66031
+ * falsifies the invariant the region guard's comment above states, so both
66032
+ * captures are held to the same rule rather than only the one that was
66033
+ * measured first.
66034
+ *
66035
+ * Dots are admitted because a suffix is a dotted DNS name; every other
66036
+ * character is refused, so the accepted set stays exactly the partition
66037
+ * table's own literals.
66038
+ */
66039
+ const CANONICAL_URL_SUFFIX = /^[A-Za-z0-9][A-Za-z0-9.-]*$/;
66040
+ /**
66041
+ * The ONE case-normalization boundary of this module (issue #1786).
66042
+ *
66043
+ * DNS is case-INSENSITIVE, so `US-ISO-EAST-1.amazonaws.com` and
66044
+ * `us-iso-east-1.amazonaws.com` name the same host — but both halves of the
66045
+ * #1758 suffix check were case-SENSITIVE, in OPPOSITE directions:
66046
+ *
66047
+ * - an upper-cased REGION fails every `startsWith` prefix test in
66048
+ * `derivePartitionAndUrlSuffix`, so it falls through to the commercial
66049
+ * partition whose suffix is `amazonaws.com` — which is exactly what an ISO
66050
+ * look-alike host carries, so the strict check ACCEPTED it (the bypass);
66051
+ * - an upper-cased region or SUFFIX on a GENUINE host stopped matching its own
66052
+ * partition's suffix, so a real ECR registry was REJECTED and the caller
66053
+ * silently classified it as a user-managed image — the #1758 regression, one
66054
+ * layer up.
66055
+ *
66056
+ * The normalization lives HERE rather than inside `derivePartitionAndUrlSuffix`
66057
+ * because this is the boundary where an untrusted, DNS-shaped string enters
66058
+ * cdkd, and keeping it in ONE function means both exported entry points below —
66059
+ * and the suffix COMPARISON itself — cannot drift apart.
66060
+ *
66061
+ * This does NOT duplicate `canonicalizeRegion`, which issue #1795 added inside
66062
+ * `derivePartitionAndUrlSuffix` so the mapping folds its own input. The two
66063
+ * answer different questions and both are needed: that one makes the partition
66064
+ * LOOKUP case-insensitive for every caller, while these guards decide whether
66065
+ * the captured segments are a region id and a URL suffix AT ALL. Folding alone
66066
+ * cannot do that — folding is precisely what turns `us-eKst-1` into the
66067
+ * plausible `us-ekst-1` (see the guards' own comments), so a fold-only helper
66068
+ * makes the substitution easier to reach, not harder. Double-folding is a
66069
+ * no-op, which is why the redundant `toLowerCase()` calls below are left in
66070
+ * rather than removed: they keep this function the single answer for the
66071
+ * comparison even if the helper's behaviour changes again.
66072
+ *
66073
+ * The derived suffix is lower-cased too. Every entry in the partition table is
66074
+ * a lower-case literal today, so that is a no-op; it is there so this function
66075
+ * stays the single answer even if that table ever gains a mixed-case entry.
66076
+ */
66077
+ function matchEcrRegistryHost(imageUri) {
66078
+ const m = ECR_URI_HOST_REGEX.exec(imageUri);
66079
+ if (!m) return void 0;
66080
+ if (!CANONICAL_REGION_SEGMENT.test(m[3])) return void 0;
66081
+ if (!CANONICAL_URL_SUFFIX.test(m[4])) return void 0;
66082
+ const form = ECR_HOST_FORM_BY_LABELS.get(m[2].toLowerCase());
66083
+ if (!form) return void 0;
66084
+ const region = m[3].toLowerCase();
66085
+ return {
66086
+ accountId: m[1],
66087
+ region,
66088
+ suffix: m[4].toLowerCase(),
66089
+ expectedSuffix: (form.fixedUrlSuffix ?? derivePartitionAndUrlSuffix(region).urlSuffix).toLowerCase()
66090
+ };
66091
+ }
66092
+ /**
66093
+ * The account + region of an ECR image URI, or `undefined` when the URI is not
66094
+ * an ECR registry host — either because it does not have one of the shapes
66095
+ * {@link ECR_REGISTRY_HOST_FORMS} lists, or because its suffix is not the one
66096
+ * the matched form carries for the region it names.
66097
+ *
66098
+ * Lives in `src/utils/` rather than beside its first caller because it has TWO
66099
+ * consumers in different layers — `src/local/ecr-puller.ts` (which needs the
66100
+ * `:tag` tail too) and `src/local/ecs-task-resolver.ts` (which classifies an
66101
+ * image that may carry a digest or no tag at all). Before issue #1758 they each
66102
+ * carried their own copy of the hardcoded commercial pattern, and fixing only
66103
+ * the pull path left `cdkd local run-task` broken outside the commercial
66104
+ * partition; ONE definition is what stops them drifting apart again.
66105
+ *
66106
+ * The module is deliberately free of AWS SDK imports, which is what lets
66107
+ * `ecs-task-resolver.ts` consume it without breaking the invariant that module
66108
+ * documents about itself (it resolves secrets through a separate module for
66109
+ * exactly that reason).
66110
+ *
66111
+ * The returned `region` is LOWER-CASED (issue #1786) — it is consumed as an SDK
66112
+ * client region by `ecr-puller.ts` and as the `region` of a `kind: 'ecr'` image
66113
+ * by `ecs-task-resolver.ts`, and the canonical spelling of an AWS region id is
66114
+ * lower case.
66115
+ */
66116
+ function parseEcrRegistryHost(imageUri) {
66117
+ const m = matchEcrRegistryHost(imageUri);
66118
+ if (!m) return void 0;
66119
+ if (m.suffix !== m.expectedSuffix) return void 0;
66120
+ return {
66121
+ accountId: m.accountId,
66122
+ region: m.region
66123
+ };
66124
+ }
66125
+ /**
66126
+ * True when the URI has one of the ECR HOST SHAPES but carries a suffix that
66127
+ * form does not serve for the region it names — i.e. exactly the case
66128
+ * {@link parseEcrRegistryHost} rejects for a reason the caller may want to
66129
+ * report.
66130
+ *
66131
+ * Since issue #1793 that covers a form / suffix MISPAIRING too, not only a
66132
+ * foreign suffix: `<acct>.dkr-ecr.<region>.amazonaws.com` and
66133
+ * `<acct>.dkr.ecr.<region>.on.aws` each spell one form's labels with the
66134
+ * other's suffix, and AWS serves neither.
66135
+ *
66136
+ * The two rejections are worth telling apart at a call site that degrades
66137
+ * silently: a genuinely public image and a registry in a partition
66138
+ * `derivePartitionAndUrlSuffix` does not know yet (issue #1764) both come back
66139
+ * `undefined`, and only the second is a cdkd gap rather than a user choice.
66140
+ */
66141
+ function looksLikeEcrHostWithForeignSuffix(imageUri) {
66142
+ const m = matchEcrRegistryHost(imageUri);
66143
+ return m !== void 0 && m.suffix !== m.expectedSuffix;
66144
+ }
66145
+
65789
66146
  //#endregion
65790
66147
  //#region src/cli/commands/gc.ts
65791
66148
  /**
@@ -65845,10 +66202,6 @@ function parseOlderThan(value) {
65845
66202
  const multiplier = match[2] === "d" ? 1440 * 60 * 1e3 : 3600 * 1e3;
65846
66203
  return Math.round(num * multiplier);
65847
66204
  }
65848
- /** Escape a literal string for embedding in a RegExp. */
65849
- function escapeRegExp(value) {
65850
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
65851
- }
65852
66205
  /**
65853
66206
  * Characters that terminate an S3 key extracted from a URL-shaped string.
65854
66207
  * Whitespace / quotes never appear in cdkd's content-addressed asset keys
@@ -65886,30 +66239,50 @@ const GC_EXTRA_URL_SUFFIXES = [];
65886
66239
  */
65887
66240
  const URL_SUFFIX_ALTERNATION = `(?:${[...[.../* @__PURE__ */ new Set([...PARTITION_TABLE.map((row) => row.urlSuffix), ...GC_EXTRA_URL_SUFFIXES])]].sort((a, b) => b.length - a.length).map(escapeRegExp).join("|")})`;
65888
66241
  /**
65889
- * The HOST half of an ECR registry, as an alternation over the forms AWS
65890
- * serves: `<acct>.dkr.ecr.<region>.<urlSuffix>`, its FIPS sibling
65891
- * `<acct>.dkr.ecr-fips.<region>.<urlSuffix>`, and the short-form alias
65892
- * `<acct>.dkr-ecr.<region>.on.aws`.
65893
- *
65894
- * Kept DELIBERATELY separate from the S3 matchers' shared
65895
- * {@link URL_SUFFIX_ALTERNATION} rather than folded into it: `on.aws` is an
65896
- * ECR-only endpoint, so adding it to the shared suffix set would also make
65897
- * `https://<assetBucket>.s3.<region>.on.aws/<key>` a recognized S3 reference.
65898
- * Widening either matcher only ever over-PROTECTS, but the two services' host
65899
- * grammars genuinely differ and conflating them is how the next widening goes
65900
- * wrong.
66242
+ * The HOST half of an ECR registry, built from the ONE authoritative FORM TABLE
66243
+ * in `src/utils/ecr-uri.ts` (issue #1793) instead of spelled a second time here.
66244
+ *
66245
+ * That module owns which segment spellings AWS serves — the plain
66246
+ * `<acct>.dkr.ecr.<region>.<urlSuffix>`, its FIPS sibling
66247
+ * `<acct>.dkr.ecr-fips.<region>.<urlSuffix>`, and the two dual-stack forms
66248
+ * `<acct>.dkr-ecr[-fips].<region>.on.aws` — while each matcher keeps its own
66249
+ * suffix-ACCEPTANCE rule on top: gc passes the union over every partition
66250
+ * ({@link URL_SUFFIX_ALTERNATION}), whereas `parseEcrRegistryHost` pairs the
66251
+ * captured suffix WITH the region. See the {@link AWS_URL_SUFFIXES} doc for why
66252
+ * the two strictnesses are deliberate and must not be merged.
66253
+ *
66254
+ * gc is the ONLY consumer of {@link ecrRegistryHostPattern}: the strict matcher
66255
+ * there needs the suffix captured at a fixed group index and so re-spells the
66256
+ * alternation off the same table's labels column. What the two share is the
66257
+ * TABLE, not the pattern — enough to stop the FORMS drifting, which is what had
66258
+ * gone wrong, and the cross-matcher test in `tests/unit/cli/gc.test.ts` is driven
66259
+ * off the table so a row added there must work on both sides.
66260
+ *
66261
+ * Sharing the table immediately brought a form gc was MISSING:
66262
+ * `<acct>.dkr-ecr-fips.<region>.on.aws`, the dual-stack FIPS endpoint. A form
66263
+ * missing here reads a live image as unreferenced and DELETES it, so this is the
66264
+ * irreversible direction — which is the whole argument for one definition.
66265
+ *
66266
+ * `on.aws` still does not reach the S3 matchers, and now cannot: it is an
66267
+ * ECR-only endpoint, so folding it into the shared suffix set would also make
66268
+ * `https://<assetBucket>.s3.<region>.on.aws/<key>` a recognized S3 reference —
66269
+ * and in the form table a fixed suffix travels WITH its own form rather than
66270
+ * sitting in a shared set, so that separation is mechanical instead of
66271
+ * remembered.
65901
66272
  *
65902
66273
  * Account and region are matched loosely on purpose (see the extractor doc):
65903
66274
  * collecting a reference from another account's or region's URI can only KEEP
65904
- * more, never delete more — which is also why the two extra forms are worth
65905
- * carrying even though cdkd's own publisher writes the plain one. Issue #1793
65906
- * tracks deriving these host forms from ONE place shared with
65907
- * `src/utils/ecr-uri.ts` instead of two hand-kept copies — that module matches
65908
- * only the plain `dkr.ecr` form today, so the two already disagree.
66275
+ * more, never delete more — which is also why the extra forms are worth
66276
+ * carrying even though cdkd's own publisher writes the plain one.
65909
66277
  *
65910
- * Every group is non-capturing, for the {@link URL_SUFFIX_ALTERNATION} reason.
66278
+ * Every group is non-capturing, for the {@link URL_SUFFIX_ALTERNATION} reason;
66279
+ * {@link ecrRegistryHostPattern} guarantees it for the groups IT adds.
65911
66280
  */
65912
- const ECR_REGISTRY_HOST = `\\d{12}\\.(?:dkr\\.ecr(?:-fips)?\\.[a-z0-9-]+\\.${URL_SUFFIX_ALTERNATION}|dkr-ecr\\.[a-z0-9-]+\\.on\\.aws)`;
66281
+ const ECR_REGISTRY_HOST = ecrRegistryHostPattern({
66282
+ accountId: "\\d{12}",
66283
+ region: "[a-z0-9-]+",
66284
+ partitionUrlSuffix: URL_SUFFIX_ALTERNATION
66285
+ });
65913
66286
  /**
65914
66287
  * cdkd's publishers write content-addressed keys (`<sha256>.<ext>`). A
65915
66288
  * second, name-independent pass collects every such token from every
@@ -65967,7 +66340,7 @@ function buildReferenceExtractors(marker) {
65967
66340
  const s3UriRe = new RegExp(`s3://${bucket}/(${KEY_TERMINATORS}+)`, "g");
65968
66341
  const virtualHostedRe = new RegExp(`https://${bucket}\\.s3[^/\\s]*\\.${URL_SUFFIX_ALTERNATION}/(${KEY_TERMINATORS}+)`, "g");
65969
66342
  const pathStyleRe = new RegExp(`https://s3[^/\\s]*\\.${URL_SUFFIX_ALTERNATION}/${bucket}/(${KEY_TERMINATORS}+)`, "g");
65970
- const ecrRe = new RegExp(`${ECR_REGISTRY_HOST}/${repo}(?::([A-Za-z0-9_][A-Za-z0-9._-]{0,127}))?(?:@(sha256:[0-9a-f]{64}))?`, "g");
66343
+ const ecrRe = new RegExp(`${ECR_REGISTRY_HOST}/${repo}(?::([A-Za-z0-9_][A-Za-z0-9._-]{0,127}))?(?:@(sha256:[0-9a-f]{64}))?`, "gi");
65971
66344
  const extractOnce = (value, refs) => {
65972
66345
  for (const re of [
65973
66346
  s3UriRe,
@@ -65976,7 +66349,7 @@ function buildReferenceExtractors(marker) {
65976
66349
  ]) for (const match of value.matchAll(re)) if (match[1]) refs.s3Keys.add(match[1]);
65977
66350
  for (const match of value.matchAll(ecrRe)) {
65978
66351
  if (match[1]) refs.imageTags.add(match[1]);
65979
- if (match[2]) refs.imageDigests.add(match[2]);
66352
+ if (match[2]) refs.imageDigests.add(match[2].toLowerCase());
65980
66353
  }
65981
66354
  for (const match of value.matchAll(CONTENT_HASH_KEY_RE)) refs.s3Keys.add(match[0]);
65982
66355
  };
@@ -69183,183 +69556,6 @@ function notFoundError$1(target, stack, resources) {
69183
69556
  return new LocalInvokeResolutionError(msg.trimEnd());
69184
69557
  }
69185
69558
 
69186
- //#endregion
69187
- //#region src/utils/ecr-uri.ts
69188
- /**
69189
- * Matching the HOST half of an ECR image URI:
69190
- * `<acct>.dkr.ecr.<region>.<urlSuffix>/`.
69191
- *
69192
- * The suffix is CAPTURED rather than spelled out (issue #1758): the previous
69193
- * pattern hardcoded `amazonaws.com` with an optional `.cn` tail, so a `us-iso*`
69194
- * registry (`c2s.ic.gov` / `sc2s.sgov.gov`) never matched and the caller
69195
- * silently classified a real ECR image as a user-managed one — skipping the
69196
- * `docker login` it needs.
69197
- *
69198
- * KNOWN BOUND (issue #1792): the `dkr.ecr` LABELS are still matched
69199
- * case-sensitively, so `<acct>.DKR.ECR.<region>.<suffix>/` does not match at
69200
- * all. Audited deliberately while fixing issue #1786 rather than missed — that
69201
- * issue's normalization covers the captured region + suffix, i.e. the strict
69202
- * CHECK, whereas widening the shape MATCHER makes cdkd `docker login` to a
69203
- * host shape it previously skipped, which is a behavior expansion wanting its
69204
- * own decision. The failure direction here is the safe one: NO credentials are
69205
- * sent on any of the four call sites. Traced — `local run-task` and
69206
- * `local invoke-agentcore` fall through to an anonymous `docker pull` that
69207
- * fails, while `local invoke` and `local start-api` REFUSE before any pull.
69208
- */
69209
- const ECR_URI_HOST_REGEX = /^(\d{12})\.dkr\.ecr\.([^.]+)\.([^/]+)\//;
69210
- /**
69211
- * A region segment cdkd is willing to treat as a region id.
69212
- *
69213
- * Case is not the only way the `startsWith` partition classification can be
69214
- * side-stepped: any leading junk defeats it the same way, and the captured
69215
- * segment does not stay inert — it becomes an `ECRClient({region})` and is
69216
- * interpolated into the fallback `docker login` endpoint. Measured before this
69217
- * guard: `…dkr.ecr. us-iso-east-1.amazonaws.com/…` (leading space) and a
69218
- * combining-mark form both parsed, yielding regions `" us-iso-east-1"` and
69219
- * `"us-i̇so-east-1"`. Neither leaks credentials — an ACCEPTED suffix is always
69220
- * an AWS-owned domain — but a region id is ASCII alphanumeric plus `-`, so
69221
- * anything else is a malformed host and refusing it keeps the classification
69222
- * honest.
69223
- *
69224
- * TESTED AGAINST THE RAW SEGMENT, BEFORE `toLowerCase()`, and that order is the
69225
- * whole point: `String.prototype.toLowerCase` performs full Unicode case
69226
- * folding, so the Kelvin sign U+212A folds to a plain ASCII `k`. Checking the
69227
- * FOLDED segment therefore accepts `us-eKst-1` and yields the region
69228
- * `us-ekst-1` — a region the host does not name — which is precisely the
69229
- * class of substitution this guard exists to refuse. Matching `[A-Za-z0-9-]`
69230
- * on the raw capture keeps case-insensitivity (the point of issue #1786) while
69231
- * admitting only characters that fold to themselves.
69232
- *
69233
- * ONE DELIBERATE SIDE EFFECT, called out because it is a withdrawal rather than
69234
- * an addition: a malformed-region host is now classified as an ordinary public
69235
- * image with NO cdkd-side signal at all — neither the ECR verdict nor the #1764
69236
- * foreign-suffix diagnostic. Both arms change, not just the foreign one:
69237
- * `<acct>.dkr.ecr.us_east_1.example.com/…` used to log the partition-gap
69238
- * warning and now logs nothing, and `<acct>.dkr.ecr.us_east_1.amazonaws.com/…`
69239
- * used to be ACCEPTED as ECR and is now refused. That is the correct verdict
69240
- * rather than a regression — the diagnostic's whole subject is "this suffix does
69241
- * not belong to its region's PARTITION", and a segment that is not a region id
69242
- * has no partition to belong to, so reporting it would send the reader hunting
69243
- * for a table entry that could never exist — but it does mean a typo'd region
69244
- * gets no cdkd-side hint, only docker's own pull failure. Both arms are pinned
69245
- * by tests so a future widening of the guard cannot flip them silently.
69246
- */
69247
- const CANONICAL_REGION_SEGMENT = /^[A-Za-z0-9][A-Za-z0-9-]*$/;
69248
- /**
69249
- * A URL suffix cdkd is willing to compare against the partition table.
69250
- *
69251
- * The SAME raw-capture rule as the region, and for the same reason one layer
69252
- * over: the suffix is folded before the comparison, so a code point folding
69253
- * into ASCII can impersonate a partition's suffix. This became reachable the
69254
- * moment issue #1764 landed `aws-isoe`'s `cloud.adc-e.uk` — that suffix
69255
- * contains a `k`, so `…dkr.ecr.eu-isoe-west-1.cloud.adc-e.uK/…` (Kelvin
69256
- * sign) folded onto it and was ACCEPTED. Not a credential leak, since UTS-46
69257
- * maps U+212A to `k` too and the host resolves to the real AWS domain, but it
69258
- * falsifies the invariant the region guard's comment above states, so both
69259
- * captures are held to the same rule rather than only the one that was
69260
- * measured first.
69261
- *
69262
- * Dots are admitted because a suffix is a dotted DNS name; every other
69263
- * character is refused, so the accepted set stays exactly the partition
69264
- * table's own literals.
69265
- */
69266
- const CANONICAL_URL_SUFFIX = /^[A-Za-z0-9][A-Za-z0-9.-]*$/;
69267
- /**
69268
- * The ONE case-normalization boundary of this module (issue #1786).
69269
- *
69270
- * DNS is case-INSENSITIVE, so `US-ISO-EAST-1.amazonaws.com` and
69271
- * `us-iso-east-1.amazonaws.com` name the same host — but both halves of the
69272
- * #1758 suffix check were case-SENSITIVE, in OPPOSITE directions:
69273
- *
69274
- * - an upper-cased REGION fails every `startsWith` prefix test in
69275
- * `derivePartitionAndUrlSuffix`, so it falls through to the commercial
69276
- * partition whose suffix is `amazonaws.com` — which is exactly what an ISO
69277
- * look-alike host carries, so the strict check ACCEPTED it (the bypass);
69278
- * - an upper-cased region or SUFFIX on a GENUINE host stopped matching its own
69279
- * partition's suffix, so a real ECR registry was REJECTED and the caller
69280
- * silently classified it as a user-managed image — the #1758 regression, one
69281
- * layer up.
69282
- *
69283
- * The normalization lives HERE rather than inside `derivePartitionAndUrlSuffix`
69284
- * because this is the boundary where an untrusted, DNS-shaped string enters
69285
- * cdkd, and keeping it in ONE function means both exported entry points below —
69286
- * and the suffix COMPARISON itself — cannot drift apart.
69287
- *
69288
- * This does NOT duplicate `canonicalizeRegion`, which issue #1795 added inside
69289
- * `derivePartitionAndUrlSuffix` so the mapping folds its own input. The two
69290
- * answer different questions and both are needed: that one makes the partition
69291
- * LOOKUP case-insensitive for every caller, while these guards decide whether
69292
- * the captured segments are a region id and a URL suffix AT ALL. Folding alone
69293
- * cannot do that — folding is precisely what turns `us-eKst-1` into the
69294
- * plausible `us-ekst-1` (see the guards' own comments), so a fold-only helper
69295
- * makes the substitution easier to reach, not harder. Double-folding is a
69296
- * no-op, which is why the redundant `toLowerCase()` calls below are left in
69297
- * rather than removed: they keep this function the single answer for the
69298
- * comparison even if the helper's behaviour changes again.
69299
- *
69300
- * The derived suffix is lower-cased too. Every entry in the partition table is
69301
- * a lower-case literal today, so that is a no-op; it is there so this function
69302
- * stays the single answer even if that table ever gains a mixed-case entry.
69303
- */
69304
- function matchEcrRegistryHost(imageUri) {
69305
- const m = ECR_URI_HOST_REGEX.exec(imageUri);
69306
- if (!m) return void 0;
69307
- if (!CANONICAL_REGION_SEGMENT.test(m[2])) return void 0;
69308
- if (!CANONICAL_URL_SUFFIX.test(m[3])) return void 0;
69309
- const region = m[2].toLowerCase();
69310
- return {
69311
- accountId: m[1],
69312
- region,
69313
- suffix: m[3].toLowerCase(),
69314
- expectedSuffix: derivePartitionAndUrlSuffix(region).urlSuffix.toLowerCase()
69315
- };
69316
- }
69317
- /**
69318
- * The account + region of an ECR image URI, or `undefined` when the URI is not
69319
- * an ECR registry host for the region it names.
69320
- *
69321
- * Lives in `src/utils/` rather than beside its first caller because it has TWO
69322
- * consumers in different layers — `src/local/ecr-puller.ts` (which needs the
69323
- * `:tag` tail too) and `src/local/ecs-task-resolver.ts` (which classifies an
69324
- * image that may carry a digest or no tag at all). Before issue #1758 they each
69325
- * carried their own copy of the hardcoded commercial pattern, and fixing only
69326
- * the pull path left `cdkd local run-task` broken outside the commercial
69327
- * partition; ONE definition is what stops them drifting apart again.
69328
- *
69329
- * The module is deliberately free of AWS SDK imports, which is what lets
69330
- * `ecs-task-resolver.ts` consume it without breaking the invariant that module
69331
- * documents about itself (it resolves secrets through a separate module for
69332
- * exactly that reason).
69333
- *
69334
- * The returned `region` is LOWER-CASED (issue #1786) — it is consumed as an SDK
69335
- * client region by `ecr-puller.ts` and as the `region` of a `kind: 'ecr'` image
69336
- * by `ecs-task-resolver.ts`, and the canonical spelling of an AWS region id is
69337
- * lower case.
69338
- */
69339
- function parseEcrRegistryHost(imageUri) {
69340
- const m = matchEcrRegistryHost(imageUri);
69341
- if (!m) return void 0;
69342
- if (m.suffix !== m.expectedSuffix) return void 0;
69343
- return {
69344
- accountId: m.accountId,
69345
- region: m.region
69346
- };
69347
- }
69348
- /**
69349
- * True when the URI has the ECR HOST SHAPE but its suffix does not belong to
69350
- * the region it names — i.e. exactly the case {@link parseEcrRegistryHost}
69351
- * rejects for a reason the caller may want to report.
69352
- *
69353
- * The two rejections are worth telling apart at a call site that degrades
69354
- * silently: a genuinely public image and a registry in a partition
69355
- * `derivePartitionAndUrlSuffix` does not know yet (issue #1764) both come back
69356
- * `undefined`, and only the second is a cdkd gap rather than a user choice.
69357
- */
69358
- function looksLikeEcrHostWithForeignSuffix(imageUri) {
69359
- const m = matchEcrRegistryHost(imageUri);
69360
- return m !== void 0 && m.suffix !== m.expectedSuffix;
69361
- }
69362
-
69363
69559
  //#endregion
69364
69560
  //#region src/local/ecs-task-resolver.ts
69365
69561
  function detectEcsImageResolutionNeeds(stack) {
@@ -78062,7 +78258,7 @@ function createMigrateCommand() {
78062
78258
  */
78063
78259
  function buildProgram() {
78064
78260
  const program = new Command();
78065
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.283.10");
78261
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.283.12");
78066
78262
  program.hook("preAction", (_thisCommand, actionCommand) => {
78067
78263
  const { profile } = actionCommand.optsWithGlobals();
78068
78264
  if (profile !== void 0) process.env["AWS_PROFILE"] = profile;