@fjall/util 2.12.0 → 2.14.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
- 59 files minified at 2026-06-09T10:15:26.341Z
1
+ 64 files minified at 2026-06-12T01:12:20.740Z
package/dist/Config.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { type Result } from "./docker/result.js";
2
3
  import { type AccountTier } from "./environments.js";
3
4
  /**
4
5
  * Backup Vault Lock modes for an account's DisasterRecovery vault.
@@ -13,6 +14,54 @@ export type VaultLockMode = (typeof VAULT_LOCK_MODES)[number];
13
14
  */
14
15
  export declare const S3_BPA_MODES: readonly ["enforced", "off"];
15
16
  export type S3BpaMode = (typeof S3_BPA_MODES)[number];
17
+ /**
18
+ * Org-level centralised root-access management mode (ADR
19
+ * 2026-06-10-centralised-root-access-default-on). Absent ⇒ "centralised"
20
+ * (default-on); "off" maps to OrgSetupConfig.skipRootAccessManagement at the
21
+ * adapter boundaries (CLI buildOrgSetupConfig, webapp setup route). The
22
+ * webapp carries a twin of this pair at
23
+ * webapp/app/.server/constants/rootAccess.ts.
24
+ * TODO 2026-06-11, owner: paul — tracked in
25
+ * aiDocs/plans/tasks/2026-06-11-root-access-p2-t11-org-config-field-fjall.md:
26
+ * once a published @fjall/util ships this tuple, convert the webapp twin into
27
+ * a re-export from "@fjall/util/config" (TRAIL_LIFECYCLE_STATES precedent).
28
+ */
29
+ export declare const ROOT_ACCESS_MANAGEMENT_MODES: readonly ["centralised", "off"];
30
+ export type RootAccessManagementMode = (typeof ROOT_ACCESS_MANAGEMENT_MODES)[number];
31
+ /**
32
+ * Per-account management-events-trail lifecycle for the organisation-trail
33
+ * migration. Absent ⇒ legacy per-account trail, auto-eligible for migration
34
+ * once org-trail delivery is verified. "account" pins the per-account trail
35
+ * (the explicit reverse path). "draining" removes the trail resource while
36
+ * retaining its bucket + CMK. "org" is terminal: the organisation trail
37
+ * covers the account and local storage has been decommissioned.
38
+ */
39
+ export declare const TRAIL_LIFECYCLE_STATES: readonly ["account", "draining", "org"];
40
+ export type TrailLifecycleState = (typeof TRAIL_LIFECYCLE_STATES)[number];
41
+ /**
42
+ * Trail names shared between the CDK constructs
43
+ * (@fjall/components-infrastructure) and the deploy-core org-trail migration —
44
+ * SDK probes (DescribeTrails) must match what the constructs synthesise.
45
+ */
46
+ export declare const ACCOUNT_TRAIL_NAME = "managementEvents";
47
+ export declare const ORGANISATION_TRAIL_NAME = "organisationManagementEvents";
48
+ /**
49
+ * CDK-side trail-state vocabulary (the `fjallAccountTrailState` context
50
+ * value), shared between the CDK constructs and deploy-core's context
51
+ * builders. Distinct from TRAIL_LIFECYCLE_STATES: config intent
52
+ * ("account"/"draining"/"org") maps onto synth behaviour
53
+ * ("active"/"draining"/"removed").
54
+ */
55
+ export declare const ACCOUNT_TRAIL_STATES: readonly ["active", "draining", "removed"];
56
+ export type AccountTrailState = (typeof ACCOUNT_TRAIL_STATES)[number];
57
+ /**
58
+ * Stack output keys (CfnOutput key + exportName) emitted by the Account /
59
+ * Organisation CDK patterns and read back by the deploy-core org-trail
60
+ * migration reconciler — both sides must use the same literals.
61
+ */
62
+ export declare const TRAIL_BUCKET_OUTPUT_KEY = "FjallTrailBucketName";
63
+ export declare const TRAIL_KEY_ARN_OUTPUT_KEY = "FjallTrailKeyArn";
64
+ export declare const ORG_TRAIL_BUCKET_OUTPUT_KEY = "OrganisationTrailBucketName";
16
65
  export type ProviderAccount = {
17
66
  id: string;
18
67
  name: string;
@@ -52,6 +101,17 @@ export type ProviderAccount = {
52
101
  * instead). "enforced" sets all four account-level public-access flags true.
53
102
  */
54
103
  s3BlockPublicAccess?: S3BpaMode;
104
+ /**
105
+ * Management-events-trail lifecycle for the org-trail migration. Absent ⇒
106
+ * legacy per-account trail (auto-eligible); see TRAIL_LIFECYCLE_STATES.
107
+ */
108
+ trailLifecycle?: TrailLifecycleState;
109
+ /**
110
+ * Explicit acknowledgement that decommissioning the per-account trail
111
+ * discards its retained log history. Bucket deletion never proceeds without
112
+ * it — back up the bucket first if the history matters.
113
+ */
114
+ acknowledgeTrailHistoryLoss?: boolean;
55
115
  };
