@fjall/deploy-core 3.8.0 → 3.9.0
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/.minified +1 -1
- package/dist/src/orchestration/domain/delegatedDomainDeploy.js +1 -1
- package/dist/src/orchestration/domain/types.d.ts +1 -1
- package/dist/src/orchestration/domain/types.js +1 -1
- package/dist/src/orchestration/domain/zoneClassifier.d.ts +50 -4
- package/dist/src/orchestration/domain/zoneClassifier.js +1 -1
- package/package.json +4 -4
package/dist/.minified
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
233 files minified at 2026-07-
|
|
1
|
+
233 files minified at 2026-07-20T22:26:35.124Z
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var $=Object.defineProperty;var d=(e,r)=>$(e,"name",{value:r,configurable:!0});import{success as v,failure as p}from"@fjall/generator";import{getErrorMessage as
|
|
1
|
+
var $=Object.defineProperty;var d=(e,r)=>$(e,"name",{value:r,configurable:!0});import{success as v,failure as p}from"@fjall/generator";import{getErrorMessage as c,maskSensitiveOutput as g}from"@fjall/util";import{isAborted as b}from"../../aws/organisations/types.js";import{waitForNsPropagation as z}from"./nsPropagationGate.js";import{runCaaPreflight as w}from"./caaPreflight.js";import{DomainDelegationError as h}from"./types.js";const C=4;function i(e,r,o){e.onProgress?.({type:"step",message:o,metadata:{source:"domain-delegation",stepIndex:r,totalSteps:C}})}d(i,"step");async function F(e){const{deployer:r,callbacks:o}=e,t=r.zoneName,n=e.retryCommand??`fjall domain deploy ${t}`;let m="deployed",l;const s=await r.zonePhaseState();if(s.success&&s.data.complete)m="resumed",l=s.data.nameServers,i(o,0,`Zone phase for ${t} already deployed \u2014 resuming at the propagation gate`);else{s.success||o.onProgress?.({type:"info",message:`Could not probe the zone-phase state for ${t} (${g(c(s.error))}) \u2014 deploying the zone phase (idempotent)`}),i(o,0,`Deploying ${t} zone phase (phase: "zone" \u2014 child zone + NS delegation, no certificates)`);const a=await r.deployPhase("zone",o);if(!a.success)return p(new h(`Delegated-domain deploy for ${t}: the zone-phase deploy failed: ${g(c(a.error))}. No certificate was requested. Cure: fix the reported stack failure and re-run \`${n}\` \u2014 a completed zone phase is skipped on resume.`,"zone_phase_failed",{zoneName:t},!1,a.error));const S=await r.zonePhaseState();S.success&&(l=S.data.nameServers)}if(b(e.abortSignal))return p(P(t,n));i(o,1,`Waiting for the NS delegation of ${t} to propagate (public resolvers)`);const f=await z({zoneName:t,...l!==void 0?{expectedNameServers:l}:{},...e.propagation?.maxAttempts!==void 0?{maxAttempts:e.propagation.maxAttempts}:{},...e.propagation?.intervalMs!==void 0?{intervalMs:e.propagation.intervalMs}:{},...e.resolvers!==void 0?{resolvers:e.resolvers}:{},...e.sleepFn!==void 0?{sleepFn:e.sleepFn}:{},...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{},callbacks:o,retryCommand:n});if(!f.success)return f;let y="passed";if(e.skipCaaPreflight===!0)y="skipped",o.onProgress?.({type:"warning",message:`CAA preflight for ${t} SKIPPED (skipCaaPreflight) \u2014 if a parent CAA record restricts issuance, ACM will hang until CloudFormation rolls back`});else{i(o,2,`CAA preflight for ${t} (parent chain, public resolvers)`);const a=await w({certificateDomain:t,...e.resolvers!==void 0?{resolvers:e.resolvers}:{},...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{},callbacks:o});if(!a.success)return a}if(b(e.abortSignal))return p(P(t,n));i(o,3,`Deploying ${t} full phase (phase: "full" \u2014 certificates)`);const u=await r.deployPhase("full",o);return u.success?v({zonePhase:m,propagation:f.data,caaPreflight:y,fullPhase:"deployed"}):p(new h(`Delegated-domain deploy for ${t}: the full-phase deploy failed: ${g(c(u.error))}. The zone phase is deployed and the delegation is live. Cure: fix the reported stack failure and re-run \`${n}\` \u2014 the deploy resumes at the propagation gate and re-runs only the full phase.`,"full_phase_failed",{zoneName:t},!1,u.error))}d(F,"deployDelegatedDomain");function P(e,r){return new h(`Delegated-domain deploy for ${e} aborted between phases \u2014 the zone phase (when deployed) is retained. Re-run \`${r}\` to resume.`,"aborted",{zoneName:e},!0)}d(P,"abortedMidDeploy");export{F as deployDelegatedDomain};
|
|
@@ -32,7 +32,7 @@ export type DomainDelegationErrorType = (typeof DOMAIN_DELEGATION_ERROR_TYPES)[n
|
|
|
32
32
|
*/
|
|
33
33
|
export declare class DomainDelegationError extends BaseServiceError {
|
|
34
34
|
readonly errorType: DomainDelegationErrorType;
|
|
35
|
-
constructor(message: string, errorType: DomainDelegationErrorType, details?: unknown, recoverable?: boolean);
|
|
35
|
+
constructor(message: string, errorType: DomainDelegationErrorType, details?: unknown, recoverable?: boolean, cause?: Error);
|
|
36
36
|
}
|
|
37
37
|
/**
|
|
38
38
|
* One CAA record as node's resolver returns it. Structurally compatible with
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var
|
|
1
|
+
var l=Object.defineProperty;var t=(e,r)=>l(e,"name",{value:r,configurable:!0});import{Resolver as i}from"node:dns/promises";import{BaseServiceError as _}from"../../types/errors/ServiceError.js";const S=["delegation_unreachable","caa_restrictive","caa_unverifiable","zone_phase_failed","full_phase_failed","delegation_role_unusable","template_unreadable","ns_delete_failed","aborted"];class m extends _{static{t(this,"DomainDelegationError")}errorType;constructor(r,o,s,n=!1,a){super(`DOMAIN_${o.toUpperCase()}`,r,s,n,a),this.errorType=o}}const c=["1.1.1.1","8.8.8.8"],p=1e4;function N(e=c){return e.map(r=>{const o=new i({timeout:p,tries:1});return o.setServers([r]),{address:r,resolveNs:t(s=>o.resolveNs(s),"resolveNs"),resolveCaa:t(s=>o.resolveCaa(s),"resolveCaa")}})}t(N,"createPublicResolvers");function v(e){return e.trim().toLowerCase().replace(/\.$/,"")}t(v,"normaliseDnsName");const E=new Set(["ENODATA","ENOTFOUND"]);function O(e){if(!(e instanceof Error))return!1;const r=e.code;return typeof r=="string"&&E.has(r)}t(O,"isDnsEmptyAnswer");export{c as DEFAULT_PUBLIC_RESOLVER_ADDRESSES,S as DOMAIN_DELEGATION_ERROR_TYPES,m as DomainDelegationError,p as PUBLIC_DNS_QUERY_TIMEOUT_MS,N as createPublicResolvers,O as isDnsEmptyAnswer,v as normaliseDnsName};
|
|
@@ -13,6 +13,18 @@ import { type Result } from "@fjall/generator";
|
|
|
13
13
|
* affected records resolve to the conservative bucket — `unknown`, never
|
|
14
14
|
* `residue` — and the report carries a warning naming the gap and its cure.
|
|
15
15
|
*
|
|
16
|
+
* Template evidence is each live Fjall stack's Processed template body, with
|
|
17
|
+
* ONE honest widening for non-JSON bodies (hand-written YAML templates, e.g.
|
|
18
|
+
* the CDK bootstrap stack): a CloudFormation GetTemplateSummary listing no
|
|
19
|
+
* AWS::Route53::* and no Custom::* resource type PROVES the stack claims no
|
|
20
|
+
* record keys, so it indexes as contributing nothing, with no warning and
|
|
21
|
+
* template evidence still complete. The summary is absence-only evidence -
|
|
22
|
+
* it names resource types, never their properties - so a summary listing
|
|
23
|
+
* either namespace, or no readable summary at all, keeps the fail-closed
|
|
24
|
+
* warning, and a summary can never prove a positive claim (the cross-account
|
|
25
|
+
* delegation proof still needs the literal DelegatedZoneName from a JSON
|
|
26
|
+
* template).
|
|
27
|
+
*
|
|
16
28
|
* Evidence is account-local with ONE deliberate exception: callers may name
|
|
17
29
|
* delegated child domains (ClassifyZoneOptions.delegatedChildren) plus a
|
|
18
30
|
* child-account client factory, and the classifier then proves a child's
|
|
@@ -182,6 +194,13 @@ export interface DelegatedChildDomain {
|
|
|
182
194
|
* the child is skipped fail-closed with a warning naming the config cure.
|
|
183
195
|
*/
|
|
184
196
|
readonly accountId?: string;
|
|
197
|
+
/**
|
|
198
|
+
* Region the child's domain stack last deployed into. Threaded through to
|
|
199
|
+
* the ChildAccountClientFactory so child clients are minted where the
|
|
200
|
+
* stack lives; absent, the factory falls back to its own region
|
|
201
|
+
* resolution.
|
|
202
|
+
*/
|
|
203
|
+
readonly region?: string;
|
|
185
204
|
/**
|
|
186
205
|
* Child domain stack name override. Defaults to the deploy-state
|
|
187
206
|
* convention, getDomainStackName(zoneName).
|
|
@@ -193,13 +212,15 @@ export interface ChildAccountClients {
|
|
|
193
212
|
readonly cloudFormation: AwsSdkClientLike;
|
|
194
213
|
}
|
|
195
214
|
/**
|
|
196
|
-
* Mint clients for a delegated child's account.
|
|
215
|
+
* Mint clients for a delegated child's account. `region` names where the
|
|
216
|
+
* child's stack deployed (config-carried, DelegatedChildDomain.region);
|
|
217
|
+
* omitted, the factory chooses its own fallback. Any error, including an
|
|
197
218
|
* interactivity requirement (an expired SSO session must never hang a
|
|
198
219
|
* non-interactive run), resolves the Result to failure; the classifier then
|
|
199
220
|
* warns and leaves that child's NS row 'unknown' without touching its
|
|
200
221
|
* siblings.
|
|
201
222
|
*/
|
|
202
|
-
export type ChildAccountClientFactory = (accountId: string) => Promise<Result<ChildAccountClients, Error>>;
|
|
223
|
+
export type ChildAccountClientFactory = (accountId: string, region?: string) => Promise<Result<ChildAccountClients, Error>>;
|
|
203
224
|
/**
|
|
204
225
|
* Route53 returns names with a trailing dot and octal-escaped bytes
|
|
205
226
|
* (`\052` = `*`). Normalise both directions (live names AND template names)
|
|
@@ -214,6 +235,24 @@ export type ChildAccountClientFactory = (accountId: string) => Promise<Result<Ch
|
|
|
214
235
|
* backslash sequence must stay literal.
|
|
215
236
|
*/
|
|
216
237
|
export declare function normaliseRecordName(name: string): string;
|
|
238
|
+
/**
|
|
239
|
+
* Why a stack's template evidence could not be indexed. Discriminated so
|
|
240
|
+
* callers dispatch on structure, never on message text: readStackRecordKeys
|
|
241
|
+
* keys its GetTemplateSummary fallback off `non-json-template` alone, and
|
|
242
|
+
* buildSatelliteRecordIndex appends its cloudformation:GetTemplate cure only
|
|
243
|
+
* to untyped transport failures (every TemplateEvidenceError message already
|
|
244
|
+
* names its own cure inline).
|
|
245
|
+
*/
|
|
246
|
+
export type TemplateEvidenceFailureReason = "non-json-template" | "non-literal-delegation" | "missing-template-body";
|
|
247
|
+
/**
|
|
248
|
+
* Typed template-evidence failure; the message always names the cure but
|
|
249
|
+
* never the owning stack - each warning-producing caller wraps the message
|
|
250
|
+
* with the stack named, so naming it here too would double it.
|
|
251
|
+
*/
|
|
252
|
+
export declare class TemplateEvidenceError extends Error {
|
|
253
|
+
readonly reason: TemplateEvidenceFailureReason;
|
|
254
|
+
constructor(reason: TemplateEvidenceFailureReason, message: string);
|
|
255
|
+
}
|
|
217
256
|
/**
|
|
218
257
|
* Extract every (name, type, setIdentifier) a CloudFormation template
|
|
219
258
|
* declares via `AWS::Route53::RecordSet` or `AWS::Route53::RecordSetGroup`.
|
|
@@ -238,7 +277,10 @@ export declare function normaliseRecordName(name: string): string;
|
|
|
238
277
|
* and stays success(empty). Deliberately no YAML parsing here: correct
|
|
239
278
|
* CloudFormation YAML requires the short-form intrinsic tag schema, a
|
|
240
279
|
* dependency and correctness surface of its own, so the cure named in the
|
|
241
|
-
* failure is conversion, not a lossy parse.
|
|
280
|
+
* failure is conversion, not a lossy parse. Failures are typed
|
|
281
|
+
* ({@link TemplateEvidenceError}); readStackRecordKeys dispatches on the
|
|
282
|
+
* `non-json-template` reason to mount its absence-only GetTemplateSummary
|
|
283
|
+
* fallback (see module doc).
|
|
242
284
|
*/
|
|
243
285
|
export declare function extractTemplateRecordKeys(templateBody: string): Result<Set<string>, Error>;
|
|
244
286
|
/**
|
|
@@ -246,7 +288,11 @@ export declare function extractTemplateRecordKeys(templateBody: string): Result<
|
|
|
246
288
|
* record their CURRENT deployed templates declare. Read-only; build once per
|
|
247
289
|
* run and share across zones (D6 "cached per run"). Enumeration or template
|
|
248
290
|
* failures degrade to warnings — affected records classify `unknown`, never
|
|
249
|
-
* `satellite` or `residue`.
|
|
291
|
+
* `satellite` or `residue`. A non-JSON template proven record-free by its
|
|
292
|
+
* resource-type summary (see readStackRecordKeys) indexes as contributing
|
|
293
|
+
* nothing with NO warning: warnings here flip templateEvidenceComplete and
|
|
294
|
+
* suppress dangling-alias residue probing zone-wide, so only genuine
|
|
295
|
+
* evidence gaps may land in them.
|
|
250
296
|
*/
|
|
251
297
|
export declare function buildSatelliteRecordIndex(cloudFormation: AwsSdkClientLike, options?: {
|
|
252
298
|
readonly excludeStackNames?: readonly string[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var B=Object.defineProperty;var u=(t,e)=>B(t,"name",{value:e,configurable:!0});import{ListResourceRecordSetsCommand as U}from"@aws-sdk/client-route-53";import{DescribeStacksCommand as P,DescribeStackResourcesCommand as G,GetTemplateCommand as W}from"@aws-sdk/client-cloudformation";import{ListCertificatesCommand as Z,DescribeCertificateCommand as q}from"@aws-sdk/client-acm";import{Resolver as J}from"node:dns/promises";import{success as T,failure as A}from"@fjall/generator";import{maskSensitiveOutput as N,getErrorMessage as y,getDomainExportNames as X,getDomainStackName as Y}from"@fjall/util";import{composeSdkAbortSignal as E,isAborted as I}from"../../aws/organisations/types.js";import{CROSS_ACCOUNT_DELEGATION_RESOURCE_TYPE as V}from"./delegationDestroyMirror.js";const H=5e3,Q=2;async function ee(t,e,n){const a=new J({timeout:H,tries:Q}),r=u(()=>{a.cancel()},"cancel");n?.addEventListener("abort",r,{once:!0});try{return e==="A"?await a.resolve4(t):await a.resolve6(t)}finally{n?.removeEventListener("abort",r)}}u(ee,"resolveWithSystemResolver");function te(t=ee){const e=u(async(n,a,r)=>{try{return(await t(n,a,r)).length>0?"answers":"indeterminate"}catch(c){const o=c.code;return o==="ENOTFOUND"?"nxdomain":o==="ENODATA"?"nodata":"indeterminate"}},"queryFamily");return async(n,a)=>{const r=await e(n,"A",a);if(r==="answers")return"resolves";if(r==="nxdomain")return"nxdomain";if(r==="indeterminate")return"indeterminate";const c=await e(n,"AAAA",a);return c==="answers"?"resolves":c==="nxdomain"?"nxdomain":"indeterminate"}}u(te,"createSystemDnsNameProbe");const ne=te(),j=new Set(["CREATE_COMPLETE","UPDATE_COMPLETE","UPDATE_ROLLBACK_COMPLETE","IMPORT_COMPLETE","IMPORT_ROLLBACK_COMPLETE","UPDATE_IN_PROGRESS","UPDATE_COMPLETE_CLEANUP_IN_PROGRESS","UPDATE_ROLLBACK_IN_PROGRESS","UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS"]),ae="fjall:",oe="Fjall",re=/^_[0-9a-f]{8,64}$/,se=".acm-validations.aws",ie=[/\.cloudfront\.net$/,/\.elb\.amazonaws\.com$/,/\.elb\.[a-z0-9-]+\.amazonaws\.com$/];function b(t){const e=t.replace(/\\(\d{3})/g,(a,r)=>String.fromCharCode(parseInt(r,8)));return(e.endsWith(".")?e.slice(0,-1):e).toLowerCase()}u(b,"normaliseRecordName");function C(t,e,n){return`${b(t)}|${e.toUpperCase()}|${n??""}`}u(C,"recordKey");async function ce(t,e,n){const a=[];let r,c,o;do{if(I(n))throw new Error("record listing aborted by shutdown signal");const i=await t.send(new U({HostedZoneId:e,StartRecordName:r,StartRecordType:c,StartRecordIdentifier:o}),{abortSignal:E(n)});for(const s of i.ResourceRecordSets??[]){if(!s.Name||!s.Type)continue;const f=s.AliasTarget?.DNSName!==void 0?b(s.AliasTarget.DNSName):void 0,l=s.AliasTarget?.DNSName!==void 0?[s.AliasTarget.DNSName]:(s.ResourceRecords??[]).flatMap(h=>h.Value!==void 0?[h.Value]:[]);a.push({name:b(s.Name),type:s.Type,...s.SetIdentifier!==void 0?{setIdentifier:s.SetIdentifier}:{},values:l,...f!==void 0?{aliasTargetDnsName:f}:{}})}i.IsTruncated?(r=i.NextRecordName,c=i.NextRecordType,o=i.NextRecordIdentifier):(r=void 0,c=void 0,o=void 0)}while(r);return a}u(ce,"listLiveRecords");function de(t){const e=new Set;let n;try{n=JSON.parse(t)}catch{return A(new Error("stack template is not JSON (likely a hand-written YAML template), so its Route53 record declarations cannot be indexed. Cure: convert the template to JSON or accept conservative classification."))}if(typeof n!="object"||n===null)return T(e);const a=n.Resources;if(typeof a!="object"||a===null)return T(e);const r=u(c=>{const o=c.Name,i=c.Type,s=c.SetIdentifier;typeof o!="string"||typeof i!="string"||s!==void 0&&typeof s!="string"||e.add(C(o,i,s))},"addKey");for(const[c,o]of Object.entries(a)){if(typeof o!="object"||o===null)continue;const{Type:i,Properties:s}=o,f=typeof s=="object"&&s!==null?s:void 0;if(i===V){const l=f?.DelegatedZoneName;if(typeof l!="string")return A(new Error(`${V} resource '${c}' has no literal DelegatedZoneName, so the parent-zone NS row it writes cannot be indexed. Cure: synthesise the delegation with a literal delegated zone name or accept conservative classification.`));e.add(C(l,"NS"));continue}if(f!==void 0){if(i==="AWS::Route53::RecordSet")r(f);else if(i==="AWS::Route53::RecordSetGroup"){const l=f.RecordSets;if(!Array.isArray(l))continue;for(const h of l)typeof h=="object"&&h!==null&&r(h)}}}return T(e)}u(de,"extractTemplateRecordKeys");async function _(t,e,n){try{const a=await t.send(new W({StackName:e,TemplateStage:"Processed"}),{abortSignal:E(n)});if(a.TemplateBody===void 0)return A(new Error(`zone classification: CloudFormation returned no template body for stack '${e}'`));const r=de(a.TemplateBody);return r.success?r:A(new Error(`stack '${e}': ${r.error.message}`))}catch(a){return A(a instanceof Error?a:new Error(String(a)))}}u(_,"readStackRecordKeys");function le(t){return t.Tags?.some(e=>e.Key?.startsWith(ae))===!0?!0:t.StackName?.startsWith(oe)===!0}u(le,"isFjallStack");async function ue(t,e={}){const n=new Map,a=[],r=new Set(e.excludeStackNames??[]),c=[];try{let o;do{if(I(e.abortSignal))throw new Error("stack enumeration aborted by shutdown signal");const i=await t.send(new P({...o!==void 0?{NextToken:o}:{}}),{abortSignal:E(e.abortSignal)});for(const s of i.Stacks??[])s.StackName===void 0||r.has(s.StackName)||s.StackStatus===void 0||!j.has(s.StackStatus)||!le(s)||c.push(s.StackName);o=i.NextToken}while(o!==void 0)}catch(o){return a.push(N(`zone classification: could not enumerate CloudFormation stacks (${y(o)}) \u2014 satellite ownership cannot be proven, so unmatched records classify 'unknown'. Cure: grant cloudformation:DescribeStacks and retry.`)),{byRecordKey:n,stackNames:[],warnings:a}}for(const o of c){const i=await _(t,o,e.abortSignal);if(!i.success){a.push(N(`zone classification: could not read template for stack '${o}' (${y(i.error)}) \u2014 its records classify 'unknown'. Cure: grant cloudformation:GetTemplate and retry.`));continue}for(const s of i.data)n.has(s)||n.set(s,o)}return{byRecordKey:n,stackNames:c,warnings:a}}u(ue,"buildSatelliteRecordIndex");function F(t){return new Set(t.map(e=>b(e.trim())).filter(e=>e!==""))}u(F,"normaliseNsValueSet");function fe(t,e){if(t.size!==e.size)return!1;for(const n of t)if(!e.has(n))return!1;return!0}u(fe,"nsValueSetsEqual");async function me(t){const{factory:e,accountId:n,zoneName:a,stackName:r,abortSignal:c}=t,o=u(p=>A(new Error(`zone classification: cross-account delegation for '${a}': ${p}`)),"fail");let i;try{const p=await e(n);if(!p.success)return o(`account ${n} is unreachable (${y(p.error)}), so the NS row classifies 'unknown'. Cure: restore access to account ${n} and retry.`);i=p.data}catch(p){return o(`account ${n} is unreachable (${y(p)}), so the NS row classifies 'unknown'. Cure: restore access to account ${n} and retry.`)}let s;try{s=await i.cloudFormation.send(new P({StackName:r}),{abortSignal:E(c)})}catch(p){return o(`could not describe stack '${r}' in account ${n} (${y(p)}), so the NS row classifies 'unknown'. Cure: deploy the child with 'fjall domain deploy ${a}' or grant cloudformation:DescribeStacks in that account.`)}const f=s.Stacks?.[0];if(f===void 0)return o(`stack '${r}' was not found in account ${n}, so the NS row classifies 'unknown'. Cure: deploy the child with 'fjall domain deploy ${a}'.`);if(f.StackStatus===void 0||!j.has(f.StackStatus))return o(`stack '${r}' in account ${n} has status ${f.StackStatus??"unknown"}, which does not describe live resources, so the NS row classifies 'unknown'. Cure: repair the child stack, then redeploy with 'fjall domain deploy ${a}'.`);const l=await _(i.cloudFormation,r,c);if(!l.success)return o(`template evidence failed in account ${n} (${y(l.error)}), so the NS row classifies 'unknown'.`);if(!l.data.has(C(a,"NS")))return o(`stack '${r}' in account ${n} does not claim the NS row for '${a}', so the row classifies 'unknown'. Cure: redeploy the child with 'fjall domain deploy ${a}' so its delegation resource is current.`);const h=X(a).nameservers,k=(f.Outputs??[]).find(p=>p.ExportName===h)?.OutputValue;if(k===void 0||k.trim()==="")return o(`stack '${r}' in account ${n} publishes no '${h}' output, so the delegated nameserver values cannot be verified and the NS row classifies 'unknown'. Cure: redeploy the child with 'fjall domain deploy ${a}' so the export contract is current.`);const R=F(k.split(",")),$=F(t.liveNsValues);if(!fe(R,$)){const p=u(v=>`[${[...v].sort().join(", ")}]`,"quote");return o(`stack '${r}' in account ${n} publishes nameservers ${p(R)} but the live NS row holds ${p($)}, so the row classifies 'unknown'. Cure: re-run 'fjall domain deploy ${a}' so parent and child agree.`)}return T({ownerStack:r,accountId:n})}u(me,"proveCrossAccountDelegation");async function we(t,e){const n=new Map;for(const[a,r]of t)try{const c=[];let o;do{if(I(e))throw new Error("certificate listing aborted by shutdown signal");const i=await r.send(new Z({...o!==void 0?{NextToken:o}:{}}),{abortSignal:E(e)});for(const s of i.CertificateSummaryList??[])s.CertificateArn!==void 0&&c.push(s.CertificateArn);o=i.NextToken}while(o!==void 0);for(const i of c){if(I(e))throw new Error("certificate description aborted by shutdown signal");const s=await r.send(new q({CertificateArn:i}),{abortSignal:E(e)});for(const f of s.Certificate?.DomainValidationOptions??[]){const l=f.ResourceRecord;if(l?.Type==="CNAME"&&l.Name!==void 0){const h=b(l.Name),k=n.get(h)??new Set;k.add(i),n.set(h,k)}}}}catch(c){return A(new Error(`zone classification: could not read ACM certificates in ${a} (${y(c)}). Cure: grant acm:ListCertificates and acm:DescribeCertificate in ${a}, then retry.`))}return T(n)}u(we,"collectAcmValidationReferences");async function pe(t,e,n,a){const r=new Map,c=[];for(const o of e){if(r.size===n.size)break;try{if(I(a))throw new Error("stack resource listing aborted by shutdown signal");const i=await t.send(new G({StackName:o}),{abortSignal:E(a)});for(const s of i.StackResources??[])s.ResourceType!=="AWS::CertificateManager::Certificate"||s.PhysicalResourceId===void 0||!n.has(s.PhysicalResourceId)||r.has(s.PhysicalResourceId)||r.set(s.PhysicalResourceId,o)}catch(i){c.push(N(`zone classification: could not list resources for stack '${o}' (${y(i)}), so certificate ownership cannot be proven there and its validation CNAMEs stay 'unknown'. Cure: grant cloudformation:DescribeStackResources and retry.`))}}return{ownerByArn:r,warnings:c}}u(pe,"resolveCertificateOwnership");function D(t){if(t.type!=="CNAME")return!1;const e=t.name.split(".")[0]??"";return re.test(e)?t.values.some(n=>b(n).endsWith(se)):!1}u(D,"isAcmValidationShaped");function K(t){if(t.type!=="A"&&t.type!=="AAAA")return!1;const e=t.aliasTargetDnsName;return e===void 0?!1:ie.some(n=>n.test(e))}u(K,"isAwsManagedAliasCandidate");async function Te(t){const{zone:e,domainStackName:n,clients:a,abortSignal:r,onPhase:c}=t,o=[];c?.("read-records");let i;try{i=await ce(a.route53,e.hostedZoneId,r)}catch(d){return A(new Error(`zone classification: could not list record sets for zone ${e.hostedZoneId} (${e.zoneName}): ${y(d)}. Cure: grant route53:ListResourceRecordSets on the zone and retry.`))}let s=new Set,f=!0;if(n!==void 0){const d=await _(a.cloudFormation,n,r);d.success?s=d.data:(f=!1,o.push(N(`zone classification: could not read the domain stack template for '${n}' (${y(d.error)}) \u2014 only zone-owned SOA/NS classify 'declared'. Cure: deploy the domain stack with 'fjall domain deploy ${e.zoneName}' or grant cloudformation:GetTemplate.`)))}c?.("index-stacks");const l=t.satelliteIndex??await ue(a.cloudFormation,{...n!==void 0?{excludeStackNames:[n]}:{},...r!==void 0?{abortSignal:r}:{}});o.push(...l.warnings);const h=new Map;if(t.delegatedChildren!==void 0){const d=new Map;for(const m of i)m.type==="NS"&&d.set(C(m.name,m.type,m.setIdentifier),m.values);const w=t.delegatedChildren.filter(m=>{const g=C(m.zoneName,"NS");return d.has(g)&&!s.has(g)&&!l.byRecordKey.has(g)}),S=t.childAccountClientFactory;if(w.length>0&&S===void 0)o.push(N("zone classification: delegated child domains are configured but no child-account client factory was wired, so cross-account delegations cannot be verified and their NS rows classify 'unknown'. Cure: supply childAccountClientFactory in ClassifyZoneOptions."));else if(S!==void 0)for(const m of w){if(m.accountId===void 0){o.push(N(`zone classification: delegated child '${m.zoneName}' has no AWS account id, so its cross-account delegation cannot be verified and its NS row classifies 'unknown'. Cure: set "account" on the delegated domain entry in fjall-config.json.`));continue}const g=C(m.zoneName,"NS"),O=await me({factory:S,accountId:m.accountId,zoneName:m.zoneName,stackName:m.stackName??Y(m.zoneName),liveNsValues:d.get(g)??[],...r!==void 0?{abortSignal:r}:{}});if(!O.success){o.push(N(y(O.error)));continue}h.set(g,O.data)}}c?.("check-acm");const k=i.some(D);let R;if(k){const d=await we(a.acmByRegion,r);d.success?R=d.data:o.push(N(d.error.message))}let $;if(R!==void 0){const d=new Set;for(const w of i)if(D(w))for(const S of R.get(w.name)??[])d.add(S);if(d.size>0){const w=await pe(a.cloudFormation,[...n!==void 0?[n]:[],...l.stackNames],d,r);$=w.ownerByArn,o.push(...w.warnings)}}const p=f&&l.warnings.length===0,v=new Set;if(p)for(const d of i){if(!K(d))continue;const w=C(d.name,d.type,d.setIdentifier);s.has(w)||l.byRecordKey.has(w)||v.add(d.aliasTargetDnsName)}let x;if(v.size>0){const d=t.dnsProbe??ne,w=new Map;for(const S of v){let m;try{m=await d(S,r)}catch(g){m="indeterminate",o.push(N(`zone classification: DNS probe for alias target '${S}' failed (${y(g)}) \u2014 dependent alias records classify 'unknown', never 'residue'. Cure: retry with a working resolver.`)),w.set(S,m);continue}m==="indeterminate"&&o.push(N(`zone classification: DNS probe for alias target '${S}' was inconclusive \u2014 dependent alias records classify 'unknown', never 'residue'. Cure: retry once the resolver answers definitively.`)),w.set(S,m)}x=w}c?.("classify");const z=b(e.zoneName),M=i.map(d=>ye(d,{zoneApex:z,domainStackName:n,declaredKeys:s,satelliteIndex:l,crossAccountDelegations:h,acmValidationReferences:R,certificateOwnerByArn:$,templateEvidenceComplete:p,aliasProbeVerdicts:x})).sort((d,w)=>d.name.localeCompare(w.name)||d.type.localeCompare(w.type)||(d.setIdentifier??"").localeCompare(w.setIdentifier??"")),L={declared:0,satellite:0,residue:0,unknown:0};for(const d of M)L[d.classification]+=1;return T({hostedZoneId:e.hostedZoneId,zoneName:z,records:M,counts:L,warnings:o})}u(Te,"classifyZoneRecords");function he(t,e){const n=new Set;for(const a of t){const r=e?.get(a);if(r===void 0)return;n.add(r)}return n}u(he,"provenCertificateOwners");function ye(t,e){const n=C(t.name,t.type,t.setIdentifier),a={name:t.name,type:t.type,...t.setIdentifier!==void 0?{setIdentifier:t.setIdentifier}:{},values:t.values};if(t.name===e.zoneApex&&(t.type==="SOA"||t.type==="NS"))return{...a,classification:"declared",...e.domainStackName!==void 0?{ownerStack:e.domainStackName}:{},detail:"zone-owned record, created with the hosted zone"};if(e.declaredKeys.has(n))return{...a,classification:"declared",...e.domainStackName!==void 0?{ownerStack:e.domainStackName}:{}};const r=e.satelliteIndex.byRecordKey.get(n);if(r!==void 0)return{...a,classification:"satellite",ownerStack:r};const c=e.crossAccountDelegations.get(n);if(c!==void 0)return{...a,classification:"satellite",ownerStack:c.ownerStack,detail:`cross-account delegation: stack ${c.ownerStack} in account ${c.accountId}`};if(D(t)){if(e.acmValidationReferences===void 0)return{...a,classification:"unknown",detail:"ACM validation CNAME shape, but live-certificate evidence was unavailable \u2014 fail-closed as unknown rather than residue"};const o=e.acmValidationReferences.get(t.name);if(o!==void 0&&o.size>0){const i=he(o,e.certificateOwnerByArn);if(i!==void 0){const s=[...i].sort();if(e.domainStackName!==void 0&&s.every(l=>l===e.domainStackName))return{...a,classification:"declared",ownerStack:e.domainStackName,detail:"ACM validation CNAME for the domain stack's live certificates; required for issuance and auto-renewal"};const f=s.find(l=>l!==e.domainStackName);if(f!==void 0)return{...a,classification:"satellite",ownerStack:f,detail:`ACM validation CNAME for live certificates owned by ${s.map(l=>`'${l}'`).join(", ")}; required for issuance and auto-renewal`}}return{...a,classification:"unknown",detail:"ACM validation CNAME still referenced by a live certificate \u2014 keep it; it is not residue"}}return{...a,classification:"residue",detail:"ACM validation CNAME not referenced by any live certificate in the scanned regions"}}if(K(t)){if(!e.templateEvidenceComplete)return{...a,classification:"unknown",detail:"alias to an AWS-managed target, but template evidence was incomplete \u2014 fail-closed as unknown rather than residue"};const o=e.aliasProbeVerdicts?.get(t.aliasTargetDnsName);return o==="nxdomain"?{...a,classification:"residue",detail:`dangling alias: target ${t.aliasTargetDnsName} no longer exists (NXDOMAIN) and no live template references this record`}:o==="resolves"?{...a,classification:"unknown",detail:"alias target still exists in DNS, so the record cannot be dangling; not residue"}:{...a,classification:"unknown",detail:"alias target probe was inconclusive \u2014 fail-closed as unknown rather than residue"}}return{...a,classification:"unknown"}}u(ye,"classifyRecord");export{ue as buildSatelliteRecordIndex,Te as classifyZoneRecords,te as createSystemDnsNameProbe,de as extractTemplateRecordKeys,b as normaliseRecordName,ne as systemDnsNameProbe};
|
|
1
|
+
var U=Object.defineProperty;var u=(t,e)=>U(t,"name",{value:e,configurable:!0});import{ListResourceRecordSetsCommand as W}from"@aws-sdk/client-route-53";import{DescribeStacksCommand as V,DescribeStackResourcesCommand as G,GetTemplateCommand as J,GetTemplateSummaryCommand as Z}from"@aws-sdk/client-cloudformation";import{ListCertificatesCommand as q,DescribeCertificateCommand as X}from"@aws-sdk/client-acm";import{Resolver as Y}from"node:dns/promises";import{success as v,failure as A}from"@fjall/generator";import{maskSensitiveOutput as N,getErrorMessage as h,getDomainExportNames as H,getDomainStackName as Q}from"@fjall/util";import{composeSdkAbortSignal as C,isAborted as b}from"../../aws/organisations/types.js";import{CROSS_ACCOUNT_DELEGATION_RESOURCE_TYPE as j}from"./delegationDestroyMirror.js";const ee=5e3,te=2;async function ne(t,e,n){const r=new Y({timeout:ee,tries:te}),a=u(()=>{r.cancel()},"cancel");n?.addEventListener("abort",a,{once:!0});try{return e==="A"?await r.resolve4(t):await r.resolve6(t)}finally{n?.removeEventListener("abort",a)}}u(ne,"resolveWithSystemResolver");function ae(t=ne){const e=u(async(n,r,a)=>{try{return(await t(n,r,a)).length>0?"answers":"indeterminate"}catch(s){const o=s.code;return o==="ENOTFOUND"?"nxdomain":o==="ENODATA"?"nodata":"indeterminate"}},"queryFamily");return async(n,r)=>{const a=await e(n,"A",r);if(a==="answers")return"resolves";if(a==="nxdomain")return"nxdomain";if(a==="indeterminate")return"indeterminate";const s=await e(n,"AAAA",r);return s==="answers"?"resolves":s==="nxdomain"?"nxdomain":"indeterminate"}}u(ae,"createSystemDnsNameProbe");const re=ae(),F=new Set(["CREATE_COMPLETE","UPDATE_COMPLETE","UPDATE_ROLLBACK_COMPLETE","IMPORT_COMPLETE","IMPORT_ROLLBACK_COMPLETE","UPDATE_IN_PROGRESS","UPDATE_COMPLETE_CLEANUP_IN_PROGRESS","UPDATE_ROLLBACK_IN_PROGRESS","UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS"]),oe="fjall:",se="Fjall",ie=/^_[0-9a-f]{8,64}$/,ce=".acm-validations.aws",de=[/\.cloudfront\.net$/,/\.elb\.amazonaws\.com$/,/\.elb\.[a-z0-9-]+\.amazonaws\.com$/];function T(t){const e=t.replace(/\\(\d{3})/g,(r,a)=>String.fromCharCode(parseInt(a,8)));return(e.endsWith(".")?e.slice(0,-1):e).toLowerCase()}u(T,"normaliseRecordName");function R(t,e,n){return`${T(t)}|${e.toUpperCase()}|${n??""}`}u(R,"recordKey");async function le(t,e,n){const r=[];let a,s,o;do{if(b(n))throw new Error("record listing aborted by shutdown signal");const c=await t.send(new W({HostedZoneId:e,StartRecordName:a,StartRecordType:s,StartRecordIdentifier:o}),{abortSignal:C(n)});for(const i of c.ResourceRecordSets??[]){if(!i.Name||!i.Type)continue;const p=i.AliasTarget?.DNSName!==void 0?T(i.AliasTarget.DNSName):void 0,l=i.AliasTarget?.DNSName!==void 0?[i.AliasTarget.DNSName]:(i.ResourceRecords??[]).flatMap(w=>w.Value!==void 0?[w.Value]:[]);r.push({name:T(i.Name),type:i.Type,...i.SetIdentifier!==void 0?{setIdentifier:i.SetIdentifier}:{},values:l,...p!==void 0?{aliasTargetDnsName:p}:{}})}c.IsTruncated?(a=c.NextRecordName,s=c.NextRecordType,o=c.NextRecordIdentifier):(a=void 0,s=void 0,o=void 0)}while(a);return r}u(le,"listLiveRecords");class $ extends Error{static{u(this,"TemplateEvidenceError")}reason;constructor(e,n){super(n),this.reason=e,this.name="TemplateEvidenceError"}}function ue(t){const e=new Set;let n;try{n=JSON.parse(t)}catch{return A(new $("non-json-template","stack template is not JSON (likely a hand-written YAML template), so its Route53 record declarations cannot be indexed. Cure: convert the template to JSON or accept conservative classification."))}if(typeof n!="object"||n===null)return v(e);const r=n.Resources;if(typeof r!="object"||r===null)return v(e);const a=u(s=>{const o=s.Name,c=s.Type,i=s.SetIdentifier;typeof o!="string"||typeof c!="string"||i!==void 0&&typeof i!="string"||e.add(R(o,c,i))},"addKey");for(const[s,o]of Object.entries(r)){if(typeof o!="object"||o===null)continue;const{Type:c,Properties:i}=o,p=typeof i=="object"&&i!==null?i:void 0;if(c===j){const l=p?.DelegatedZoneName;if(typeof l!="string")return A(new $("non-literal-delegation",`${j} resource '${s}' has no literal DelegatedZoneName, so the parent-zone NS row it writes cannot be indexed. Cure: synthesise the delegation with a literal delegated zone name or accept conservative classification.`));e.add(R(l,"NS"));continue}if(p!==void 0){if(c==="AWS::Route53::RecordSet")a(p);else if(c==="AWS::Route53::RecordSetGroup"){const l=p.RecordSets;if(!Array.isArray(l))continue;for(const w of l)typeof w=="object"&&w!==null&&a(w)}}}return v(e)}u(ue,"extractTemplateRecordKeys");function fe(t){return t.startsWith("AWS::Route53::")||t.startsWith("Custom::")||t==="AWS::CloudFormation::CustomResource"}u(fe,"isRecordClaimingResourceType");async function me(t,e,n){const r=u((o,c)=>A(new $("non-json-template",`template is not JSON (likely a hand-written YAML template) and ${o}, so its record declarations cannot be indexed. Cure: ${c}`)),"failNonJson");if(b(n))return r("the resource-type summary check was aborted by the shutdown signal","retry, convert the template to JSON, or accept conservative classification.");let a;try{a=(await t.send(new Z({StackName:e}),{abortSignal:C(n)})).ResourceTypes}catch(o){return b(n)?r("the resource-type summary read was aborted by the shutdown signal","retry, convert the template to JSON, or accept conservative classification."):r(`its resource-type summary could not be read (${h(o)})`,"grant cloudformation:GetTemplateSummary and retry, convert the template to JSON, or accept conservative classification.")}if(a===void 0)return r("its resource-type summary carried no resource types","convert the template to JSON or accept conservative classification.");const s=[...new Set(a.filter(fe))].sort();return s.length>0?r(`its resource-type summary lists record-claiming types (${s.join(", ")}) whose keys a summary cannot name`,"convert the template to JSON or accept conservative classification."):v(new Set)}u(me,"proveRecordFreeViaResourceTypeSummary");async function x(t,e,n){let r;try{const s=await t.send(new J({StackName:e,TemplateStage:"Processed"}),{abortSignal:C(n)});if(s.TemplateBody===void 0)return A(new $("missing-template-body","CloudFormation returned no template body. Cure: retry, or accept conservative classification."));r=s.TemplateBody}catch(s){return A(s instanceof Error?s:new Error(String(s)))}const a=ue(r);return a.success?a:a.error instanceof $&&a.error.reason==="non-json-template"?me(t,e,n):a}u(x,"readStackRecordKeys");function pe(t){return t.Tags?.some(e=>e.Key?.startsWith(oe))===!0?!0:t.StackName?.startsWith(se)===!0}u(pe,"isFjallStack");async function ye(t,e={}){const n=new Map,r=[],a=new Set(e.excludeStackNames??[]),s=[];try{let o;do{if(b(e.abortSignal))throw new Error("stack enumeration aborted by shutdown signal");const c=await t.send(new V({...o!==void 0?{NextToken:o}:{}}),{abortSignal:C(e.abortSignal)});for(const i of c.Stacks??[])i.StackName===void 0||a.has(i.StackName)||i.StackStatus===void 0||!F.has(i.StackStatus)||!pe(i)||s.push(i.StackName);o=c.NextToken}while(o!==void 0)}catch(o){return r.push(N(`zone classification: could not enumerate CloudFormation stacks (${h(o)}) \u2014 satellite ownership cannot be proven, so unmatched records classify 'unknown'. Cure: grant cloudformation:DescribeStacks and retry.`)),{byRecordKey:n,stackNames:[],warnings:r}}for(const o of s){const c=await x(t,o,e.abortSignal);if(!c.success){const i=c.error instanceof $?"":" Cure: grant cloudformation:GetTemplate and retry.";r.push(N(`zone classification: could not read template evidence for stack '${o}' (${h(c.error)}) \u2014 its records classify 'unknown'.${i}`));continue}for(const i of c.data)n.has(i)||n.set(i,o)}return{byRecordKey:n,stackNames:s,warnings:r}}u(ye,"buildSatelliteRecordIndex");function K(t){return new Set(t.map(e=>T(e.trim())).filter(e=>e!==""))}u(K,"normaliseNsValueSet");function we(t,e){if(t.size!==e.size)return!1;for(const n of t)if(!e.has(n))return!1;return!0}u(we,"nsValueSetsEqual");async function he(t){const{factory:e,accountId:n,region:r,zoneName:a,stackName:s,abortSignal:o}=t,c=u(y=>A(new Error(`zone classification: cross-account delegation for '${a}': ${y}`)),"fail");let i;try{const y=await e(n,r);if(!y.success)return c(`account ${n} is unreachable (${h(y.error)}), so the NS row classifies 'unknown'. Cure: restore access to account ${n} and retry.`);i=y.data}catch(y){return c(`account ${n} is unreachable (${h(y)}), so the NS row classifies 'unknown'. Cure: restore access to account ${n} and retry.`)}let p;try{p=await i.cloudFormation.send(new V({StackName:s}),{abortSignal:C(o)})}catch(y){return c(`could not describe stack '${s}' in account ${n} (${h(y)}), so the NS row classifies 'unknown'. Cure: deploy the child with 'fjall domain deploy ${a}' or grant cloudformation:DescribeStacks in that account.`)}const l=p.Stacks?.[0];if(l===void 0)return c(`stack '${s}' was not found in account ${n}, so the NS row classifies 'unknown'. Cure: deploy the child with 'fjall domain deploy ${a}'.`);if(l.StackStatus===void 0||!F.has(l.StackStatus))return c(`stack '${s}' in account ${n} has status ${l.StackStatus??"unknown"}, which does not describe live resources, so the NS row classifies 'unknown'. Cure: repair the child stack, then redeploy with 'fjall domain deploy ${a}'.`);const w=await x(i.cloudFormation,s,o);if(!w.success)return c(`template evidence for stack '${s}' failed in account ${n} (${h(w.error)}), so the NS row classifies 'unknown'.`);if(!w.data.has(R(a,"NS")))return c(`stack '${s}' in account ${n} does not claim the NS row for '${a}', so the row classifies 'unknown'. Cure: redeploy the child with 'fjall domain deploy ${a}' so its delegation resource is current.`);const E=H(a).nameservers,k=(l.Outputs??[]).find(y=>y.ExportName===E)?.OutputValue;if(k===void 0||k.trim()==="")return c(`stack '${s}' in account ${n} publishes no '${E}' output, so the delegated nameserver values cannot be verified and the NS row classifies 'unknown'. Cure: redeploy the child with 'fjall domain deploy ${a}' so the export contract is current.`);const I=K(k.split(",")),O=K(t.liveNsValues);if(!we(I,O)){const y=u(_=>`[${[..._].sort().join(", ")}]`,"quote");return c(`stack '${s}' in account ${n} publishes nameservers ${y(I)} but the live NS row holds ${y(O)}, so the row classifies 'unknown'. Cure: re-run 'fjall domain deploy ${a}' so parent and child agree.`)}return v({ownerStack:s,accountId:n})}u(he,"proveCrossAccountDelegation");async function Se(t,e){const n=new Map;for(const[r,a]of t)try{const s=[];let o;do{if(b(e))throw new Error("certificate listing aborted by shutdown signal");const c=await a.send(new q({...o!==void 0?{NextToken:o}:{}}),{abortSignal:C(e)});for(const i of c.CertificateSummaryList??[])i.CertificateArn!==void 0&&s.push(i.CertificateArn);o=c.NextToken}while(o!==void 0);for(const c of s){if(b(e))throw new Error("certificate description aborted by shutdown signal");const i=await a.send(new X({CertificateArn:c}),{abortSignal:C(e)});for(const p of i.Certificate?.DomainValidationOptions??[]){const l=p.ResourceRecord;if(l?.Type==="CNAME"&&l.Name!==void 0){const w=T(l.Name),E=n.get(w)??new Set;E.add(c),n.set(w,E)}}}}catch(s){return A(new Error(`zone classification: could not read ACM certificates in ${r} (${h(s)}). Cure: grant acm:ListCertificates and acm:DescribeCertificate in ${r}, then retry.`))}return v(n)}u(Se,"collectAcmValidationReferences");async function Ne(t,e,n,r){const a=new Map,s=[];for(const o of e){if(a.size===n.size)break;try{if(b(r))throw new Error("stack resource listing aborted by shutdown signal");const c=await t.send(new G({StackName:o}),{abortSignal:C(r)});for(const i of c.StackResources??[])i.ResourceType!=="AWS::CertificateManager::Certificate"||i.PhysicalResourceId===void 0||!n.has(i.PhysicalResourceId)||a.has(i.PhysicalResourceId)||a.set(i.PhysicalResourceId,o)}catch(c){s.push(N(`zone classification: could not list resources for stack '${o}' (${h(c)}), so certificate ownership cannot be proven there and its validation CNAMEs stay 'unknown'. Cure: grant cloudformation:DescribeStackResources and retry.`))}}return{ownerByArn:a,warnings:s}}u(Ne,"resolveCertificateOwnership");function z(t){if(t.type!=="CNAME")return!1;const e=t.name.split(".")[0]??"";return ie.test(e)?t.values.some(n=>T(n).endsWith(ce)):!1}u(z,"isAcmValidationShaped");function B(t){if(t.type!=="A"&&t.type!=="AAAA")return!1;const e=t.aliasTargetDnsName;return e===void 0?!1:de.some(n=>n.test(e))}u(B,"isAwsManagedAliasCandidate");async function Oe(t){const{zone:e,domainStackName:n,clients:r,abortSignal:a,onPhase:s}=t,o=[];s?.("read-records");let c;try{c=await le(r.route53,e.hostedZoneId,a)}catch(d){return A(new Error(`zone classification: could not list record sets for zone ${e.hostedZoneId} (${e.zoneName}): ${h(d)}. Cure: grant route53:ListResourceRecordSets on the zone and retry.`))}let i=new Set,p=!0;if(n!==void 0){const d=await x(r.cloudFormation,n,a);if(d.success)i=d.data;else{p=!1;const m=d.error instanceof $?"":" or grant cloudformation:GetTemplate";o.push(N(`zone classification: could not read the domain stack template for '${n}' (${h(d.error)}) \u2014 only zone-owned SOA/NS classify 'declared'. Cure: deploy the domain stack with 'fjall domain deploy ${e.zoneName}'${m}.`))}}s?.("index-stacks");const l=t.satelliteIndex??await ye(r.cloudFormation,{...n!==void 0?{excludeStackNames:[n]}:{},...a!==void 0?{abortSignal:a}:{}});o.push(...l.warnings);const w=new Map;if(t.delegatedChildren!==void 0){const d=new Map;for(const f of c)f.type==="NS"&&d.set(R(f.name,f.type,f.setIdentifier),f.values);const m=t.delegatedChildren.filter(f=>{const g=R(f.zoneName,"NS");return d.has(g)&&!i.has(g)&&!l.byRecordKey.has(g)}),S=t.childAccountClientFactory;if(m.length>0&&S===void 0)o.push(N("zone classification: delegated child domains are configured but no child-account client factory was wired, so cross-account delegations cannot be verified and their NS rows classify 'unknown'. Cure: supply childAccountClientFactory in ClassifyZoneOptions."));else if(S!==void 0)for(const f of m){if(f.accountId===void 0){o.push(N(`zone classification: delegated child '${f.zoneName}' has no AWS account id, so its cross-account delegation cannot be verified and its NS row classifies 'unknown'. Cure: set "account" on the delegated domain entry in fjall-config.json.`));continue}const g=R(f.zoneName,"NS"),D=await he({factory:S,accountId:f.accountId,...f.region!==void 0?{region:f.region}:{},zoneName:f.zoneName,stackName:f.stackName??Q(f.zoneName),liveNsValues:d.get(g)??[],...a!==void 0?{abortSignal:a}:{}});if(!D.success){o.push(N(h(D.error)));continue}w.set(g,D.data)}}s?.("check-acm");const E=c.some(z);let k;if(E){const d=await Se(r.acmByRegion,a);d.success?k=d.data:o.push(N(d.error.message))}let I;if(k!==void 0){const d=new Set;for(const m of c)if(z(m))for(const S of k.get(m.name)??[])d.add(S);if(d.size>0){const m=await Ne(r.cloudFormation,[...n!==void 0?[n]:[],...l.stackNames],d,a);I=m.ownerByArn,o.push(...m.warnings)}}const O=p&&l.warnings.length===0,y=new Set;if(O)for(const d of c){if(!B(d))continue;const m=R(d.name,d.type,d.setIdentifier);i.has(m)||l.byRecordKey.has(m)||y.add(d.aliasTargetDnsName)}let _;if(y.size>0){const d=t.dnsProbe??re,m=new Map;for(const S of y){let f;try{f=await d(S,a)}catch(g){f="indeterminate",o.push(N(`zone classification: DNS probe for alias target '${S}' failed (${h(g)}) \u2014 dependent alias records classify 'unknown', never 'residue'. Cure: retry with a working resolver.`)),m.set(S,f);continue}f==="indeterminate"&&o.push(N(`zone classification: DNS probe for alias target '${S}' was inconclusive \u2014 dependent alias records classify 'unknown', never 'residue'. Cure: retry once the resolver answers definitively.`)),m.set(S,f)}_=m}s?.("classify");const M=T(e.zoneName),L=c.map(d=>ge(d,{zoneApex:M,domainStackName:n,declaredKeys:i,satelliteIndex:l,crossAccountDelegations:w,acmValidationReferences:k,certificateOwnerByArn:I,templateEvidenceComplete:O,aliasProbeVerdicts:_})).sort((d,m)=>d.name.localeCompare(m.name)||d.type.localeCompare(m.type)||(d.setIdentifier??"").localeCompare(m.setIdentifier??"")),P={declared:0,satellite:0,residue:0,unknown:0};for(const d of L)P[d.classification]+=1;return v({hostedZoneId:e.hostedZoneId,zoneName:M,records:L,counts:P,warnings:o})}u(Oe,"classifyZoneRecords");function ke(t,e){const n=new Set;for(const r of t){const a=e?.get(r);if(a===void 0)return;n.add(a)}return n}u(ke,"provenCertificateOwners");function ge(t,e){const n=R(t.name,t.type,t.setIdentifier),r={name:t.name,type:t.type,...t.setIdentifier!==void 0?{setIdentifier:t.setIdentifier}:{},values:t.values};if(t.name===e.zoneApex&&(t.type==="SOA"||t.type==="NS"))return{...r,classification:"declared",...e.domainStackName!==void 0?{ownerStack:e.domainStackName}:{},detail:"zone-owned record, created with the hosted zone"};if(e.declaredKeys.has(n))return{...r,classification:"declared",...e.domainStackName!==void 0?{ownerStack:e.domainStackName}:{}};const a=e.satelliteIndex.byRecordKey.get(n);if(a!==void 0)return{...r,classification:"satellite",ownerStack:a};const s=e.crossAccountDelegations.get(n);if(s!==void 0)return{...r,classification:"satellite",ownerStack:s.ownerStack,detail:`cross-account delegation: stack ${s.ownerStack} in account ${s.accountId}`};if(z(t)){if(e.acmValidationReferences===void 0)return{...r,classification:"unknown",detail:"ACM validation CNAME shape, but live-certificate evidence was unavailable \u2014 fail-closed as unknown rather than residue"};const o=e.acmValidationReferences.get(t.name);if(o!==void 0&&o.size>0){const c=ke(o,e.certificateOwnerByArn);if(c!==void 0){const i=[...c].sort();if(e.domainStackName!==void 0&&i.every(l=>l===e.domainStackName))return{...r,classification:"declared",ownerStack:e.domainStackName,detail:"ACM validation CNAME for the domain stack's live certificates; required for issuance and auto-renewal"};const p=i.find(l=>l!==e.domainStackName);if(p!==void 0)return{...r,classification:"satellite",ownerStack:p,detail:`ACM validation CNAME for live certificates owned by ${i.map(l=>`'${l}'`).join(", ")}; required for issuance and auto-renewal`}}return{...r,classification:"unknown",detail:"ACM validation CNAME still referenced by a live certificate \u2014 keep it; it is not residue"}}return{...r,classification:"residue",detail:"ACM validation CNAME not referenced by any live certificate in the scanned regions"}}if(B(t)){if(!e.templateEvidenceComplete)return{...r,classification:"unknown",detail:"alias to an AWS-managed target, but template evidence was incomplete \u2014 fail-closed as unknown rather than residue"};const o=e.aliasProbeVerdicts?.get(t.aliasTargetDnsName);return o==="nxdomain"?{...r,classification:"residue",detail:`dangling alias: target ${t.aliasTargetDnsName} no longer exists (NXDOMAIN) and no live template references this record`}:o==="resolves"?{...r,classification:"unknown",detail:"alias target still exists in DNS, so the record cannot be dangling; not residue"}:{...r,classification:"unknown",detail:"alias target probe was inconclusive \u2014 fail-closed as unknown rather than residue"}}return{...r,classification:"unknown"}}u(ge,"classifyRecord");export{$ as TemplateEvidenceError,ye as buildSatelliteRecordIndex,Oe as classifyZoneRecords,ae as createSystemDnsNameProbe,ue as extractTemplateRecordKeys,T as normaliseRecordName,re as systemDnsNameProbe};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fjall/deploy-core",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.9.0",
|
|
4
4
|
"description": "Shared deployment engine for Fjall — used by CLI and webapp worker",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/src/index.js",
|
|
@@ -88,8 +88,8 @@
|
|
|
88
88
|
"@aws-sdk/client-ssm": "^3.1038.0",
|
|
89
89
|
"@aws-sdk/client-sso-admin": "^3.1038.0",
|
|
90
90
|
"@aws-sdk/client-sts": "^3.1038.0",
|
|
91
|
-
"@fjall/generator": "^3.
|
|
92
|
-
"@fjall/util": "^3.
|
|
91
|
+
"@fjall/generator": "^3.9.0",
|
|
92
|
+
"@fjall/util": "^3.9.0",
|
|
93
93
|
"@smithy/node-http-handler": "^4.6.1",
|
|
94
94
|
"aws-cdk": "^2.1128.1",
|
|
95
95
|
"tsx": "^4.21.0",
|
|
@@ -100,5 +100,5 @@
|
|
|
100
100
|
"typescript": "^6.0.3",
|
|
101
101
|
"vitest": "^4.1.5"
|
|
102
102
|
},
|
|
103
|
-
"gitHead": "
|
|
103
|
+
"gitHead": "7cef1c5c3c09bb333888e65ede3d92b2d88c3c63"
|
|
104
104
|
}
|