@fjall/util 34.0.0 → 36.0.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 CHANGED
@@ -1 +1 @@
1
- 124 files minified at 2026-09-05T22:58:50.480Z
1
+ 129 files minified at 2026-09-10T10:35:24.637Z
@@ -6,6 +6,6 @@ export { FAILURE_ANALYSIS_CATEGORIES, type FailureAnalysisCategory } from "./fai
6
6
  export { maskFailureAnalysis } from "./maskFailureAnalysis.js";
7
7
  export { classifyDriftSuspectEvents, findCurrentOperationStart, isDriftSuspectReason, isOperationOpeningEvent, type DriftSuspectEvent } from "./driftSuspects.js";
8
8
  export { IPAM_OPERATIONS_POOL_TAG_KEY, formatIpamPairTagValue } from "./ipamTags.js";
9
- export { KMS_RETENTION_TAG_KEY, S3_VERSIONING_EXEMPT_TAG_KEY, S3_VERSIONING_EXEMPTIONS, SDK_PRE_EMPTY_TAG_KEY, type S3VersioningExemption } from "./infraTags.js";
9
+ export { FJALL_RESOURCE_TYPE_TAG_KEY, FJALL_RESOURCE_TYPES, type FjallResourceType, KMS_RETENTION_TAG_KEY, S3_VERSIONING_EXEMPT_TAG_KEY, S3_VERSIONING_EXEMPTIONS, SDK_PRE_EMPTY_TAG_KEY, type S3VersioningExemption } from "./infraTags.js";
10
10
  export { COST_ALLOCATION_TAGS, DEFAULT_COST_ALLOCATION_ENVIRONMENT, DEFAULT_COST_ALLOCATION_OWNER, ecrCreateRepositoryTags, rdsPreMigrationSnapshotTags, type AwsSdkTag, type EcrCreateTag } from "./costAllocationTags.js";
11
11
  export { ACCOUNT_MONITORING_ROLE_NAME } from "./monitoringRole.js";
package/dist/aws/index.js CHANGED
@@ -1 +1 @@
1
- import{AWSError as e,NoRolesFoundError as E,InvalidCredentialsError as _,SSOTokenExpiredError as t,MissingRegionError as T,ProfileNotFoundError as O,CommandError as s,isAWSError as i,isNoRolesFoundError as A,isSSOUnauthorizedError as S}from"./errors.js";import{STACK_NOT_FOUND_PATTERN as N,CDK_NO_STACKS_MATCH as n,isResourceEvent as p}from"./cloudformationTypes.js";import{countResourceProgress as R,formatElapsed as f}from"./deployProgress.js";import{CloudFormationFailureAnalyser as C}from"./CloudFormationFailureAnalyser.js";import{FAILURE_ANALYSIS_CATEGORIES as l}from"./failureAnalysisCategories.js";import{maskFailureAnalysis as x}from"./maskFailureAnalysis.js";import{classifyDriftSuspectEvents as P,findCurrentOperationStart as M,isDriftSuspectReason as F,isOperationOpeningEvent as G}from"./driftSuspects.js";import{IPAM_OPERATIONS_POOL_TAG_KEY as g,formatIpamPairTagValue as c}from"./ipamTags.js";import{KMS_RETENTION_TAG_KEY as U,S3_VERSIONING_EXEMPT_TAG_KEY as Y,S3_VERSIONING_EXEMPTIONS as v,SDK_PRE_EMPTY_TAG_KEY as y}from"./infraTags.js";import{COST_ALLOCATION_TAGS as W,DEFAULT_COST_ALLOCATION_ENVIRONMENT as h,DEFAULT_COST_ALLOCATION_OWNER as k,ecrCreateRepositoryTags as X,rdsPreMigrationSnapshotTags as z}from"./costAllocationTags.js";import{ACCOUNT_MONITORING_ROLE_NAME as b}from"./monitoringRole.js";export{b as ACCOUNT_MONITORING_ROLE_NAME,e as AWSError,n as CDK_NO_STACKS_MATCH,W as COST_ALLOCATION_TAGS,C as CloudFormationFailureAnalyser,s as CommandError,h as DEFAULT_COST_ALLOCATION_ENVIRONMENT,k as DEFAULT_COST_ALLOCATION_OWNER,l as FAILURE_ANALYSIS_CATEGORIES,g as IPAM_OPERATIONS_POOL_TAG_KEY,_ as InvalidCredentialsError,U as KMS_RETENTION_TAG_KEY,T as MissingRegionError,E as NoRolesFoundError,O as ProfileNotFoundError,v as S3_VERSIONING_EXEMPTIONS,Y as S3_VERSIONING_EXEMPT_TAG_KEY,y as SDK_PRE_EMPTY_TAG_KEY,t as SSOTokenExpiredError,N as STACK_NOT_FOUND_PATTERN,P as classifyDriftSuspectEvents,R as countResourceProgress,X as ecrCreateRepositoryTags,M as findCurrentOperationStart,f as formatElapsed,c as formatIpamPairTagValue,i as isAWSError,F as isDriftSuspectReason,A as isNoRolesFoundError,G as isOperationOpeningEvent,p as isResourceEvent,S as isSSOUnauthorizedError,x as maskFailureAnalysis,z as rdsPreMigrationSnapshotTags};
1
+ import{AWSError as E,NoRolesFoundError as e,InvalidCredentialsError as _,SSOTokenExpiredError as T,MissingRegionError as t,ProfileNotFoundError as O,CommandError as A,isAWSError as s,isNoRolesFoundError as S,isSSOUnauthorizedError as i}from"./errors.js";import{STACK_NOT_FOUND_PATTERN as N,CDK_NO_STACKS_MATCH as n,isResourceEvent as R}from"./cloudformationTypes.js";import{countResourceProgress as C,formatElapsed as I}from"./deployProgress.js";import{CloudFormationFailureAnalyser as m}from"./CloudFormationFailureAnalyser.js";import{FAILURE_ANALYSIS_CATEGORIES as u}from"./failureAnalysisCategories.js";import{maskFailureAnalysis as l}from"./maskFailureAnalysis.js";import{classifyDriftSuspectEvents as x,findCurrentOperationStart as F,isDriftSuspectReason as M,isOperationOpeningEvent as G}from"./driftSuspects.js";import{IPAM_OPERATIONS_POOL_TAG_KEY as Y,formatIpamPairTagValue as g}from"./ipamTags.js";import{FJALL_RESOURCE_TYPE_TAG_KEY as c,FJALL_RESOURCE_TYPES as D,KMS_RETENTION_TAG_KEY as v,S3_VERSIONING_EXEMPT_TAG_KEY as y,S3_VERSIONING_EXEMPTIONS as V,SDK_PRE_EMPTY_TAG_KEY as W}from"./infraTags.js";import{COST_ALLOCATION_TAGS as k,DEFAULT_COST_ALLOCATION_ENVIRONMENT as J,DEFAULT_COST_ALLOCATION_OWNER as X,ecrCreateRepositoryTags as z,rdsPreMigrationSnapshotTags as H}from"./costAllocationTags.js";import{ACCOUNT_MONITORING_ROLE_NAME as j}from"./monitoringRole.js";export{j as ACCOUNT_MONITORING_ROLE_NAME,E as AWSError,n as CDK_NO_STACKS_MATCH,k as COST_ALLOCATION_TAGS,m as CloudFormationFailureAnalyser,A as CommandError,J as DEFAULT_COST_ALLOCATION_ENVIRONMENT,X as DEFAULT_COST_ALLOCATION_OWNER,u as FAILURE_ANALYSIS_CATEGORIES,D as FJALL_RESOURCE_TYPES,c as FJALL_RESOURCE_TYPE_TAG_KEY,Y as IPAM_OPERATIONS_POOL_TAG_KEY,_ as InvalidCredentialsError,v as KMS_RETENTION_TAG_KEY,t as MissingRegionError,e as NoRolesFoundError,O as ProfileNotFoundError,V as S3_VERSIONING_EXEMPTIONS,y as S3_VERSIONING_EXEMPT_TAG_KEY,W as SDK_PRE_EMPTY_TAG_KEY,T as SSOTokenExpiredError,N as STACK_NOT_FOUND_PATTERN,x as classifyDriftSuspectEvents,C as countResourceProgress,z as ecrCreateRepositoryTags,F as findCurrentOperationStart,I as formatElapsed,g as formatIpamPairTagValue,s as isAWSError,M as isDriftSuspectReason,S as isNoRolesFoundError,G as isOperationOpeningEvent,R as isResourceEvent,i as isSSOUnauthorizedError,l as maskFailureAnalysis,H as rdsPreMigrationSnapshotTags};
@@ -32,6 +32,46 @@ export declare const SDK_PRE_EMPTY_TAG_KEY = "fjall:sdk-pre-empty";
32
32
  * `__tests__/infraTags.test.ts` guards against casual renames.