56
116
  export type Profile = {
57
117
  type: "sso" | "oidc";
@@ -100,17 +160,50 @@ export type RootConfig = z.infer<typeof RootConfigSchema>;
100
160
  export declare class Config {
101
161
  rootConfig: RootConfig;
102
162
  private configPath;
163
+ /**
164
+ * True when the config file was FOUND on disk but could not be read.
165
+ * saveConfig refuses to write in that state — the in-memory config never
166
+ * included the real file's contents, so writing would clobber them.
167
+ */
168
+ private loadFailed;
169
+ /**
170
+ * Top-level keys explicitly cleared this session (clearActiveTarget).
171
+ * The disk-preserving merge in saveConfig would otherwise resurrect them
172
+ * from the on-disk copy.
173
+ */
174
+ private readonly clearedKeys;
103
175
  constructor(rootConfig?: RootConfig, configPath?: string);
104
176
  /**
105
177
  * Find the config directory by walking up the directory tree.
106
178
  * Looks for fjall/fjall-config.json or direct fjall-config.json.
179
+ * Walks up from `startDir` when provided, otherwise from process.cwd().
107
180
  */
108
181
  private static findConfigDirectory;
109
182
  private static loadConfigFile;
110
- static loadConfig(): Config;
183
+ /**
184
+ * Load the config, walking up from `startDir` (default process.cwd()).
185
+ */
186
+ static loadConfig(startDir?: string): Config;
111
187
  private static formatZodError;
112
- saveConfig(): void;
113
- static getConfigDirectory(): string | null;
188
+ saveConfig(): Result<void, Error>;
189
+ /**
190
+ * Writability is checked at save time, not load time: POSIX rename replaces
191
+ * a read-only destination whenever the directory is writable, so without
192
+ * this guard the tmp-plus-rename write would silently replace a chmod-444
193
+ * config.
194
+ */
195
+ private static assertWritable;
196
+ /**
197
+ * Disk-preserving merge: re-reads the on-disk config immediately before
198
+ * writing so sibling top-level keys written by a concurrent process between
199
+ * this session's load and save survive. In-memory keys win at top-level-key
200
+ * granularity; keys explicitly cleared this session are removed even when
201
+ * the disk copy still carries them. Unreadable or invalid disk state falls
202
+ * back to the in-memory state with a warning rather than blocking the save.
203
+ */
204
+ private mergeWithDisk;
205
+ private static readDiskConfigForMerge;
206
+ static getConfigDirectory(startDir?: string): string | null;
114
207
  getActiveTarget(): string | undefined;
115
208
  setActiveTarget(name: string): void;
116
209
  clearActiveTarget(): void;
package/dist/Config.js CHANGED
@@ -1 +1 @@
1
- import*as r from"fs";import*as s from"path";import{z as e}from"zod";import{logger as g}from"./logger.js";const l=10,h=["compliance","governance","none"],p=["enforced","off"],m=e.object({name:e.string(),type:e.enum(["apex","delegated"]),parentDomain:e.string().optional(),account:e.string().optional()}).strict(),d=e.object({activeTarget:e.string().optional(),domains:e.array(m).optional()}).strict();class a{rootConfig;configPath=null;constructor(o,t){this.rootConfig=o??{},this.configPath=t??null}static findConfigDirectory(){let o=process.cwd();for(let t=0;t<l;t++){const n=s.join(o,"fjall"),i=s.join(n,"fjall-config.json");if(r.existsSync(i))return n;const c=s.join(o,"fjall-config.json");if(r.existsSync(c))return o;const f=s.dirname(o);if(f===o)break;o=f}return null}static loadConfigFile(o){try{return r.accessSync(o,r.constants.R_OK|r.constants.W_OK),r.readFileSync(o,{encoding:"utf8"})}catch(t){return g.debug("Config","Config file not accessible",{file:o,error:t instanceof Error?t.message:String(t)}),null}}static loadConfig(){const o=a.findConfigDirectory();if(!o)return new a;const t=s.join(o,"fjall-config.json"),n=a.loadConfigFile(t);let i;if(n)try{i=d.parse(JSON.parse(n))}catch(c){throw a.formatZodError(c,"fjall-config.json")}return new a(i,t)}static formatZodError(o,t){if(o instanceof e.ZodError&&o.issues.length>0){const c=o.issues.map(f=>`${f.path.join(".")}: ${f.message}`).join("; ");return new Error(`Failed to parse ${t}: ${c}`)}const i=(o instanceof Error?o.message:String(o)).replace(/\n/g," ").substring(0,500);return new Error(`Failed to parse ${t}: ${i}`)}saveConfig(){let o=this.configPath;if(!o){const c=a.findConfigDirectory()||s.join(process.cwd(),"fjall");o=s.join(c,"fjall-config.json")}const t=s.dirname(o);r.mkdirSync(t,{recursive:!0});const n=JSON.stringify(this.rootConfig,null,2),i=`${o}.tmp`;r.writeFileSync(i,n,{mode:384}),r.renameSync(i,o)}static getConfigDirectory(){return a.findConfigDirectory()}getActiveTarget(){return this.rootConfig.activeTarget}setActiveTarget(o){this.rootConfig.activeTarget=o}clearActiveTarget(){this.rootConfig.activeTarget=void 0}getDomains(){return this.rootConfig.domains??[]}setDomains(o){this.rootConfig.domains=o}addDomain(o){this.rootConfig.domains||(this.rootConfig.domains=[]),this.rootConfig.domains.push(o)}getDomain(o){return this.rootConfig.domains?.find(t=>t.name.toLowerCase()===o.toLowerCase())}removeDomain(o){if(!this.rootConfig.domains)return!1;const t=this.rootConfig.domains.findIndex(n=>n.name.toLowerCase()===o.toLowerCase());return t===-1?!1:(this.rootConfig.domains.splice(t,1),!0)}}export{a as Config,d as RootConfigSchema,p as S3_BPA_MODES,h as VAULT_LOCK_MODES};
1
+ import*as i from"fs";import*as f from"path";import{z as c}from"zod";import{failure as d,success as C}from"./docker/result.js";import{getErrorMessage as g}from"./errorUtils.js";import{logger as u}from"./logger.js";import{maskSensitiveOutput as l}from"./securityHelpers.js";const y=10,D=["compliance","governance","none"],_=["enforced","off"],$=["centralised","off"],x=["account","draining","org"],F="managementEvents",K="organisationManagementEvents",M=["active","draining","removed"],R="FjallTrailBucketName",b="FjallTrailKeyArn",k="OrganisationTrailBucketName",T=c.object({name:c.string(),type:c.enum(["apex","delegated"]),parentDomain:c.string().optional(),account:c.string().optional()}).strict(),m=c.object({activeTarget:c.string().optional(),domains:c.array(T).optional()}).strict(),w=m.keyof().options;function E(h,e,t){const n=e[t];n!==void 0&&(h[t]=n)}class s{rootConfig;configPath=null;loadFailed=!1;clearedKeys=new Set;constructor(e,t){this.rootConfig=e??{},this.configPath=t??null}static findConfigDirectory(e){let t=e!==void 0&&e!==""?e:process.cwd();for(let n=0;n<y;n++){const o=f.join(t,"fjall"),a=f.join(o,"fjall-config.json");if(i.existsSync(a))return o;const r=f.join(t,"fjall-config.json");if(i.existsSync(r))return t;const p=f.dirname(t);if(p===t)break;t=p}return null}static loadConfigFile(e){try{return i.accessSync(e,i.constants.R_OK),i.readFileSync(e,{encoding:"utf8"})}catch(t){return u.warn("Config",`Config file at ${e} could not be read; using defaults`,{file:e,error:l(g(t))}),null}}static loadConfig(e){const t=s.findConfigDirectory(e);if(!t)return new s;const n=f.join(t,"fjall-config.json"),o=s.loadConfigFile(n);if(o===null){const r=new s(void 0,n);return r.loadFailed=!0,r}let a;if(o!=="")try{a=m.parse(JSON.parse(o))}catch(r){throw s.formatZodError(r,"fjall-config.json")}return new s(a,n)}static formatZodError(e,t){if(e instanceof c.ZodError&&e.issues.length>0){const a=e.issues.map(r=>`${r.path.join(".")}: ${r.message}`).join("; ");return new Error(`Failed to parse ${t}: ${a}`)}const o=(e instanceof Error?e.message:String(e)).replace(/\n/g," ").substring(0,500);return new Error(`Failed to parse ${t}: ${o}`)}saveConfig(){let e=this.configPath;if(!e){const r=s.findConfigDirectory()||f.join(process.cwd(),"fjall");e=f.join(r,"fjall-config.json")}if(this.loadFailed)return d(new Error(`Refusing to save ${e}: the file exists but could not be read when this config loaded, so saving would replace its contents with state that never included them. Fix the file permissions (e.g. chmod u+rw ${e}) and retry.`));const t=f.dirname(e);try{i.mkdirSync(t,{recursive:!0})}catch(r){return d(new Error(`Cannot create config directory ${t}: ${l(g(r))}`))}const n=s.assertWritable(e,t);if(!n.success)return n;const o=JSON.stringify(this.mergeWithDisk(e),null,2),a=`${e}.tmp-${process.pid}`;try{i.writeFileSync(a,o,{mode:384}),i.renameSync(a,e)}catch(r){return d(new Error(`Failed to save ${e}: ${l(g(r))}`))}return C(void 0)}static assertWritable(e,t){if(i.existsSync(e))try{i.accessSync(e,i.constants.W_OK)}catch{return d(new Error(`Cannot save ${e}: the file is read-only. Make it writable (e.g. chmod u+w ${e}) and retry.`))}try{i.accessSync(t,i.constants.W_OK)}catch{return d(new Error(`Cannot save ${e}: the directory ${t} is not writable. Make it writable (e.g. chmod u+w ${t}) and retry.`))}return C(void 0)}mergeWithDisk(e){const t=s.readDiskConfigForMerge(e);if(t===void 0)return this.rootConfig;const n={...t};for(const o of w)E(n,this.rootConfig,o);for(const o of this.clearedKeys)delete n[o];return n}static readDiskConfigForMerge(e){if(!i.existsSync(e))return;let t;try{t=JSON.parse(i.readFileSync(e,{encoding:"utf8"}))}catch(o){u.warn("Config",`Could not re-read ${e} before saving; writing in-memory state without merging`,{file:e,error:l(g(o))});return}const n=m.safeParse(t);if(!n.success){u.warn("Config",`On-disk ${e} failed validation before saving; writing in-memory state without merging`,{file:e,error:l(n.error.message)});return}return n.data}static getConfigDirectory(e){return s.findConfigDirectory(e)}getActiveTarget(){return this.rootConfig.activeTarget}setActiveTarget(e){this.rootConfig.activeTarget=e,this.clearedKeys.delete("activeTarget")}clearActiveTarget(){this.rootConfig.activeTarget=void 0,this.clearedKeys.add("activeTarget")}getDomains(){return this.rootConfig.domains??[]}setDomains(e){this.rootConfig.domains=e}addDomain(e){this.rootConfig.domains||(this.rootConfig.domains=[]),this.rootConfig.domains.push(e)}getDomain(e){return this.rootConfig.domains?.find(t=>t.name.toLowerCase()===e.toLowerCase())}removeDomain(e){if(!this.rootConfig.domains)return!1;const t=this.rootConfig.domains.findIndex(n=>n.name.toLowerCase()===e.toLowerCase());return t===-1?!1:(this.rootConfig.domains.splice(t,1),!0)}}export{F as ACCOUNT_TRAIL_NAME,M as ACCOUNT_TRAIL_STATES,s as Config,K as ORGANISATION_TRAIL_NAME,k as ORG_TRAIL_BUCKET_OUTPUT_KEY,$ as ROOT_ACCESS_MANAGEMENT_MODES,m as RootConfigSchema,_ as S3_BPA_MODES,R as TRAIL_BUCKET_OUTPUT_KEY,b as TRAIL_KEY_ARN_OUTPUT_KEY,x as TRAIL_LIFECYCLE_STATES,D as VAULT_LOCK_MODES};
@@ -1,3 +1,6 @@
1
1
  export { AWSError, NoRolesFoundError, InvalidCredentialsError, SSOTokenExpiredError, MissingRegionError, ProfileNotFoundError, CommandError, isAWSError, isNoRolesFoundError, isSSOUnauthorizedError } from "./errors.js";
2
2
  export { STACK_NOT_FOUND_PATTERN, CDK_NO_STACKS_MATCH, type ResourceEvent, isResourceEvent } from "./cloudformationTypes.js";
3
3
  export { CloudFormationFailureAnalyser, type RootCause, type FailureAnalysis } from "./CloudFormationFailureAnalyser.js";
4
+ export { IPAM_OPERATIONS_POOL_TAG_KEY, formatIpamPairTagValue } from "./ipamTags.js";
5
+ export { SDK_PRE_EMPTY_TAG_KEY } from "./infraTags.js";
6
+ export { ACCOUNT_MONITORING_ROLE_NAME } from "./monitoringRole.js";
package/dist/aws/index.js CHANGED
@@ -1 +1 @@
1
- import{AWSError as e,NoRolesFoundError as i,InvalidCredentialsError as n,SSOTokenExpiredError as E,MissingRegionError as s,ProfileNotFoundError as d,CommandError as S,isAWSError as l,isNoRolesFoundError as t,isSSOUnauthorizedError as a}from"./errors.js";import{STACK_NOT_FOUND_PATTERN as A,CDK_NO_STACKS_MATCH as C,isResourceEvent as N}from"./cloudformationTypes.js";import{CloudFormationFailureAnalyser as m}from"./CloudFormationFailureAnalyser.js";export{e as AWSError,C as CDK_NO_STACKS_MATCH,m as CloudFormationFailureAnalyser,S as CommandError,n as InvalidCredentialsError,s as MissingRegionError,i as NoRolesFoundError,d as ProfileNotFoundError,E as SSOTokenExpiredError,A as STACK_NOT_FOUND_PATTERN,l as isAWSError,t as isNoRolesFoundError,N as isResourceEvent,a as isSSOUnauthorizedError};
1
+ import{AWSError as e,NoRolesFoundError as E,InvalidCredentialsError as _,SSOTokenExpiredError as i,MissingRegionError as T,ProfileNotFoundError as n,CommandError as A,isAWSError as O,isNoRolesFoundError as a,isSSOUnauthorizedError as t}from"./errors.js";import{STACK_NOT_FOUND_PATTERN as m,CDK_NO_STACKS_MATCH as s,isResourceEvent as S}from"./cloudformationTypes.js";import{CloudFormationFailureAnalyser as l}from"./CloudFormationFailureAnalyser.js";import{IPAM_OPERATIONS_POOL_TAG_KEY as R,formatIpamPairTagValue as f}from"./ipamTags.js";import{SDK_PRE_EMPTY_TAG_KEY as u}from"./infraTags.js";import{ACCOUNT_MONITORING_ROLE_NAME as x}from"./monitoringRole.js";export{x as ACCOUNT_MONITORING_ROLE_NAME,e as AWSError,s as CDK_NO_STACKS_MATCH,l as CloudFormationFailureAnalyser,A as CommandError,R as IPAM_OPERATIONS_POOL_TAG_KEY,_ as InvalidCredentialsError,T as MissingRegionError,E as NoRolesFoundError,n as ProfileNotFoundError,u as SDK_PRE_EMPTY_TAG_KEY,i as SSOTokenExpiredError,m as STACK_NOT_FOUND_PATTERN,f as formatIpamPairTagValue,O as isAWSError,a as isNoRolesFoundError,S as isResourceEvent,t as isSSOUnauthorizedError};
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The SDK pre-empty marker tag, shared by producer and consumer:
3
+ *
4
+ * - Producer (`@fjall/components-infrastructure`): the `S3Bucket` wrapper
5
+ * (`storage/s3.ts`) tags every DESTROY-policy bucket so destroy paths
6
+ * know it is safe to SDK-empty before CloudFormation delete (Phase 3
7
+ * autoDeleteObjects retirement).
8
+ * - Consumer (`@fjall/deploy-core`): `stackCleanup/bucketOps.ts` includes
9
+ * the key in `PRE_EMPTY_TAG_KEYS` — presence with value "true" opts a
10
+ * stack bucket into the pre-empty pass; a bucket carrying no pre-empty
11
+ * tag (Retain) is never touched.
12
+ *
13
+ * The literal is deployed infrastructure state — already-tagged buckets
14
+ * would silently lose pre-empty coverage (and so re-expose the quarantine
15
+ * window) on any rename. The pin test in `__tests__/infraTags.test.ts`
16
+ * guards against casual renames.
17
+ */
18
+ export declare const SDK_PRE_EMPTY_TAG_KEY = "fjall:sdk-pre-empty";
@@ -0,0 +1 @@
1
+ const _="fjall:sdk-pre-empty";export{_ as SDK_PRE_EMPTY_TAG_KEY};
@@ -0,0 +1,15 @@
1
+ /**
2
+ * The IPAM operations-pool pair tag, shared by producer and consumer:
3
+ *
4
+ * - Producer (`@fjall/components-infrastructure`): `ipamPool.ts` tags each
5
+ * account-scoped IPAM pool, and `standardTagsAspect.ts` tags each
6
+ * IPAM-allocated VPC, with `formatIpamPairTagValue(accountId, region)`.
7
+ * - Consumer (`@fjall/deploy-core`): `targetReadiness.ts` matches the
8
+ * owner-side pool for a deploy target by the same key + value.
9
+ *
10
+ * The literal shapes are deployed infrastructure state — changing either
11
+ * orphans every already-tagged pool and VPC. The pin test in
12
+ * `__tests__/ipamTags.test.ts` guards against casual renames.
13
+ */
14
+ export declare const IPAM_OPERATIONS_POOL_TAG_KEY = "fjall:operations:pool";
15
+ export declare function formatIpamPairTagValue(accountId: string, region: string): string;
@@ -0,0 +1 @@
1
+ const r="fjall:operations:pool";function t(o,a){return`${o}-${a}`}export{r as IPAM_OPERATIONS_POOL_TAG_KEY,t as formatIpamPairTagValue};
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The fixed-name cross-account monitoring role, shared by producer and
3
+ * consumer:
4
+ *
5
+ * - Producer (`@fjall/components-infrastructure`): `AccountMonitoringRole`
6
+ * (`config/aws/accountMonitoringRole.ts`) creates the role under this
7
+ * name — the only resource the account-globals branch creates
8
+ * unconditionally (`patterns/aws/account.ts`).
9
+ * - Consumer (`@fjall/deploy-core`): `accountGlobals.ts` probes the role by
10
+ * name (IAM GetRole) — its presence IS "account globals deployed", the
11
+ * prerequisite for a non-primary-region deploy.
12
+ *
13
+ * The literal is deployed infrastructure state — the platform assumes this
14
+ * exact name in every connected account, so a rename strands existing
15
+ * estates. The pin test in `__tests__/monitoringRole.test.ts` guards
16
+ * against casual renames.
17
+ */
18
+ export declare const ACCOUNT_MONITORING_ROLE_NAME = "FjallMonitoring";
@@ -0,0 +1 @@
1
+ const o="FjallMonitoring";export{o as ACCOUNT_MONITORING_ROLE_NAME};
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Fixed name of the disaster-recovery backup vault. Single source of truth for
3
+ * the @fjall/components-infrastructure DisasterRecovery construct (which
4
+ * creates the vault under this name) and deploy-core's adopt-vs-create and
5
+ * survival probes (which look it up by the same name).
6
+ */
7
+ export declare const BACKUP_VAULT_NAME = "backupVault";
@@ -0,0 +1 @@
1
+ const t="backupVault";export{t as BACKUP_VAULT_NAME};
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Wire contract for `GET /api/connections/list` — the single shared shape
3
+ * between the webapp producer (`webapp/app/routes/api/connections/list.ts`)
4
+ * and the CLI consumer (`FjallApiClient.getConnectedAccounts`). Neither side
5
+ * may redeclare this record: the producer conforms at compile time via
6
+ * `satisfies ConnectionWire`, the consumer parses with `safeParse`.
7
+ */
8
+ import { z } from "zod";
9
+ export declare const ConnectionWireSchema: z.ZodObject<{
10
+ awsAccountId: z.ZodString;
11
+ accountName: z.ZodNullable<z.ZodString>;
12
+ status: z.ZodString;
13
+ role: z.ZodString;
14
+ roleArn: z.ZodString;
15
+ environment: z.ZodOptional<z.ZodString>;
16
+ connectedAt: z.ZodString;
17
+ }, z.core.$strip>;
18
+ export type ConnectionWire = z.infer<typeof ConnectionWireSchema>;
19
+ export declare const ConnectionsListResponseSchema: z.ZodObject<{
20
+ accounts: z.ZodArray<z.ZodObject<{
21
+ awsAccountId: z.ZodString;
22
+ accountName: z.ZodNullable<z.ZodString>;
23
+ status: z.ZodString;
24
+ role: z.ZodString;
25
+ roleArn: z.ZodString;
26
+ environment: z.ZodOptional<z.ZodString>;
27
+ connectedAt: z.ZodString;
28
+ }, z.core.$strip>>;
29
+ }, z.core.$strip>;
30
+ export type ConnectionsListResponse = z.infer<typeof ConnectionsListResponseSchema>;
@@ -0,0 +1 @@
1
+ import{z as n}from"zod";const t=n.object({awsAccountId:n.string(),accountName:n.string().nullable(),status:n.string(),role:n.string(),roleArn:n.string(),environment:n.string().optional(),connectedAt:n.string()}),e=n.object({accounts:n.array(t)});export{t as ConnectionWireSchema,e as ConnectionsListResponseSchema};
@@ -2,8 +2,9 @@
2
2
  * Environment + account-axis constants shared across CLI, webapp, and
3
3
  * deploy-core. Two independent axes live here: the workload STAGE
4
4
  * ([[ACCOUNT_STAGES]]) and the structural TIER ([[ACCOUNT_TIERS]]). "root" is
5
- * NOT a stage — it is the management-account TIER, derived from
6
- * role === "organisation" and never user-selectable.
5
+ * NOT a stage and NOT a tier — it is the wire environment marker that decodes
6
+ * to tier "organisation" via [[environmentToTier]], and is never
7
+ * user-selectable.
7
8
  */
8
9
  import { z } from "zod";
9
10
  export declare const ACCOUNT_STAGES: readonly ["production", "staging", "development", "platform", "compliance"];
@@ -27,8 +28,12 @@ export declare const STRUCTURAL_ENVIRONMENTS: {
27
28
  * GET→PUT round-trip accept this superset, then decode "root" → null at
28
29
  * PERSISTENCE via [[stageFromWireEnvironment]] (the DB never stores structural
29
30
  * root). NOT a user-facing picker vocabulary — pickers use ACCOUNT_STAGES.
30
- * Retained through the old-CLI drain; wire-rejection of "root" is deferred to a
31
- * future follow-up. See decisions/2026-06-07-account-tier-vs-stage-separation.md.
31
+ * PERMANENT ingest vocabulary, not a drain-gated shim: the separate-root
32
+ * connect deliberately carries environment:"root" as its intent marker
33
+ * (solo-to-org lifecycle ADR, decision 7), so wire-rejection of "root" is
34
+ * unreachable; only derived-EMIT of "root" retires, per-org, behind the CLI
35
+ * telemetry gate. See decisions/2026-06-07-account-tier-vs-stage-separation.md
36
+ * § "Wire back-compat (permanent, not transitional)".
32
37
  */
33
38
  export declare const ACCOUNT_STAGES_WITH_ROOT: readonly ["production", "staging", "development", "platform", "compliance", "root"];
34
39
  export type AccountStageWithRoot = (typeof ACCOUNT_STAGES_WITH_ROOT)[number];
@@ -37,8 +42,9 @@ export declare function getEnvironmentLabel(env: string): string;
37
42
  /**
38
43
  * Structural TIER axis: an account's role in the organisation. Independent of
39
44
  * the workload STAGE axis ([[ACCOUNT_STAGES]]) — a tier is never derived from a
40
- * stage and vice versa. Same literals as [[ACCOUNT_ROLES]], which `satisfies`
41
- * this type so the two cannot drift.
45
+ * stage and vice versa. Same literals as [[ACCOUNT_ROLES]]: its `satisfies`
46
+ * clause rejects non-tier values, and [[ACCOUNT_ROLE_TIER_PRESENCE]] rejects a
47
+ * missing or duplicated tier, so the two cannot drift.
42
48
  */
43
49
  export declare const ACCOUNT_TIERS: readonly ["organisation", "platform", "account"];
44
50
  export type AccountTier = (typeof ACCOUNT_TIERS)[number];
@@ -47,8 +53,6 @@ export declare const AccountTierSchema: z.ZodEnum<{
47
53
  organisation: "organisation";
48
54
  account: "account";
49
55
  }>;
50
- /** Type guard: checks whether a string is a valid account tier. */
51
- export declare function isAccountTier(value: string): value is AccountTier;
52
56
  /**
53
57
  * AWS account roles in the wire format used across the CLI, webapp API
54
58
  * responses, and the Prisma `AccountRole` enum. This is the SINGLE SOURCE
@@ -69,6 +73,8 @@ export declare const ACCOUNT_ROLES: {
69
73
  readonly PLATFORM: "platform";
70
74
  readonly ACCOUNT: "account";
71
75
  };
76
+ /** Type guard: checks whether a string is a valid account tier. */
77
+ export declare function isAccountTier(value: string): value is AccountTier;
72
78
  /**
73
79
  * Decode an inbound wire `environment` to its structural TIER. The wire stays
74
80
  * superset-tolerant: out-of-version CLIs and the post-`fjall create org`
@@ -1 +1 @@
1
- import{z as c}from"zod";const r=["production","staging","development","platform","compliance"],i={production:"Production",staging:"Staging",development:"Development",platform:"Platform",compliance:"Compliance"},T=new Set(r);function n(t){return T.has(t)}const o={ROOT:"root",PLATFORM:"platform"},O=[...r,o.ROOT];function A(t){return n(t)?i[t]:t.charAt(0).toUpperCase()+t.slice(1)}const e=["organisation","platform","account"],l=c.enum(e),a=new Set(e);function s(t){return a.has(t)}const C={ORGANISATION:"organisation",PLATFORM:"platform",ACCOUNT:"account"};function p(t){return t===o.ROOT?"organisation":t===o.PLATFORM?"platform":"account"}function S(t){return t==null||t===""||t===o.ROOT?null:n(t)?t:null}function f(t){return t.tier??p(t.environment)}export{C as ACCOUNT_ROLES,r as ACCOUNT_STAGES,O as ACCOUNT_STAGES_WITH_ROOT,i as ACCOUNT_STAGE_LABELS,e as ACCOUNT_TIERS,l as AccountTierSchema,o as STRUCTURAL_ENVIRONMENTS,f as accountTier,p as environmentToTier,A as getEnvironmentLabel,n as isAccountStage,s as isAccountTier,S as stageFromWireEnvironment};
1
+ import{z as c}from"zod";const n=["production","staging","development","platform","compliance"],T={production:"Production",staging:"Staging",development:"Development",platform:"Platform",compliance:"Compliance"},i=new Set(n);function e(t){return i.has(t)}const o={ROOT:"root",PLATFORM:"platform"},C=[...n,o.ROOT];function s(t){return e(t)?T[t]:t.charAt(0).toUpperCase()+t.slice(1)}const u=["organisation","platform","account"],l=c.enum(u),r={ORGANISATION:"organisation",PLATFORM:"platform",ACCOUNT:"account"},O={[r.ORGANISATION]:!0,[r.PLATFORM]:!0,[r.ACCOUNT]:!0},a=new Set(Object.keys(O));function S(t){return a.has(t)}function p(t){return t===o.ROOT?"organisation":t===o.PLATFORM?"platform":"account"}function R(t){return t==null||t===""||t===o.ROOT?null:e(t)?t:null}function E(t){return t.tier??p(t.environment)}export{r as ACCOUNT_ROLES,n as ACCOUNT_STAGES,C as ACCOUNT_STAGES_WITH_ROOT,T as ACCOUNT_STAGE_LABELS,u as ACCOUNT_TIERS,l as AccountTierSchema,o as STRUCTURAL_ENVIRONMENTS,E as accountTier,p as environmentToTier,s as getEnvironmentLabel,e as isAccountStage,S as isAccountTier,R as stageFromWireEnvironment};
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { DNS_APEX, getDomainExportNames, type ManagedDomainExports } from "./domainExports.js";
2
+ export { BACKUP_VAULT_NAME } from "./backupVault.js";
2
3
  export { toPascalCase, toKebab, toValidDatabaseName, toScreamingSnake, capitalise, getSafeZoneName, accountConstructKey, hasAsciiStableConstructKey } from "./caseConversion.js";
3
4
  export { findAccountNameCollision, type AccountNameCollision } from "./accountNameCollision.js";
4
5
  export { normaliseError, getErrorMessage, hasErrorCode, getErrorCode, getErrorStack, formatErrorString } from "./errorUtils.js";
@@ -9,9 +10,10 @@ export { mapSettledWithConcurrency } from "./concurrency.js";
9
10
  export { ACCOUNT_STAGES_WITH_ROOT, STRUCTURAL_ENVIRONMENTS, ACCOUNT_STAGES, ACCOUNT_STAGE_LABELS, isAccountStage, ACCOUNT_TIERS, type AccountTier, AccountTierSchema, isAccountTier, environmentToTier, stageFromWireEnvironment, accountTier, type AccountStageWithRoot, type AccountStage, getEnvironmentLabel, ACCOUNT_ROLES } from "./environments.js";
10
11
  export { RESOURCE_CATEGORIES, type ResourceCategory, categoriseResource, getExpectedDuration, getFriendlyResourceType } from "./resourceCategorisation.js";
11
12
  export { parseGitRemoteUrl, type GitProvider, type ParsedGitRemote } from "./gitRemoteParser.js";
12
- export { abbreviateRegion } from "./regions.js";
13
+ export { abbreviateRegion, AWS_REGIONS_METADATA, DEFAULT_REGION, getRegionInfo, MAX_SECONDARY_REGIONS, regions, suggestRegionForTimezone, type RegionCode, type RegionInfo } from "./regions.js";
13
14
  export { SCOPE_VALUES, type TokenScope } from "./tokenScopes.js";
14
- export { deriveRegionsFromOrgConfig, deriveTargets, deriveAllTargets, findTarget, generateTargetName, type OrgConfigRegions, type TargetAccount, type DerivedTarget } from "./targets.js";
15
+ export { ConnectionWireSchema, type ConnectionWire, ConnectionsListResponseSchema, type ConnectionsListResponse } from "./connectionsWire.js";
16
+ export { deriveRegionsFromOrgConfig, deriveTargets, deriveAllTargets, environmentOrTier, findTarget, generateTargetName, type OrgConfigRegions, type TargetAccount, type DerivedTarget } from "./targets.js";
15
17
  export { buildAppConfigPath } from "./appPath.js";
16
18
  export { type ScanPath } from "./scanTypes.js";
17
19
  export { findInfrastructurePaths, findBoundaryPath, isInfrastructureFile, type MarkerEntry, type FindInfrastructurePathsOptions } from "./findInfrastructurePaths.js";
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- import{DNS_APEX as o,getDomainExportNames as t}from"./domainExports.js";import{toPascalCase as a,toKebab as n,toValidDatabaseName as i,toScreamingSnake as S,capitalise as _,getSafeZoneName as m,accountConstructKey as s,hasAsciiStableConstructKey as A}from"./caseConversion.js";import{findAccountNameCollision as T}from"./accountNameCollision.js";import{normaliseError as N,getErrorMessage as f,hasErrorCode as R,getErrorCode as c,getErrorStack as g,formatErrorString as O}from"./errorUtils.js";import{singleton as I}from"./singleton.js";import{DANGEROUS_ENV_VARS as l,filterDangerousEnvVars as d,maskSensitiveOutput as P,parseShellArgs as M}from"./securityHelpers.js";import{sleep as D}from"./sleep.js";import{mapSettledWithConcurrency as v}from"./concurrency.js";import{ACCOUNT_STAGES_WITH_ROOT as h,STRUCTURAL_ENVIRONMENTS as G,ACCOUNT_STAGES as b,ACCOUNT_STAGE_LABELS as L,isAccountStage as y,ACCOUNT_TIERS as F,AccountTierSchema as K,isAccountTier as X,environmentToTier as W,stageFromWireEnvironment as k,accountTier as B,getEnvironmentLabel as Z,ACCOUNT_ROLES as j}from"./environments.js";import{RESOURCE_CATEGORIES as w,categoriseResource as z,getExpectedDuration as J,getFriendlyResourceType as Q}from"./resourceCategorisation.js";import{parseGitRemoteUrl as $}from"./gitRemoteParser.js";import{abbreviateRegion as re}from"./regions.js";import{SCOPE_VALUES as te}from"./tokenScopes.js";import{deriveRegionsFromOrgConfig as ae,deriveTargets as ne,deriveAllTargets as ie,findTarget as Se,generateTargetName as _e}from"./targets.js";import{buildAppConfigPath as se}from"./appPath.js";import{findInfrastructurePaths as Ce,findBoundaryPath as Te,isInfrastructureFile as pe}from"./findInfrastructurePaths.js";import{inferContainerFromCandidates as fe}from"./inferContainerFromCandidates.js";import{RESERVED_APP_NAMES as ce,isReservedAppName as ge}from"./reservedAppNames.js";import{deriveContentHashTag as xe}from"./deriveContentHashTag.js";import{MIGRATION_SNAPSHOT_NAME_PREFIX as ue,EXPECTED_SCHEMA_VERSION_ENV as le,EXPECTED_SCHEMA_VERSION_TOOL_ENV as de,EXPECTED_CH_SCHEMA_VERSION_ENV as Pe,SCHEMA_ADMIN_USER_ENV as Me,SCHEMA_ADMIN_PASSWORD_ENV as Ve,PRISMA_MIGRATION_DIR_RE as De,CLICKHOUSE_MIGRATION_SKIP_RE as Ue}from"./migration/constants.js";export{j as ACCOUNT_ROLES,b as ACCOUNT_STAGES,h as ACCOUNT_STAGES_WITH_ROOT,L as ACCOUNT_STAGE_LABELS,F as ACCOUNT_TIERS,K as AccountTierSchema,Ue as CLICKHOUSE_MIGRATION_SKIP_RE,l as DANGEROUS_ENV_VARS,o as DNS_APEX,Pe as EXPECTED_CH_SCHEMA_VERSION_ENV,le as EXPECTED_SCHEMA_VERSION_ENV,de as EXPECTED_SCHEMA_VERSION_TOOL_ENV,ue as MIGRATION_SNAPSHOT_NAME_PREFIX,De as PRISMA_MIGRATION_DIR_RE,ce as RESERVED_APP_NAMES,w as RESOURCE_CATEGORIES,Ve as SCHEMA_ADMIN_PASSWORD_ENV,Me as SCHEMA_ADMIN_USER_ENV,te as SCOPE_VALUES,G as STRUCTURAL_ENVIRONMENTS,re as abbreviateRegion,s as accountConstructKey,B as accountTier,se as buildAppConfigPath,_ as capitalise,z as categoriseResource,ie as deriveAllTargets,xe as deriveContentHashTag,ae as deriveRegionsFromOrgConfig,ne as deriveTargets,W as environmentToTier,d as filterDangerousEnvVars,T as findAccountNameCollision,Te as findBoundaryPath,Ce as findInfrastructurePaths,Se as findTarget,O as formatErrorString,_e as generateTargetName,t as getDomainExportNames,Z as getEnvironmentLabel,c as getErrorCode,f as getErrorMessage,g as getErrorStack,J as getExpectedDuration,Q as getFriendlyResourceType,m as getSafeZoneName,A as hasAsciiStableConstructKey,R as hasErrorCode,fe as inferContainerFromCandidates,y as isAccountStage,X as isAccountTier,pe as isInfrastructureFile,ge as isReservedAppName,v as mapSettledWithConcurrency,P as maskSensitiveOutput,N as normaliseError,$ as parseGitRemoteUrl,M as parseShellArgs,I as singleton,D as sleep,k as stageFromWireEnvironment,n as toKebab,a as toPascalCase,S as toScreamingSnake,i as toValidDatabaseName};
1
+ import{DNS_APEX as o,getDomainExportNames as t}from"./domainExports.js";import{BACKUP_VAULT_NAME as n}from"./backupVault.js";import{toPascalCase as i,toKebab as S,toValidDatabaseName as A,toScreamingSnake as _,capitalise as s,getSafeZoneName as m,accountConstructKey as C,hasAsciiStableConstructKey as T}from"./caseConversion.js";import{findAccountNameCollision as N}from"./accountNameCollision.js";import{normaliseError as f,getErrorMessage as g,hasErrorCode as c,getErrorCode as O,getErrorStack as I,formatErrorString as x}from"./errorUtils.js";import{singleton as l}from"./singleton.js";import{DANGEROUS_ENV_VARS as M,filterDangerousEnvVars as D,maskSensitiveOutput as P,parseShellArgs as U}from"./securityHelpers.js";import{sleep as v}from"./sleep.js";import{mapSettledWithConcurrency as h}from"./concurrency.js";import{ACCOUNT_STAGES_WITH_ROOT as H,STRUCTURAL_ENVIRONMENTS as b,ACCOUNT_STAGES as F,ACCOUNT_STAGE_LABELS as y,isAccountStage as K,ACCOUNT_TIERS as W,AccountTierSchema as X,isAccountTier as k,environmentToTier as B,stageFromWireEnvironment as z,accountTier as Y,getEnvironmentLabel as Z,ACCOUNT_ROLES as j}from"./environments.js";import{RESOURCE_CATEGORIES as w,categoriseResource as J,getExpectedDuration as Q,getFriendlyResourceType as $}from"./resourceCategorisation.js";import{parseGitRemoteUrl as re}from"./gitRemoteParser.js";import{abbreviateRegion as te,AWS_REGIONS_METADATA as Ee,DEFAULT_REGION as ne,getRegionInfo as ae,MAX_SECONDARY_REGIONS as ie,regions as Se,suggestRegionForTimezone as Ae}from"./regions.js";import{SCOPE_VALUES as se}from"./tokenScopes.js";import{ConnectionWireSchema as Ce,ConnectionsListResponseSchema as Te}from"./connectionsWire.js";import{deriveRegionsFromOrgConfig as Ne,deriveTargets as pe,deriveAllTargets as fe,environmentOrTier as ge,findTarget as ce,generateTargetName as Oe}from"./targets.js";import{buildAppConfigPath as xe}from"./appPath.js";import{findInfrastructurePaths as le,findBoundaryPath as de,isInfrastructureFile as Me}from"./findInfrastructurePaths.js";import{inferContainerFromCandidates as Pe}from"./inferContainerFromCandidates.js";import{RESERVED_APP_NAMES as Ve,isReservedAppName as ve}from"./reservedAppNames.js";import{deriveContentHashTag as he}from"./deriveContentHashTag.js";import{MIGRATION_SNAPSHOT_NAME_PREFIX as He,EXPECTED_SCHEMA_VERSION_ENV as be,EXPECTED_SCHEMA_VERSION_TOOL_ENV as Fe,EXPECTED_CH_SCHEMA_VERSION_ENV as ye,SCHEMA_ADMIN_USER_ENV as Ke,SCHEMA_ADMIN_PASSWORD_ENV as We,PRISMA_MIGRATION_DIR_RE as Xe,CLICKHOUSE_MIGRATION_SKIP_RE as ke}from"./migration/constants.js";export{j as ACCOUNT_ROLES,F as ACCOUNT_STAGES,H as ACCOUNT_STAGES_WITH_ROOT,y as ACCOUNT_STAGE_LABELS,W as ACCOUNT_TIERS,Ee as AWS_REGIONS_METADATA,X as AccountTierSchema,n as BACKUP_VAULT_NAME,ke as CLICKHOUSE_MIGRATION_SKIP_RE,Ce as ConnectionWireSchema,Te as ConnectionsListResponseSchema,M as DANGEROUS_ENV_VARS,ne as DEFAULT_REGION,o as DNS_APEX,ye as EXPECTED_CH_SCHEMA_VERSION_ENV,be as EXPECTED_SCHEMA_VERSION_ENV,Fe as EXPECTED_SCHEMA_VERSION_TOOL_ENV,ie as MAX_SECONDARY_REGIONS,He as MIGRATION_SNAPSHOT_NAME_PREFIX,Xe as PRISMA_MIGRATION_DIR_RE,Ve as RESERVED_APP_NAMES,w as RESOURCE_CATEGORIES,We as SCHEMA_ADMIN_PASSWORD_ENV,Ke as SCHEMA_ADMIN_USER_ENV,se as SCOPE_VALUES,b as STRUCTURAL_ENVIRONMENTS,te as abbreviateRegion,C as accountConstructKey,Y as accountTier,xe as buildAppConfigPath,s as capitalise,J as categoriseResource,fe as deriveAllTargets,he as deriveContentHashTag,Ne as deriveRegionsFromOrgConfig,pe as deriveTargets,ge as environmentOrTier,B as environmentToTier,D as filterDangerousEnvVars,N as findAccountNameCollision,de as findBoundaryPath,le as findInfrastructurePaths,ce as findTarget,x as formatErrorString,Oe as generateTargetName,t as getDomainExportNames,Z as getEnvironmentLabel,O as getErrorCode,g as getErrorMessage,I as getErrorStack,Q as getExpectedDuration,$ as getFriendlyResourceType,ae as getRegionInfo,m as getSafeZoneName,T as hasAsciiStableConstructKey,c as hasErrorCode,Pe as inferContainerFromCandidates,K as isAccountStage,k as isAccountTier,Me as isInfrastructureFile,ve as isReservedAppName,h as mapSettledWithConcurrency,P as maskSensitiveOutput,f as normaliseError,re as parseGitRemoteUrl,U as parseShellArgs,Se as regions,l as singleton,v as sleep,z as stageFromWireEnvironment,Ae as suggestRegionForTimezone,S as toKebab,i as toPascalCase,_ as toScreamingSnake,A as toValidDatabaseName};
@@ -147,10 +147,9 @@ export type McpProtocolFrame = z.infer<typeof McpProtocolFrameSchema>;
147
147
  * Scaffold protocol schemas — wire types for the `plan_app_scaffold`,
148
148
  * `apply_app_scaffold`, and dry-run helper paths in `@fjall/mcp`.
149
149
  *
150
- * The shapes here mirror the CLI-side `DryRunArtefacts` (in
151
- * `cli/src/operations/types.ts`) by hand because `@fjall/util` is a leaf
152
- * dependency of the CLI and cannot import from it. Drift between the two
153
- * is a wire-level bug; both sides MUST move together.
150
+ * These schemas are the single source of truth: the CLI imports them from
151
+ * `@fjall/util/mcpProtocol` and re-exports aliases in
152
+ * `cli/src/operations/types.ts`.
154
153
  */
155
154
  export declare const FileToWriteSchema: z.ZodObject<{
156
155
  path: z.ZodString;
package/dist/regions.d.ts CHANGED
@@ -1,8 +1,169 @@
1
1
  /**
2
- * Region abbreviation utilities shared across CLI, webapp, and deploy-core.
2
+ * Region utilities shared across CLI, webapp, generator, and deploy-core.
3
+ *
4
+ * Single source of truth for AWS region metadata (ADR
5
+ * decisions/2026-06-11-region-semantics-and-provisioning.md D8). The
6
+ * generator and CLI re-export from here; the webapp imports directly.
3
7
  */
8
+ export interface RegionInfo {
9
+ code: string;
10
+ name: string;
11
+ city: string;
12
+ country: string;
13
+ }
14
+ export declare const DEFAULT_REGION = "us-east-2";
15
+ /** Cap on Organisation.secondaryRegions — API contract shared by every write surface. */
16
+ export declare const MAX_SECONDARY_REGIONS = 10;
17
+ export declare const AWS_REGIONS_METADATA: readonly [{
18
+ readonly code: "us-east-2";
19
+ readonly name: "US East";
20
+ readonly city: "Ohio";
21
+ readonly country: "USA";
22
+ }, {
23
+ readonly code: "us-west-2";
24
+ readonly name: "US West";
25
+ readonly city: "Oregon";
26
+ readonly country: "USA";
27
+ }, {
28
+ readonly code: "us-east-1";
29
+ readonly name: "US East";
30
+ readonly city: "N. Virginia";
31
+ readonly country: "USA";
32
+ }, {
33
+ readonly code: "eu-west-1";
34
+ readonly name: "EU West";
35
+ readonly city: "Ireland";
36
+ readonly country: "Ireland";
37
+ }, {
38
+ readonly code: "eu-central-1";
39
+ readonly name: "EU Central";
40
+ readonly city: "Frankfurt";
41
+ readonly country: "Germany";
42
+ }, {
43
+ readonly code: "ap-southeast-1";
44
+ readonly name: "Asia Pacific";
45
+ readonly city: "Singapore";
46
+ readonly country: "Singapore";
47
+ }, {
48
+ readonly code: "ap-northeast-1";
49
+ readonly name: "Asia Pacific";
50
+ readonly city: "Tokyo";
51
+ readonly country: "Japan";
52
+ }, {
53
+ readonly code: "ap-southeast-2";
54
+ readonly name: "Asia Pacific";
55
+ readonly city: "Sydney";
56
+ readonly country: "Australia";
57
+ }, {
58
+ readonly code: "us-west-1";
59
+ readonly name: "US West";
60
+ readonly city: "N. California";
61
+ readonly country: "USA";
62
+ }, {
63
+ readonly code: "ca-central-1";
64
+ readonly name: "Canada";
65
+ readonly city: "Central";
66
+ readonly country: "Canada";
67
+ }, {
68
+ readonly code: "af-south-1";
69
+ readonly name: "Africa";
70
+ readonly city: "Cape Town";
71
+ readonly country: "South Africa";
72
+ }, {
73
+ readonly code: "ap-east-1";
74
+ readonly name: "Asia Pacific";
75
+ readonly city: "Hong Kong";
76
+ readonly country: "China";
77
+ }, {
78
+ readonly code: "ap-northeast-2";
79
+ readonly name: "Asia Pacific";
80
+ readonly city: "Seoul";
81
+ readonly country: "South Korea";
82
+ }, {
83
+ readonly code: "ap-northeast-3";
84
+ readonly name: "Asia Pacific";
85
+ readonly city: "Osaka";
86
+ readonly country: "Japan";
87
+ }, {
88
+ readonly code: "ap-south-1";
89
+ readonly name: "Asia Pacific";
90
+ readonly city: "Mumbai";
91
+ readonly country: "India";
92
+ }, {
93
+ readonly code: "ap-south-2";
94
+ readonly name: "Asia Pacific";
95
+ readonly city: "Hyderabad";
96
+ readonly country: "India";
97
+ }, {
98
+ readonly code: "ap-southeast-3";
99
+ readonly name: "Asia Pacific";
100
+ readonly city: "Jakarta";
101
+ readonly country: "Indonesia";
102
+ }, {
103
+ readonly code: "ap-southeast-4";
104
+ readonly name: "Asia Pacific";
105
+ readonly city: "Melbourne";
106
+ readonly country: "Australia";
107
+ }, {
108
+ readonly code: "eu-central-2";
109
+ readonly name: "EU Central";
110
+ readonly city: "Zurich";
111
+ readonly country: "Switzerland";
112
+ }, {
113
+ readonly code: "eu-north-1";
114
+ readonly name: "EU North";
115
+ readonly city: "Stockholm";
116
+ readonly country: "Sweden";
117
+ }, {
118
+ readonly code: "eu-south-1";
119
+ readonly name: "EU South";
120
+ readonly city: "Milan";
121
+ readonly country: "Italy";
122
+ }, {
123
+ readonly code: "eu-south-2";
124
+ readonly name: "EU South";
125
+ readonly city: "Spain";
126
+ readonly country: "Spain";
127
+ }, {
128
+ readonly code: "eu-west-2";
129
+ readonly name: "EU West";
130
+ readonly city: "London";
131
+ readonly country: "UK";
132
+ }, {
133
+ readonly code: "eu-west-3";
134
+ readonly name: "EU West";
135
+ readonly city: "Paris";
136
+ readonly country: "France";
137
+ }, {
138
+ readonly code: "me-central-1";
139
+ readonly name: "Middle East";
140
+ readonly city: "UAE";
141
+ readonly country: "UAE";
142
+ }, {
143
+ readonly code: "me-south-1";
144
+ readonly name: "Middle East";
145
+ readonly city: "Bahrain";
146
+ readonly country: "Bahrain";
147
+ }, {
148
+ readonly code: "sa-east-1";
149
+ readonly name: "South America";
150
+ readonly city: "São Paulo";
151
+ readonly country: "Brazil";
152
+ }];
153
+ /** Union of supported region codes, derived from the metadata (SSoT). */
154
+ export type RegionCode = (typeof AWS_REGIONS_METADATA)[number]["code"];
155
+ export declare const regions: readonly string[];
156
+ export declare function getRegionInfo(code: string): RegionInfo | undefined;
4
157
  /**
5
158
  * Abbreviate an AWS region name to a compact form.
6
159
  * e.g. "us-east-1" → "use1", "eu-west-1" → "euw1"
7
160
  */
8
161
  export declare function abbreviateRegion(region: string): string;
162
+ /**
163
+ * Suggest the geographically nearest supported AWS region for an IANA
164
+ * timezone (e.g. from Intl.DateTimeFormat().resolvedOptions().timeZone).
165
+ * Returns undefined when no sensible suggestion exists (UTC, Etc/*,
166
+ * unrecognised input) — callers fall back to DEFAULT_REGION without
167
+ * "closest to you" justification copy.
168
+ */
169
+ export declare function suggestRegionForTimezone(timeZone: string): RegionCode | undefined;
package/dist/regions.js CHANGED
@@ -1 +1 @@
1
- function n(e){const t=e.split("-");return t.length===3&&t[0]&&t[1]&&t[1].length>0&&t[2]?`${t[0]}${t[1][0]}${t[2]}`:e}export{n as abbreviateRegion};
1
+ const i="us-east-2",n=10,o=Object.freeze([{code:"us-east-2",name:"US East",city:"Ohio",country:"USA"},{code:"us-west-2",name:"US West",city:"Oregon",country:"USA"},{code:"us-east-1",name:"US East",city:"N. Virginia",country:"USA"},{code:"eu-west-1",name:"EU West",city:"Ireland",country:"Ireland"},{code:"eu-central-1",name:"EU Central",city:"Frankfurt",country:"Germany"},{code:"ap-southeast-1",name:"Asia Pacific",city:"Singapore",country:"Singapore"},{code:"ap-northeast-1",name:"Asia Pacific",city:"Tokyo",country:"Japan"},{code:"ap-southeast-2",name:"Asia Pacific",city:"Sydney",country:"Australia"},{code:"us-west-1",name:"US West",city:"N. California",country:"USA"},{code:"ca-central-1",name:"Canada",city:"Central",country:"Canada"},{code:"af-south-1",name:"Africa",city:"Cape Town",country:"South Africa"},{code:"ap-east-1",name:"Asia Pacific",city:"Hong Kong",country:"China"},{code:"ap-northeast-2",name:"Asia Pacific",city:"Seoul",country:"South Korea"},{code:"ap-northeast-3",name:"Asia Pacific",city:"Osaka",country:"Japan"},{code:"ap-south-1",name:"Asia Pacific",city:"Mumbai",country:"India"},{code:"ap-south-2",name:"Asia Pacific",city:"Hyderabad",country:"India"},{code:"ap-southeast-3",name:"Asia Pacific",city:"Jakarta",country:"Indonesia"},{code:"ap-southeast-4",name:"Asia Pacific",city:"Melbourne",country:"Australia"},{code:"eu-central-2",name:"EU Central",city:"Zurich",country:"Switzerland"},{code:"eu-north-1",name:"EU North",city:"Stockholm",country:"Sweden"},{code:"eu-south-1",name:"EU South",city:"Milan",country:"Italy"},{code:"eu-south-2",name:"EU South",city:"Spain",country:"Spain"},{code:"eu-west-2",name:"EU West",city:"London",country:"UK"},{code:"eu-west-3",name:"EU West",city:"Paris",country:"France"},{code:"me-central-1",name:"Middle East",city:"UAE",country:"UAE"},{code:"me-south-1",name:"Middle East",city:"Bahrain",country:"Bahrain"},{code:"sa-east-1",name:"South America",city:"S\xE3o Paulo",country:"Brazil"}]),r=Object.freeze(o.map(e=>e.code));function c(e){return o.find(a=>a.code===e)}function A(e){const a=e.split("-");return a.length===3&&a[0]&&a[1]&&a[2]?`${a[0]}${a[1][0]}${a[2]}`:e}const s=Object.freeze({"Europe/London":"eu-west-2","Europe/Jersey":"eu-west-2","Europe/Guernsey":"eu-west-2","Europe/Isle_of_Man":"eu-west-2","Europe/Dublin":"eu-west-1","Europe/Paris":"eu-west-3","Europe/Berlin":"eu-central-1","Europe/Amsterdam":"eu-central-1","Europe/Brussels":"eu-central-1","Europe/Luxembourg":"eu-central-1","Europe/Vienna":"eu-central-1","Europe/Prague":"eu-central-1","Europe/Warsaw":"eu-central-1","Europe/Budapest":"eu-central-1","Europe/Zurich":"eu-central-2","Europe/Stockholm":"eu-north-1","Europe/Oslo":"eu-north-1","Europe/Copenhagen":"eu-north-1","Europe/Helsinki":"eu-north-1","Europe/Tallinn":"eu-north-1","Europe/Riga":"eu-north-1","Europe/Vilnius":"eu-north-1","Europe/Rome":"eu-south-1","Europe/Malta":"eu-south-1","Europe/Athens":"eu-south-1","Europe/Madrid":"eu-south-2","Europe/Lisbon":"eu-south-2","America/New_York":"us-east-1","America/Detroit":"us-east-1","America/Toronto":"ca-central-1","America/Montreal":"ca-central-1","America/Halifax":"ca-central-1","America/Winnipeg":"ca-central-1","America/Chicago":"us-east-2","America/Indiana/Indianapolis":"us-east-2","America/Mexico_City":"us-east-2","America/Denver":"us-west-2","America/Phoenix":"us-west-2","America/Edmonton":"us-west-2","America/Vancouver":"us-west-2","America/Los_Angeles":"us-west-1","America/Tijuana":"us-west-1","America/Sao_Paulo":"sa-east-1","America/Argentina/Buenos_Aires":"sa-east-1","America/Santiago":"sa-east-1","America/Montevideo":"sa-east-1","America/Lima":"sa-east-1","America/Bogota":"us-east-1","Asia/Tokyo":"ap-northeast-1","Asia/Seoul":"ap-northeast-2","Asia/Hong_Kong":"ap-east-1","Asia/Shanghai":"ap-east-1","Asia/Taipei":"ap-east-1","Asia/Macau":"ap-east-1","Asia/Singapore":"ap-southeast-1","Asia/Kuala_Lumpur":"ap-southeast-1","Asia/Bangkok":"ap-southeast-1","Asia/Ho_Chi_Minh":"ap-southeast-1","Asia/Manila":"ap-southeast-1","Asia/Jakarta":"ap-southeast-3","Asia/Kolkata":"ap-south-1","Asia/Calcutta":"ap-south-1","Asia/Karachi":"ap-south-1","Asia/Dhaka":"ap-south-1","Asia/Colombo":"ap-south-1","Asia/Dubai":"me-central-1","Asia/Muscat":"me-central-1","Asia/Qatar":"me-south-1","Asia/Bahrain":"me-south-1","Asia/Riyadh":"me-south-1","Asia/Kuwait":"me-south-1","Asia/Jerusalem":"me-south-1","Australia/Sydney":"ap-southeast-2","Australia/Brisbane":"ap-southeast-2","Australia/Canberra":"ap-southeast-2","Australia/Melbourne":"ap-southeast-4","Australia/Hobart":"ap-southeast-4","Australia/Adelaide":"ap-southeast-4","Pacific/Auckland":"ap-southeast-2","Pacific/Honolulu":"us-west-1","Africa/Johannesburg":"af-south-1","Africa/Casablanca":"eu-south-2","Africa/Cairo":"me-south-1","Indian/Maldives":"ap-south-1"}),u=Object.freeze({Europe:"eu-central-1",America:"us-east-1",Asia:"ap-southeast-1",Australia:"ap-southeast-2",Pacific:"ap-southeast-2",Africa:"af-south-1",Atlantic:"eu-west-1",Indian:"ap-south-1"});function p(e){if(!e)return;const a=s[e];if(a!==void 0)return a;const t=e.split("/")[0];if(t!==void 0)return u[t]}export{o as AWS_REGIONS_METADATA,i as DEFAULT_REGION,n as MAX_SECONDARY_REGIONS,A as abbreviateRegion,c as getRegionInfo,r as regions,p as suggestRegionForTimezone};
@@ -32,8 +32,8 @@ export declare const SCOPED_TOKEN_REGEX: RegExp;
32
32
  * Mask sensitive information in output strings to prevent credential leakage.
33
33
  * Patterns: postgres://user:pass@host, password=xxx, secret=xxx, apikey=xxx,
34
34
  * GitHub tokens (ghu_/ghs_/ghp_/gho_/github_pat_), bare AWS access-key IDs
35
- * (AKIA-prefixed and ASIA-prefixed), AWS secret keys, ARN account IDs,
36
- * scoped agent tokens.
35
+ * (AKIA-prefixed and ASIA-prefixed), AWS secret keys (env, INI, and JSON key
36
+ * spellings), session tokens, ARN account IDs, scoped agent tokens.
37
37
  *
38
38
  * Single source of truth — consumer loggers (CLI, worker, webapp) MUST
39
39
  * NOT re-implement these patterns inline. See
@@ -1 +1 @@
1
- const n=new Set(["NODE_OPTIONS","NODE_PATH","NODE_EXTRA_CA_CERTS","NODE_DEBUG","NODE_PRESERVE_SYMLINKS","LD_PRELOAD","LD_LIBRARY_PATH","LD_AUDIT","LD_BIND_NOW","DYLD_INSERT_LIBRARIES","DYLD_LIBRARY_PATH","DYLD_FRAMEWORK_PATH","PYTHONPATH","PYTHONSTARTUP","PERL5LIB","PERL5OPT","RUBYLIB","RUBYOPT","HOME","XDG_CONFIG_HOME","AWS_SHARED_CREDENTIALS_FILE","AWS_CONFIG_FILE","SHELL","BASH_ENV","ENV","ZDOTDIR"]);function c(e){return Object.fromEntries(Object.entries(e).filter(([t])=>!n.has(t.toUpperCase())))}const i=/fjall_ak_[A-Z2-7]{16}\.[A-Z2-7]{40}/,l=/fjall_ak_[A-Z2-7]{16}\.[A-Z2-7]{40}/g;function o(e){return e.replace(/-----BEGIN [A-Z ]*PRIVATE KEY-----(?:[^"\\]|\\[\\nrt"])*?-----END [A-Z ]*PRIVATE KEY-----/g,"-----BEGIN [REDACTED] PRIVATE KEY-----...-----END [REDACTED] PRIVATE KEY-----").replace(/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,"-----BEGIN [REDACTED] PRIVATE KEY-----...-----END [REDACTED] PRIVATE KEY-----").replace(/(\w+:\/\/[^:]+:)[^@]+(@)/gi,"$1***$2").replace(/(?<![a-zA-Z])(password|passwd|secret|api[_-]?key|token|auth|credential)([=:])["']?[^\s"']+/gi,"$1$2***").replace(/\b(ghu_|ghs_|ghp_|gho_|github_pat_)[A-Za-z0-9_]+/g,"$1***").replace(/\b(sk|rk)_(live|test)_[A-Za-z0-9]{8,}/g,"$1_$2_***").replace(/\bwhsec_[A-Za-z0-9]{8,}/g,"whsec_***").replace(/(?<=Authorization:\s*Bearer\s+)[A-Za-z0-9._~+/=-]+/gi,"***").replace(/\b(AKIA|ASIA)[A-Z0-9]{12,}\b/g,"$1***").replace(/(?<=AWS_SECRET_ACCESS_KEY=|SecretAccessKey[=:]\s*|"secretAccessKey":\s*")[A-Za-z0-9/+=]{40,}/g,"***").replace(/(arn:aws[^:]*:[^:]*:[^:]*:)(\d{12})(:[^\s]*)/g,"$1***$3").replace(/(?<="(aws)?[Ss]essionToken":\s*")[^"]+/g,"***").replace(/(?<="(internal[Aa]piKey|fjallCallbackToken)":\s*")[^"]+/g,"***").replace(l,"fjall_ak_***")}function D(e){const t=[];let E="",r=!1,A=!1,_=!1,s=!1;for(const a of e){if(_){E+=a,_=!1;continue}if(a==="\\"&&!r){_=!0;continue}if(a==="'"&&!A){r=!r,s=!0;continue}if(a==='"'&&!r){A=!A,s=!0;continue}if(a===" "&&!r&&!A){(E||s)&&(t.push(E),E="",s=!1);continue}E+=a}if(r||A)throw new Error(`Unbalanced ${r?"single":"double"} quote in command: ${e.slice(0,80)}`);if(_)throw new Error(`Trailing backslash in command: ${e.slice(0,80)}`);return(E||s)&&t.push(E),t}export{n as DANGEROUS_ENV_VARS,i as SCOPED_TOKEN_REGEX,c as filterDangerousEnvVars,o as maskSensitiveOutput,D as parseShellArgs};
1
+ const A=new Set(["NODE_OPTIONS","NODE_PATH","NODE_EXTRA_CA_CERTS","NODE_DEBUG","NODE_PRESERVE_SYMLINKS","LD_PRELOAD","LD_LIBRARY_PATH","LD_AUDIT","LD_BIND_NOW","DYLD_INSERT_LIBRARIES","DYLD_LIBRARY_PATH","DYLD_FRAMEWORK_PATH","PYTHONPATH","PYTHONSTARTUP","PERL5LIB","PERL5OPT","RUBYLIB","RUBYOPT","HOME","XDG_CONFIG_HOME","AWS_SHARED_CREDENTIALS_FILE","AWS_CONFIG_FILE","SHELL","BASH_ENV","ENV","ZDOTDIR"]);function l(e){return Object.fromEntries(Object.entries(e).filter(([r])=>!A.has(r.toUpperCase())))}const i=/fjall_ak_[A-Z2-7]{16}\.[A-Z2-7]{40}/,c=/fjall_ak_[A-Z2-7]{16}\.[A-Z2-7]{40}/g;function o(e){return e.replace(/-----BEGIN [A-Z ]*PRIVATE KEY-----(?:[^"\\]|\\[\\nrt"])*?-----END [A-Z ]*PRIVATE KEY-----/g,"-----BEGIN [REDACTED] PRIVATE KEY-----...-----END [REDACTED] PRIVATE KEY-----").replace(/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,"-----BEGIN [REDACTED] PRIVATE KEY-----...-----END [REDACTED] PRIVATE KEY-----").replace(/(\w+:\/\/[^:]+:)[^@]+(@)/gi,"$1***$2").replace(/(?<![a-zA-Z])(password|passwd|secret|api[_-]?key|token|auth|credential)([=:])["']?[^\s"']+/gi,"$1$2***").replace(/\b(ghu_|ghs_|ghp_|gho_|github_pat_)[A-Za-z0-9_]+/g,"$1***").replace(/\b(sk|rk)_(live|test)_[A-Za-z0-9]{8,}/g,"$1_$2_***").replace(/\bwhsec_[A-Za-z0-9]{8,}/g,"whsec_***").replace(/(?<=Authorization:\s*Bearer\s+)[A-Za-z0-9._~+/=-]+/gi,"***").replace(/\b(AKIA|ASIA)[A-Z0-9]{12,}\b/g,"$1***").replace(/(?<=aws_secret_access_key\s*=\s*["']?|SecretAccessKey[=:]\s*|"(aws)?secretAccessKey":\s*"|"aws_secret_access_key":\s*")[A-Za-z0-9/+=]{40,}/gi,"***").replace(/(arn:aws[^:]*:[^:]*:[^:]*:)(\d{12})(:[^\s]*)/g,"$1***$3").replace(/(?<="(aws)?sessionToken":\s*"|"aws_session_token":\s*")[^"]+/gi,"***").replace(/(?<=aws_session_token\s*[=:]\s*["']?|SessionToken[=:]\s*["']?)[^\s"']+/gi,"***").replace(/(?<="(internal[Aa]piKey|fjallCallbackToken)":\s*")[^"]+/g,"***").replace(c,"fjall_ak_***")}function D(e){const r=[];let s="",E=!1,_=!1,n=!1,t=!1;for(const a of e){if(n){s+=a,n=!1;continue}if(a==="\\"&&!E){n=!0;continue}if(a==="'"&&!_){E=!E,t=!0;continue}if(a==='"'&&!E){_=!_,t=!0;continue}if(a===" "&&!E&&!_){(s||t)&&(r.push(s),s="",t=!1);continue}s+=a}if(E||_)throw new Error(`Unbalanced ${E?"single":"double"} quote in command: ${e.slice(0,80)}`);if(n)throw new Error(`Trailing backslash in command: ${e.slice(0,80)}`);return(s||t)&&r.push(s),r}export{A as DANGEROUS_ENV_VARS,i as SCOPED_TOKEN_REGEX,l as filterDangerousEnvVars,o as maskSensitiveOutput,D as parseShellArgs};
package/dist/targets.d.ts CHANGED
@@ -30,6 +30,15 @@ export interface DerivedTarget {
30
30
  environment: string;
31
31
  region: string;
32
32
  }
33
+ /**
34
+ * Canonical stage-or-tier fallback. Structural accounts carry a null workload
35
+ * stage, so display/naming/OU derivation fall back to the structural tier to
36
+ * stay non-null. Single source for every `environment ?? tier` consumer.
37
+ */
38
+ export declare function environmentOrTier(account: {
39
+ environment?: string | null;
40
+ tier?: AccountTier | null;
41
+ }): string;
33
42
  /**
34
43
  * Generate a canonical target name from account name and region.
35
44
  * e.g. ("Production-US", "us-east-1") → "production-us-use1"
package/dist/targets.js CHANGED
@@ -1 +1 @@
1
- import{abbreviateRegion as s}from"./regions.js";import{accountTier as c}from"./environments.js";function m(e){const r=[e.primaryRegion,...e.secondaryRegions??[],e.disasterRecoveryRegion].filter(t=>t!==void 0);return[...new Set(r)]}function u(e,r){return`${e.toLowerCase()}-${s(r)}`}function f(e,r){const t=[];for(const n of e){if(c(n)==="organisation")continue;const o=n.environment??c(n);for(const i of r)t.push({name:u(n.name,i),accountName:n.name,accountId:n.id,environment:o,region:i})}return t.sort((n,o)=>{const i=n.environment.localeCompare(o.environment);if(i!==0)return i;const a=n.accountName.localeCompare(o.accountName);return a!==0?a:n.region.localeCompare(o.region)})}function p(e){return f(e.providerAccounts,m(e))}function v(e,r){if(r.length===0)return e;const t=new Set([e.primaryRegion,e.disasterRecoveryRegion].filter(Boolean)),n=r.filter(o=>!t.has(o));return{...e,secondaryRegions:n.length>0?n:void 0}}function l(e,r){return e.find(t=>t.name===r)}export{p as deriveAllTargets,m as deriveRegionsFromOrgConfig,f as deriveTargets,l as findTarget,u as generateTargetName,v as mergeSecondaryRegions};
1
+ import{abbreviateRegion as m}from"./regions.js";import{accountTier as c}from"./environments.js";function s(e){const r=[e.primaryRegion,...e.secondaryRegions??[],e.disasterRecoveryRegion].filter(t=>t!==void 0);return[...new Set(r)]}function u(e){return e.environment??c(e)}function f(e,r){return`${e.toLowerCase()}-${m(r)}`}function p(e,r){const t=[];for(const n of e){if(c(n)==="organisation")continue;const o=u(n);for(const i of r)t.push({name:f(n.name,i),accountName:n.name,accountId:n.id,environment:o,region:i})}return t.sort((n,o)=>{const i=n.environment.localeCompare(o.environment);if(i!==0)return i;const a=n.accountName.localeCompare(o.accountName);return a!==0?a:n.region.localeCompare(o.region)})}function v(e){return p(e.providerAccounts,s(e))}function l(e,r){if(r.length===0)return e;const t=new Set([e.primaryRegion,e.disasterRecoveryRegion].filter(Boolean)),n=r.filter(o=>!t.has(o));return{...e,secondaryRegions:n.length>0?n:void 0}}function R(e,r){return e.find(t=>t.name===r)}export{v as deriveAllTargets,s as deriveRegionsFromOrgConfig,p as deriveTargets,u as environmentOrTier,R as findTarget,f as generateTargetName,l as mergeSecondaryRegions};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fjall/util",
3
- "version": "2.12.0",
3
+ "version": "2.14.0",
4
4
  "description": "Common utility methods",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -117,5 +117,5 @@
117
117
  "engines": {
118
118
  "node": ">=22.0.0"
119
119
  },
120
- "gitHead": "dca39a47da956d3d94c689dd782fe285d711d25e"
120
+ "gitHead": "ce434478f9e3d5e5ccf979c152a237c81e4acee5"
121
121
  }