/** * 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 []; }