@go-to-k/cdkd 0.284.7 → 0.284.9
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-DOdJgPLS.js → asg-provider-B09ceB1r.js} +2 -2
- package/dist/{asg-provider-DOdJgPLS.js.map → asg-provider-B09ceB1r.js.map} +1 -1
- package/dist/cli.js +170 -50
- package/dist/cli.js.map +1 -1
- package/dist/{deploy-engine-CJncZQEi.js → deploy-engine-CY2fx4K1.js} +413 -11
- package/dist/deploy-engine-CY2fx4K1.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/deploy-engine-CJncZQEi.js.map +0 -1
|
@@ -296,6 +296,182 @@ function isThrottlingError(error) {
|
|
|
296
296
|
return false;
|
|
297
297
|
}
|
|
298
298
|
/**
|
|
299
|
+
* HTTP status codes that indicate a TRANSIENT SERVER-side failure worth
|
|
300
|
+
* retrying (issue #2026).
|
|
301
|
+
*
|
|
302
|
+
* Mirrors `@smithy/service-error-classification`'s own
|
|
303
|
+
* `TRANSIENT_ERROR_STATUS_CODES` (`[500, 502, 503, 504]`), which is what the
|
|
304
|
+
* AWS SDK's default retry strategy treats as transient. Deliberately a
|
|
305
|
+
* SEPARATE set from {@link RETRYABLE_HTTP_STATUS_CODES} rather than an
|
|
306
|
+
* extension of it, because that one is consumed by {@link isThrottlingError},
|
|
307
|
+
* which SEVEN call sites across four files pass as a deliberately NARROW
|
|
308
|
+
* `isRetryable`: `describe-type.ts:67` (which states the intent outright --
|
|
309
|
+
* "retry ONLY throttle-shaped failures"), `dynamodb-globaltable-provider.ts`
|
|
310
|
+
* (x4), `export.ts:1744`, and `intrinsic-function-resolver.ts:5216`. Widening
|
|
311
|
+
* the shared set would have silently converted every one of them from "retry
|
|
312
|
+
* throttles" into "retry throttles and server errors", which none of them
|
|
313
|
+
* asked for.
|
|
314
|
+
*
|
|
315
|
+
* Three FURTHER sites call it as a bare classification rather than as a retry
|
|
316
|
+
* filter -- `drift.ts:518`, `export.ts:1755`, `dynamodb-index-busy-delete.ts:381`
|
|
317
|
+
* -- and they make the case stronger, not weaker: `drift.ts` would have started
|
|
318
|
+
* returning `undefined` (reporting "cannot compare") for a resource whose read
|
|
319
|
+
* merely 500'd, and the index poll would have waited a server error out as
|
|
320
|
+
* though it were a throttle.
|
|
321
|
+
*
|
|
322
|
+
* Measured, not inferred. `tests/integration/iam-propagation-stress` against
|
|
323
|
+
* real AWS (us-east-1, 2026-08-19 08:57:30Z, round 11 of 11) produced:
|
|
324
|
+
*
|
|
325
|
+
* StressQueuePolicyDC3E35C3: gave up after 5 IAM-propagation retries over
|
|
326
|
+
* 5.75s of propagation backoff - Failed to create SQS queue policy
|
|
327
|
+
* StressQueuePolicyDC3E35C3: UnknownError
|
|
328
|
+
* [name=InternalFailure http=500 requestId=ebf581cc-6072-5ffc-943a-e33312488615]
|
|
329
|
+
*
|
|
330
|
+
* SQS answered a `SetQueueAttributes` mid-propagation with HTTP 500
|
|
331
|
+
* `InternalFailure` and an empty message body -- hence the `UnknownError`
|
|
332
|
+
* placeholder, which matches no message pattern. With 500 absent from every
|
|
333
|
+
* status set, `withRetry` classified it non-retryable and threw at 5.75s of a
|
|
334
|
+
* 47.75s budget the sequence needed roughly 10s of.
|
|
335
|
+
*
|
|
336
|
+
* Why 502 and 504 come along rather than only the measured 500: they are the
|
|
337
|
+
* same class (a gateway or timeout between AWS's edge and the service), the
|
|
338
|
+
* SDK groups all four, and adding only the one status seen would leave the
|
|
339
|
+
* identical defect behind for its siblings.
|
|
340
|
+
*
|
|
341
|
+
* Note the SDK has ALREADY retried these before cdkd sees them (default
|
|
342
|
+
* `maxAttempts` is 3), so a 5xx reaching this classifier is one that persisted
|
|
343
|
+
* across the SDK's own attempts. That is an argument FOR retrying it here, not
|
|
344
|
+
* against: the eventual-consistency window this schedule exists to cover is
|
|
345
|
+
* measured in seconds, while the SDK's three attempts span well under one.
|
|
346
|
+
*
|
|
347
|
+
* ACCEPTED RISK, stated rather than discovered later: this makes a
|
|
348
|
+
* NON-IDEMPOTENT create retryable on a 500 that may have succeeded
|
|
349
|
+
* server-side. `EC2Provider.createInstance` issues `RunInstances` with no
|
|
350
|
+
* `ClientToken` (only four providers use one at all), and
|
|
351
|
+
* `IAMAccessKeyProvider` mints an unnamed key, so a replay can leave a
|
|
352
|
+
* resource that is absent from state and therefore from destroy. The class is
|
|
353
|
+
* PRE-EXISTING -- the SDK's own three attempts already reach it, and 503 was
|
|
354
|
+
* already retryable here -- but this widens the window from ~1s to the full
|
|
355
|
+
* schedule. Judged worth it because the alternative is the measured failure
|
|
356
|
+
* (a deploy that dies outright on a transient 500), and because the durable
|
|
357
|
+
* remedy is per-provider idempotency tokens rather than a blanket refusal to
|
|
358
|
+
* retry server errors. Tracked in issue #2039.
|
|
359
|
+
*/
|
|
360
|
+
const TRANSIENT_SERVER_ERROR_STATUS_CODES = /* @__PURE__ */ new Set([
|
|
361
|
+
500,
|
|
362
|
+
502,
|
|
363
|
+
503,
|
|
364
|
+
504
|
|
365
|
+
]);
|
|
366
|
+
/**
|
|
367
|
+
* Walk the error + its `.cause` chain (bounded, same depth 5 as
|
|
368
|
+
* {@link isThrottlingError}) looking for a transient SERVER-side HTTP status
|
|
369
|
+
* ({@link TRANSIENT_SERVER_ERROR_STATUS_CODES}) on `$metadata`.
|
|
370
|
+
*
|
|
371
|
+
* The walk is what makes it work in practice: providers wrap the AWS error in
|
|
372
|
+
* a `ProvisioningError`, so the `$metadata` carrying the status sits one link
|
|
373
|
+
* down, and the wrapper's interpolated message is all a message-based
|
|
374
|
+
* classifier can see. In the measured failure that message was the literal
|
|
375
|
+
* `UnknownError`, so the status was the ONLY usable evidence in the whole
|
|
376
|
+
* error.
|
|
377
|
+
*/
|
|
378
|
+
function isTransientServerError(error) {
|
|
379
|
+
let current = error;
|
|
380
|
+
for (let depth = 0; depth < 5 && current != null; depth++) {
|
|
381
|
+
const status = current.$metadata?.httpStatusCode;
|
|
382
|
+
if (status !== void 0 && TRANSIENT_SERVER_ERROR_STATUS_CODES.has(status)) return true;
|
|
383
|
+
current = current.cause;
|
|
384
|
+
}
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Collect {@link RetryClassificationSignals} from an error and its bounded
|
|
389
|
+
* `.cause` chain — the SAME walk, to the same depth 5, that
|
|
390
|
+
* {@link isThrottlingError} performs, so the line reports what the classifier
|
|
391
|
+
* genuinely saw rather than a second opinion gathered differently.
|
|
392
|
+
*
|
|
393
|
+
* The signals are taken from the first link carrying a `$metadata` object,
|
|
394
|
+
* because that link IS the AWS SDK error by construction: `$metadata` is
|
|
395
|
+
* attached by the SDK's own `deserializeMetadata`, so nothing else can carry
|
|
396
|
+
* it. When no link has one, the fallback is the deepest name found BELOW depth
|
|
397
|
+
* 0 -- which keeps the field useful for the wrapped-network-error case, where
|
|
398
|
+
* the name is all that survives.
|
|
399
|
+
*
|
|
400
|
+
* Excluding depth 0 from that fallback is deliberate and is what stops the
|
|
401
|
+
* suffix from being noise. The error `withRetry` is handed is the provider's
|
|
402
|
+
* own wrapper by construction (every provider catches the AWS error and
|
|
403
|
+
* rethrows a `ProvisioningError`), so its `name` is a cdkd class name and says
|
|
404
|
+
* nothing about the service. Reporting it produced the actively misleading
|
|
405
|
+
* ` [name=ProvisioningError no-$metadata]` on a wrapper carrying no cause at
|
|
406
|
+
* all -- a suffix asserting the SDK never parsed a response, about an error
|
|
407
|
+
* that never came from the SDK.
|
|
408
|
+
*
|
|
409
|
+
* Nothing is lost in the case this helper exists for. A degenerate
|
|
410
|
+
* `UnknownError` message can only be produced by `decorateServiceException`,
|
|
411
|
+
* i.e. by a smithy `ServiceException`, and those always carry `$metadata` --
|
|
412
|
+
* so that case is answered by the FIRST branch and never reaches this
|
|
413
|
+
* fallback.
|
|
414
|
+
*
|
|
415
|
+
* Known narrowness: the first link with a NUMERIC status wins, so an outer
|
|
416
|
+
* link carrying a 400 that wraps a cause carrying a 500 reports the 400 while
|
|
417
|
+
* `isTransientServerError` retried on the 500. Left as-is because cdkd's own
|
|
418
|
+
* wrappers carry no `$metadata` at all, so producing that shape takes two
|
|
419
|
+
* stacked SDK errors -- but it is the same "must not contradict the
|
|
420
|
+
* classifier" case one link further out, and is the thing to revisit if such a
|
|
421
|
+
* chain is ever observed. A cdkd module deliberately importing no other module, this one
|
|
422
|
+
* cannot ask `error instanceof CdkdError` directly: `error-handler.ts` imports
|
|
423
|
+
* `markNonRetryable` from here, so the dependency only runs one way.
|
|
424
|
+
*/
|
|
425
|
+
function describeRetryClassificationSignals(error) {
|
|
426
|
+
let current = error;
|
|
427
|
+
let deepestName;
|
|
428
|
+
let sawMetadata = false;
|
|
429
|
+
let metadataName;
|
|
430
|
+
let metadataRequestId;
|
|
431
|
+
for (let depth = 0; depth < 5 && current != null; depth++) {
|
|
432
|
+
const name = current.name;
|
|
433
|
+
if (depth > 0 && typeof name === "string" && name !== "") deepestName = name;
|
|
434
|
+
const metadata = current.$metadata;
|
|
435
|
+
if (metadata != null && typeof metadata === "object" && !Array.isArray(metadata)) {
|
|
436
|
+
const { httpStatusCode, requestId } = metadata;
|
|
437
|
+
const linkName = typeof name === "string" && name !== "" ? name : void 0;
|
|
438
|
+
if (!sawMetadata) {
|
|
439
|
+
sawMetadata = true;
|
|
440
|
+
metadataName = linkName;
|
|
441
|
+
metadataRequestId = typeof requestId === "string" && requestId !== "" ? requestId : void 0;
|
|
442
|
+
}
|
|
443
|
+
if (typeof httpStatusCode === "number") return {
|
|
444
|
+
name: linkName ?? deepestName,
|
|
445
|
+
httpStatusCode,
|
|
446
|
+
requestId: (typeof requestId === "string" && requestId !== "" ? requestId : void 0) ?? metadataRequestId,
|
|
447
|
+
noMetadata: false
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
current = current.cause;
|
|
451
|
+
}
|
|
452
|
+
return {
|
|
453
|
+
name: metadataName ?? deepestName,
|
|
454
|
+
requestId: metadataRequestId,
|
|
455
|
+
noMetadata: !sawMetadata
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Render {@link describeRetryClassificationSignals} as a compact log suffix.
|
|
460
|
+
*
|
|
461
|
+
* Returns `''` when there is nothing to say (no name and no metadata), so a
|
|
462
|
+
* caller can append unconditionally without emitting an empty bracket pair.
|
|
463
|
+
*/
|
|
464
|
+
function formatRetryClassificationSignals(error) {
|
|
465
|
+
const signals = describeRetryClassificationSignals(error);
|
|
466
|
+
if (signals.name === void 0 && signals.httpStatusCode === void 0 && signals.requestId === void 0) return "";
|
|
467
|
+
const parts = [];
|
|
468
|
+
if (signals.name !== void 0) parts.push(`name=${signals.name}`);
|
|
469
|
+
if (signals.httpStatusCode !== void 0) parts.push(`http=${signals.httpStatusCode}`);
|
|
470
|
+
if (signals.requestId !== void 0) parts.push(`requestId=${signals.requestId}`);
|
|
471
|
+
if (signals.noMetadata) parts.push("no-$metadata");
|
|
472
|
+
return ` [${parts.join(" ")}]`;
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
299
475
|
* Determine whether an AWS error should be retried.
|
|
300
476
|
*
|
|
301
477
|
* Checks (in order):
|
|
@@ -307,11 +483,16 @@ function isThrottlingError(error) {
|
|
|
307
483
|
* `name` or retryable HTTP status (most AWS throttles are HTTP 400, not
|
|
308
484
|
* 429, so the name check carries most of the weight). See
|
|
309
485
|
* {@link isThrottlingError}.
|
|
310
|
-
* 2.
|
|
486
|
+
* 2. Transient SERVER-side HTTP status (500 / 502 / 503 / 504) on the error
|
|
487
|
+
* or any wrapped cause — see {@link isTransientServerError}. Ahead of the
|
|
488
|
+
* message patterns because it is the only check that still works when the
|
|
489
|
+
* response carried NO message (issue #2026).
|
|
490
|
+
* 3. Substring match against {@link RETRYABLE_ERROR_MESSAGE_PATTERNS}
|
|
311
491
|
*/
|
|
312
492
|
function isRetryableTransientError(error, message) {
|
|
313
493
|
if (isMarkedNonRetryable(error)) return false;
|
|
314
494
|
if (isThrottlingError(error)) return true;
|
|
495
|
+
if (isTransientServerError(error)) return true;
|
|
315
496
|
return RETRYABLE_ERROR_MESSAGE_PATTERNS.some((p) => message.includes(p));
|
|
316
497
|
}
|
|
317
498
|
/**
|
|
@@ -8748,6 +8929,7 @@ async function withRetry(operation, logicalId, opts = {}) {
|
|
|
8748
8929
|
let sawPropagation = false;
|
|
8749
8930
|
let propagationRetries = 0;
|
|
8750
8931
|
let propagationSleptMs = 0;
|
|
8932
|
+
let serverErrorRetries = 0;
|
|
8751
8933
|
for (let attempt = 0; attempt <= attemptCeiling; attempt++) try {
|
|
8752
8934
|
return await operation();
|
|
8753
8935
|
} catch (error) {
|
|
@@ -8759,11 +8941,14 @@ async function withRetry(operation, logicalId, opts = {}) {
|
|
|
8759
8941
|
if (propagation) sawPropagation = true;
|
|
8760
8942
|
const attemptLimit = sawPropagation ? 26 : maxRetries;
|
|
8761
8943
|
if (!retryable || attempt >= attemptLimit) {
|
|
8762
|
-
if (propagationRetries > 0) {
|
|
8944
|
+
if (propagationRetries > 0 || serverErrorRetries > 0) {
|
|
8763
8945
|
const budgetExhausted = sawPropagation && attempt >= attemptLimit;
|
|
8764
|
-
const
|
|
8946
|
+
const spent = [];
|
|
8947
|
+
if (propagationRetries > 0) spent.push(`${propagationRetries} IAM-propagation ${propagationRetries === 1 ? "retry" : "retries"} over ${(propagationSleptMs / 1e3).toFixed(2)}s of propagation backoff${budgetExhausted ? " (the full propagation budget)" : ""}`);
|
|
8948
|
+
if (serverErrorRetries > 0) spent.push(`${serverErrorRetries} transient server-error ${serverErrorRetries === 1 ? "retry" : "retries"} (HTTP 5xx)`);
|
|
8949
|
+
const summary = () => `${logicalId}: gave up after ${spent.join(" and ")} - ${message}` + formatRetryClassificationSignals(error);
|
|
8765
8950
|
try {
|
|
8766
|
-
opts.logger?.warn?.(summary);
|
|
8951
|
+
opts.logger?.warn?.(summary());
|
|
8767
8952
|
} catch {}
|
|
8768
8953
|
}
|
|
8769
8954
|
throw error;
|
|
@@ -8778,7 +8963,7 @@ async function withRetry(operation, logicalId, opts = {}) {
|
|
|
8778
8963
|
if (propagation) {
|
|
8779
8964
|
propagationRetries++;
|
|
8780
8965
|
propagationSleptMs = backoffThroughThisAttemptMs;
|
|
8781
|
-
}
|
|
8966
|
+
} else if (opts.isRetryable === void 0 && isTransientServerError(error)) serverErrorRetries++;
|
|
8782
8967
|
}
|
|
8783
8968
|
throw lastError;
|
|
8784
8969
|
}
|
|
@@ -10604,13 +10789,227 @@ function redactByPath(bag, source, secrets, rules, secretExpressions) {
|
|
|
10604
10789
|
if (rules.descendArrays && bag.length === source.length) return bag.map((item, i) => redactByPath(item, source[i], secrets, rules, secretExpressions));
|
|
10605
10790
|
}
|
|
10606
10791
|
if (isPlainObject$2(bag) && isPlainObject$2(source)) {
|
|
10607
|
-
const out =
|
|
10792
|
+
const out = Object.create(null);
|
|
10608
10793
|
for (const [k, v] of Object.entries(bag)) out[k] = Object.hasOwn(source, k) ? redactByPath(v, source[k], secrets, rules, secretExpressions) : redactSecretsForState(v, secrets);
|
|
10609
10794
|
return out;
|
|
10610
10795
|
}
|
|
10611
10796
|
return redactSecretsForState(bag, secrets);
|
|
10612
10797
|
}
|
|
10613
10798
|
/**
|
|
10799
|
+
* Is this rules constant one whose BAG is an AWS readback and whose SOURCE is a
|
|
10800
|
+
* persisted STATE bag?
|
|
10801
|
+
*
|
|
10802
|
+
* Today that is {@link STATE_SOURCED_READBACK_RULES} alone: the path where the
|
|
10803
|
+
* secrets map can be EMPTY by construction (nothing was resolved), so the value
|
|
10804
|
+
* scan has no needles and POSITION is the only mechanism left. Derived from the
|
|
10805
|
+
* flags rather than compared against the constant so a future one with the same
|
|
10806
|
+
* shape is covered automatically. `trustAnyExpression` says the source is a
|
|
10807
|
+
* persisted record (holding no PUBLIC reference, so any `{{resolve:...}}` in it
|
|
10808
|
+
* is by construction a secret); `!descendArrays` says the bag came back from
|
|
10809
|
+
* AWS and may be reordered.
|
|
10810
|
+
*
|
|
10811
|
+
* `sourceIsSameGeneration` is the third conjunct and it is the one that took a
|
|
10812
|
+
* measurement to get right. Without it this also selected
|
|
10813
|
+
* {@link STATE_SOURCED_CROSS_GENERATION_RULES} — `cdkd scrub`'s observed walk,
|
|
10814
|
+
* whose `properties` have ALREADY been repositioned onto TODAY's template — and
|
|
10815
|
+
* taking a source subtree there rewrote a baseline holding the DEPLOYED
|
|
10816
|
+
* `:AWSPREVIOUS` reference onto the template's edited `:AWSCURRENT` one. That
|
|
10817
|
+
* is precisely the issue #1917 hazard, and `cdkd drift --revert` pushes the
|
|
10818
|
+
* baseline to AWS, so it would have applied a reference the stack never
|
|
10819
|
+
* deployed. A refusal may only take a source that is the same generation as the
|
|
10820
|
+
* bag beside it.
|
|
10821
|
+
*
|
|
10822
|
+
* A TEMPLATE-sourced caller is deliberately excluded: its source can carry a
|
|
10823
|
+
* public `ssm:` reference whose resolved value must STAY resolved (issue
|
|
10824
|
+
* #1901), and it always has a populated map, so the value scan already covers
|
|
10825
|
+
* the shapes below. So is the rollback replay
|
|
10826
|
+
* ({@link STATE_DERIVED_RULES}) — full map, and its bag descends positionally
|
|
10827
|
+
* because it was produced by resolving the source.
|
|
10828
|
+
*/
|
|
10829
|
+
function isReadbackProjectedFromState(rules) {
|
|
10830
|
+
return rules.trustAnyExpression && !rules.descendArrays && rules.sourceIsSameGeneration;
|
|
10831
|
+
}
|
|
10832
|
+
/**
|
|
10833
|
+
* Does this subtree carry a dynamic reference anywhere?
|
|
10834
|
+
*
|
|
10835
|
+
* A BOOLEAN, not the occurrence COUNTS an earlier revision collected. The
|
|
10836
|
+
* counts existed to decide whether a bag "covered" every reference its source
|
|
10837
|
+
* carried, which was the vouching rule for taking a source array wholesale —
|
|
10838
|
+
* and that rule is gone (see the array arm), so counting would be a
|
|
10839
|
+
* measurement nothing reads.
|
|
10840
|
+
*/
|
|
10841
|
+
function subtreeHasDynamicReference(value) {
|
|
10842
|
+
if (isDynamicReferenceString(value)) return true;
|
|
10843
|
+
if (Array.isArray(value)) return value.some(subtreeHasDynamicReference);
|
|
10844
|
+
if (isPlainObject$2(value)) return Object.values(value).some(subtreeHasDynamicReference);
|
|
10845
|
+
return false;
|
|
10846
|
+
}
|
|
10847
|
+
/** Every complete `{{resolve:...}}` token inside a string. */
|
|
10848
|
+
function dynamicReferenceTokens(value) {
|
|
10849
|
+
return value.match(/\{\{resolve:[^{}]*\}\}/g) ?? [];
|
|
10850
|
+
}
|
|
10851
|
+
/**
|
|
10852
|
+
* Does this MIXED leaf embed a reference that may be PUBLIC config?
|
|
10853
|
+
*
|
|
10854
|
+
* A plain `{{resolve:ssm:...}}` is classified by the parameter's TYPE, not by
|
|
10855
|
+
* its spelling (issue #1901): a `String` / `StringList` parameter is public and
|
|
10856
|
+
* is legitimately persisted RESOLVED. Substituting the expression over it gives
|
|
10857
|
+
* the drift baseline a value AWS does not hold, which is phantom drift on
|
|
10858
|
+
* ordinary config — and `--revert` then pushes the literal expression.
|
|
10859
|
+
*
|
|
10860
|
+
* `trustAnyExpression` is what would otherwise wave this through, and its
|
|
10861
|
+
* premise ("a persisted STATE bag holds no public expression") is documented as
|
|
10862
|
+
* FALSE in one place: `cdkd import`'s warn path can leave one there. The
|
|
10863
|
+
* whole-token arm accepts that risk knowingly and `cdkd drift --accept`
|
|
10864
|
+
* re-checks its write; the MIXED arm added later has no such re-check, so it
|
|
10865
|
+
* declines instead.
|
|
10866
|
+
*
|
|
10867
|
+
* A reference the resolver RECORDED as secret is kept: that is the ssm
|
|
10868
|
+
* `SecureString` case, where the verdict came off the same `GetParameter`
|
|
10869
|
+
* response that carried the value. `{{resolve:ssm-secure:` does not match this
|
|
10870
|
+
* prefix at all (the next character is `-`), so it is never refused here.
|
|
10871
|
+
*
|
|
10872
|
+
* The verdict store only carries signal where something RESOLVED, so this
|
|
10873
|
+
* splits on whether a secrets map exists at all.
|
|
10874
|
+
*
|
|
10875
|
+
* WITH a map, a pass resolved this bag: the engine's change detection walks
|
|
10876
|
+
* every template property with `skipDynamicReferences`, which only flips
|
|
10877
|
+
* `decrypt` on the ssm branch — the `GetParameter` still runs, a definitive
|
|
10878
|
+
* `SecureString` is recorded, and a parameter that comes back public has its
|
|
10879
|
+
* memo RETRACTED. Absence from the store is then real evidence of a public
|
|
10880
|
+
* parameter, and the resolved value is kept.
|
|
10881
|
+
*
|
|
10882
|
+
* WITHOUT one, absence means only that the question was never asked HERE. It
|
|
10883
|
+
* does not mean nothing was resolved: the deploy path resolves every template
|
|
10884
|
+
* property with `skipDynamicReferences`, which records or retracts the
|
|
10885
|
+
* `SecureString` verdict even for an UNCHANGED resource -- that bag simply is
|
|
10886
|
+
* not the one this call receives. The leaf is treated as
|
|
10887
|
+
* secret-bearing and refused. That is not merely the cautious branch, it is the
|
|
10888
|
+
* SAME premise the whole-token arm one level up already acts on: a PUBLIC
|
|
10889
|
+
* `String` / `StringList` reference is persisted RESOLVED (issue #1901), so a
|
|
10890
|
+
* `{{resolve:ssm:` token that SURVIVES in a persisted state bag is a
|
|
10891
|
+
* SecureString by construction. An earlier revision applied a stricter rule to
|
|
10892
|
+
* a MIXED leaf than to a whole token on the identical source, and that
|
|
10893
|
+
* inconsistency is what persisted a decrypted secret.
|
|
10894
|
+
*
|
|
10895
|
+
* ACCEPTED CONSEQUENCE, recorded rather than papered over: on the empty-map
|
|
10896
|
+
* paths a genuinely PUBLIC ssm mixed leaf is now OVER-redacted, so the baseline
|
|
10897
|
+
* no longer matches AWS and `cdkd drift` reports a phantom on it. That is
|
|
10898
|
+
* reachable only through the one documented hole in the premise above — `cdkd
|
|
10899
|
+
* import`'s warn path, which can leave a public expression in state. The trade
|
|
10900
|
+
* is deliberate and asymmetric: under-redaction persists a decrypted secret,
|
|
10901
|
+
* which is a disclosure and is what this lane exists to prevent, while
|
|
10902
|
+
* over-redaction is visible, recoverable and discloses nothing. Closing it
|
|
10903
|
+
* properly needs a real TYPE classification on these paths, which is issue
|
|
10904
|
+
* [#2012](https://github.com/go-to-k/cdkd/issues/2012)'s mechanism; the
|
|
10905
|
+
* over-redaction itself is tracked as issue
|
|
10906
|
+
* [#2036](https://github.com/go-to-k/cdkd/issues/2036).
|
|
10907
|
+
*
|
|
10908
|
+
* `tests/integration/secrets-dynamic-ref` is the end-to-end proof on BOTH
|
|
10909
|
+
* paths, and it is the only place the empty-map defect surfaced: Phase 1g
|
|
10910
|
+
* covers the populated-map deploy and Phase 1f the empty-map command.
|
|
10911
|
+
*/
|
|
10912
|
+
function mixedLeafMayCarryPublicReference(source, secrets) {
|
|
10913
|
+
if (secrets.size === 0) return false;
|
|
10914
|
+
return dynamicReferenceTokens(source).some((token) => token.startsWith("{{resolve:ssm:") && !isRecordedSecretExpression(token));
|
|
10915
|
+
}
|
|
10916
|
+
/**
|
|
10917
|
+
* Refuse to persist a readback leaf the path pass could not CERTIFY, at any
|
|
10918
|
+
* position the STATE source proves is secret-bearing (issue #1926 review).
|
|
10919
|
+
*
|
|
10920
|
+
* {@link redactByPath} substitutes only where the source leaf is a WHOLE
|
|
10921
|
+
* `{{resolve:...}}` token. On the paths {@link isReadbackProjectedFromState}
|
|
10922
|
+
* selects the secrets map may be EMPTY, so its value-scan fallback is a no-op,
|
|
10923
|
+
* and four shapes reached `state.json` holding the DECRYPTED value. Measured
|
|
10924
|
+
* against this module before this pass existed — three by `cdkd state
|
|
10925
|
+
* refresh-observed`, and the same three by a plain `cdkd deploy`, whose
|
|
10926
|
+
* `drainObservedCaptures` baseline reaches the persist choke point with exactly
|
|
10927
|
+
* this configuration. `cdkd scrub` is NOT one of them: its observed walk is
|
|
10928
|
+
* CROSS-generation, so {@link isReadbackProjectedFromState} excludes it by
|
|
10929
|
+
* design:
|
|
10930
|
+
*
|
|
10931
|
+
* ```text
|
|
10932
|
+
* source leaf in the STATE record before now
|
|
10933
|
+
* ----------------------------------------------- ------------ ---------------
|
|
10934
|
+
* `postgres://u:{{resolve:...}}@h` (MIXED string) LEAK take source
|
|
10935
|
+
* ...the same MIXED leaf inside a PAIRED element LEAK take source
|
|
10936
|
+
* `['--pw', '{{resolve:...}}']` (no identity key) LEAK LEAK (#2012)
|
|
10937
|
+
* `[{Field, Val: '{{resolve:...}}'}]` (no `Name`) LEAK LEAK (#2012)
|
|
10938
|
+
* an UNPAIRED element beside a paired one LEAK LEAK (#2012)
|
|
10939
|
+
* an observed KEY the source does not carry LEAK LEAK (#2012)
|
|
10940
|
+
* whole `{{resolve:...}}` token ok ok
|
|
10941
|
+
* `Environment[]` keyed by `Name` (issue #1915) ok ok
|
|
10942
|
+
* PUBLIC ssm MIXED leaf, POPULATED map ok ok
|
|
10943
|
+
* PUBLIC ssm MIXED leaf, EMPTY map ok over-redacts
|
|
10944
|
+
* ```
|
|
10945
|
+
*
|
|
10946
|
+
* The last row is the price of the row above it and is tracked as issue
|
|
10947
|
+
* [#2036](https://github.com/go-to-k/cdkd/issues/2036): with no map nothing was
|
|
10948
|
+
* resolved, so nothing distinguishes a public parameter from a `SecureString`
|
|
10949
|
+
* and the leaf is refused. Phantom drift, not a disclosure — see
|
|
10950
|
+
* {@link mixedLeafMayCarryPublicReference} for why that is the right way to be
|
|
10951
|
+
* wrong here.
|
|
10952
|
+
*
|
|
10953
|
+
* What this pass closes is the row POSITION can actually justify: a leaf whose
|
|
10954
|
+
* KEY the source carries, where the source is the same generation and the only
|
|
10955
|
+
* thing the older code lacked was the willingness to substitute a leaf that was
|
|
10956
|
+
* not a WHOLE token. Everything it takes is the record's own value at the
|
|
10957
|
+
* record's own path.
|
|
10958
|
+
*
|
|
10959
|
+
* The four residual rows are one root cause, not four: no needle and no
|
|
10960
|
+
* position, so nothing distinguishes a resolved secret from an ordinary
|
|
10961
|
+
* literal. They are NOT closed by taking the source subtree, which an earlier
|
|
10962
|
+
* revision did and the issue #1915 fences correctly rejected — measured, it
|
|
10963
|
+
* rewrote `{Name:'', Value:'an-unrelated-literal'}` onto the expression and
|
|
10964
|
+
* turned an AWS-reported `[{Value:'x'}]` into `[{Name:'db', Value:<expr>}]`,
|
|
10965
|
+
* fabricating drift-baseline content AWS never reported that `cdkd drift
|
|
10966
|
+
* --revert` then pushes to the live resource. Redaction may not buy itself a
|
|
10967
|
+
* fabricated baseline.
|
|
10968
|
+
*
|
|
10969
|
+
* The MIXED row is the shape this module itself calls DOMINANT for CDK — an
|
|
10970
|
+
* `Fn::Join` around `secret.secretValueFromJson(...)`.
|
|
10971
|
+
*
|
|
10972
|
+
* TAKE SOURCE rather than a {@link SECRET_MASK} on the rows it does close, for
|
|
10973
|
+
* the same reason the whole-token arm does: a mask is not a value `cdkd drift`
|
|
10974
|
+
* can re-resolve, so it would report a permanent phantom — and `cdkd drift
|
|
10975
|
+
* --revert` pushes the BASELINE to AWS, so a masked baseline would write the
|
|
10976
|
+
* literal `***` onto the live resource (the issue #1498 / #1501 class).
|
|
10977
|
+
*
|
|
10978
|
+
* KNOWN RESIDUAL, the last row: an observed KEY the source object does not
|
|
10979
|
+
* carry has no source leaf to take and no needle to match. It is NOT refused
|
|
10980
|
+
* the way an unpaired array ELEMENT is, and the asymmetry is deliberate rather
|
|
10981
|
+
* than an oversight — an extra array element is a PEER of the secret-bearing
|
|
10982
|
+
* ones (another `Environment` entry), so suspicion is warranted and extras are
|
|
10983
|
+
* rare, while an extra object KEY is a different FIELD entirely (`Runtime`,
|
|
10984
|
+
* `FunctionArn`, `LastModified`) and is the NORM in an AWS readback. Refusing
|
|
10985
|
+
* those would empty the drift baseline of every secret-bearing resource.
|
|
10986
|
+
* Tracked as issue [#2012](https://github.com/go-to-k/cdkd/issues/2012).
|
|
10987
|
+
*/
|
|
10988
|
+
function refuseUncertifiedReadbackPositions(bag, source, secrets) {
|
|
10989
|
+
if (isDynamicReferenceString(source) && typeof bag === "string") {
|
|
10990
|
+
if (isSingleDynamicReferenceToken(source)) return source;
|
|
10991
|
+
if (mixedLeafMayCarryPublicReference(source, secrets)) return bag;
|
|
10992
|
+
return source;
|
|
10993
|
+
}
|
|
10994
|
+
if (!subtreeHasDynamicReference(source)) return bag;
|
|
10995
|
+
if (isPlainObject$2(bag) && isPlainObject$2(source)) {
|
|
10996
|
+
const out = Object.create(null);
|
|
10997
|
+
for (const [k, v] of Object.entries(bag)) out[k] = Object.hasOwn(source, k) ? refuseUncertifiedReadbackPositions(v, source[k], secrets) : v;
|
|
10998
|
+
return out;
|
|
10999
|
+
}
|
|
11000
|
+
if (Array.isArray(bag) && Array.isArray(source)) {
|
|
11001
|
+
const key = identityKeyFor(bag, source);
|
|
11002
|
+
if (key === void 0) return bag;
|
|
11003
|
+
const sourceByIdentity = /* @__PURE__ */ new Map();
|
|
11004
|
+
for (const item of source) sourceByIdentity.set(item[key], item);
|
|
11005
|
+
return bag.map((item) => {
|
|
11006
|
+
const partner = sourceByIdentity.get(item[key]);
|
|
11007
|
+
return partner === void 0 ? item : refuseUncertifiedReadbackPositions(item, partner, secrets);
|
|
11008
|
+
});
|
|
11009
|
+
}
|
|
11010
|
+
return bag;
|
|
11011
|
+
}
|
|
11012
|
+
/**
|
|
10614
11013
|
* Deep-clone `bag`, replacing every occurrence of a recorded secret value with
|
|
10615
11014
|
* the unresolved `{{resolve:...}}` expression it came from. A string whose WHOLE
|
|
10616
11015
|
* value equals a secret is replaced by that secret's expression exactly; a
|
|
@@ -10621,7 +11020,10 @@ function redactByPath(bag, source, secrets, rules, secretExpressions) {
|
|
|
10621
11020
|
*/
|
|
10622
11021
|
function redactSecretsForState(bag, secrets, source, rules = TEMPLATE_DERIVED_RULES) {
|
|
10623
11022
|
if (secrets.size === 0 && source === void 0) return bag;
|
|
10624
|
-
if (source !== void 0)
|
|
11023
|
+
if (source !== void 0) {
|
|
11024
|
+
const positioned = redactByPath(bag, source, secrets, rules, new Set(secrets.values()));
|
|
11025
|
+
return isReadbackProjectedFromState(rules) ? refuseUncertifiedReadbackPositions(positioned, source, secrets) : positioned;
|
|
11026
|
+
}
|
|
10625
11027
|
const regex = buildNeedleRegex(secrets.keys());
|
|
10626
11028
|
const wholeValueExpr = (s) => s === "" ? void 0 : secrets.get(s);
|
|
10627
11029
|
const walk = (value) => {
|
|
@@ -10637,7 +11039,7 @@ function redactSecretsForState(bag, secrets, source, rules = TEMPLATE_DERIVED_RU
|
|
|
10637
11039
|
}
|
|
10638
11040
|
if (Array.isArray(value)) return value.map(walk);
|
|
10639
11041
|
if (value !== null && typeof value === "object") {
|
|
10640
|
-
const out =
|
|
11042
|
+
const out = Object.create(null);
|
|
10641
11043
|
for (const [k, v] of Object.entries(value)) out[k] = walk(v);
|
|
10642
11044
|
return out;
|
|
10643
11045
|
}
|
|
@@ -15643,7 +16045,7 @@ var CloudControlProvider = class {
|
|
|
15643
16045
|
if (context?.finalSnapshotIdentifier !== void 0) throw new ProvisioningError(`${logicalId} (${resourceType}) requires a final snapshot (DeletionPolicy: Snapshot), but the Cloud Control API delete route has no final-snapshot parameter. Re-run with --skip-final-snapshot after snapshotting manually, or retain the resource.`, resourceType, logicalId, physicalId);
|
|
15644
16046
|
if (context?.removeProtection === true && resourceType === "AWS::AutoScaling::AutoScalingGroup") {
|
|
15645
16047
|
this.logger.debug(`Delegating protected AutoScalingGroup ${logicalId} delete to the SDK ASGProvider (Cloud Control cannot force-delete a protected ASG)`);
|
|
15646
|
-
const { ASGProvider } = await import("./asg-provider-
|
|
16048
|
+
const { ASGProvider } = await import("./asg-provider-B09ceB1r.js").then((n) => n.n);
|
|
15647
16049
|
return await new ASGProvider().delete(logicalId, physicalId, resourceType, _properties, context);
|
|
15648
16050
|
}
|
|
15649
16051
|
const isProtectedEc2Instance = context?.removeProtection === true && resourceType === "AWS::EC2::Instance";
|
|
@@ -23221,7 +23623,7 @@ const FLUSH_INTERVAL_MS = 2e3;
|
|
|
23221
23623
|
const FLUSH_EVENT_THRESHOLD = 50;
|
|
23222
23624
|
/** Build-time cdkd version, with a dev fallback for non-built contexts. */
|
|
23223
23625
|
function getCdkdVersion() {
|
|
23224
|
-
return "0.284.
|
|
23626
|
+
return "0.284.9";
|
|
23225
23627
|
}
|
|
23226
23628
|
/**
|
|
23227
23629
|
* Generate a time-sortable unique run id, e.g.
|
|
@@ -25690,4 +26092,4 @@ var DeployEngine = class {
|
|
|
25690
26092
|
|
|
25691
26093
|
//#endregion
|
|
25692
26094
|
export { IntrinsicFunctionResolver as $, StackHasActiveImportsError as $n, validateContainerRepoName as $t, formatResourceLine as A, AssemblyReader as An, describeTypeWithThrottleRetry as At, isExportAliasCollision as B, ConfigError as Bn, WorkGraph as Bt, refusesFinalSnapshot as C, MIGRATE_TMP_PREFIX as Cn, s3BucketDomainName as Ct, MULTI_REGION_RECREATE_BLOCKED_TYPES as D, PARTITION_TABLE as Dn, applyRoleArnIfSet as Dt, extractDeploymentEventError as E, expectedOwnerParam as En, s3BucketWebsiteUrl as Et, red as F, getAwsClients as Fn, S3StateBackend as Ft, clearOnUpdateRemoval as G, LocalStartServiceError as Gn, escapeRegExp$1 as Gt, stateKeySecretExposure as H, DeployCancelledError as Hn, createAssetRedirectResolver as Ht, yellow as I, resetAwsClients as In, rebuildClientForBucketRegion as It, findSilentDropProperties as J, NestedStackChildDirectDestroyError as Jn, BOOTSTRAP_MARKER_PREFIX as Jt, ProviderRegistry as K, LockError as Kn, stripControlChars as Kt, collectDeclaredOutputNames as L, setAwsClients as Ln, shouldRetainResource as Lt, cyan as M, clearBucketRegionCache as Mn, DagBuilder as Mt, gray as N, resolveBucketRegion as Nn, TemplateParser as Nt, isStatefulRecreateTargetSync as O, canonicalizeRegion as On, DiffCalculator as Ot, green as P, AwsClients as Pn, LockManager as Pt, isTerminationProtectionPropagationError as Q, ResourceUpdateNotSupportedError as Qn, validateAssetBucketName as Qt, collectPublishedOutputNames as R, AssetError as Rn, AssetPublisher as Rt, isFinalSnapshotError as S, CFN_TEMPLATE_URL_LIMIT as Sn, s3BucketArn as St, makeCanonicalizePropertiesFn as T, uploadCfnTemplate as Tn, s3BucketRegionalDomainName as Tt, IAMRoleProvider as U, LocalInvokeBuildError as Un, loadPublishableAssetManifest as Ut, secretBearingStateKeyWarning as V, DependencyError as Vn, buildAssetRedirectMap as Vt, collectInlinePolicyNamesManagedBySiblings as W, LocalMigrateError as Wn, rewriteTemplateAssetReferences as Wt, slowCcOperationTimeoutMs as X, ProvisioningError as Xn, getBootstrapMarkerKey as Xt, CloudControlProvider as Y, PartialFailureError as Yn, ensureAssetStorage as Yt, disableInstanceApiTermination as Z, ResourceTimeoutError as Zn, parseBootstrapMarker as Zt, ATOMIC_FINAL_SNAPSHOT_TYPES as _, resolveStateBucketWithDefaultAndSource as _n, TEMPLATE_SOURCED_RULES as _t, DeploymentEventsStore as a, runDockerStreaming as an, normalizeAwsError as ar, resolveExplicitPhysicalId as at, ccRoutedFinalSnapshotError as b, warnDeprecatedNoPrefixCliFlag as bn, redactSecretsForState as bt, replayFailedOperations as c, Synthesizer as cn, isRetryableTransientError as cr, configBooleanRefusal as ct, updatePartialReason as d, getLegacyStateBucketName as dn, __exportAll as dr, replayWarn as dt, buildDenyExternalAccessPolicy as en, StackTerminationProtectionError as er, cfnRefValueFromPhysicalId as et, UNSPECIFIED_SKIP_REASON as f, resolveApp as fn, requireConfigArray as ft, computeImplicitDeleteEdges as g, resolveStateBucketWithDefault as gn, STATE_SOURCED_READBACK_RULES as gt, IMPLICIT_DELETE_DEPENDENCIES as h, resolveSkipPrefix as hn, STATE_SOURCED_CROSS_GENERATION_RULES as ht, DeploymentEventsReader as i, runDockerForeground as in, isCdkdError as ir, normalizeAwsTagsToCfn as it, bold as j, processStackMessages as jn, withRetry as jt, renderStatefulReason as k, derivePartitionAndUrlSuffix as kn, INTRINSIC_KEYS as kt, replayRollback as l, synthesisStatusMessage as ln, isThrottlingError as lr, configStringRefusal as lt, withResourceDeadline as m, resolveCaptureObservedState as mn, requireConfigString as mt, DEFAULT_RESOURCE_WARN_AFTER_MS as n, formatDockerLoginError as nn, SynthesisError as nr, refStateLookupFromResource as nt, planFailedOps as o, AssetManifestLoader as on, withErrorHandling as or, assertRegionMatch as ot, deleteSkipReason as p, resolveAutoAssetStorage as pn, requireConfigObject as pt, findActionableSilentDrops as q, MissingCdkCliError as qn, AssetModeResolver as qt, DeployEngine as r, getDockerCmd as rn, formatError as rr, WAFv2WebACLProvider as rt, planRollback as s, getDockerImageBySourceHash as sn, isMarkedNonRetryable as sr, coerceCfnBoolean as st, DEFAULT_RESOURCE_TIMEOUT_MS as t, buildDockerImage as tn, StateError as tr, getAccountInfo as tt, updatePartialMessage as u, getDefaultStateBucketName as un, markNonRetryable as ur, readConfigString as ut, PRE_DELETE_SNAPSHOT_TYPES as v, resolveUseCdkBootstrapAssets as vn, createSecretMasker as vt, unsupportedFinalSnapshotError as w, findLargeInlineResources as wn, s3BucketDualStackDomainName as wt, createPreDeleteFinalSnapshot as x, CFN_TEMPLATE_BODY_LIMIT as xn, scrubResourceRecord as xt, buildFinalSnapshotIdentifier as y, stateBucketExistenceConfirmed as yn, maskSecretsInText as yt, exportAliasCollisionScrubWarning as z, CdkdError as zn, stringifyValue as zt };
|
|
25693
|
-
//# sourceMappingURL=deploy-engine-
|
|
26095
|
+
//# sourceMappingURL=deploy-engine-CY2fx4K1.js.map
|