fix: omit AWS's empty-object placeholders from the cloned PDF policy
Build and deploy / build-and-deploy (push) Failing after 4s
Build and deploy / build-and-deploy (push) Failing after 4s
`configure.mjs --apply` failed on its first write, 2026-09-04, and nothing
reached the distribution. `get-response-headers-policy` returns
`"ContentSecurityPolicy": {}` for a member the source does not define, and
sending that back fails `create-response-headers-policy` on ParamValidation
before the call leaves the machine — a config AWS hands back is not
necessarily a config AWS will accept.
Of the 16 structures reachable from `ResponseHeadersPolicyConfig` in the CLI's
service model, 15 declare a required field, so `{}` is illegal there and can
only be the placeholder; the one exception is `SecurityHeadersConfig` itself,
which section 4 already skips on when empty. The strip is therefore recursive.
The dry run now asserts the generated config carries no empty object, and does
so as a section-4 SKIP rather than a throw — section 4 must never block
sections 1-3 from re-applying `router.js`.
The two functions move to `policy-shapes.mjs` with a 23-case test (7 of 7
mutations killed), because `configure.mjs` reads argv and calls AWS at import
time and the runbook was otherwise claiming a proof nobody could re-run.
Also: the handler was redeployed 2026-09-04 via docs/09 §5.5. Re-read against
production — the two bundled SDK clients moved 3.1125.0 -> 3.1126.0 with no
file in this repository changing, which is what §7's own row predicted. §12
gains R22, because that row named itself as the reminder covering them while
no such reminder existed. docs/05, docs/06 and docs/09 §5.5 each held their
own stale copy of the deployed commit; all three now cite §7.
Reviewed twice by adversarial-reviewer: 7 findings, then 8, of which five were
defects in the first round's repairs. All 15 fixed.
Nothing was applied to the distribution and nothing was deployed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Md3GndFqWPzK78xAoebsg5
This commit is contained in:
co-authored by
Claude Opus 5
parent
3c3ba5dc6e
commit
bbe535d158
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* Shape helpers for the CloudFront policy configs `configure.mjs` builds.
|
||||
*
|
||||
* ⚠️ **A POLICY AWS HANDS BACK IS NOT A POLICY AWS WILL ACCEPT.**
|
||||
* `get-response-headers-policy` returns `{}` for a member the source does not
|
||||
* define — `Managed-SecurityHeadersPolicy` does it for `ContentSecurityPolicy`
|
||||
* — and sending that back fails `create-response-headers-policy` on
|
||||
* ParamValidation before the call leaves the machine. `docs/09` Part 3 carries
|
||||
* the incident and the exact error.
|
||||
*
|
||||
* **Dropping an empty member is safe at every depth, and that is a measurement
|
||||
* rather than a hope.** Of the 16 structures reachable from
|
||||
* `ResponseHeadersPolicyConfig` in the CLI's own service model, **15 declare at
|
||||
* least one required field** — so `{}` is not a legal value there and can only
|
||||
* be the placeholder. The single exception is `SecurityHeadersConfig` itself,
|
||||
* and `configure.mjs` skips before it can build one of those empty, because a
|
||||
* PDF policy cloning no security headers is the thing that section exists to
|
||||
* avoid.
|
||||
*
|
||||
* They live in their own module so they can be tested: `configure.mjs` reads
|
||||
* argv and calls AWS at import time, so importing THAT to reach two pure
|
||||
* functions is not possible. Same reason `fields.mjs` sits beside
|
||||
* `handler.mjs`. See `policy-shapes.test.mjs`.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Every empty-object member removed, at every depth, bottom-up — so a member
|
||||
* left empty by stripping its own children is removed in turn.
|
||||
*
|
||||
* Arrays are recursed into but never have elements removed: an element index is
|
||||
* load-bearing against its `Quantity` sibling, and an empty object inside one
|
||||
* would be this script's own construction rather than an AWS placeholder. That
|
||||
* case is left for `emptyObjectPaths` to report.
|
||||
*/
|
||||
export const withoutEmptyMembers = (value) => {
|
||||
if (Array.isArray(value)) return value.map(withoutEmptyMembers);
|
||||
if (!value || typeof value !== 'object') return value;
|
||||
const out = {};
|
||||
for (const [k, v] of Object.entries(value)) {
|
||||
const cleaned = withoutEmptyMembers(v);
|
||||
const isEmptyObject =
|
||||
cleaned &&
|
||||
typeof cleaned === 'object' &&
|
||||
!Array.isArray(cleaned) &&
|
||||
Object.keys(cleaned).length === 0;
|
||||
if (!isEmptyObject) out[k] = cleaned;
|
||||
}
|
||||
return out;
|
||||
};
|
||||
|
||||
/** True for `{}` — the value AWS accepts nowhere in these configs. */
|
||||
export const isEmptyObject = (v) =>
|
||||
Boolean(v) &&
|
||||
typeof v === 'object' &&
|
||||
!Array.isArray(v) &&
|
||||
Object.keys(v).length === 0;
|
||||
|
||||
/**
|
||||
* The dotted path of every empty object left in a config. A post-condition on
|
||||
* the strip above, not a filter: if this returns anything, the strip did not do
|
||||
* what this module claims it does.
|
||||
*
|
||||
* Empty ARRAYS are not reported — `{Quantity: 0, Items: []}` is valid and
|
||||
* common, while an empty object is valid nowhere.
|
||||
*/
|
||||
export function emptyObjectPaths(value, path = '') {
|
||||
if (Array.isArray(value)) {
|
||||
return value.flatMap((v, i) => emptyObjectPaths(v, `${path}[${i}]`));
|
||||
}
|
||||
if (value && typeof value === 'object') {
|
||||
if (Object.keys(value).length === 0) return [path || '(root)'];
|
||||
return Object.entries(value).flatMap(([k, v]) =>
|
||||
emptyObjectPaths(v, path ? `${path}.${k}` : k),
|
||||
);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
Reference in New Issue
Block a user