33
33
  */
34
34
  export declare const KMS_RETENTION_TAG_KEY = "fjall:kms:retention";
35
+ /**
36
+ * The Fjall resource-type marker tag key, shared by producers and consumers:
37
+ *
38
+ * - Producer (`@fjall/components-infrastructure`): `applyResourceTypeTag` /
39
+ * `markDeployTimeWriter` (`lib/utils/resourceTypeTags.ts`) stamp it on the
40
+ * VPC peering constructs and on every custom-resource writer subtree.
41
+ * - Producer (the Quick-Create connector template,
42
+ * `public-assets/fjall/public-assets/assets/oidc-connector.yaml`): the three
43
+ * writer role/function pairs carry the same key and value as the CDK twin.
44
+ * - Consumer (webapp posture): `isDeployTimeOnlyRole`
45
+ * (`app/.server/services/compliance-rules/complianceUtils.ts`) matches the
46
+ * marker before any role-name pattern, so a CloudFormation-named role is
47
+ * excluded from `IAM_ROLE_UNUSED` by what it IS, not what it is called.
48
+ *
49
+ * The literal is deployed infrastructure state: renaming the key strips every
50
+ * already-tagged resource of its marker until the next deploy, and the posture
51
+ * exclusion silently falls back to the legacy name patterns. The pin test in
52
+ * `__tests__/infraTags.test.ts` guards against casual renames.
53
+ */
54
+ export declare const FJALL_RESOURCE_TYPE_TAG_KEY = "fjall:resource:type";
55
+ /**
56
+ * The closed vocabulary of `fjall:resource:type` values. One value per class
57
+ * of resource whose identity cannot be read off its physical name — never a
58
+ * second tag key, and never a per-construct value: `deployTimeWriter` is the
59
+ * single class for every role that CloudFormation assumes only while driving a
60
+ * custom resource (connector writers, IPAM, RDS, TLS, return-routes, identity
61
+ * store, aws-cdk-lib's own hoisted providers), the class the unused-role
62
+ * exclusion carves out — roles Fjall does not emit (CDK bootstrap, AWS
63
+ * Organizations) stay on that exclusion's name patterns.
64
+ *
65
+ * CDK resolves tag conflicts deeper-scope-wins, so a writer nested inside a
66
+ * `vpcPeering` subtree resolves to `deployTimeWriter` — the more specific
67
+ * truth about that role.
68
+ */
69
+ export declare const FJALL_RESOURCE_TYPES: {
70
+ readonly vpcPeering: "vpc-peering";
71
+ readonly vpcPeeringAccepter: "vpc-peering-accepter";
72
+ readonly deployTimeWriter: "deploy-time-writer";
73
+ };
74
+ export type FjallResourceType = (typeof FJALL_RESOURCE_TYPES)[keyof typeof FJALL_RESOURCE_TYPES];
35
75
  /**
36
76
  * The S3 versioning-exemption marker tag, shared by producer and consumer:
37
77
  *
@@ -1 +1 @@
1
- const t="fjall:sdk-pre-empty",e="fjall:kms:retention",o="fjall:s3:versioning-exempt",E=["build-output","isr-cache","cold-tier","access-logs"];export{e as KMS_RETENTION_TAG_KEY,E as S3_VERSIONING_EXEMPTIONS,o as S3_VERSIONING_EXEMPT_TAG_KEY,t as SDK_PRE_EMPTY_TAG_KEY};
1
+ const e="fjall:sdk-pre-empty",t="fjall:kms:retention",r="fjall:resource:type",p={vpcPeering:"vpc-peering",vpcPeeringAccepter:"vpc-peering-accepter",deployTimeWriter:"deploy-time-writer"},c="fjall:s3:versioning-exempt",o=["build-output","isr-cache","cold-tier","access-logs"];export{p as FJALL_RESOURCE_TYPES,r as FJALL_RESOURCE_TYPE_TAG_KEY,t as KMS_RETENTION_TAG_KEY,o as S3_VERSIONING_EXEMPTIONS,c as S3_VERSIONING_EXEMPT_TAG_KEY,e as SDK_PRE_EMPTY_TAG_KEY};
@@ -0,0 +1,13 @@
1
+ /**
2
+ * `@fjall/util/posturePolicy` — the tier-aware alert policy's client-safe
3
+ * data and words (design 2026-09-01-tier-aware-alert-policy § 13): the
4
+ * expected and mitigated registries, the status-label table, the machine
5
+ * lanes and the sentence builders the create surfaces print. No node
6
+ * built-ins: the webapp imports this subpath in browser code and the CLI in
7
+ * its Ink screens. The webapp's scan predicate, the rule titles and the
8
+ * lane writers' actor sentinel stay server-side in the webapp.
9
+ */
10
+ export { ISSUE_STATUS_LABELS, countClause, listStatusClauses, storedStatusClause, type IssueStatusLabels } from "./issueStatusLabels.js";
11
+ export { MACHINE_LANE_STATUSES, isMachineLaneStatus, type MachineLaneStatus } from "./machineLane.js";
12
+ export { DEFAULT_POLICY_REGISTRY, EXPECTED_LANE_STAGES, EXPECTED_POLICY, MITIGATED_POLICY, cureClause, evidenceClause, expectedRowCause, findPolicyRow, findPolicyRowByKey, isExpectedLaneStage, laneGroupHeading, policyRowKey, policyRowPresentation, type ExpectedLaneStage, type ExpectedPolicyRow, type MitigatedPolicyRow, type PolicyRegistry, type PolicyResourceScope, type PolicyRowLookup, type PolicyRowMatch, type PolicyRowPresentation } from "./registry.js";
13
+ export { TIER_CONSENT_NOTE_LABEL, expectedLaneWhereClause, expectedRowsAtDefault, tierConsentLead, tierConsentLine, type TierConsentLead, type TierConsentLine } from "./tierConsent.js";
@@ -0,0 +1 @@
1
+ import{ISSUE_STATUS_LABELS as o,countClause as E,listStatusClauses as a,storedStatusClause as s}from"./issueStatusLabels.js";import{MACHINE_LANE_STATUSES as i,isMachineLaneStatus as C}from"./machineLane.js";import{DEFAULT_POLICY_REGISTRY as r,EXPECTED_LANE_STAGES as L,EXPECTED_POLICY as u,MITIGATED_POLICY as T,cureClause as l,evidenceClause as _,expectedRowCause as c,findPolicyRow as p,findPolicyRowByKey as A,isExpectedLaneStage as d,laneGroupHeading as I,policyRowKey as R,policyRowPresentation as x}from"./registry.js";import{TIER_CONSENT_NOTE_LABEL as f,expectedLaneWhereClause as y,expectedRowsAtDefault as w,tierConsentLead as N,tierConsentLine as D}from"./tierConsent.js";export{r as DEFAULT_POLICY_REGISTRY,L as EXPECTED_LANE_STAGES,u as EXPECTED_POLICY,o as ISSUE_STATUS_LABELS,i as MACHINE_LANE_STATUSES,T as MITIGATED_POLICY,f as TIER_CONSENT_NOTE_LABEL,E as countClause,l as cureClause,_ as evidenceClause,y as expectedLaneWhereClause,c as expectedRowCause,w as expectedRowsAtDefault,p as findPolicyRow,A as findPolicyRowByKey,d as isExpectedLaneStage,C as isMachineLaneStatus,I as laneGroupHeading,a as listStatusClauses,R as policyRowKey,x as policyRowPresentation,s as storedStatusClause,N as tierConsentLead,D as tierConsentLine};
@@ -0,0 +1,83 @@
1
+ /**
2
+ * The words a ComplianceIssue status is shown with — the one home for that
3
+ * copy. `../complianceIssue` owns the vocabulary's meaning
4
+ * (`COMPLIANCE_ISSUE_STATUSES`); this table renders it, and every surface
5
+ * that prints a status — the webapp's badges, panels, digest and export, the
6
+ * CLI's create-time consent note — takes the words from here, so two clients
7
+ * cannot disagree on a sentence. Client-safe.
8
+ *
9
+ * Three renderings per status, because they do not collapse into one (alert
10
+ * policy design § 13.11 (e)): the badge is a single capitalised word beside
11
+ * a finding; the clause is a predicate that reads after "is" and after a
12
+ * count ("this finding is expected at this stage", "3 expected at this
13
+ * stage"); the heading is what a lane's rows sit under on the asset panel,
14
+ * the lane digest and the CSV export. The two machine lanes never share a
15
+ * word (ruled). A full `Record` over the vocabulary, so a status added to
16
+ * it fails here at typecheck, at the one place its copy is authored.
17
+ */
18
+ import { type ComplianceIssueStatus } from "../complianceIssue.js";
19
+ export interface IssueStatusLabels {
20
+ /** One capitalised word, the badge beside a finding. */
21
+ badge: string;
22
+ /** Lower-case predicate: reads after "is" and after a count. */
23
+ clause: string;
24
+ /**
25
+ * The heading a lane's rows sit under on the asset panel and the digest,
26
+ * and the export's count column for that lane. `null` for `open`: its rows
27
+ * are the work band and sit under no heading. (A verifying row joins that
28
+ * band on the panel — the band is the scoring set, the webapp's
29
+ * `issue-display-groups` — yet keeps a heading for its export column.)
30
+ */
31
+ heading: string | null;
32
+ }
33
+ export declare const ISSUE_STATUS_LABELS: {
34
+ readonly open: {
35
+ readonly badge: "Open";
36
+ readonly clause: "open";
37
+ readonly heading: null;
38
+ };
39
+ readonly acknowledged: {
40
+ readonly badge: "Acknowledged";
41
+ readonly clause: "acknowledged";
42
+ readonly heading: "Acknowledged";
43
+ };
44
+ readonly pending_verification: {
45
+ readonly badge: "Verifying";
46
+ readonly clause: "awaiting verification";
47
+ readonly heading: "Awaiting verification";
48
+ };
49
+ readonly resolved: {
50
+ readonly badge: "Resolved";
51
+ readonly clause: "resolved";
52
+ readonly heading: "Resolved";
53
+ };
54
+ readonly suppressed: {
55
+ readonly badge: "Suppressed";
56
+ readonly clause: "suppressed";
57
+ readonly heading: "Suppressed";
58
+ };
59
+ readonly expected: {
60
+ readonly badge: "Expected";
61
+ readonly clause: "expected at this stage";
62
+ readonly heading: "Expected at this stage";
63
+ };
64
+ readonly mitigated: {
65
+ readonly badge: "Mitigated";
66
+ readonly clause: "mitigated";
67
+ readonly heading: "Mitigated";
68
+ };
69
+ };
70
+ /**
71
+ * The clause of a stored status, for a message that names the state a row
72
+ * is in. A stored value outside the vocabulary is a later writer's: it is
73
+ * shown quoted as the token it is, never dressed as one of ours.
74
+ */
75
+ export declare function storedStatusClause(status: string): string;
76
+ /** `"N <clause>"` — the count clause every conformance figure's sublabel is built from. */
77
+ export declare function countClause(count: number, status: ComplianceIssueStatus): string;
78
+ /**
79
+ * The statuses' clauses as one list in vocabulary order — "a, b or c" —
80
+ * whatever order the caller holds them in, so a sentence derived from a
81
+ * policy set reads the same as the set's own declaration order changes.
82
+ */
83
+ export declare function listStatusClauses(statuses: readonly ComplianceIssueStatus[]): string;
@@ -0,0 +1 @@
1
+ var c=Object.defineProperty;var t=(e,a)=>c(e,"name",{value:a,configurable:!0});import{COMPLIANCE_ISSUE_STATUSES as g,isComplianceIssueStatus as u}from"../complianceIssue.js";const i={open:{badge:"Open",clause:"open",heading:null},acknowledged:{badge:"Acknowledged",clause:"acknowledged",heading:"Acknowledged"},pending_verification:{badge:"Verifying",clause:"awaiting verification",heading:"Awaiting verification"},resolved:{badge:"Resolved",clause:"resolved",heading:"Resolved"},suppressed:{badge:"Suppressed",clause:"suppressed",heading:"Suppressed"},expected:{badge:"Expected",clause:"expected at this stage",heading:"Expected at this stage"},mitigated:{badge:"Mitigated",clause:"mitigated",heading:"Mitigated"}};function r(e){return u(e)?i[e].clause:`"${e}"`}t(r,"storedStatusClause");function S(e,a){return`${e} ${i[a].clause}`}t(S,"countClause");const d=new Map(g.map((e,a)=>[e,a]));function f(e){const a=[...new Set(e)].sort((s,o)=>(d.get(s)??0)-(d.get(o)??0)).map(s=>i[s].clause),n=a.pop();return n===void 0?"":a.length===0?n:`${a.join(", ")} or ${n}`}t(f,"listStatusClauses");export{i as ISSUE_STATUS_LABELS,S as countClause,f as listStatusClauses,r as storedStatusClause};
@@ -0,0 +1,13 @@
1
+ /**
2
+ * The machine lanes of the alert policy (design
3
+ * 2026-09-01-tier-aware-alert-policy § 13): the two statuses only the scan
4
+ * writes, each spoken for by one registry row (`./registry`). The lane
5
+ * arbitration's write type, the digest's read predicate and the asset
6
+ * panel's grouping all derive from this pair, so a third lane reaches each
7
+ * of them at once. Lives beside the registry because `PolicyRowMatch` is
8
+ * typed on it; the webapp's status classifier re-exports it.
9
+ */
10
+ export declare const MACHINE_LANE_STATUSES: readonly ["expected", "mitigated"];
11
+ export type MachineLaneStatus = (typeof MACHINE_LANE_STATUSES)[number];
12
+ /** Narrows a stored status to a machine lane — the read guard of every lane surface. */
13
+ export declare function isMachineLaneStatus(status: string): status is MachineLaneStatus;
@@ -0,0 +1 @@
1
+ var E=Object.defineProperty;var e=(t,n)=>E(t,"name",{value:n,configurable:!0});const S=["expected","mitigated"],a=new Set(S);function i(t){return a.has(t)}e(i,"isMachineLaneStatus");export{S as MACHINE_LANE_STATUSES,i as isMachineLaneStatus};
@@ -0,0 +1,202 @@
1
+ /**
2
+ * The expected and mitigated lanes of the alert policy (design
3
+ * 2026-09-01-tier-aware-alert-policy § 13). Two static registries keyed by
4
+ * rule id, client-safe (no node built-ins) and shared by every surface that
5
+ * speaks for a finding: the webapp's scan predicate, asset panel, digest and
6
+ * export, and the CLI's create-time consent note.
7
+ *
8
+ * - `EXPECTED_POLICY`: findings a construct knob's default deliberately
9
+ * produces on Fjall-managed infrastructure. Softened to `expected` only on
10
+ * a development- or staging-stage account (`EXPECTED_LANE_STAGES`) of the
11
+ * account tier; on every other stage, on an organisation- or platform-tier
12
+ * account, and for an organisation that set posture to strict, they flag.
13
+ * - `MITIGATED_POLICY`: findings a compensating control already meets
14
+ * (§ 13.4a). Never a finding at any stage or tier, counted as met in both
15
+ * scores, visible with the evidence pointer.
16
+ *
17
+ * Everything unlisted flags, and every predicate failure flags
18
+ * (fail-toward-flag). The predicate is the webapp's, server-side
19
+ * (`app/.server/services/compliance-rules/expectedPolicy.ts`): it reads the
20
+ * scan's policy context, which no client has. The rule ids are the webapp's
21
+ * rule registry's; its tests cross-check every row against it, and against
22
+ * the remediation edit each cure names.
23
+ */
24
+ import type { AccountStage } from "../environments.js";
25
+ import type { BackupTierDefaults } from "../backupTiers.js";
26
+ import type { MachineLaneStatus } from "./machineLane.js";
27
+ /** Account stages on which an expected-lane finding is softened. */
28
+ export declare const EXPECTED_LANE_STAGES: readonly ["development", "staging"];
29
+ export type ExpectedLaneStage = (typeof EXPECTED_LANE_STAGES)[number];
30
+ export declare function isExpectedLaneStage(stage: AccountStage): stage is ExpectedLaneStage;
31
+ /**
32
+ * Narrows a row to the resources it speaks for, by one of two signals:
33
+ *
34
+ * - `logicalIdContains`: CloudFormation tags each resource with its logical
35
+ * id, which the scan stores as `iacInfo.logicalId`; the id is the construct
36
+ * path joined with a trailing hash, so the row matches on the construct's
37
+ * own id as a substring. Only for an id nothing but that construct can
38
+ * carry — a fragment a customer's own construct could share
39
+ * (`assetsBucket`) would match their production uploads bucket.
40
+ * - `tag`: a tag the construct stamps on the resource, read from the asset's
41
+ * stored tags. The key alone matches on presence; with `value`, on that
42
+ * value. An asset whose tags the scan has never read (`tags: null`)
43
+ * matches nothing, and a partial read keeps the prior tags, so a throttled
44
+ * scan neither gains nor loses a row here.
45
+ */
46
+ export type PolicyResourceScope = {
47
+ logicalIdContains: string;
48
+ } | {
49
+ tag: {
50
+ key: string;
51
+ value?: string;
52
+ };
53
+ };
54
+ /**
55
+ * A `BACKUP_TIER_DEFAULTS` column whose `false` produces a K1 finding: the
56
+ * knob the app's backup tier owns and the construct reads through
57
+ * `tierDefault`. Boolean columns only — the K1 rows are cost-saving defaults
58
+ * that are off, so "off" is the one polarity the registry speaks.
59
+ */
60
+ type BackupTierFindingKnob = {
61
+ [K in keyof BackupTierDefaults]: BackupTierDefaults[K] extends boolean ? K : never;
62
+ }[keyof BackupTierDefaults];
63
+ export interface ExpectedPolicyRow {
64
+ ruleId: string;
65
+ /** Present on a resource-scoped row: for that resource a scoped row wins over the rule's unscoped row. */
66
+ resource?: PolicyResourceScope;
67
+ /** The knob whose default causes the finding: what the user sets, and on which construct. Groups findings by cause on the asset page. */
68
+ knob: {
69
+ name: string;
70
+ construct: string;
71
+ };
72
+ /**
73
+ * Where the knob is a tier (`backup.tier`): the tier-defaults column that
74
+ * decides the finding, `false` producing it. The create-app wizard and
75
+ * the CLI's create flow read it to list, for a chosen tier, the rows the
76
+ * tier leaves at that default (`./tierConsent`); a row without one is
77
+ * produced at every tier.
78
+ */
79
+ tierDefault?: BackupTierFindingKnob;
80
+ /**
81
+ * What the knob's default chose and why it is cheaper. A lower-case clause
82
+ * that reads after "Expected on a development-stage account: ", no
83
+ * trailing full stop.
84
+ */
85
+ expectation: string;
86
+ /**
87
+ * The cure exactly as the user writes it in `infrastructure.ts`: the one
88
+ * edit the rule's RemediationHint applies, so this clause and the Remediate
89
+ * button name the same change (`${edit.property}: ${formatEditValue(edit.value)}`
90
+ * — a string value quoted, as the PR writes it — pinned by the webapp's
91
+ * expectedPolicy.test). Where the knob is a tier (`backup.tier`) the cure is
92
+ * deliberately the per-construct override the remediation lane writes, not
93
+ * the tier: the heading names the cause, the cure names the edit.
94
+ */
95
+ cure: string;
96
+ /** A high-severity finding is softened only with this per-row opt-in (§ 5c); critical never is. */
97
+ allowHigh?: true;
98
+ }
99
+ export interface MitigatedPolicyRow {
100
+ ruleId: string;
101
+ resource?: PolicyResourceScope;
102
+ /**
103
+ * The resource the control covers — a short noun phrase ("cold-tier
104
+ * bucket"; capitals only for an acronym, "ISR cache bucket") that reads
105
+ * after "Mitigated — " as the row's cause on every surface that heads its
106
+ * group. The S3 rows take it from their exemption control, so a stamp
107
+ * value without a name fails typecheck like one without a control.
108
+ */
109
+ name: string;
110
+ /** The compensating control. A lower-case clause that reads after "Mitigated: ", no trailing full stop. */
111
+ control: string;
112
+ /** Where a reader verifies the control. */
113
+ evidence: string;
114
+ allowHigh?: true;
115
+ }
116
+ /**
117
+ * The K1 rows: findings a construct's cost-saving default produces on
118
+ * Fjall-managed infrastructure by design. A rule two constructs produce for
119
+ * two causes carries two rows — the construct with a fixed path scoped by
120
+ * its logical id, the open set unscoped — and the scoped row wins.
121
+ */
122
+ export declare const EXPECTED_POLICY: readonly ExpectedPolicyRow[];
123
+ /**
124
+ * The K4 rows: a shape Fjall's own engineering chose for one construct at
125
+ * every tier, met by a compensating control. Each is scoped to the resources
126
+ * its construct produces (`PolicyResourceScope`); precedence among scoped
127
+ * rows is `findPolicyRow`'s. The S3 rows are one per value of the
128
+ * versioning-exemption stamp, in the stamp's own order (the pinned wire
129
+ * order, not a precedence — a tag carries one value), and the stamp is the
130
+ * whole contract: a bucket a construct left unversioned before the stamp
131
+ * existed (constructs < 33.1.0) falls to the K1 row until it is redeployed —
132
+ * an existing mitigated row reopens as `no_mitigation_row`, the reason the
133
+ * customer reads — because the stamp is what tells posture the omission is
134
+ * by design.
135
+ */
136
+ export declare const MITIGATED_POLICY: readonly MitigatedPolicyRow[];
137
+ export interface PolicyRegistry {
138
+ expected: readonly ExpectedPolicyRow[];
139
+ mitigated: readonly MitigatedPolicyRow[];
140
+ }
141
+ export declare const DEFAULT_POLICY_REGISTRY: PolicyRegistry;
142
+ export type PolicyRowMatch = {
143
+ lane: Extract<MachineLaneStatus, "expected">;
144
+ row: ExpectedPolicyRow;
145
+ } | {
146
+ lane: Extract<MachineLaneStatus, "mitigated">;
147
+ row: MitigatedPolicyRow;
148
+ };
149
+ export interface PolicyRowLookup {
150
+ ruleId: string;
151
+ /** `iacInfo.logicalId`: the CloudFormation logical id the resource carries, or `null`. */
152
+ logicalId: string | null;
153
+ /** The asset's stored tags, or `null` when the scan has never read them — a tag-scoped row then matches nothing. */
154
+ tags: Readonly<Record<string, string>> | null;
155
+ }
156
+ /**
157
+ * The row that speaks for this finding. A resource-scoped row wins over an
158
+ * unscoped one across both registries; at equal scope a mitigated row wins
159
+ * over an expected one, a compensating control being the stronger claim;
160
+ * among scoped rows of one lane, registry order. Nothing matches: the rule
161
+ * flags.
162
+ */
163
+ export declare function findPolicyRow(input: PolicyRowLookup, registry?: PolicyRegistry): PolicyRowMatch | undefined;
164
+ /**
165
+ * The identity of a registry row, stable across the wire: lane, rule and
166
+ * scope. `findPolicyRowByKey` inverts it. The pair is what lets a finding
167
+ * carry its row to the client as one string and the digest fold a lane's
168
+ * findings under one heading per row — a cause with many findings reads as
169
+ * one cause, the cure or the evidence written once.
170
+ */
171
+ export declare function policyRowKey(match: PolicyRowMatch): string;
172
+ /** The row a `policyRowKey` names, or `undefined` once the registry no longer carries it. */
173
+ export declare function findPolicyRowByKey(key: string, registry?: PolicyRegistry): PolicyRowMatch | undefined;
174
+ /**
175
+ * What a surface writes once for a row's group — the cause its heading
176
+ * carries (an expected row's knob, and the construct it sits on; a
177
+ * mitigated row's `name`, the resource its control covers) and the cure or
178
+ * the evidence. Derived here so the overview digest (server) and the asset
179
+ * panel (client) present a row identically. Every registry row has a cause;
180
+ * `cause` is null only where a surface presents an uncovered group itself.
181
+ */
182
+ export interface PolicyRowPresentation {
183
+ cause: string | null;
184
+ cure?: string;
185
+ evidence?: string;
186
+ }
187
+ export declare function policyRowPresentation(match: PolicyRowMatch): PolicyRowPresentation;
188
+ /** The cause an expected row's findings are grouped under: the knob, and the construct it sits on. */
189
+ export declare function expectedRowCause(row: ExpectedPolicyRow): string;
190
+ /** The cure clause of an expected row, worded once for the reason, the digest and the asset panel. */
191
+ export declare function cureClause(row: ExpectedPolicyRow): string;
192
+ /** The evidence clause of a mitigated row, worded once for the reason, the digest and the asset panel. */
193
+ export declare function evidenceClause(row: MitigatedPolicyRow): string;
194
+ /**
195
+ * The heading a lane's findings sit under, on the overview digest, the
196
+ * asset panel and the export alike: the lane's own heading from the
197
+ * status-label table (the two lanes share no word), then the cause — an
198
+ * expected row's knob, a mitigated row's resource. Only an uncovered group
199
+ * has no cause, so only it is headed by the lane word alone.
200
+ */
201
+ export declare function laneGroupHeading(lane: MachineLaneStatus, cause: string | null): string;
202
+ export {};
@@ -0,0 +1 @@
1
+ var g=Object.defineProperty;var r=(e,t)=>g(e,"name",{value:t,configurable:!0});import{KMS_RETENTION_TAG_KEY as b,S3_VERSIONING_EXEMPT_TAG_KEY as d,S3_VERSIONING_EXEMPTIONS as m}from"../aws/index.js";import{ISSUE_STATUS_LABELS as I}from"./issueStatusLabels.js";const y=["development","staging"];function C(e){return y.includes(e)}r(C,"isExpectedLaneStage");const E=[{ruleId:"SNS_TOPIC_NOT_ENCRYPTED",resource:{logicalIdContains:"AlarmTopicAlarmNotificationsAlarmNotificationsTopic"},knob:{name:"alarmTopicEncryption",construct:"the alarm topic"},allowHigh:!0,expectation:"the alarm topic is unencrypted because alarmTopicEncryption is off (a customer-managed key costs about $1 to $3 a month per account and region)",cure:"alarmTopicEncryption: true"},{ruleId:"SNS_TOPIC_NOT_ENCRYPTED",knob:{name:"backup.tier",construct:"the app"},tierDefault:"customerManagedKeys",allowHigh:!0,expectation:"the topic is unencrypted because the app's backup tier does not encrypt topics (the enterprise tier's shared customer-managed key costs about $1 to $3 a month per stack)",cure:'encryption: "SSE_KMS"'},{ruleId:"ECS_CONTAINER_INSIGHTS_DISABLED",knob:{name:"backup.tier",construct:"the app"},tierDefault:"containerInsights",expectation:"Container Insights is off because the app's backup tier does not enable it (it bills per metric per cluster)",cure:"containerInsights: true"},{ruleId:"S3_VERSIONING_DISABLED",knob:{name:"backup.tier",construct:"the app"},tierDefault:"s3Versioned",expectation:"bucket versioning is off because the app's backup tier does not version buckets (every overwrite would keep a billed copy)",cure:"versioned: true"}],k={"build-output":{name:"build-output bucket",control:"the bucket holds OpenNext build output that every deploy rewrites and prunes, so versioning would retain a billed copy of each superseded build; the build is reproduced from the commit that made it",site:"createAssetsBucket in fjall/components/infrastructure/lib/patterns/aws/payload.ts",stamped:"on the assets bucket it leaves unversioned"},"isr-cache":{name:"ISR cache bucket",control:"the bucket is the ISR cache Next.js regenerates at runtime, so versioning would retain a billed copy of every regenerated page; a lost entry is rendered again on the next request",site:"createCacheBucket in fjall/components/infrastructure/lib/patterns/aws/payload.ts",stamped:"on the cache bucket it leaves unversioned"},"cold-tier":{name:"cold-tier bucket",control:"the cold tier is MergeTree tiered storage owned by the database, so parts are written and deleted continuously and object versioning would retain every deleted part at unbounded cost; recoverability is the ClickHouse backup lane",site:"ClickHouseColdTierBucket in fjall/components/infrastructure/lib/patterns/aws/clickhouseDatabase.ts",stamped:"(versioned: false by design), the versioned backup bucket beside it"},"access-logs":{name:"access-log bucket",control:"the bucket is the stack's access-log delivery target (application load balancer and CloudFront access logs, expired after 90 days by its lifecycle rule), so versioning would only multiply expired objects",site:"stackAccessLogBucket in fjall/components/infrastructure/lib/resources/aws/logging/accessLogBucket.ts",stamped:"(versioned: false, ACCESS_LOG_RETENTION_DAYS expiry)"}},h=[...m.map(e=>{const{name:t,control:n,site:i,stamped:a}=k[e];return{ruleId:"S3_VERSIONING_DISABLED",resource:{tag:{key:d,value:e}},name:t,control:n,evidence:`${i} stamps ${d}=${e} ${a}`}}),{ruleId:"KMS_KEY_ORPHAN",resource:{tag:{key:b}},name:"retained customer key",control:"the key was retained on purpose (D17) so existing ciphertext stays decryptable; scheduling deletion would render that data unreadable",evidence:"CustomerManagedKey in fjall/components/infrastructure/lib/resources/aws/secrets/kms.ts stamps fjall:kms:retention with the protects scope on every retained key"}],l={expected:E,mitigated:h};function p(e,t){const n=e.resource;if(n===void 0)return!1;if("logicalIdContains"in n)return t.logicalId!==null&&t.logicalId.includes(n.logicalIdContains);if(t.tags===null)return!1;const i=t.tags[n.tag.key];return i!==void 0&&(n.tag.value===void 0||i===n.tag.value)}r(p,"scopeMatches");function O(e,t=l){const n=t.mitigated.filter(o=>o.ruleId===e.ruleId),i=t.expected.filter(o=>o.ruleId===e.ruleId),a=n.find(o=>p(o,e));if(a!==void 0)return{lane:"mitigated",row:a};const s=i.find(o=>p(o,e));if(s!==void 0)return{lane:"expected",row:s};const c=n.find(o=>o.resource===void 0);if(c!==void 0)return{lane:"mitigated",row:c};const u=i.find(o=>o.resource===void 0);return u===void 0?void 0:{lane:"expected",row:u}}r(O,"findPolicyRow");function S(e){return e===void 0?"":"logicalIdContains"in e?e.logicalIdContains:e.tag.value===void 0?`tag:${e.tag.key}`:`tag:${e.tag.key}=${e.tag.value}`}r(S,"scopeKey");function f(e){return`${e.lane}:${e.row.ruleId}:${S(e.row.resource)}`}r(f,"policyRowKey");function R(e,t=l){const n=t.expected.find(a=>f({lane:"expected",row:a})===e);if(n!==void 0)return{lane:"expected",row:n};const i=t.mitigated.find(a=>f({lane:"mitigated",row:a})===e);return i===void 0?void 0:{lane:"mitigated",row:i}}r(R,"findPolicyRowByKey");function A(e){return e.lane==="expected"?{cause:x(e.row),cure:_(e.row)}:{cause:e.row.name,evidence:w(e.row)}}r(A,"policyRowPresentation");function x(e){return`${e.knob.name} on ${e.knob.construct}`}r(x,"expectedRowCause");function _(e){return`Cure: set ${e.cure} in your infrastructure code.`}r(_,"cureClause");function w(e){return`Evidence: ${e.evidence}.`}r(w,"evidenceClause");function $(e,t){const n=I[e].heading;return t===null?n:`${n} \u2014 ${t}`}r($,"laneGroupHeading");export{l as DEFAULT_POLICY_REGISTRY,y as EXPECTED_LANE_STAGES,E as EXPECTED_POLICY,h as MITIGATED_POLICY,_ as cureClause,w as evidenceClause,x as expectedRowCause,O as findPolicyRow,R as findPolicyRowByKey,C as isExpectedLaneStage,$ as laneGroupHeading,f as policyRowKey,A as policyRowPresentation};
@@ -0,0 +1,62 @@
1
+ /**
2
+ * The consent lines a create flow shows for a tier (alert policy design
3
+ * § 13.10): for the backup tier a chosen tier implies, the K1 rows the tier
4
+ * leaves at the default that produces the finding — so the create-app
5
+ * wizard and `fjall create` say, before the app exists, what the first
6
+ * posture scan will list and where. Derived from the registry and from the
7
+ * tier table the constructs read, so the wizard, the CLI, the scan and the
8
+ * deploy cannot disagree; the words are built here once so the two clients
9
+ * print them byte-identically. It informs the choice and gates nothing.
10
+ * Client-safe: the tier table is zod-only.
11
+ */
12
+ import { type BackupTier } from "../backupTiers.js";
13
+ import { type ExpectedPolicyRow } from "./registry.js";
14
+ /**
15
+ * The K1 rows a backup tier leaves at the default that produces the finding,
16
+ * in registry order. An undefined tier reads the `standard` column, as the
17
+ * constructs do. Only rows keyed on the backup tier are lines: creating the
18
+ * app is what produces them. A row on another knob — the alarm topic's
19
+ * account-level `alarmTopicEncryption`, scoped to the Account's own
20
+ * construct — is produced by the account, not by the app being created, so
21
+ * it is not a line at any tier.
22
+ */
23
+ export declare function expectedRowsAtDefault(backupTier: BackupTier | undefined, registry?: readonly ExpectedPolicyRow[]): ExpectedPolicyRow[];
24
+ /** One line of the note: the cause, the finding as it is listed, and the registry's own expectation words. */
25
+ export interface TierConsentLine {
26
+ /** `policyRowKey` of the expected row: the line's identity. */
27
+ rowKey: string;
28
+ ruleId: string;
29
+ /**
30
+ * The finding as the scan lists it: the rule's title where the caller
31
+ * holds the rule registry (the webapp wizard), else the rule id (the CLI).
32
+ */
33
+ title: string;
34
+ /** The knob and the construct it sits on, as the asset page groups the finding. */
35
+ cause: string;
36
+ expectation: string;
37
+ }
38
+ export declare function tierConsentLine(row: ExpectedPolicyRow, title: string): TierConsentLine;
39
+ /**
40
+ * Where the lines land, in the words the scan uses: the expected-lane stages
41
+ * and the status-label table's clauses, under the organisation's posture
42
+ * setting — the scan's ladder flags on a strict organisation before it
43
+ * looks at the stage, so a strict organisation is told `open` everywhere.
44
+ * `null` is a posture setting the caller could not read; the clause says so
45
+ * rather than promise either lane. Rendered once as the note's lead,
46
+ * because every K1 row shares the one lane rule.
47
+ */
48
+ export declare function expectedLaneWhereClause(postureStrict: boolean | null): string;
49
+ /** The note's name: the wizard panel's accessible name and the CLI note's heading. */
50
+ export declare const TIER_CONSENT_NOTE_LABEL = "Posture findings this tier produces";
51
+ /**
52
+ * The note's lead in three parts, so a renderer may emphasise the tier name
53
+ * between them: "Choosing <tier> keeps these defaults, and the first posture
54
+ * scan lists them. <where the scan lists them> Nothing is accepted on your
55
+ * behalf." The where-clause is `expectedLaneWhereClause`.
56
+ */
57
+ export interface TierConsentLead {
58
+ prefix: string;
59
+ tierName: string;
60
+ rest: string;
61
+ }
62
+ export declare function tierConsentLead(tierName: string, postureStrict: boolean | null): TierConsentLead;
@@ -0,0 +1 @@
1
+ var i=Object.defineProperty;var s=(e,t)=>i(e,"name",{value:t,configurable:!0});import{tierDefault as a}from"../backupTiers.js";import{ISSUE_STATUS_LABELS as r}from"./issueStatusLabels.js";import{EXPECTED_LANE_STAGES as c,EXPECTED_POLICY as u,expectedRowCause as p,policyRowKey as l}from"./registry.js";function $(e,t=u){return t.filter(n=>n.knob.name==="backup.tier"&&n.tierDefault!==void 0&&a(e,n.tierDefault)===!1)}s($,"expectedRowsAtDefault");function g(e,t){return{rowKey:l({lane:"expected",row:e}),ruleId:e.ruleId,title:t,cause:p(e),expectation:e.expectation}}s(g,"tierConsentLine");function f(e){const t=e.slice(0,-1),n=e[e.length-1]??"";return t.length===0?n:`${t.join(", ")} or ${n}`}s(f,"orList");function d(e){const t=r.expected.clause,n=r.open.clause,o=f(c);return e===!0?`This organisation's posture is strict, so a posture scan lists each as ${n} on every account.`:e===null?`A posture scan lists each as ${t} on a ${o} account, or as ${n} everywhere if the organisation's posture is strict; the posture setting could not be read.`:`On a ${o} account a posture scan lists each as ${t}; on any other account it is ${n}.`}s(d,"expectedLaneWhereClause");const C="Posture findings this tier produces";function T(e,t){return{prefix:"Choosing ",tierName:e,rest:` keeps these defaults, and the first posture scan lists them. ${d(t)} Nothing is accepted on your behalf.`}}s(T,"tierConsentLead");export{C as TIER_CONSENT_NOTE_LABEL,d as expectedLaneWhereClause,$ as expectedRowsAtDefault,T as tierConsentLead,g as tierConsentLine};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fjall/util",
3
- "version": "34.0.0",
3
+ "version": "36.0.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/fjall-tech/fjall.git",
@@ -63,6 +63,10 @@
63
63
  "types": "./dist/organisationBinding/index.d.ts",
64
64
  "default": "./dist/organisationBinding/index.js"
65
65
  },
66
+ "./posturePolicy": {
67
+ "types": "./dist/posturePolicy/index.d.ts",
68
+ "default": "./dist/posturePolicy/index.js"
69
+ },
66
70
  "./environments": {
67
71
  "types": "./dist/environments.d.ts",
68
72
  "default": "./dist/environments.js"