@go-to-k/cdkd 0.289.2 → 0.289.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{asg-provider-C5FQJwUN.js → asg-provider-CpxZ9sVH.js} +3 -3
- package/dist/{asg-provider-C5FQJwUN.js.map → asg-provider-CpxZ9sVH.js.map} +1 -1
- package/dist/cli.js +2 -2
- package/dist/{deploy-engine-ClLLdy68.js → deploy-engine-BEpYvI14.js} +599 -196
- package/dist/deploy-engine-BEpYvI14.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/{logger-D2Md1kTq.js → logger-CecsQ1UT.js} +53 -3
- package/dist/logger-CecsQ1UT.js.map +1 -0
- package/dist/{program-Du5rJeYT.js → program-CyIOIvtX.js} +140 -33
- package/dist/{program-Du5rJeYT.js.map → program-CyIOIvtX.js.map} +1 -1
- package/dist/{version-DUhfbsEF.js → version-C3wRsCF2.js} +2 -2
- package/dist/{version-DUhfbsEF.js.map → version-C3wRsCF2.js.map} +1 -1
- package/package.json +4 -4
- package/dist/deploy-engine-ClLLdy68.js.map +0 -1
- package/dist/logger-D2Md1kTq.js.map +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { d as applyDefaultNameForFallback, f as explicitNamePropertyFor, g as looksLikeCdkdGeneratedName, h as getCurrentStackName, m as generateResourceNameWithFallback, n as getLogger, p as generateResourceName, r as isStdoutReservedForPayload, s as getLiveRenderer, v as withStackName } from "./logger-
|
|
1
|
+
import { d as applyDefaultNameForFallback, f as explicitNamePropertyFor, g as looksLikeCdkdGeneratedName, h as getCurrentStackName, m as generateResourceNameWithFallback, n as getLogger, p as generateResourceName, r as isStdoutReservedForPayload, s as getLiveRenderer, v as withStackName, y as displaySafe } from "./logger-CecsQ1UT.js";
|
|
2
2
|
import { t as awsClientDefaults } from "./aws-client-defaults-D-iYiIJ6.js";
|
|
3
|
-
import { t as getCdkdVersion } from "./version-
|
|
3
|
+
import { t as getCdkdVersion } from "./version-C3wRsCF2.js";
|
|
4
4
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
5
5
|
import { createHash, randomUUID } from "node:crypto";
|
|
6
6
|
import { CreateBucketCommand, DeleteObjectCommand, DeleteObjectsCommand, GetBucketLocationCommand, GetBucketReplicationCommand, GetObjectCommand, HeadBucketCommand, HeadObjectCommand, ListObjectVersionsCommand, ListObjectsV2Command, NoSuchKey, PutBucketEncryptionCommand, PutBucketPolicyCommand, PutObjectCommand, PutPublicAccessBlockCommand, S3Client, S3ServiceException } from "@aws-sdk/client-s3";
|
|
@@ -1554,7 +1554,10 @@ function isCdkdError(error) {
|
|
|
1554
1554
|
function formatError(error) {
|
|
1555
1555
|
if (isCdkdError(error)) {
|
|
1556
1556
|
let message = `${error.name}: ${error.message}`;
|
|
1557
|
-
if (error.cause)
|
|
1557
|
+
if (error.cause) {
|
|
1558
|
+
const cause = displaySafe(error.cause.message);
|
|
1559
|
+
if (cause) message += `\nCaused by: ${cause}`;
|
|
1560
|
+
}
|
|
1558
1561
|
return message;
|
|
1559
1562
|
}
|
|
1560
1563
|
if (error instanceof Error) return `${error.name}: ${error.message}`;
|
|
@@ -3987,56 +3990,6 @@ async function expectedOwnerParam(client) {
|
|
|
3987
3990
|
return owner ? { ExpectedBucketOwner: owner } : {};
|
|
3988
3991
|
}
|
|
3989
3992
|
|
|
3990
|
-
//#endregion
|
|
3991
|
-
//#region src/utils/display-safe.ts
|
|
3992
|
-
/**
|
|
3993
|
-
* Make an untrusted value safe to render in a terminal or persist into a log.
|
|
3994
|
-
*
|
|
3995
|
-
* A LEAF module with no imports, and deliberately in `src/utils/` rather than
|
|
3996
|
-
* beside its first caller: issue [#2170](https://github.com/go-to-k/cdkd/issues/2170)'s
|
|
3997
|
-
* review found the same rule being widened BY HAND one module at a time and
|
|
3998
|
-
* missing an instance every round — the change sanitized 1 of 5 readers of
|
|
3999
|
-
* `LockInfo.owner`. One shared definition, imported by everything that renders
|
|
4000
|
-
* such a value, is what stops the next reader from inheriting nothing.
|
|
4001
|
-
*
|
|
4002
|
-
* The stripped class is wider than C0 + DEL, which a first cut used and which
|
|
4003
|
-
* misses every mechanism that actually forges a line:
|
|
4004
|
-
*
|
|
4005
|
-
* - `U+0085` (NEL) and the C1 range — xterm reads `U+009B` as CSI in UTF-8;
|
|
4006
|
-
* - `U+2028` / `U+2029` — this text is PERSISTED and re-rendered by JSON and
|
|
4007
|
-
* web log viewers, where both are line terminators;
|
|
4008
|
-
* - `U+202A`-`U+202E` / `U+2066`-`U+2069` — the Trojan-Source bidi overrides
|
|
4009
|
-
* and isolates, which visually REORDER the command being pasted.
|
|
4010
|
-
*
|
|
4011
|
-
* Known residual, recorded rather than implied away: the invisible formatters
|
|
4012
|
-
* (`U+200B`-`U+200D`, `U+FEFF`) and the bidi MARKS (`U+200E` / `U+200F` /
|
|
4013
|
-
* `U+061C`) survive, as do bare RTL letters, which no denylist can reach. All
|
|
4014
|
-
* of them can only make a rendered name differ visually from its bytes — the
|
|
4015
|
-
* command a user pastes still acts on exactly what is shown, and the blast
|
|
4016
|
-
* radius stays the attacker's own stack name.
|
|
4017
|
-
*
|
|
4018
|
-
* A caller whose value has a KNOWN ASCII charset (a stack name, an AWS region)
|
|
4019
|
-
* should pass `asciiOnly`, which is a positive allowlist and therefore has no
|
|
4020
|
-
* such residual at all.
|
|
4021
|
-
*
|
|
4022
|
-
* ONE CALLER DELIBERATELY GOES WIDER, and it is recorded here so an editor of
|
|
4023
|
-
* the residual note above knows a second module now disagrees with it.
|
|
4024
|
-
* `src/deployment/outputs-export-alias.ts` deletes a class derived from
|
|
4025
|
-
* `\p{Cc}` / `\p{Cf}` / `\p{Zl}` / `\p{Zp}` /
|
|
4026
|
-
* `\p{Default_Ignorable_Code_Point}`, because on THAT path the subject is a possibly
|
|
4027
|
-
* secret-bearing name in an operator's log: a plaintext split by a zero-width
|
|
4028
|
-
* character is READ as if it were contiguous, so it is disclosed without any
|
|
4029
|
-
* paste, and the command-forgery reasoning above does not transfer (issue
|
|
4030
|
-
* [#2874](https://github.com/go-to-k/cdkd/issues/2874)). Nothing here changes
|
|
4031
|
-
* -- widening this helper would alter every caller that merely wants a
|
|
4032
|
-
* terminal-safe string.
|
|
4033
|
-
*/
|
|
4034
|
-
function displaySafe(value, opts) {
|
|
4035
|
-
if (value === void 0 || value === null) return "";
|
|
4036
|
-
const text = String(value);
|
|
4037
|
-
return (opts?.asciiOnly ? text.replace(/[^ -~]/g, " ") : text.replace(/[\u0000-\u001f\u007f-\u009f\u2028\u2029\u202a-\u202e\u2066-\u2069]/g, " ")).trim();
|
|
4038
|
-
}
|
|
4039
|
-
|
|
4040
3993
|
//#endregion
|
|
4041
3994
|
//#region src/state/s3-replication-purge-gap.ts
|
|
4042
3995
|
/**
|
|
@@ -6075,16 +6028,16 @@ var FileAssetPublisher = class {
|
|
|
6075
6028
|
* Create ZIP archive and upload to S3
|
|
6076
6029
|
*/
|
|
6077
6030
|
async uploadZip(client, dirPath, bucket, key) {
|
|
6078
|
-
const
|
|
6031
|
+
const { ZipArchive } = await import("archiver");
|
|
6079
6032
|
const body = await new Promise((resolve, reject) => {
|
|
6080
6033
|
const chunks = [];
|
|
6081
|
-
const archive =
|
|
6034
|
+
const archive = new ZipArchive({ zlib: { level: 9 } });
|
|
6082
6035
|
archive.on("data", (chunk) => chunks.push(chunk));
|
|
6083
6036
|
archive.on("end", () => resolve(Buffer.concat(chunks)));
|
|
6084
6037
|
archive.on("error", reject);
|
|
6085
6038
|
if (statSync(dirPath).isDirectory()) archive.directory(dirPath, false);
|
|
6086
6039
|
else archive.file(dirPath, { name: basename(dirPath) });
|
|
6087
|
-
archive.finalize();
|
|
6040
|
+
archive.finalize().catch(reject);
|
|
6088
6041
|
});
|
|
6089
6042
|
await client.send(new PutObjectCommand({
|
|
6090
6043
|
Bucket: bucket,
|
|
@@ -9656,18 +9609,20 @@ var S3StateBackend = class {
|
|
|
9656
9609
|
async getState(stackName, region) {
|
|
9657
9610
|
await this.ensureClientForBucket();
|
|
9658
9611
|
const newKey = this.getStateKey(stackName, region);
|
|
9612
|
+
const shownStackName = this.displayName(stackName);
|
|
9613
|
+
const shownRegionName = displaySafe(region, { asciiOnly: true }) || "<unrenderable>";
|
|
9659
9614
|
try {
|
|
9660
|
-
this.logger.debug(`Getting state for stack: ${
|
|
9615
|
+
this.logger.debug(`Getting state for stack: ${shownStackName} (${shownRegionName})`);
|
|
9661
9616
|
const response = await this.s3Client.send(new GetObjectCommand({
|
|
9662
9617
|
Bucket: this.config.bucket,
|
|
9663
9618
|
...await this.ownerParam(),
|
|
9664
9619
|
Key: newKey
|
|
9665
9620
|
}));
|
|
9666
|
-
if (!response.Body) throw new StateError(`State file for stack '${
|
|
9667
|
-
if (!response.ETag) throw new StateError(`State file for stack '${
|
|
9621
|
+
if (!response.Body) throw new StateError(`State file for stack '${shownStackName}' (${shownRegionName}) has no body`);
|
|
9622
|
+
if (!response.ETag) throw new StateError(`State file for stack '${shownStackName}' (${shownRegionName}) has no ETag`);
|
|
9668
9623
|
const bodyString = await response.Body.transformToString();
|
|
9669
9624
|
const state = this.parseStateBody(bodyString, stackName);
|
|
9670
|
-
this.logger.debug(`Retrieved state: ${
|
|
9625
|
+
this.logger.debug(`Retrieved state: ${shownStackName} (${shownRegionName}), ETag: ${response.ETag}`);
|
|
9671
9626
|
return {
|
|
9672
9627
|
state,
|
|
9673
9628
|
etag: response.ETag
|
|
@@ -9675,13 +9630,15 @@ var S3StateBackend = class {
|
|
|
9675
9630
|
} catch (error) {
|
|
9676
9631
|
if (!isNoSuchKey(error)) {
|
|
9677
9632
|
if (error instanceof StateError) throw error;
|
|
9678
|
-
|
|
9633
|
+
const detail = displaySafe(error instanceof Error ? error.message : String(error), { asciiOnly: true }) || "<unrenderable>";
|
|
9634
|
+
throw new StateError(`Failed to get state for stack '${shownStackName}' (${shownRegionName}): ${detail}`, error instanceof Error ? error : void 0);
|
|
9679
9635
|
}
|
|
9680
|
-
this.logger.debug(`No state at new key for stack: ${
|
|
9636
|
+
this.logger.debug(`No state at new key for stack: ${shownStackName} (${shownRegionName})`);
|
|
9681
9637
|
}
|
|
9682
9638
|
const legacy = await this.tryGetLegacy(stackName, region);
|
|
9683
9639
|
if (legacy) {
|
|
9684
|
-
|
|
9640
|
+
const shownKey = displaySafe(this.getLegacyStateKey(stackName), { asciiOnly: true }) || "<unrenderable>";
|
|
9641
|
+
this.logger.warn(`Loaded legacy state for stack '${shownStackName}' from '${shownKey}'. It will be migrated to the region-scoped layout on next save.`);
|
|
9685
9642
|
return {
|
|
9686
9643
|
...legacy,
|
|
9687
9644
|
migrationPending: true
|
|
@@ -10309,7 +10266,7 @@ var S3StateBackend = class {
|
|
|
10309
10266
|
const bodyString = await response.Body.transformToString();
|
|
10310
10267
|
const state = this.parseStateBody(bodyString, stackName);
|
|
10311
10268
|
if (state.region && state.region !== region) {
|
|
10312
|
-
this.logger.debug(`Legacy state for stack '${stackName}' has region '${state.region}', not '${region}' — skipping legacy fallback.`);
|
|
10269
|
+
this.logger.debug(`Legacy state for stack '${this.displayName(stackName)}' has region '${displaySafe(state.region, { asciiOnly: true }) || "<unrenderable>"}', not '${displaySafe(region, { asciiOnly: true }) || "<unrenderable>"}' — skipping legacy fallback.`);
|
|
10313
10270
|
return null;
|
|
10314
10271
|
}
|
|
10315
10272
|
return {
|
|
@@ -10318,7 +10275,8 @@ var S3StateBackend = class {
|
|
|
10318
10275
|
};
|
|
10319
10276
|
} catch (error) {
|
|
10320
10277
|
if (isNoSuchKey(error)) return null;
|
|
10321
|
-
|
|
10278
|
+
const detail = displaySafe(error instanceof Error ? error.message : String(error), { asciiOnly: true }) || "<unrenderable>";
|
|
10279
|
+
throw new StateError(`Failed to get legacy state for stack '${this.displayName(stackName)}': ${detail}`, error instanceof Error ? error : void 0);
|
|
10322
10280
|
}
|
|
10323
10281
|
}
|
|
10324
10282
|
/**
|
|
@@ -10332,10 +10290,15 @@ var S3StateBackend = class {
|
|
|
10332
10290
|
try {
|
|
10333
10291
|
parsed = JSON.parse(bodyString);
|
|
10334
10292
|
} catch (error) {
|
|
10335
|
-
|
|
10293
|
+
const raw = error instanceof Error ? error.message : String(error);
|
|
10294
|
+
const detail = displaySafe(raw, { asciiOnly: true }) || "<unrenderable>";
|
|
10295
|
+
throw new StateError(`State file for stack '${this.displayName(stackName)}' is not valid JSON: ${detail}`, error instanceof Error ? error : void 0);
|
|
10336
10296
|
}
|
|
10337
10297
|
const v = parsed.version;
|
|
10338
|
-
if (v !== void 0 && !STATE_SCHEMA_VERSIONS_READABLE.includes(v))
|
|
10298
|
+
if (v !== void 0 && !STATE_SCHEMA_VERSIONS_READABLE.includes(v)) {
|
|
10299
|
+
const shown = displaySafe(String(v), { asciiOnly: true }) || "<unrenderable>";
|
|
10300
|
+
throw new StateError(`Unsupported state schema version ${shown} for stack '${this.displayName(stackName)}'. This cdkd binary supports versions ${STATE_SCHEMA_VERSIONS_READABLE.join(", ")}. Upgrade cdkd to a version that supports schema ${shown}.`);
|
|
10301
|
+
}
|
|
10339
10302
|
return parsed;
|
|
10340
10303
|
}
|
|
10341
10304
|
};
|
|
@@ -10381,6 +10344,20 @@ const MIN_RENEWAL_INTERVAL_MS = 1e3;
|
|
|
10381
10344
|
*/
|
|
10382
10345
|
const LOCK_OBJECT_DESCRIPTION = "a stack lock heartbeat, which records the lock owner, its acquisition timestamp, its deadline and the operation name — no secret, but one row per renewal";
|
|
10383
10346
|
/**
|
|
10347
|
+
* One spelling of "this value came from an S3 key or a lock record, and is
|
|
10348
|
+
* about to be interpolated into a message a terminal will render". A stack
|
|
10349
|
+
* name and an AWS region both have a known charset, so the ASCII allowlist is
|
|
10350
|
+
* a no-op on every legitimate input while an S3 key admits any UTF-8.
|
|
10351
|
+
*
|
|
10352
|
+
* Call it for a stack name or a region. Not every value in this file goes
|
|
10353
|
+
* through it, and this comment does not say which do: three revisions of that
|
|
10354
|
+
* sentence carried a count or a scope claim and each was wrong.
|
|
10355
|
+
* `grep safeSegment` answers it exactly.
|
|
10356
|
+
*/
|
|
10357
|
+
function safeSegment(value) {
|
|
10358
|
+
return displaySafe(value, { asciiOnly: true }) || "<unrenderable>";
|
|
10359
|
+
}
|
|
10360
|
+
/**
|
|
10384
10361
|
* S3-based lock manager using conditional writes (If-None-Match)
|
|
10385
10362
|
*
|
|
10386
10363
|
* Implements distributed locking using S3's If-None-Match: "*" condition
|
|
@@ -10585,7 +10562,7 @@ var LockManager = class {
|
|
|
10585
10562
|
}
|
|
10586
10563
|
return false;
|
|
10587
10564
|
}
|
|
10588
|
-
throw new LockError(`Failed to acquire lock for stack '${
|
|
10565
|
+
throw new LockError(`Failed to acquire lock for stack '${safeSegment(stackName)}' (${safeSegment(region)}): ${displaySafe(error instanceof Error ? error.message : String(error), { asciiOnly: true }) || "<unrenderable>"}`, error instanceof Error ? error : void 0);
|
|
10589
10566
|
}
|
|
10590
10567
|
}
|
|
10591
10568
|
/**
|
|
@@ -10628,18 +10605,19 @@ var LockManager = class {
|
|
|
10628
10605
|
async getLockRecord(stackName, region) {
|
|
10629
10606
|
await this.ensureClientForBucket();
|
|
10630
10607
|
const key = this.getLockKey(stackName, region);
|
|
10608
|
+
const shownStack = safeSegment(stackName);
|
|
10631
10609
|
try {
|
|
10632
|
-
this.logger.debug(`Getting lock info for stack: ${
|
|
10610
|
+
this.logger.debug(`Getting lock info for stack: ${shownStack}`);
|
|
10633
10611
|
const response = await this.s3Client.send(new GetObjectCommand({
|
|
10634
10612
|
Bucket: this.config.bucket,
|
|
10635
10613
|
...await this.ownerParam(),
|
|
10636
10614
|
Key: key
|
|
10637
10615
|
}));
|
|
10638
|
-
if (!response.Body) throw new LockError(`Lock file for stack '${
|
|
10616
|
+
if (!response.Body) throw new LockError(`Lock file for stack '${shownStack}' has no body`);
|
|
10639
10617
|
const bodyString = await response.Body.transformToString();
|
|
10640
10618
|
const raw = JSON.parse(bodyString);
|
|
10641
10619
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
10642
|
-
this.logger.debug(`Lock file for stack ${
|
|
10620
|
+
this.logger.debug(`Lock file for stack ${shownStack} is not an object; treating as absent`);
|
|
10643
10621
|
return null;
|
|
10644
10622
|
}
|
|
10645
10623
|
const parsed = raw;
|
|
@@ -10648,18 +10626,19 @@ var LockManager = class {
|
|
|
10648
10626
|
owner: displaySafe(parsed.owner),
|
|
10649
10627
|
...parsed.operation !== void 0 && { operation: displaySafe(parsed.operation) }
|
|
10650
10628
|
};
|
|
10651
|
-
this.logger.debug(`Lock info for stack: ${
|
|
10629
|
+
this.logger.debug(`Lock info for stack: ${shownStack}:`, lockInfo);
|
|
10652
10630
|
return {
|
|
10653
10631
|
info: lockInfo,
|
|
10654
10632
|
etag: response.ETag
|
|
10655
10633
|
};
|
|
10656
10634
|
} catch (error) {
|
|
10657
10635
|
if (error instanceof NoSuchKey) {
|
|
10658
|
-
this.logger.debug(`No lock exists for stack: ${
|
|
10636
|
+
this.logger.debug(`No lock exists for stack: ${shownStack}`);
|
|
10659
10637
|
return null;
|
|
10660
10638
|
}
|
|
10661
10639
|
if (error instanceof LockError) throw error;
|
|
10662
|
-
|
|
10640
|
+
const detail = displaySafe(error instanceof Error ? error.message : String(error), { asciiOnly: true }) || "<unrenderable>";
|
|
10641
|
+
throw new LockError(`Failed to get lock info for stack '${shownStack}': ${detail}`, error instanceof Error ? error : void 0);
|
|
10663
10642
|
}
|
|
10664
10643
|
}
|
|
10665
10644
|
/**
|
|
@@ -11156,7 +11135,7 @@ var LockManager = class {
|
|
|
11156
11135
|
if (lockInfo) {
|
|
11157
11136
|
const remainingMs = lockInfo.expiresAt - Date.now();
|
|
11158
11137
|
if (attempt < maxRetries) {
|
|
11159
|
-
this.logger.info(`Stack '${
|
|
11138
|
+
this.logger.info(`Stack '${safeSegment(stackName)}' (${safeSegment(region)}) is locked by ${lockInfo.owner}${lockInfo.operation ? ` (operation: ${lockInfo.operation})` : ""}. Lock expires in ${this.formatDuration(remainingMs)}. Retrying in ${this.formatDuration(retryDelay)}... (attempt ${attempt + 1}/${maxRetries})`);
|
|
11160
11139
|
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
11161
11140
|
continue;
|
|
11162
11141
|
}
|
|
@@ -11166,8 +11145,8 @@ var LockManager = class {
|
|
|
11166
11145
|
const expiresIn = lockInfo ? this.formatDuration(lockInfo.expiresAt - Date.now()) : "unknown";
|
|
11167
11146
|
const forceUnlockCommand = buildForceUnlockCommand(stackName, region);
|
|
11168
11147
|
const recovery = forceUnlockCommand ? `If you are certain no other process is active, run: ${forceUnlockCommand}` : `If you are certain no other process is active, ${UNREPRODUCIBLE_LOCK_CLAUSE.charAt(0).toLowerCase()}${UNREPRODUCIBLE_LOCK_CLAUSE.slice(1)}`;
|
|
11169
|
-
const safeStack =
|
|
11170
|
-
const safeRegion =
|
|
11148
|
+
const safeStack = safeSegment(stackName);
|
|
11149
|
+
const safeRegion = safeSegment(region);
|
|
11171
11150
|
throw new LockError(`Failed to acquire lock for stack '${safeStack}' (${safeRegion}) after ${maxRetries + 1} attempts. ` + (lockInfo ? `Locked by: ${lockInfo.owner}${lockInfo.operation ? `, operation: ${lockInfo.operation}` : ""}, expires in: ${expiresIn}. ` + recovery : `Lock exists but could not read lock info. ${recovery}`));
|
|
11172
11151
|
}
|
|
11173
11152
|
};
|
|
@@ -11272,6 +11251,15 @@ function resolvedPlaintextOf(secrets, expression) {
|
|
|
11272
11251
|
return typeof recorded === "string" ? recorded : void 0;
|
|
11273
11252
|
}
|
|
11274
11253
|
/**
|
|
11254
|
+
* Every expression the pass that owns `secrets` resolved — the pair table's
|
|
11255
|
+
* keys, a CONFLICTING one included (it is still an expression this pass saw,
|
|
11256
|
+
* and a second sighting is what a uniqueness rule must count). The map's own
|
|
11257
|
+
* values name only the SURVIVOR per plaintext; this names the losers too.
|
|
11258
|
+
*/
|
|
11259
|
+
function resolvedExpressionsOf(secrets) {
|
|
11260
|
+
return [...resolvedPairsOf.get(secrets)?.keys() ?? []];
|
|
11261
|
+
}
|
|
11262
|
+
/**
|
|
11275
11263
|
* The bag OBJECTS a deploy pass produced ITSELF and installed on a success
|
|
11276
11264
|
* path — the fact the resolved-pair evidence above cannot state (issue
|
|
11277
11265
|
* [#2516](https://github.com/go-to-k/cdkd/issues/2516)).
|
|
@@ -11289,8 +11277,9 @@ function resolvedPlaintextOf(secrets, expression) {
|
|
|
11289
11277
|
* is carried on the object: {@link markSameGenerationBag} at the moment the
|
|
11290
11278
|
* successful result is installed, consulted by {@link redactSecretsForState}
|
|
11291
11279
|
* for the object it is handed. A copy of the bag, a derived needle map, a
|
|
11292
|
-
* previous generation's record, a scrub /
|
|
11293
|
-
*
|
|
11280
|
+
* previous generation's record, a scrub / drift walk and a bag nobody marked
|
|
11281
|
+
* all answer `false` and keep the fall-through. `cdkd import` marks the one
|
|
11282
|
+
* bag its own resolver produced (the sixth site below, issue #2745).
|
|
11294
11283
|
*
|
|
11295
11284
|
* A `WeakSet` for the reason {@link resolvedPairsOf} is a `WeakMap`: the mark
|
|
11296
11285
|
* dies with the object it is on, and nothing has to clear it.
|
|
@@ -11313,9 +11302,11 @@ const sameGenerationBags = /* @__PURE__ */ new WeakSet();
|
|
|
11313
11302
|
* be written as the token this pass recorded. What never qualifies is a bag of
|
|
11314
11303
|
* MIXED provenance: a provider's `effectiveProperties` replacement may carry
|
|
11315
11304
|
* previous-state values IN, so an object-level mark on it would prove nothing
|
|
11316
|
-
* for those leaves — it stays unmarked and keeps the residual. Nor does
|
|
11317
|
-
* object the resolver
|
|
11318
|
-
*
|
|
11305
|
+
* for those leaves — it stays unmarked and keeps the residual. Nor does an
|
|
11306
|
+
* object marked INSIDE the resolver, at resolution time: whether that object,
|
|
11307
|
+
* a narrowed copy of it or a provider's replacement is what gets redacted and
|
|
11308
|
+
* stored is the CALLER's decision, so the mark is taken by the caller at the
|
|
11309
|
+
* redaction call — the engine's five sites, and `cdkd import`'s one.
|
|
11319
11310
|
*
|
|
11320
11311
|
* The CONDITIONS are per SITE, not global, and stating them globally is what
|
|
11321
11312
|
* this paragraph kept getting wrong (PR 2753, rounds 3 and 4). "After the
|
|
@@ -11323,9 +11314,9 @@ const sameGenerationBags = /* @__PURE__ */ new WeakSet();
|
|
|
11323
11314
|
* and may skip it entirely. "Every leaf this pass produced" is false for the
|
|
11324
11315
|
* auto-refresh readback of an UNCHANGED resource, where nothing was resolved
|
|
11325
11316
|
* at all — there the safety comes from the empty secrets map, not from the
|
|
11326
|
-
* mark. Read the per-site list below rather than a rule over all
|
|
11317
|
+
* mark. Read the per-site list below rather than a rule over all six.
|
|
11327
11318
|
*
|
|
11328
|
-
* The
|
|
11319
|
+
* The six call sites, and which of them the record HOLDS, because the earlier
|
|
11329
11320
|
* "two never-installed copies" reading of this paragraph was false once the
|
|
11330
11321
|
* third copy arrived:
|
|
11331
11322
|
* - `propertiesToRecord` — the record's `properties`, installed. The resolved
|
|
@@ -11359,11 +11350,28 @@ const sameGenerationBags = /* @__PURE__ */ new WeakSet();
|
|
|
11359
11350
|
* - the rollback journal's FAILED-op `attemptedProperties` — a marked copy, so
|
|
11360
11351
|
* that persisted artifact does not carry the plaintext on exactly the
|
|
11361
11352
|
* failure path. NOT installed on the record.
|
|
11353
|
+
* - `cdkd import`'s `resolveImportedProperties` (issue #2745) — the bag
|
|
11354
|
+
* import's OWN resolver produced from the imported template, marked as the
|
|
11355
|
+
* redaction input; the record holds the redacted COPY, unmarked. Not a
|
|
11356
|
+
* deploy pass, and the mark does not say it is: what it claims is PAIR
|
|
11357
|
+
* PROVENANCE — this bag was produced from this source, in this pass, by the
|
|
11358
|
+
* resolver that recorded these pairs — which import's resolve satisfies
|
|
11359
|
+
* exactly as the create arm's does, since `unresolvedProperties` is the
|
|
11360
|
+
* template bag it resolved and its per-resource map holds the pairs. What
|
|
11361
|
+
* it does NOT claim is that AWS holds the value: `observedProperties` is
|
|
11362
|
+
* captured separately, against the redacted record, and is never marked
|
|
11363
|
+
* here.
|
|
11362
11364
|
*/
|
|
11363
11365
|
function markSameGenerationBag(bag) {
|
|
11364
11366
|
sameGenerationBags.add(bag);
|
|
11365
11367
|
return bag;
|
|
11366
11368
|
}
|
|
11369
|
+
/**
|
|
11370
|
+
* Exported for `cdkd import`'s unit test, which asserts that the STORED record
|
|
11371
|
+
* is not the marked object (maintainer review of PR 3052). Read-only over the
|
|
11372
|
+
* `WeakSet`: no behavior in this module changes with the export, and the only
|
|
11373
|
+
* writer, {@link markSameGenerationBag}, was exported already.
|
|
11374
|
+
*/
|
|
11367
11375
|
function isSameGenerationBag(bag) {
|
|
11368
11376
|
return sameGenerationBags.has(bag);
|
|
11369
11377
|
}
|
|
@@ -12683,6 +12691,55 @@ function joinSkeletonSegments(segments) {
|
|
|
12683
12691
|
* choice, not the only option.
|
|
12684
12692
|
*/
|
|
12685
12693
|
function intrinsicSkeletonPattern(source) {
|
|
12694
|
+
const segments = intrinsicSkeletonSegments(source);
|
|
12695
|
+
if (segments === void 0) return void 0;
|
|
12696
|
+
return anchoredSkeletonPattern(segments);
|
|
12697
|
+
}
|
|
12698
|
+
/**
|
|
12699
|
+
* {@link joinSkeletonSegments} anchored at both ends, or `undefined` where the
|
|
12700
|
+
* wildcard cap refused it. The one place the skeleton's regex is built, for
|
|
12701
|
+
* both readers of the segment form — and the one place a literal segment is
|
|
12702
|
+
* ESCAPED: it takes the segment form itself (literal text, or
|
|
12703
|
+
* {@link UNKNOWN_PART}), never pre-built regex text, so a third caller cannot
|
|
12704
|
+
* hand it raw template text and get a pattern with live metacharacters and no
|
|
12705
|
+
* type error (maintainer review of PR 3052).
|
|
12706
|
+
*/
|
|
12707
|
+
function anchoredSkeletonPattern(segments) {
|
|
12708
|
+
const body = joinSkeletonSegments(segments.map((segment) => segment === UNKNOWN_PART ? SKELETON_WILDCARD : escapeRegExp(segment)));
|
|
12709
|
+
return body === void 0 ? void 0 : new RegExp(`^${body}$`);
|
|
12710
|
+
}
|
|
12711
|
+
/**
|
|
12712
|
+
* A source part the skeleton cannot know — an `Fn::Join` element that is
|
|
12713
|
+
* itself an intrinsic, or an `Fn::Sub` `${...}` variable. The segment form of
|
|
12714
|
+
* {@link SKELETON_WILDCARD}: what a part IS, before {@link anchoredSkeletonPattern}
|
|
12715
|
+
* — the one speller — turns it into the wildcard.
|
|
12716
|
+
*/
|
|
12717
|
+
const UNKNOWN_PART = Symbol("unknown intrinsic part");
|
|
12718
|
+
/**
|
|
12719
|
+
* Stands in, while a source is RENDERED to text, for a part the skeleton cannot
|
|
12720
|
+
* know. A single character so the rendered string keeps the literal parts at
|
|
12721
|
+
* their true offsets, and NOT `}` so {@link dynamicReferenceSpans} reads it as
|
|
12722
|
+
* a token's INNER text (the resolver's `[^}]+`) rather than as its terminator —
|
|
12723
|
+
* which is exactly how a `{Ref}` sitting inside a `{{resolve:...}}` opening
|
|
12724
|
+
* must read. A NUL, RESERVED here as the placeholder: a template string can
|
|
12725
|
+
* carry one (CDK preserves it), so a source whose literal text does is refused
|
|
12726
|
+
* outright rather than rendered with one unknowable part too many.
|
|
12727
|
+
*/
|
|
12728
|
+
const UNKNOWN_PART_PLACEHOLDER = "\0";
|
|
12729
|
+
/**
|
|
12730
|
+
* The text of an `Fn::Join` / `Fn::Sub` source in order: a literal part as its
|
|
12731
|
+
* RAW text, an unknowable part as {@link UNKNOWN_PART}. ONE parser for the two
|
|
12732
|
+
* readers of that shape — {@link intrinsicSkeletonPattern}, which hands every
|
|
12733
|
+
* segment to {@link anchoredSkeletonPattern} to spell as a regex, and
|
|
12734
|
+
* {@link positionByIntrinsicFrame}, which needs the
|
|
12735
|
+
* literal text VERBATIM to know where a token's frame begins and ends — so
|
|
12736
|
+
* the two cannot disagree about what a source says (issue #2745).
|
|
12737
|
+
*
|
|
12738
|
+
* `undefined` for any source this cannot describe, per the pattern reader's
|
|
12739
|
+
* doc: a delimiter that is itself an intrinsic, a non-array `Fn::Join`, a
|
|
12740
|
+
* non-string `Fn::Sub` template, an object that is not a single-key intrinsic.
|
|
12741
|
+
*/
|
|
12742
|
+
function intrinsicSkeletonSegments(source) {
|
|
12686
12743
|
const keys = Object.keys(source);
|
|
12687
12744
|
if (keys.length !== 1) return void 0;
|
|
12688
12745
|
const key = keys[0];
|
|
@@ -12691,14 +12748,12 @@ function intrinsicSkeletonPattern(source) {
|
|
|
12691
12748
|
if (!Array.isArray(args) || args.length !== 2) return void 0;
|
|
12692
12749
|
const [delimiter, parts] = args;
|
|
12693
12750
|
if (typeof delimiter !== "string" || !Array.isArray(parts)) return void 0;
|
|
12694
|
-
const separator = escapeRegExp(delimiter);
|
|
12695
12751
|
const segments = [];
|
|
12696
12752
|
parts.forEach((part, index) => {
|
|
12697
|
-
if (index > 0) segments.push(
|
|
12698
|
-
segments.push(typeof part === "string" ?
|
|
12753
|
+
if (index > 0) segments.push(delimiter);
|
|
12754
|
+
segments.push(typeof part === "string" ? part : UNKNOWN_PART);
|
|
12699
12755
|
});
|
|
12700
|
-
|
|
12701
|
-
return body === void 0 ? void 0 : new RegExp(`^${body}$`);
|
|
12756
|
+
return segments;
|
|
12702
12757
|
}
|
|
12703
12758
|
if (key === "Fn::Sub") {
|
|
12704
12759
|
const args = source[key];
|
|
@@ -12709,14 +12764,13 @@ function intrinsicSkeletonPattern(source) {
|
|
|
12709
12764
|
const variable = /\$\{([^}]*)\}/g;
|
|
12710
12765
|
let hit;
|
|
12711
12766
|
while ((hit = variable.exec(template)) !== null) {
|
|
12712
|
-
segments.push(
|
|
12767
|
+
segments.push(template.slice(cursor, hit.index));
|
|
12713
12768
|
const inner = hit[1];
|
|
12714
|
-
segments.push(inner.startsWith("!") ?
|
|
12769
|
+
segments.push(inner.startsWith("!") ? `\${${inner.slice(1)}}` : UNKNOWN_PART);
|
|
12715
12770
|
cursor = hit.index + hit[0].length;
|
|
12716
12771
|
}
|
|
12717
|
-
segments.push(
|
|
12718
|
-
|
|
12719
|
-
return body === void 0 ? void 0 : new RegExp(`^${body}$`);
|
|
12772
|
+
segments.push(template.slice(cursor));
|
|
12773
|
+
return segments;
|
|
12720
12774
|
}
|
|
12721
12775
|
}
|
|
12722
12776
|
/** Poison for an expression this pass recorded against two different plaintexts. */
|
|
@@ -12754,10 +12808,11 @@ const CONFLICTING_PLAINTEXT = Symbol("conflicting plaintext");
|
|
|
12754
12808
|
* iterating it never yields one plaintext twice and a second sighting of an
|
|
12755
12809
|
* expression is always a DIFFERENT plaintext.
|
|
12756
12810
|
*
|
|
12757
|
-
* SHARED by {@link positionByIntrinsicSkeleton}
|
|
12758
|
-
* {@link positionByCrossStackSource} (issue #2059)
|
|
12759
|
-
*
|
|
12760
|
-
*
|
|
12811
|
+
* SHARED by {@link positionByIntrinsicSkeleton},
|
|
12812
|
+
* {@link positionByCrossStackSource} (issue #2059) and
|
|
12813
|
+
* {@link positionByIntrinsicFrame} (issue #2745) rather than copied: the
|
|
12814
|
+
* poisoning rule is the subtle half of condition 3, and every copy is another
|
|
12815
|
+
* place for it to be relaxed independently.
|
|
12761
12816
|
*/
|
|
12762
12817
|
function plaintextIndexOf(secrets) {
|
|
12763
12818
|
const plaintextOf = /* @__PURE__ */ new Map();
|
|
@@ -12903,8 +12958,9 @@ function associationForSource(source, secrets) {
|
|
|
12903
12958
|
* leaf identity; the arm fires only for the spellings
|
|
12904
12959
|
* {@link crossStackSourceKey} can key; and
|
|
12905
12960
|
* condition 1 still demands that the bag leaf be a plaintext this pass
|
|
12906
|
-
* resolved. Every rejection degrades to {@link positionByIntrinsicSkeleton}
|
|
12907
|
-
* then to the value scan, i.e. to today's
|
|
12961
|
+
* resolved. Every rejection degrades to {@link positionByIntrinsicSkeleton},
|
|
12962
|
+
* {@link positionByIntrinsicFrame} and then to the value scan, i.e. to today's
|
|
12963
|
+
* behavior.
|
|
12908
12964
|
*/
|
|
12909
12965
|
function positionByCrossStackSource(bag, source, secrets) {
|
|
12910
12966
|
const association = associationForSource(source, secrets);
|
|
@@ -12930,7 +12986,8 @@ function positionByCrossStackSource(bag, source, secrets) {
|
|
|
12930
12986
|
* 1. The bag leaf's WHOLE value is a recorded secret plaintext. The skeleton
|
|
12931
12987
|
* describes ONE complete `{{resolve:...}}` token, so a leaf that merely
|
|
12932
12988
|
* EMBEDS a secret (a join with surrounding text) is not this shape at all
|
|
12933
|
-
* and
|
|
12989
|
+
* and goes on to {@link positionByIntrinsicFrame} (issue #2745), and only
|
|
12990
|
+
* where that refuses to the value scan, which rewrites just the substring.
|
|
12934
12991
|
* 2. EXACTLY ONE candidate matches. Two matching candidates mean the skeleton
|
|
12935
12992
|
* genuinely cannot separate them (`{Ref}` in the position that differs), and
|
|
12936
12993
|
* guessing would be the collapse this fix exists to remove, one step over.
|
|
@@ -12942,8 +12999,9 @@ function positionByCrossStackSource(bag, source, secrets) {
|
|
|
12942
12999
|
* candidate absent from that map is a collapsed LOSER, which is the case
|
|
12943
13000
|
* this whole function exists to serve, so it is accepted.
|
|
12944
13001
|
*
|
|
12945
|
-
* Every rejection
|
|
12946
|
-
*
|
|
13002
|
+
* Every rejection falls to {@link positionByIntrinsicFrame} (issue #2745) and,
|
|
13003
|
+
* where that arm refuses too, to the value scan, i.e. to today's behavior, so
|
|
13004
|
+
* no case gets worse than it is without this pass.
|
|
12947
13005
|
*
|
|
12948
13006
|
* One residual is worth naming rather than leaving to be rediscovered: a single
|
|
12949
13007
|
* WRONG candidate can win only when this leaf's own expression is in NEITHER
|
|
@@ -12990,13 +13048,14 @@ function positionByIntrinsicSkeleton(bag, source, secrets, secretExpressions) {
|
|
|
12990
13048
|
return matched;
|
|
12991
13049
|
}
|
|
12992
13050
|
/**
|
|
12993
|
-
* The ONE-span frame shared by {@link positionByEmbeddedSpan}
|
|
13051
|
+
* The ONE-span frame shared by {@link positionByEmbeddedSpan},
|
|
13052
|
+
* {@link positionByIntrinsicFrame} (over the source's rendered text) and
|
|
12994
13053
|
* {@link learnMixedLeafNeedle}: a source holding exactly one `{{resolve:...}}`
|
|
12995
13054
|
* token, and a bag that starts with the source's prefix and ends with its
|
|
12996
13055
|
* suffix with something non-empty between them that is NOT itself a complete
|
|
12997
13056
|
* token (an already-redacted record is a persisted answer, not a plaintext).
|
|
12998
|
-
* `undefined` for any other shape. One helper rather than
|
|
12999
|
-
*
|
|
13057
|
+
* `undefined` for any other shape. One helper rather than three copies so the
|
|
13058
|
+
* three refusals cannot drift apart.
|
|
13000
13059
|
*/
|
|
13001
13060
|
function singleSpanFrame(bag, source) {
|
|
13002
13061
|
const spans = dynamicReferenceSpans(source);
|
|
@@ -13105,32 +13164,28 @@ function singleSpanFrame(bag, source) {
|
|
|
13105
13164
|
* for a 4+ character coincidence in a readback. Every refusal of this arm
|
|
13106
13165
|
* returns the scan's answer, so a leaf refused for interference (a frame
|
|
13107
13166
|
* that is itself a recorded plaintext, say) has its frame rewritten and its
|
|
13108
|
-
* sub-floor middle left in plaintext.
|
|
13109
|
-
*
|
|
13110
|
-
*
|
|
13111
|
-
*
|
|
13112
|
-
*
|
|
13113
|
-
* the value scan and persists in plaintext. `cdkd
|
|
13114
|
-
* repair tool for a pre-GHSA record, cannot repair a
|
|
13115
|
-
*
|
|
13116
|
-
* unreachable from it by construction and the leaf keeps the
|
|
13117
|
-
* The MASKING channel keeps the residual whole:
|
|
13118
|
-
* substring arm carries the same four-character floor,
|
|
13119
|
-
* put `port:{{resolve:...}}` in state, a warn line
|
|
13120
|
-
* still print `port:q7`. Pre-existing and not a
|
|
13121
|
-
* and the reason the list would otherwise
|
|
13122
|
-
* (maintainer review of PR 2753, round 2).
|
|
13123
|
-
*
|
|
13124
|
-
* The next DEPLOY of that resource repairs the leaves
|
|
13125
|
-
* for — a
|
|
13126
|
-
* re-check compares unequal against a record holding the
|
|
13127
|
-
* resource is written again from a bag this pass produced.
|
|
13128
|
-
* named above (an `effectiveProperties` substitution,
|
|
13129
|
-
*
|
|
13130
|
-
* #2745. Tracked, with the sub-floor
|
|
13131
|
-
* residuals of a nested-stack child's inherited parameter and of `cdkd
|
|
13132
|
-
* import`'s own resolution, by issue
|
|
13133
|
-
* [#2745](https://github.com/go-to-k/cdkd/issues/2745).
|
|
13167
|
+
* sub-floor middle left in plaintext. This arm positions a LITERAL source
|
|
13168
|
+
* leaf; an `Fn::Join` / `Fn::Sub` source rendering the same `port:` + token
|
|
13169
|
+
* — the dominant CDK shape — is {@link positionByIntrinsicFrame}'s (issue
|
|
13170
|
+
* #2745), which shares this arm's bound and mark and refuses a frame that is
|
|
13171
|
+
* not wholly literal (a region `Ref` OUTSIDE the token), where a sub-floor
|
|
13172
|
+
* secret still falls to the value scan and persists in plaintext. `cdkd
|
|
13173
|
+
* scrub`, the documented repair tool for a pre-GHSA record, cannot repair a
|
|
13174
|
+
* sub-floor embedded leaf: it walks a STORED bag, which no deploy marked, so
|
|
13175
|
+
* both arms are unreachable from it by construction and the leaf keeps the
|
|
13176
|
+
* scan's answer. The MASKING channel keeps the residual whole:
|
|
13177
|
+
* `maskSecretsInText`'s substring arm carries the same four-character floor,
|
|
13178
|
+
* so once this arm has put `port:{{resolve:...}}` in state, a warn line
|
|
13179
|
+
* quoting an AWS message can still print `port:q7`. Pre-existing and not a
|
|
13180
|
+
* regression -- the #2453 class, and the reason the list would otherwise
|
|
13181
|
+
* read as complete when it is not (maintainer review of PR 2753, round 2).
|
|
13182
|
+
*
|
|
13183
|
+
* The next DEPLOY of that resource repairs the leaves the two arms are
|
|
13184
|
+
* eligible for — a literal-frame source leaf on a bag the engine marks —
|
|
13185
|
+
* because the re-check compares unequal against a record holding the
|
|
13186
|
+
* plaintext and the resource is written again from a bag this pass produced.
|
|
13187
|
+
* The residuals named above (an `effectiveProperties` substitution, a
|
|
13188
|
+
* nonliteral frame) are not repaired by that deploy either.
|
|
13134
13189
|
*
|
|
13135
13190
|
* One shape reaches this arm that a reader may not expect: a WHOLE-token
|
|
13136
13191
|
* source that FAILED the whole-token arm's `isKnownSecretExpression` gate (an
|
|
@@ -13140,31 +13195,58 @@ function singleSpanFrame(bag, source) {
|
|
|
13140
13195
|
* where the scan wrote the survivor. Stated so it is not mistaken for a leak.
|
|
13141
13196
|
*
|
|
13142
13197
|
* Everything else keeps the pre-#2485 fall-through: two or more spans (which
|
|
13143
|
-
* span produced which value is genuinely ambiguous when they share one),
|
|
13144
|
-
*
|
|
13145
|
-
*
|
|
13146
|
-
*
|
|
13147
|
-
*
|
|
13198
|
+
* span produced which value is genuinely ambiguous when they share one), a
|
|
13199
|
+
* frame mismatch, a middle that is itself a complete token (an
|
|
13200
|
+
* already-redacted record, per the same refusal {@link learnMixedLeafNeedle}
|
|
13201
|
+
* makes), and a middle this pass cannot vouch for. An `Fn::Sub` / `Fn::Join`
|
|
13202
|
+
* source (an object, not this arm at all — issue #2320's placeholder
|
|
13203
|
+
* primitive) is {@link positionByIntrinsicFrame}'s since #2745, and reaches
|
|
13204
|
+
* the value scan only where that arm refuses.
|
|
13148
13205
|
*
|
|
13149
13206
|
* The frame is copied from the SOURCE, not scanned. A needle occurring in the
|
|
13150
13207
|
* literal frame would be a reference the template never had at that offset —
|
|
13151
13208
|
* the fabricated-baseline direction {@link preferPositionDecisions} refuses —
|
|
13152
13209
|
* and the whole-token arm returns its source unscanned for the same reason.
|
|
13153
13210
|
*
|
|
13154
|
-
* RETURNS THE VALUE SCAN'S ANSWER ON EVERY REFUSAL, not `undefined`: the
|
|
13155
|
-
*
|
|
13156
|
-
*
|
|
13211
|
+
* RETURNS THE VALUE SCAN'S ANSWER ON EVERY REFUSAL, not `undefined`: the two
|
|
13212
|
+
* early returns below compute it for `(bag, secrets)`, and the bound in
|
|
13213
|
+
* {@link writeFramedTokenWithinScanBound} computes its own from the same two
|
|
13214
|
+
* arguments — every fall-through IS that scan — so the compared value provably
|
|
13157
13215
|
* comes from the same bag and map the arm positions. An earlier revision took
|
|
13158
13216
|
* the scan as a parameter, which left the bound one wrong caller away from
|
|
13159
|
-
* comparing against a scan of some other bag with no type error
|
|
13217
|
+
* comparing against a scan of some other bag with no type error; the shared
|
|
13218
|
+
* helper keeps that property by taking `(bag, secrets)` rather than a scan.
|
|
13160
13219
|
*/
|
|
13161
13220
|
function positionByEmbeddedSpan(bag, source, secrets, bagIsSameGeneration) {
|
|
13162
|
-
const scanned = redactSecretsForState(bag, secrets);
|
|
13163
13221
|
const frame = singleSpanFrame(bag, source);
|
|
13164
|
-
if (frame === void 0) return
|
|
13222
|
+
if (frame === void 0) return redactSecretsForState(bag, secrets);
|
|
13223
|
+
const recorded = resolvedPlaintextOf(secrets, frame.token);
|
|
13224
|
+
if (recorded === void 0 || recorded !== frame.middle) return redactSecretsForState(bag, secrets);
|
|
13225
|
+
return writeFramedTokenWithinScanBound(bag, secrets, frame, bagIsSameGeneration);
|
|
13226
|
+
}
|
|
13227
|
+
/**
|
|
13228
|
+
* Write `prefix + token + suffix` for a framed leaf whose pass-local pair the
|
|
13229
|
+
* CALLER has already verified (`token` resolved to `middle` in the pass that
|
|
13230
|
+
* owns `secrets`), and ONLY where that stays within the value scan's own class
|
|
13231
|
+
* of answer — or, below the scan's floor, on a bag the engine marked. Returns
|
|
13232
|
+
* the scan's answer on every refusal.
|
|
13233
|
+
*
|
|
13234
|
+
* ONE helper for {@link positionByEmbeddedSpan} (a literal source, issue
|
|
13235
|
+
* #2485) and {@link positionByIntrinsicFrame} (an `Fn::Join` / `Fn::Sub`
|
|
13236
|
+
* source, issue #2745), for the reason {@link singleSpanFrame} is one: the
|
|
13237
|
+
* bound and the mark are the two claims this module makes about a FRAMED
|
|
13238
|
+
* leaf, and two copies are two places for one of them to be relaxed alone.
|
|
13239
|
+
*
|
|
13240
|
+
* THE SCAN IS COMPUTED HERE, for `(bag, secrets)` — the bound below compares
|
|
13241
|
+
* against it, and every fall-through IS it — so the compared value provably
|
|
13242
|
+
* comes from the same bag and map the token is written into. An earlier
|
|
13243
|
+
* revision of the literal arm took the scan as a parameter, which left the
|
|
13244
|
+
* bound one wrong caller away from comparing against a scan of some other
|
|
13245
|
+
* bag with no type error.
|
|
13246
|
+
*/
|
|
13247
|
+
function writeFramedTokenWithinScanBound(bag, secrets, frame, bagIsSameGeneration) {
|
|
13248
|
+
const scanned = redactSecretsForState(bag, secrets);
|
|
13165
13249
|
const { token, prefix, suffix, middle } = frame;
|
|
13166
|
-
const recorded = resolvedPlaintextOf(secrets, token);
|
|
13167
|
-
if (recorded === void 0 || recorded !== middle) return scanned;
|
|
13168
13250
|
const survivor = secrets.get(middle);
|
|
13169
13251
|
if (survivor === void 0) return scanned;
|
|
13170
13252
|
if (scanned === prefix + survivor + suffix) return prefix + token + suffix;
|
|
@@ -13172,6 +13254,152 @@ function positionByEmbeddedSpan(bag, source, secrets, bagIsSameGeneration) {
|
|
|
13172
13254
|
return scanned;
|
|
13173
13255
|
}
|
|
13174
13256
|
/**
|
|
13257
|
+
* Position a bag leaf whose `Fn::Join` / `Fn::Sub` source EMBEDS exactly one
|
|
13258
|
+
* `{{resolve:...}}` token inside LITERAL surrounding text — `port:` + token —
|
|
13259
|
+
* by writing that token into the frame the source states (issue
|
|
13260
|
+
* [#2745](https://github.com/go-to-k/cdkd/issues/2745)).
|
|
13261
|
+
*
|
|
13262
|
+
* This is the intrinsic twin of {@link positionByEmbeddedSpan}, and the shape
|
|
13263
|
+
* is the DOMINANT one rather than a corner: `'port:' + secret.secretValueFromJson('pin')`
|
|
13264
|
+
* renders the secret ARN as a `Ref`, so CDK emits
|
|
13265
|
+
* `{"Fn::Join": ["", ["port:{{resolve:secretsmanager:", {"Ref": ...}, ":SecretString:pin::}}"]]}`
|
|
13266
|
+
* — the prefix FUSED into the token's opening part, the `Ref` INSIDE the
|
|
13267
|
+
* token. Before this arm such a leaf had no positioning route at all:
|
|
13268
|
+
* {@link positionByIntrinsicSkeleton} refuses unless the WHOLE bag is a
|
|
13269
|
+
* recorded plaintext (its condition 1), and {@link positionByEmbeddedSpan}
|
|
13270
|
+
* needs a source STRING to copy its frame from. So it fell to the value scan,
|
|
13271
|
+
* which for a 4+ character middle writes the map's SURVIVOR (a same-plaintext
|
|
13272
|
+
* sibling's spelling, the #1904 / #2485 class) and for a 1-3 character middle
|
|
13273
|
+
* writes NOTHING — {@link buildNeedleRegex} drops it from the alternation —
|
|
13274
|
+
* and `port:q7` was persisted in plaintext.
|
|
13275
|
+
*
|
|
13276
|
+
* HOW THE FRAME IS FOUND. The source is rendered to text by
|
|
13277
|
+
* {@link intrinsicSkeletonSegments} — the same parser the skeleton arm reads —
|
|
13278
|
+
* with each unknowable part as one {@link UNKNOWN_PART_PLACEHOLDER}, and
|
|
13279
|
+
* {@link singleSpanFrame} then finds the ONE token in that text exactly as it
|
|
13280
|
+
* does for a literal source. The prefix and suffix must be wholly literal: a
|
|
13281
|
+
* placeholder OUTSIDE the token (a region `Ref` after it, the `DB_URL` shape
|
|
13282
|
+
* of the `secrets-dynamic-ref` fixture) means the source cannot establish
|
|
13283
|
+
* where the middle ends, and the leaf is REFUSED — the nonliteral-frame shape
|
|
13284
|
+
* #2745 deliberately defers. Inside the token a placeholder becomes a
|
|
13285
|
+
* {@link SKELETON_WILDCARD}, capped by {@link MAX_SKELETON_WILDCARDS} through
|
|
13286
|
+
* the same {@link joinSkeletonSegments}, so the token pattern is the skeleton
|
|
13287
|
+
* arm's pattern restricted to the token's own extent.
|
|
13288
|
+
*
|
|
13289
|
+
* THE EVIDENCE, three checks and each removes a different way of being wrong:
|
|
13290
|
+
*
|
|
13291
|
+
* 1. EXACTLY ONE candidate expression matches the token pattern, and is not
|
|
13292
|
+
* DEMONSTRABLY another value's — the skeleton arm's conditions 2 and 3,
|
|
13293
|
+
* over its two candidate stores PLUS the pass-local pair table
|
|
13294
|
+
* ({@link resolvedExpressionsOf}; the code says why), and the same
|
|
13295
|
+
* {@link plaintextIndexOf} poisoning rule, with `middle` in the role the
|
|
13296
|
+
* whole bag plays there.
|
|
13297
|
+
* 2. THIS PASS resolved that candidate to the middle ({@link recordResolvedPair},
|
|
13298
|
+
* per map instance) — the literal arm's evidence, and what a
|
|
13299
|
+
* shape-plausible expression this pass never resolved to the middle
|
|
13300
|
+
* (another resource's, typically) cannot satisfy. What it does NOT prove
|
|
13301
|
+
* is that the candidate is THIS leaf's token. A leaf
|
|
13302
|
+
* whose own reference resolved PUBLIC (an `ssm` `String`, which nothing
|
|
13303
|
+
* records) and carries an unknowable part INSIDE its token (a `Ref` in the
|
|
13304
|
+
* parameter name — a wholly literal public token matches no candidate and
|
|
13305
|
+
* refuses), beside a same-service SECRET sibling whose value coincides
|
|
13306
|
+
* with the middle, passes all three checks and takes the sibling's
|
|
13307
|
+
* expression — a wrong REFERENCE, not a disclosure, the class the
|
|
13308
|
+
* floorless whole-value scan already accepts for a whole-leaf coincidence.
|
|
13309
|
+
* Against the value scan: the scan writes the map's SURVIVOR for a 4+
|
|
13310
|
+
* character middle and nothing below the floor, while this arm writes the
|
|
13311
|
+
* same-service candidate — the two coincide only when that candidate is
|
|
13312
|
+
* the survivor, so at 4+ the arm can change WHICH wrong reference is
|
|
13313
|
+
* taken, and below the floor on a marked bag it adds one where the scan
|
|
13314
|
+
* wrote nothing. "Not a disclosure" is a claim about the STORED artifact
|
|
13315
|
+
* only: `resolveReplayProps` (`rollback-executor.ts`) re-resolves every
|
|
13316
|
+
* `{{resolve:` token in a replayed bag without regard to which reference
|
|
13317
|
+
* the leaf named, so a later failed deploy plus `cdkd rollback` resolves
|
|
13318
|
+
* the sibling's expression and writes its CURRENT plaintext into the live
|
|
13319
|
+
* property — the transformed-value-meets-inverse-transform shape of
|
|
13320
|
+
* GHSA-p5qg-v9gv-hc7w's consumer, reached here through a wrong reference
|
|
13321
|
+
* rather than a wrong value (maintainer review of PR 3052). THREE live
|
|
13322
|
+
* consumers re-resolve a stored expression this way, each with a weaker
|
|
13323
|
+
* precondition than the last. `cdkd drift --revert`
|
|
13324
|
+
* (`resolveStateSecretExpressions`, `drift.ts`) needs no failed deploy:
|
|
13325
|
+
* once the sibling rotates, the re-resolved baseline diverges from AWS,
|
|
13326
|
+
* drift fires, and `--revert` pushes the sibling's plaintext to the live
|
|
13327
|
+
* property — and `--accept` is no way out there, since the AWS-side value
|
|
13328
|
+
* of a secret-classified path is masked and `runAccept` refuses to persist
|
|
13329
|
+
* the mask, leaving `--revert` (the harmful button) or a redeploy. A
|
|
13330
|
+
* consumer stack's cross-stack read (`reresolveCrossStackValue`,
|
|
13331
|
+
* `intrinsic-function-resolver.ts`: `Fn::ImportValue`, `Fn::GetStackOutput`,
|
|
13332
|
+
* a parent's `Fn::GetAtt Nested.Outputs.X`) needs nothing at all: the
|
|
13333
|
+
* outputs bag is marked, so a sub-floor write reaches `state.outputs`, and
|
|
13334
|
+
* an ORDINARY deploy of the consumer after the sibling rotates hands the
|
|
13335
|
+
* sibling's current plaintext to the consumer's `provider.create` /
|
|
13336
|
+
* `update`. Stated rather than closed, and pinned by the unit file:
|
|
13337
|
+
* nothing records a public resolution, so this arm cannot tell "absent
|
|
13338
|
+
* because public" from "absent because collapsed"; the PR that closes
|
|
13339
|
+
* #2745's nested-stack site weighs these consumers rather than re-deriving
|
|
13340
|
+
* them.
|
|
13341
|
+
* 3. The write stays within the value scan's class of answer, or — below the
|
|
13342
|
+
* scan's floor — the bag carries the engine's same-generation mark:
|
|
13343
|
+
* {@link writeFramedTokenWithinScanBound}, shared with the literal arm.
|
|
13344
|
+
*
|
|
13345
|
+
* One shape reaches this arm that a reader may not expect, the literal arm's
|
|
13346
|
+
* own: a WHOLE-token source, i.e. an EMPTY frame, where the leaf IS the
|
|
13347
|
+
* middle. {@link positionByIntrinsicSkeleton} runs first and answers for
|
|
13348
|
+
* every such leaf it can; what falls through is its REFUSAL, and this arm
|
|
13349
|
+
* re-asks over a wider candidate set — the pair table names a token this pass
|
|
13350
|
+
* resolved but never pinned (an `ssm` reference whose type came back
|
|
13351
|
+
* unclassifiable, #1901) and which lost the map slot to a sibling, a token
|
|
13352
|
+
* neither of the skeleton's stores can see. With the pair and the bound that
|
|
13353
|
+
* leaf is written back as ITSELF, where the scan wrote the survivor: the
|
|
13354
|
+
* leaf's own expression, never another's. Where the skeleton refused for two
|
|
13355
|
+
* matches, so does this arm (same pattern, a superset of candidates, same
|
|
13356
|
+
* rule); where it refused for want of one — nothing matched, or its only
|
|
13357
|
+
* match was skipped as another plaintext's — this arm may find the leaf's
|
|
13358
|
+
* own token in the pair table. Either way the whole-token answer changes ONLY
|
|
13359
|
+
* where the skeleton had none.
|
|
13360
|
+
*
|
|
13361
|
+
* Every refusal before the bound returns `undefined` and the walk falls to the
|
|
13362
|
+
* value scan; the bound's own refusals return that scan directly — the same
|
|
13363
|
+
* answer either way, today's behavior, so no REFUSAL makes a case worse. The
|
|
13364
|
+
* one ACCEPTANCE that can is check 2's residual above, where a public leaf
|
|
13365
|
+
* gains a wrong reference in place of its plaintext. What stays: the
|
|
13366
|
+
* nonliteral frame above; a source with more than one token; an `Fn::Join`
|
|
13367
|
+
* whose delimiter is itself an intrinsic (the parser refuses it); and
|
|
13368
|
+
* everything the shared bound refuses, listed on the literal arm.
|
|
13369
|
+
*/
|
|
13370
|
+
function positionByIntrinsicFrame(bag, source, secrets, secretExpressions, bagIsSameGeneration) {
|
|
13371
|
+
const segments = intrinsicSkeletonSegments(source);
|
|
13372
|
+
if (segments === void 0) return void 0;
|
|
13373
|
+
if (segments.some((s) => s !== UNKNOWN_PART && s.includes(UNKNOWN_PART_PLACEHOLDER))) return;
|
|
13374
|
+
const frame = singleSpanFrame(bag, segments.map((s) => s === UNKNOWN_PART ? UNKNOWN_PART_PLACEHOLDER : s).join(""));
|
|
13375
|
+
if (frame === void 0) return void 0;
|
|
13376
|
+
const { token, prefix, suffix, middle } = frame;
|
|
13377
|
+
if (prefix.includes(UNKNOWN_PART_PLACEHOLDER) || suffix.includes(UNKNOWN_PART_PLACEHOLDER)) return;
|
|
13378
|
+
const pattern = anchoredSkeletonPattern(token.split(UNKNOWN_PART_PLACEHOLDER).flatMap((literal, i) => i === 0 ? [literal] : [UNKNOWN_PART, literal]));
|
|
13379
|
+
if (pattern === void 0) return void 0;
|
|
13380
|
+
const plaintextOf = plaintextIndexOf(secrets);
|
|
13381
|
+
let matched;
|
|
13382
|
+
for (const candidate of /* @__PURE__ */ new Set([
|
|
13383
|
+
...secretExpressions,
|
|
13384
|
+
...recordedSecretExpressions$1,
|
|
13385
|
+
...resolvedExpressionsOf(secrets)
|
|
13386
|
+
])) {
|
|
13387
|
+
if (candidate.length > MAX_SKELETON_CANDIDATE_LENGTH) return void 0;
|
|
13388
|
+
if (!pattern.test(candidate)) continue;
|
|
13389
|
+
const recordedPlaintext = plaintextOf.get(candidate);
|
|
13390
|
+
if (recordedPlaintext !== void 0 && recordedPlaintext !== middle) continue;
|
|
13391
|
+
if (matched !== void 0) return void 0;
|
|
13392
|
+
matched = candidate;
|
|
13393
|
+
}
|
|
13394
|
+
if (matched === void 0 || resolvedPlaintextOf(secrets, matched) !== middle) return;
|
|
13395
|
+
return writeFramedTokenWithinScanBound(bag, secrets, {
|
|
13396
|
+
token: matched,
|
|
13397
|
+
prefix,
|
|
13398
|
+
suffix,
|
|
13399
|
+
middle
|
|
13400
|
+
}, bagIsSameGeneration);
|
|
13401
|
+
}
|
|
13402
|
+
/**
|
|
13175
13403
|
* Keys tried, in order, when pairing two arrays whose ORDER cannot be trusted
|
|
13176
13404
|
* (issue #1915).
|
|
13177
13405
|
*
|
|
@@ -13337,7 +13565,7 @@ function positionListByCrossStackSource(bag, source, secrets) {
|
|
|
13337
13565
|
* fetch and no value matching.
|
|
13338
13566
|
*
|
|
13339
13567
|
* A source leaf that is an intrinsic OBJECT has no string to copy, so it goes
|
|
13340
|
-
* through
|
|
13568
|
+
* through three positioning passes before the value scan, in this order:
|
|
13341
13569
|
*
|
|
13342
13570
|
* - {@link positionByCrossStackSource} (issue #2059), for the two CROSS-STACK
|
|
13343
13571
|
* spellings `Fn::ImportValue` / `Fn::GetStackOutput`. Those carry no text
|
|
@@ -13348,6 +13576,11 @@ function positionListByCrossStackSource(bag, source, secrets) {
|
|
|
13348
13576
|
* `Fn::Sub`: when the intrinsic's literal parts describe exactly one of the
|
|
13349
13577
|
* recorded secret expressions, THAT is persisted. This is the dominant CDK
|
|
13350
13578
|
* shape — an L2 secret token renders the ARN as a `Ref`, hence a join.
|
|
13579
|
+
* - {@link positionByIntrinsicFrame} (issue #2745), for the same two spellings
|
|
13580
|
+
* when their literal parts FRAME one token (`port:` +
|
|
13581
|
+
* `secretValueFromJson(...)`): the one candidate this pass resolved to the
|
|
13582
|
+
* framed middle is written into the frame, under the literal span arm's
|
|
13583
|
+
* bound and mark.
|
|
13351
13584
|
*
|
|
13352
13585
|
* An ARRAY leaf beside such an intrinsic OBJECT — the shape a
|
|
13353
13586
|
* `CommaDelimitedList` nested-stack parameter produces once the child has
|
|
@@ -13357,9 +13590,10 @@ function positionListByCrossStackSource(bag, source, secrets) {
|
|
|
13357
13590
|
* is aligned against a source array that does not exist; see that function for
|
|
13358
13591
|
* the two shapes it refuses.
|
|
13359
13592
|
*
|
|
13360
|
-
* The value scan is still applied wherever none can answer:
|
|
13361
|
-
*
|
|
13362
|
-
*
|
|
13593
|
+
* The value scan is still applied wherever none can answer: an embedding leaf
|
|
13594
|
+
* the span arms refuse (a nonliteral frame, a middle no pair of this pass
|
|
13595
|
+
* vouches for), an intrinsic whose skeleton matches several candidates (or
|
|
13596
|
+
* none, the pair table included), a cross-stack leaf whose identity is not
|
|
13363
13597
|
* literally computable, a diverged shape, a key the source lacks. So the passes
|
|
13364
13598
|
* are complementary rather than alternatives — path where position is knowable,
|
|
13365
13599
|
* association where the position is a cross-stack read, skeleton where it is a
|
|
@@ -13378,6 +13612,8 @@ function redactByPath(bag, source, secrets, rules, secretExpressions, bagIsSameG
|
|
|
13378
13612
|
if (certified !== void 0) return certified;
|
|
13379
13613
|
const positioned = positionByIntrinsicSkeleton(bag, source, secrets, secretExpressions);
|
|
13380
13614
|
if (positioned !== void 0) return positioned;
|
|
13615
|
+
const framed = positionByIntrinsicFrame(bag, source, secrets, secretExpressions, bagIsSameGeneration);
|
|
13616
|
+
if (framed !== void 0) return framed;
|
|
13381
13617
|
}
|
|
13382
13618
|
if (Array.isArray(bag) && isPlainObject$2(source)) {
|
|
13383
13619
|
const positionedList = positionListByCrossStackSource(bag, source, secrets);
|
|
@@ -20276,11 +20512,15 @@ const drainDeadlines = new AsyncLocalStorage();
|
|
|
20276
20512
|
*
|
|
20277
20513
|
* THE TRADE, stated because it is real: iterations that actually WAIT spend
|
|
20278
20514
|
* the shared budget, so a later one can find it exhausted and its drain get
|
|
20279
|
-
* no grace. That is the
|
|
20515
|
+
* no grace. That is the exposure issue
|
|
20280
20516
|
* [#2814](https://github.com/go-to-k/cdkd/issues/2814) records for nesting,
|
|
20281
|
-
*
|
|
20282
|
-
*
|
|
20283
|
-
*
|
|
20517
|
+
* reachable across a loop as well: a part such a drain stops waiting for
|
|
20518
|
+
* records LATE. Late, not lost -- the engine's `Export.Name` block writes each
|
|
20519
|
+
* recording through to the pass map (it used to copy a per-iteration local,
|
|
20520
|
+
* which DROPPED a late one), and the outputs readers after the pass redact
|
|
20521
|
+
* against the pass map at the moment they read -- but a reader that already
|
|
20522
|
+
* took its copy does not see it, and the resolver warns when a drain stops
|
|
20523
|
+
* waiting. What bounds it is that only real
|
|
20284
20524
|
* waiting spends the budget: reaching zero takes a full cap of drain wait
|
|
20285
20525
|
* inside one pass, not merely a slow pass. It is taken deliberately -- an
|
|
20286
20526
|
* unbounded hold on a deploy's state save costs a first deploy every
|
|
@@ -21116,9 +21356,12 @@ const NAMEABLE_INTRINSIC_KEYS = /* @__PURE__ */ new Set([
|
|
|
21116
21356
|
* now throw on an intrinsic — `route53-provider.ts` (`HostedZoneConfig`),
|
|
21117
21357
|
* `cloudfront-oai-provider.ts` (`CloudFrontOriginAccessIdentityConfig`),
|
|
21118
21358
|
* `kinesis-provider.ts` (`StreamModeDetails`), `ecs-provider.ts`
|
|
21119
|
-
* (`DeploymentController`)
|
|
21120
|
-
*
|
|
21121
|
-
*
|
|
21359
|
+
* (`DeploymentController`) and `codebuild-provider.ts` (reached from `update()`
|
|
21360
|
+
* through `mapProperties`). `secretsmanager-secret-provider.ts` was on this
|
|
21361
|
+
* list until issue #3048 moved its update path to a SKIP (the container is
|
|
21362
|
+
* refused by `requireConfigObject` before `generateSecretString` reads it;
|
|
21363
|
+
* `create()` still throws, and so does the MEMBER-level `ExcludeCharacters`
|
|
21364
|
+
* read inside a well-formed container -- issue #3056). Not a count, for the same
|
|
21122
21365
|
* reason as the near-copy list below: two successive reviews each found
|
|
21123
21366
|
* another. Re-derive with
|
|
21124
21367
|
* `grep -rn 'readConfigString(' src/provisioning/providers/`.
|
|
@@ -22776,6 +23019,13 @@ const CAP_EXPIRED = Symbol("drain-cap-expired");
|
|
|
22776
23019
|
*/
|
|
22777
23020
|
const concurrentDrainCap = {};
|
|
22778
23021
|
/**
|
|
23022
|
+
* The budgets whose drain has already reported abandoned inputs (issue
|
|
23023
|
+
* [#2814](https://github.com/go-to-k/cdkd/issues/2814)). A `WeakSet` so a
|
|
23024
|
+
* budget's entry dies with the budget, as the budget itself does with the
|
|
23025
|
+
* async context that opened it.
|
|
23026
|
+
*/
|
|
23027
|
+
const abandonReported = /* @__PURE__ */ new WeakSet();
|
|
23028
|
+
/**
|
|
22779
23029
|
* `Promise.all`'s RESULT and its choice of error, with `Promise.allSettled`'s
|
|
22780
23030
|
* TIMING: every promise started here has settled before this returns, and
|
|
22781
23031
|
* before it throws unless the cap below expires first (issue
|
|
@@ -22853,8 +23103,25 @@ const concurrentDrainCap = {};
|
|
|
22853
23103
|
* require the "healthy work slower than 60 s" shape the sizing paragraph
|
|
22854
23104
|
* describes; that shape is the cheapest way to reach it with no nesting and
|
|
22855
23105
|
* no wrapped loop, not the floor.
|
|
22856
|
-
*
|
|
22857
|
-
*
|
|
23106
|
+
*
|
|
23107
|
+
* WHAT HAPPENS TO A PART THE CAP STOPS WAITING FOR (issue
|
|
23108
|
+
* [#2814](https://github.com/go-to-k/cdkd/issues/2814)). It keeps running, and
|
|
23109
|
+
* its recording still lands in the context's map whenever it arrives -- the
|
|
23110
|
+
* cap costs it ORDER, not the write. So the answer sits with the READERS: one
|
|
23111
|
+
* that runs after the recording arrives must see it, which `DeployEngine`'s
|
|
23112
|
+
* outputs pass arranges for its `Export.Name` block (a map that writes each
|
|
23113
|
+
* recording through to the pass map at once, where a local copied in a
|
|
23114
|
+
* `finally` used to drop it) and for the persisted outputs, the exports index
|
|
23115
|
+
* and the deploy summary (each redacting against the pass map as it stands
|
|
23116
|
+
* at the moment it is written). A reader that took its
|
|
23117
|
+
* copy BEFORE the recording arrived -- a failure message already printed, a
|
|
23118
|
+
* state save already sent -- cannot be helped without waiting, and the wait
|
|
23119
|
+
* is what the cap bounds. Cancelling the
|
|
23120
|
+
* part would not close that either: a cancelled part never records, so its
|
|
23121
|
+
* plaintext would be missing for EVERY reader rather than for the early ones.
|
|
23122
|
+
* What this helper adds is the report: releasing a rejection with inputs
|
|
23123
|
+
* still running calls `onAbandoned` with how many, at most once per budget,
|
|
23124
|
+
* and the resolver turns that into a warning.
|
|
22858
23125
|
*
|
|
22859
23126
|
* ONE REMAINING CONSEQUENCE, deliberate and pinned by a case rather than only
|
|
22860
23127
|
* described: the earliest-in-time rule holds PER INVOCATION, not across
|
|
@@ -22872,11 +23139,16 @@ const concurrentDrainCap = {};
|
|
|
22872
23139
|
* failure where the old race was arbitrary. Residual: issue
|
|
22873
23140
|
* [#2805](https://github.com/go-to-k/cdkd/issues/2805).
|
|
22874
23141
|
*/
|
|
22875
|
-
async function allSettledKeepingFirstRejection(promises) {
|
|
23142
|
+
async function allSettledKeepingFirstRejection(promises, onAbandoned) {
|
|
22876
23143
|
let rejection;
|
|
22877
23144
|
const shared = drainDeadlines.getStore();
|
|
22878
23145
|
let armCap;
|
|
22879
|
-
|
|
23146
|
+
let settled = 0;
|
|
23147
|
+
const guarded = promises.map((promise) => promise.then((value) => {
|
|
23148
|
+
settled += 1;
|
|
23149
|
+
return value;
|
|
23150
|
+
}, (error) => {
|
|
23151
|
+
settled += 1;
|
|
22880
23152
|
if (rejection === void 0) {
|
|
22881
23153
|
rejection = { error };
|
|
22882
23154
|
armCap?.();
|
|
@@ -22901,7 +23173,17 @@ async function allSettledKeepingFirstRejection(promises) {
|
|
|
22901
23173
|
});
|
|
22902
23174
|
try {
|
|
22903
23175
|
const outcome = await Promise.race([Promise.all(guarded), capped]);
|
|
22904
|
-
if (rejection !== void 0)
|
|
23176
|
+
if (rejection !== void 0) {
|
|
23177
|
+
const pending = promises.length - settled;
|
|
23178
|
+
if (pending > 0) {
|
|
23179
|
+
if (shared === void 0) onAbandoned(pending);
|
|
23180
|
+
else if (!abandonReported.has(shared)) {
|
|
23181
|
+
abandonReported.add(shared);
|
|
23182
|
+
onAbandoned(pending);
|
|
23183
|
+
}
|
|
23184
|
+
}
|
|
23185
|
+
throw rejection.error;
|
|
23186
|
+
}
|
|
22905
23187
|
if (outcome === CAP_EXPIRED) throw markNonRetryable(/* @__PURE__ */ new Error("drain cap expired with no rejection recorded"));
|
|
22906
23188
|
return outcome;
|
|
22907
23189
|
} finally {
|
|
@@ -23869,6 +24151,17 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
23869
24151
|
return conditions;
|
|
23870
24152
|
}
|
|
23871
24153
|
/**
|
|
24154
|
+
* Warn that a drain stopped waiting for `pending` inputs of a failed
|
|
24155
|
+
* resolution (issue [#2814](https://github.com/go-to-k/cdkd/issues/2814)).
|
|
24156
|
+
* The helper calls this at most once per budget. The text carries a count
|
|
24157
|
+
* and the cap, nothing the template or AWS supplied, so it needs no masking.
|
|
24158
|
+
*/
|
|
24159
|
+
warnAbandonedParts(pending) {
|
|
24160
|
+
const capSeconds = (concurrentDrainCap.ms ?? DRAIN_AFTER_REJECTION_MS) / 1e3;
|
|
24161
|
+
const parts = pending === 1 ? "1 concurrent part was" : `${pending} concurrent parts were`;
|
|
24162
|
+
this.logger.warn(`A resolution failed while ${parts} still running, and cdkd stopped waiting because the wait after a failure (capped at ${capSeconds}s) was used up. A secret such a part resolves from now on is still recorded, but only what runs after it arrives can mask it: output already printed or saved is not revisited.`);
|
|
24163
|
+
}
|
|
24164
|
+
/**
|
|
23872
24165
|
* Recursively resolve a value
|
|
23873
24166
|
*/
|
|
23874
24167
|
async resolveValue(value, context) {
|
|
@@ -23876,7 +24169,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
23876
24169
|
if (typeof value === "string" && value.includes("{{resolve:")) return await this.resolveDynamicReferences(value, context);
|
|
23877
24170
|
return value;
|
|
23878
24171
|
}
|
|
23879
|
-
if (Array.isArray(value)) return (await allSettledKeepingFirstRejection(value.map((v) => this.resolveValue(v, context)))).filter((v) => v !== AWS_NO_VALUE);
|
|
24172
|
+
if (Array.isArray(value)) return (await allSettledKeepingFirstRejection(value.map((v) => this.resolveValue(v, context)), (pending) => this.warnAbandonedParts(pending))).filter((v) => v !== AWS_NO_VALUE);
|
|
23880
24173
|
const obj = value;
|
|
23881
24174
|
if ("Ref" in obj) return await this.resolveRef(obj["Ref"], context);
|
|
23882
24175
|
if ("Fn::GetAtt" in obj) return await this.resolveGetAtt(obj["Fn::GetAtt"], context);
|
|
@@ -24783,7 +25076,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
24783
25076
|
let result = (await allSettledKeepingFirstRejection(values.map(async (v) => {
|
|
24784
25077
|
const resolved = await this.resolveValue(v, context);
|
|
24785
25078
|
return String(resolved);
|
|
24786
|
-
}))).join(delimiter);
|
|
25079
|
+
}), (pending) => this.warnAbandonedParts(pending))).join(delimiter);
|
|
24787
25080
|
if (result.includes("{{resolve:")) result = await this.resolveDynamicReferences(result, context);
|
|
24788
25081
|
this.logger.debug(`Resolved Fn::Join: ${this.maskSecretsForLog(result, context)}`);
|
|
24789
25082
|
return result;
|
|
@@ -27748,7 +28041,7 @@ var CloudControlProvider = class {
|
|
|
27748
28041
|
const indeterminateGuard = await this.confirmDeleteTargetIdentity(logicalId, resourceType, physicalId, context);
|
|
27749
28042
|
if (context?.removeProtection === true && resourceType === "AWS::AutoScaling::AutoScalingGroup") {
|
|
27750
28043
|
this.logger.debug(`Delegating protected AutoScalingGroup ${logicalId} delete to the SDK ASGProvider (Cloud Control cannot force-delete a protected ASG)`);
|
|
27751
|
-
const { ASGProvider } = await import("./asg-provider-
|
|
28044
|
+
const { ASGProvider } = await import("./asg-provider-CpxZ9sVH.js").then((n) => n.n);
|
|
27752
28045
|
const asgProvider = new ASGProvider();
|
|
27753
28046
|
return withIndeterminateGuard(await asgProvider.delete(logicalId, physicalId, resourceType, _properties, context), indeterminateGuard);
|
|
27754
28047
|
}
|
|
@@ -37059,6 +37352,47 @@ var DeploymentEventsReader = class {
|
|
|
37059
37352
|
*/
|
|
37060
37353
|
const EMPTY_SECRETS = /* @__PURE__ */ new Map();
|
|
37061
37354
|
/**
|
|
37355
|
+
* A secrets map that keeps its OWN entries and also writes each one through
|
|
37356
|
+
* to `target` the moment it is recorded (issue
|
|
37357
|
+
* [#2814](https://github.com/go-to-k/cdkd/issues/2814)).
|
|
37358
|
+
*
|
|
37359
|
+
* The outputs pass resolves each intrinsic `Export.Name` through one of these.
|
|
37360
|
+
* Its own entries are what `exportNameSecretExposure` reads as "substituted
|
|
37361
|
+
* into THIS name", which must stay exact -- a view of the whole pass map
|
|
37362
|
+
* (`cdkd scrub`'s `SharedEntriesSecrets`) would report every secret the pass
|
|
37363
|
+
* has recorded as one the name holds, and refuse every alias. The write-through
|
|
37364
|
+
* is what a copy could not give: a part the drain cap stopped waiting for
|
|
37365
|
+
* records AFTER the name's block has ended, and when this was a local copied
|
|
37366
|
+
* into the pass map in a `finally`, such a record reached nothing.
|
|
37367
|
+
*
|
|
37368
|
+
* Only `set` writes through: it is the one operation the resolver performs on
|
|
37369
|
+
* a recording map (grepped -- no resolver path calls `delete` or `clear` on
|
|
37370
|
+
* one). `cdkd scrub`'s `SharedEntriesSecrets` overrides those too, and the
|
|
37371
|
+
* asymmetry is deliberate: that one is a VIEW holding no entries of its own,
|
|
37372
|
+
* while a `delete` here would leave the pass map carrying an entry this map
|
|
37373
|
+
* dropped -- one needle too many, which over-redacts rather than leaks.
|
|
37374
|
+
* The ENTRIES only, never the resolved pairs beside them
|
|
37375
|
+
* (issue #2485): those are keyed by map INSTANCE (`recordResolvedPair`), so
|
|
37376
|
+
* they stay on this one. A name never positions a leaf, and a value re-using
|
|
37377
|
+
* the same token records its own pair at the seam, so carrying them could add
|
|
37378
|
+
* nothing -- what it COULD do is mark a pair conflicting (an `ssm` reference
|
|
37379
|
+
* whose `Type` came back unclassifiable is never cached, so a name resolving
|
|
37380
|
+
* it re-asks AWS and can see a value that moved since the value pass) and
|
|
37381
|
+
* destroy the positioning the value pass had earned.
|
|
37382
|
+
*/
|
|
37383
|
+
var ForwardingSecrets = class extends Map {
|
|
37384
|
+
target;
|
|
37385
|
+
constructor(target) {
|
|
37386
|
+
super();
|
|
37387
|
+
this.target = target;
|
|
37388
|
+
}
|
|
37389
|
+
set(plaintext, expression) {
|
|
37390
|
+
super.set(plaintext, expression);
|
|
37391
|
+
this.target?.set(plaintext, expression);
|
|
37392
|
+
return this;
|
|
37393
|
+
}
|
|
37394
|
+
};
|
|
37395
|
+
/**
|
|
37062
37396
|
* Default per-resource warn threshold: warn the user when a single
|
|
37063
37397
|
* resource has been in flight for 5 minutes. Most CC API resources
|
|
37064
37398
|
* complete in under a minute; 5m is the agreed elbow.
|
|
@@ -37333,6 +37667,16 @@ var DeployEngine = class DeployEngine {
|
|
|
37333
37667
|
*/
|
|
37334
37668
|
outputSecrets = /* @__PURE__ */ new Map();
|
|
37335
37669
|
/**
|
|
37670
|
+
* The outputs pass's own recording map(s), one per `resolveOutputs` call
|
|
37671
|
+
* this deploy, so every redaction of the outputs bag re-reads them rather
|
|
37672
|
+
* than trusting the copy taken when the pass ended (issue
|
|
37673
|
+
* [#2814](https://github.com/go-to-k/cdkd/issues/2814)): a part the drain
|
|
37674
|
+
* cap stopped waiting for records into its pass map LATE, and the final
|
|
37675
|
+
* save, the exports index and the deploy summary all run after that copy.
|
|
37676
|
+
* Reset per `deploy()`.
|
|
37677
|
+
*/
|
|
37678
|
+
outputsPassSecretMaps = [];
|
|
37679
|
+
/**
|
|
37336
37680
|
* UNRESOLVED template `Outputs` values, keyed by output name (issue #1910) —
|
|
37337
37681
|
* the outputs' POSITION source, the sibling of `perResourceTemplateProps` for
|
|
37338
37682
|
* the bag `resolveOutputs` produces. Without it two outputs resolving one
|
|
@@ -37461,6 +37805,7 @@ var DeployEngine = class DeployEngine {
|
|
|
37461
37805
|
this.perResourceResolvedType = /* @__PURE__ */ new Map();
|
|
37462
37806
|
this.attemptedResolvedProps = /* @__PURE__ */ new Map();
|
|
37463
37807
|
this.outputSecrets = /* @__PURE__ */ new Map();
|
|
37808
|
+
this.outputsPassSecretMaps = [];
|
|
37464
37809
|
this.outputsTemplateSource = Object.create(null);
|
|
37465
37810
|
this.outputsSourceUsable = true;
|
|
37466
37811
|
this.skippedOutputs = void 0;
|
|
@@ -37532,12 +37877,16 @@ var DeployEngine = class DeployEngine {
|
|
|
37532
37877
|
* Redact the resolved stack OUTPUTS bag, positioned by the unresolved
|
|
37533
37878
|
* template `Outputs` values (issue #1910).
|
|
37534
37879
|
*
|
|
37535
|
-
* A single entry point because
|
|
37536
|
-
* state-persist choke point, the no-change re-check,
|
|
37537
|
-
*
|
|
37538
|
-
*
|
|
37539
|
-
*
|
|
37540
|
-
*
|
|
37880
|
+
* A single entry point because SEVEN call sites redact this same bag — the
|
|
37881
|
+
* state-persist choke point, the no-change re-check, that path's exports
|
|
37882
|
+
* index and deploy summary, the changes path's index and summary, and the
|
|
37883
|
+
* outputs pass itself. THREE of those are the ones issue #1910 unified —
|
|
37884
|
+
* the persist choke point, the re-check, and the outputs pass, which the
|
|
37885
|
+
* list above ends on rather than opens with: before it they each spelled
|
|
37886
|
+
* the value-only redaction separately, and two outputs resolving one secret
|
|
37887
|
+
* collapsed onto whichever expression was recorded last at all three. The
|
|
37888
|
+
* other FOUR are issue #2814's, both paths' index and summary: each read
|
|
37889
|
+
* the bag unredacted until then, and now redacts at the moment it reads.
|
|
37541
37890
|
*/
|
|
37542
37891
|
/**
|
|
37543
37892
|
* Record the plaintext behind every output {@link redactOutputs} just masked,
|
|
@@ -37550,9 +37899,18 @@ var DeployEngine = class DeployEngine {
|
|
|
37550
37899
|
* value read back out of a previous run's state — is skipped, because there
|
|
37551
37900
|
* is no plaintext behind it to remember.
|
|
37552
37901
|
*
|
|
37553
|
-
* Called only from the REAL-DEPLOY outputs pass.
|
|
37554
|
-
* callers hand it a bag from a previous
|
|
37555
|
-
*
|
|
37902
|
+
* Called only from the REAL-DEPLOY outputs pass. Of the six other
|
|
37903
|
+
* `redactOutputs` callers, three CAN hand it a bag from a previous
|
|
37904
|
+
* generation — the persist walk always may, and the no-change path's
|
|
37905
|
+
* exports index and summary do on the arms where that path keeps the
|
|
37906
|
+
* previous bag — and there a mask is already unrecoverable, so pretending
|
|
37907
|
+
* otherwise would
|
|
37908
|
+
* serve a stale value. The other three are not this pass either: the
|
|
37909
|
+
* no-change path performs the FIRST redaction of its own resolution there,
|
|
37910
|
+
* and the changes path's index and summary re-redact the bag this pass
|
|
37911
|
+
* already produced. (The "other two" this replaces counted the base's three
|
|
37912
|
+
* callers correctly; what was wrong was calling both of the others
|
|
37913
|
+
* previous-generation, when only the persist walk was one.)
|
|
37556
37914
|
*/
|
|
37557
37915
|
rememberRecoverableMaskedOutputs(stackName, resolved, redacted) {
|
|
37558
37916
|
if (resolved === redacted) return;
|
|
@@ -37563,7 +37921,51 @@ var DeployEngine = class DeployEngine {
|
|
|
37563
37921
|
recordRecoverableMaskedOutput(stackName, this.stackRegion, key, plaintext);
|
|
37564
37922
|
}
|
|
37565
37923
|
}
|
|
37924
|
+
/**
|
|
37925
|
+
* Fold every outputs-pass recording map into `outputSecrets` — the entries,
|
|
37926
|
+
* and the uncollapsed evidence beside them (issue #2485: without it a
|
|
37927
|
+
* literal Output embedding one of two same-plaintext references would lose
|
|
37928
|
+
* its span positioning and persist the sibling's expression). Only what the
|
|
37929
|
+
* outputs pass itself resolved, so the outputs redaction (GHSA fix) uses
|
|
37930
|
+
* only outputs-substituted references: a literal output equal to a secret
|
|
37931
|
+
* nothing resolved is not recorded, and is not touched.
|
|
37932
|
+
*
|
|
37933
|
+
* Run by every {@link redactOutputs} (issue
|
|
37934
|
+
* [#2814](https://github.com/go-to-k/cdkd/issues/2814)), because a part the
|
|
37935
|
+
* drain cap stopped waiting for records into its pass map LATE: a secret
|
|
37936
|
+
* that arrives between the pass and the final save is then still a needle
|
|
37937
|
+
* for the save, the exports index and the deploy summary. Repeating it is
|
|
37938
|
+
* safe for the ENTRIES — re-setting one the bag holds changes nothing, and
|
|
37939
|
+
* `Map.set` on an existing key keeps its insertion order, so last-wins
|
|
37940
|
+
* cannot be reordered by a refold.
|
|
37941
|
+
*
|
|
37942
|
+
* The PAIRS are where repeating is not merely a no-op, stated because the
|
|
37943
|
+
* end-of-pass copy could not see it: a part the cap stopped waiting for can
|
|
37944
|
+
* record a pair LATE, and a second value for one expression marks it
|
|
37945
|
+
* `CONFLICTING_PLAINTEXT` (a non-cacheable `{{resolve:ssm:X}}` re-resolved
|
|
37946
|
+
* to a moved value). A later refold then carries that conflict into
|
|
37947
|
+
* `outputSecrets`, the leaf loses its positioning and falls to the value
|
|
37948
|
+
* scan, which can persist a sibling's expression (the issue #2485 class).
|
|
37949
|
+
* The plaintext stays MASKED either way, so the cost is which expression
|
|
37950
|
+
* is stored, not a disclosure — and the alternative, dropping the late
|
|
37951
|
+
* pair, would keep a positioning the pass itself no longer vouches for.
|
|
37952
|
+
*/
|
|
37953
|
+
absorbOutputsPassSecrets() {
|
|
37954
|
+
for (const passSecrets of this.outputsPassSecretMaps) {
|
|
37955
|
+
for (const [value, expr] of passSecrets) this.outputSecrets.set(value, expr);
|
|
37956
|
+
mergeResolvedPairs(passSecrets, this.outputSecrets);
|
|
37957
|
+
}
|
|
37958
|
+
}
|
|
37959
|
+
/**
|
|
37960
|
+
* Redact the outputs bag. NOT a pure transform: it folds the outputs pass
|
|
37961
|
+
* map into `outputSecrets` first (issue #2814), so every call can grow that
|
|
37962
|
+
* bag. Its KEY set only grows, and the bag has no other reader, which is
|
|
37963
|
+
* what makes calling this on every save, index write and summary safe. The
|
|
37964
|
+
* resolved PAIRS beside those keys are not equally free to refold — see
|
|
37965
|
+
* {@link absorbOutputsPassSecrets}, which states what a late one costs.
|
|
37966
|
+
*/
|
|
37566
37967
|
redactOutputs(outputs) {
|
|
37968
|
+
this.absorbOutputsPassSecrets();
|
|
37567
37969
|
if (this.outputSecrets.size === 0) return outputs;
|
|
37568
37970
|
return redactSecretsForState(outputs, this.outputSecrets, this.outputsSourceUsable ? this.outputsTemplateSource : void 0, TEMPLATE_SOURCED_RULES);
|
|
37569
37971
|
}
|
|
@@ -38196,7 +38598,10 @@ var DeployEngine = class DeployEngine {
|
|
|
38196
38598
|
persistedOutputs = refreshedState.outputs;
|
|
38197
38599
|
if (outputsChanged) this.logger.info("Persisted Outputs-only change (no resource diff).");
|
|
38198
38600
|
else this.logger.debug("Persisted export-set change (no outputs-value diff, no-change path, #2193)");
|
|
38199
|
-
if (this.exportIndexStore) await this.exportIndexStore.updateForStack(stackName, this.stackRegion, importableOutputs(
|
|
38601
|
+
if (this.exportIndexStore) await this.exportIndexStore.updateForStack(stackName, this.stackRegion, importableOutputs({
|
|
38602
|
+
...refreshedState,
|
|
38603
|
+
outputs: this.redactOutputs(refreshedState.outputs)
|
|
38604
|
+
}));
|
|
38200
38605
|
} else if (observedRefresh) this.logger.debug("Persisted refreshed observedProperties (no-change path)");
|
|
38201
38606
|
else this.logger.debug("Persisted skipped-outputs record (no outputs-value diff, no-change path, #2740)");
|
|
38202
38607
|
} catch (saveError) {
|
|
@@ -38213,7 +38618,7 @@ var DeployEngine = class DeployEngine {
|
|
|
38213
38618
|
updatePartial: 0,
|
|
38214
38619
|
unchanged: Object.keys(currentState.resources).length,
|
|
38215
38620
|
durationMs: Date.now() - startTime,
|
|
38216
|
-
outputs: this.buildDisplayOutputs(template, persistedOutputs),
|
|
38621
|
+
outputs: this.buildDisplayOutputs(template, this.redactOutputs(persistedOutputs)),
|
|
38217
38622
|
attributeFallbackCount: this.resolver.getPhysicalIdFallbackCount()
|
|
38218
38623
|
};
|
|
38219
38624
|
}
|
|
@@ -38244,7 +38649,10 @@ var DeployEngine = class DeployEngine {
|
|
|
38244
38649
|
await this.drainObservedCaptures(newState.resources);
|
|
38245
38650
|
const newEtag = await this.stateBackend.saveState(stackName, this.stackRegion, this.withParentInfo(newState));
|
|
38246
38651
|
this.logger.debug(`State saved (ETag: ${newEtag})`);
|
|
38247
|
-
await Promise.all([this.deleteRollbackJournalBestEffort(stackName), this.exportIndexStore ? this.exportIndexStore.updateForStack(stackName, this.stackRegion, importableOutputs(
|
|
38652
|
+
await Promise.all([this.deleteRollbackJournalBestEffort(stackName), this.exportIndexStore ? this.exportIndexStore.updateForStack(stackName, this.stackRegion, importableOutputs({
|
|
38653
|
+
...newState,
|
|
38654
|
+
outputs: this.redactOutputs(newState.outputs)
|
|
38655
|
+
})) : Promise.resolve()]);
|
|
38248
38656
|
const durationMs = Date.now() - startTime;
|
|
38249
38657
|
const unchangedCount = this.diffCalculator.filterByType(changes, "NO_CHANGE").length + actualCounts.skipped;
|
|
38250
38658
|
return {
|
|
@@ -38256,7 +38664,7 @@ var DeployEngine = class DeployEngine {
|
|
|
38256
38664
|
updatePartial: actualCounts.updatePartial,
|
|
38257
38665
|
unchanged: unchangedCount,
|
|
38258
38666
|
durationMs,
|
|
38259
|
-
outputs: this.buildDisplayOutputs(template, newState.outputs ?? {}),
|
|
38667
|
+
outputs: this.buildDisplayOutputs(template, this.redactOutputs(newState.outputs ?? {})),
|
|
38260
38668
|
attributeFallbackCount: this.resolver.getPhysicalIdFallbackCount()
|
|
38261
38669
|
};
|
|
38262
38670
|
} finally {
|
|
@@ -39868,8 +40276,10 @@ var DeployEngine = class DeployEngine {
|
|
|
39868
40276
|
* defense in depth here — but two masking sites in one flow must not argue
|
|
39869
40277
|
* opposite sides of the same question. `secrets` is the outputs pass's own
|
|
39870
40278
|
* map: everything recorded before this handler runs, an `Export.Name`
|
|
39871
|
-
* resolution's entries included (its
|
|
39872
|
-
*
|
|
40279
|
+
* resolution's entries included (its map writes each one through to the
|
|
40280
|
+
* pass map as the resolver records it, so they are here before the `catch`
|
|
40281
|
+
* is; issue #2814 replaced the `finally` that copied them at the end of
|
|
40282
|
+
* the block). Since issue #2563 a still-pending concurrent part
|
|
39873
40283
|
* is in the PASS bag before this handler runs: the resolver drains every
|
|
39874
40284
|
* part it started before a rejection reaches a caller. Not
|
|
39875
40285
|
* unconditionally, and the weaker claim is the true one -- the drain is
|
|
@@ -40004,6 +40414,7 @@ var DeployEngine = class DeployEngine {
|
|
|
40004
40414
|
};
|
|
40005
40415
|
const outputsPassSecrets = context.recordedSecretValues ?? EMPTY_SECRETS;
|
|
40006
40416
|
const outputsPassInherited = context.inheritedSecrets ?? EMPTY_SECRETS;
|
|
40417
|
+
if (context.recordedSecretValues) this.outputsPassSecretMaps.push(context.recordedSecretValues);
|
|
40007
40418
|
const publishedOutputNames = collectPublishedOutputNames(template.Outputs, conditions);
|
|
40008
40419
|
let outputsPassCompleted = false;
|
|
40009
40420
|
try {
|
|
@@ -40029,20 +40440,16 @@ var DeployEngine = class DeployEngine {
|
|
|
40029
40440
|
if (!output.Export?.Name) continue;
|
|
40030
40441
|
const value = outputs[outputKey];
|
|
40031
40442
|
if (value === void 0) continue;
|
|
40032
|
-
const nameSecrets =
|
|
40443
|
+
const nameSecrets = new ForwardingSecrets(context.recordedSecretValues);
|
|
40033
40444
|
const nameReads = [];
|
|
40034
40445
|
let exportName;
|
|
40035
40446
|
try {
|
|
40036
|
-
|
|
40037
|
-
|
|
40038
|
-
|
|
40039
|
-
|
|
40040
|
-
|
|
40041
|
-
|
|
40042
|
-
refuseMaskedOutputReads(outputKey, nameReads);
|
|
40043
|
-
} finally {
|
|
40044
|
-
for (const [plaintext, expression] of nameSecrets) context.recordedSecretValues?.set(plaintext, expression);
|
|
40045
|
-
}
|
|
40447
|
+
exportName = typeof output.Export.Name === "string" ? output.Export.Name : await this.resolver.resolve(output.Export.Name, {
|
|
40448
|
+
...context,
|
|
40449
|
+
recordedSecretValues: nameSecrets,
|
|
40450
|
+
redactedAttributeReads: nameReads
|
|
40451
|
+
});
|
|
40452
|
+
refuseMaskedOutputReads(outputKey, nameReads);
|
|
40046
40453
|
} catch (error) {
|
|
40047
40454
|
this.handleOutputResolutionFailure(error, outputKey, outputs, outputsPassSecrets, outputsPassInherited);
|
|
40048
40455
|
continue;
|
|
@@ -40059,10 +40466,6 @@ var DeployEngine = class DeployEngine {
|
|
|
40059
40466
|
}
|
|
40060
40467
|
outputsPassCompleted = true;
|
|
40061
40468
|
} finally {
|
|
40062
|
-
if (context.recordedSecretValues) {
|
|
40063
|
-
for (const [value, expr] of context.recordedSecretValues) this.outputSecrets.set(value, expr);
|
|
40064
|
-
mergeResolvedPairs(context.recordedSecretValues, this.outputSecrets);
|
|
40065
|
-
}
|
|
40066
40469
|
if (!outputsPassCompleted) this.outputsSourceUsable = false;
|
|
40067
40470
|
}
|
|
40068
40471
|
for (const [outputKey, output] of Object.entries(template.Outputs)) {
|
|
@@ -40084,5 +40487,5 @@ var DeployEngine = class DeployEngine {
|
|
|
40084
40487
|
};
|
|
40085
40488
|
|
|
40086
40489
|
//#endregion
|
|
40087
|
-
export { collectInlinePolicyNamesManagedBySiblings as $,
|
|
40088
|
-
//# sourceMappingURL=deploy-engine-
|
|
40490
|
+
export { collectInlinePolicyNamesManagedBySiblings as $, buildDockerImage as $n, LocalInvokeBuildError as $r, DagBuilder as $t, isStatefulRecreateTargetForReplace as A, importableOutputs as An, findLargeInlineResources as Ar, coerceCfnBoolean as At, gray as B, rewriteTemplateAssetReferences as Bn, resolveBucketRegion as Br, withSharedDrainBudget as Bt, refusesFinalSnapshot as C, buildLockContentionMessage as Cn, resolveStateBucketWithDefaultAndSource as Cr, isUnboundTemplateParameter as Ct, makeSiblingClaimReader as D, DEFAULT_STATE_PREFIX as Dn, CFN_TEMPLATE_BODY_LIMIT as Dr, normalizeAwsTagsToCfn as Dt, extractDeploymentEventError as E, CUSTOM_RESOURCE_RESPONSE_PREFIX as En, warnDeprecatedNoPrefixCliFlag as Er, WAFv2WebACLProvider as Et, isWarmThroughputDecrease as F, stringifyValue as Fn, canonicalizeRegion as Fr, requireConfigArray as Ft, collectPublishedOutputNames as G, getBootstrapMarkerKey as Gn, AssetError as Gr, s3BucketWebsiteUrl as Gt, red as H, BOOTSTRAP_MARKER_PREFIX as Hn, getAwsClients as Hr, s3BucketDomainName as Ht, toFiniteNumber as I, WorkGraph as In, derivePartitionAndUrlSuffix as Ir, requireConfigObject as It, secretBearing as J, readBootstrapMarkerBody as Jn, CrossAccountSecretRefusalError as Jr, INTRINSIC_KEYS as Jt, exportAliasCollisionScrubWarning as K, isCrossRegionRedirect as Kn, CdkdError as Kr, applyRoleArnIfSet as Kt, formatResourceLine as L, buildAssetRedirectMap as Ln, AssemblyReader as Lr, requireConfigString as Lt, renderStatefulReason as M, orphansCarriedFrom as Mn, CUSTOM_RESOURCE_RESPONSE_OBJECT_DESCRIPTION as Mr, configStringRefusal as Mt, WARM_THROUGHPUT_MEMBERS as N, shouldRetainResource as Nn, expectedOwnerParam as Nr, readConfigString as Nt, planOrphanAdoption as O, exportNamesCarriedFrom as On, CFN_TEMPLATE_URL_LIMIT as Or, resolveExplicitPhysicalId as Ot, coerceWarmThroughput as P, AssetPublisher as Pn, PARTITION_TABLE as Pr, replayWarn as Pt, IAMRoleProvider as Q, describeAwsFailure as Qn, IntrinsicResolutionRefusalError as Qr, withRetry as Qt, bold as R, createAssetRedirectResolver as Rn, processStackMessages as Rr, classifyReplaySecretRegion as Rt, isFinalSnapshotError as S, __exportAll as Si, buildForceUnlockCommand as Sn, resolveStateBucketWithDefault as Sr, getAccountInfo as St, makeCanonicalizePropertiesFn as T, shellQuote as Tn, stateBucketExistenceConfirmed as Tr, refStateLookupFromResource as Tt, yellow as U, assertAssetBucketRegion as Un, resetAwsClients as Ur, s3BucketDualStackDomainName as Ut, green as V, AssetModeResolver as Vn, AwsClients as Vr, s3BucketArn as Vt, collectDeclaredOutputNames as W, ensureAssetStorage as Wn, setAwsClients as Wr, s3BucketRegionalDomainName as Wt, secretSafeKeyDisplay as X, validateContainerRepoName as Xn, DeployCancelledError as Xr, findSilentDropProperties as Xt, secretBearingStateKeyWarning as Y, validateAssetBucketName as Yn, DependencyError as Yr, findActionableSilentDrops as Yt, getCurrentResourceSecrets as Z, buildDenyExternalAccessPolicy as Zn, DynamicReferenceRegionAmbiguousError as Zr, describeTypeWithThrottleRetry as Zt, ATOMIC_FINAL_SNAPSHOT_TYPES as _, isThrottlingError as _i, scrubResourceRecord as _n, getLegacyStateBucketName as _r, isTerminationProtectionPropagationError as _t, DeploymentEventsStore as a, ResourceTimeoutError as ai, carriesSecretMask as an, getDockerCmd as ar, maskerOrIdentity as at, ccRoutedFinalSnapshotError as b, markRedactedCause as bi, rebuildClientForBucketRegion as bn, resolveCaptureObservedState as br, cfnRefValueFromPhysicalId as bt, replayFailedOperations as c, StackTerminationProtectionError as ci, errorCauseChain as cn, runDockerForeground as cr, interruptWatchListenerCount as ct, updatePartialReason as d, formatError as di, markSameGenerationBag as dn, stripControlChars as dr, CloudControlProvider as dt, LocalStartServiceError as ei, TemplateParser as en, describeDockerCapturedOutput as er, clearOnUpdateRemoval as et, withResourceDeadline as f, isCdkdError as fi, maskSecretsInError as fn, AssetManifestLoader as fr, slowCcOperationTimeoutMs as ft, bindingSkippedOutputs as g, isRetryableTransientError as gi, redactSecretsForState as gn, getDefaultStateBucketName as gr, disableInstanceApiTermination as gt, computeImplicitDeleteEdges as h, isMarkedNonRetryable as hi, recoverMaskedOutput as hn, synthesisStatusMessage as hr, deleteSkipReason as ht, DeploymentEventsReader as i, ProvisioningError as ii, TEMPLATE_SOURCED_RULES as in, formatDockerLoginError as ir, maskDeep as it, isStatefulRecreateTargetSync as j, orphansAfterRollback as jn, uploadCfnTemplate as jr, configBooleanRefusal as jt, MULTI_REGION_RECREATE_BLOCKED_TYPES as k, importableOutputKeys as kn, MIGRATE_TMP_PREFIX as kr, assertRegionMatch as kt, replayRollback as l, StateError as li, identityKeyFor as ln, runDockerStreaming as lr, isInterruptedWaitError as lt, IMPLICIT_DELETE_DEPENDENCIES as m, withErrorHandling as mi, recordMaskOnlyValue as mn, Synthesizer as mr, deleteIndeterminateGuards as mt, DEFAULT_RESOURCE_WARN_AFTER_MS as n, NestedStackChildDirectDestroyError as ni, STATE_SOURCED_CROSS_GENERATION_RULES as nn, describeDockerFailure as nr, wouldReturnToSdkProvider as nt, planFailedOps as o, ResourceUpdateNotSupportedError as oi, createSecretMasker as on, partitionSensitiveEnv as or, beginCommandInterruptScope as ot, maskingRetryLogger as p, normalizeAwsError as pi, maskSecretsInText as pn, getDockerImageBySourceHash as pr, UNSPECIFIED_SKIP_REASON as pt, isExportAliasCollision as q, parseBootstrapMarker as qn, ConfigError as qr, DiffCalculator as qt, DeployEngine as r, PartialFailureError as ri, STATE_SOURCED_READBACK_RULES as rn, dockerSpawnEnvWithSensitive as rr, createMaskedRetryLogger as rt, planRollback as s, StackHasActiveImportsError as si, dynamicReferenceTokens as sn, redactDockerArgvValues as sr, endCommandInterruptScope as st, DEFAULT_RESOURCE_TIMEOUT_MS as t, LockError as ti, STATE_SOURCED_BASELINE_RULES as tn, describeDockerExecFailure as tr, ProviderRegistry as tt, updatePartialMessage as u, SynthesisError as ui, isSingleDynamicReferenceToken as un, escapeRegExp$1 as ur, startInterruptWatch as ut, PRE_DELETE_SNAPSHOT_TYPES as v, isTransientServerError as vi, LockManager as vn, resolveApp as vr, IntrinsicFunctionResolver as vt, unsupportedFinalSnapshotError as w, forceQuitRecoveryClause as wn, resolveUseCdkBootstrapAssets as wr, parameterTypeMayLoseSecretIdentity as wt, createPreDeleteFinalSnapshot as x, retryClassificationText as xi, UNRENDERABLE as xn, resolveSkipPrefix as xr, coerceParameterTypedValue as xt, buildFinalSnapshotIdentifier as y, markNonRetryable as yi, S3StateBackend as yn, resolveAutoAssetStorage as yr, carriesDynamicReference as yt, cyan as z, loadPublishableAssetManifest as zn, clearBucketRegionCache as zr, producerRegionsFromState as zt };
|
|
40491
|
+
//# sourceMappingURL=deploy-engine-BEpYvI14.js.map
|