@fjall/util 2.11.1 → 2.13.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-07T01:01:57.688Z
1
+ 61 files minified at 2026-06-10T21:04:19.408Z
package/dist/Config.d.ts CHANGED
@@ -1,14 +1,69 @@
1
1
  import { z } from "zod";
2
+ import { type Result } from "./docker/result.js";
3
+ import { type AccountTier } from "./environments.js";
2
4
  /**
3
5
  * Backup Vault Lock modes for an account's DisasterRecovery vault.
4
6
  * See decisions/2026-06-03-backup-vault-lock-mode-by-intent.md.
5
7
  */
6
8
  export declare const VAULT_LOCK_MODES: readonly ["compliance", "governance", "none"];
7
9
  export type VaultLockMode = (typeof VAULT_LOCK_MODES)[number];
10
+ /**
11
+ * Account-level S3 Block Public Access posture. Fjall observes-and-flags public
12
+ * buckets through the posture layer rather than hard-blocking, so enforcement is
13
+ * off by default; "enforced" opts an account into all four account-level flags.
14
+ */
15
+ export declare const S3_BPA_MODES: readonly ["enforced", "off"];
16
+ export type S3BpaMode = (typeof S3_BPA_MODES)[number];
17
+ /**
18
+ * Per-account management-events-trail lifecycle for the organisation-trail
19
+ * migration. Absent ⇒ legacy per-account trail, auto-eligible for migration
20
+ * once org-trail delivery is verified. "account" pins the per-account trail
21
+ * (the explicit reverse path). "draining" removes the trail resource while
22
+ * retaining its bucket + CMK. "org" is terminal: the organisation trail
23
+ * covers the account and local storage has been decommissioned.
24
+ */
25
+ export declare const TRAIL_LIFECYCLE_STATES: readonly ["account", "draining", "org"];
26
+ export type TrailLifecycleState = (typeof TRAIL_LIFECYCLE_STATES)[number];
27
+ /**
28
+ * Trail names shared between the CDK constructs
29
+ * (@fjall/components-infrastructure) and the deploy-core org-trail migration —
30
+ * SDK probes (DescribeTrails) must match what the constructs synthesise.
31
+ */
32
+ export declare const ACCOUNT_TRAIL_NAME = "managementEvents";
33
+ export declare const ORGANISATION_TRAIL_NAME = "organisationManagementEvents";
34
+ /**
35
+ * CDK-side trail-state vocabulary (the `fjallAccountTrailState` context
36
+ * value), shared between the CDK constructs and deploy-core's context
37
+ * builders. Distinct from TRAIL_LIFECYCLE_STATES: config intent
38
+ * ("account"/"draining"/"org") maps onto synth behaviour
39
+ * ("active"/"draining"/"removed").
40
+ */
41
+ export declare const ACCOUNT_TRAIL_STATES: readonly ["active", "draining", "removed"];
42
+ export type AccountTrailState = (typeof ACCOUNT_TRAIL_STATES)[number];
43
+ /**
44
+ * Stack output keys (CfnOutput key + exportName) emitted by the Account /
45
+ * Organisation CDK patterns and read back by the deploy-core org-trail
46
+ * migration reconciler — both sides must use the same literals.
47
+ */
48
+ export declare const TRAIL_BUCKET_OUTPUT_KEY = "FjallTrailBucketName";
49
+ export declare const TRAIL_KEY_ARN_OUTPUT_KEY = "FjallTrailKeyArn";
50
+ export declare const ORG_TRAIL_BUCKET_OUTPUT_KEY = "OrganisationTrailBucketName";
8
51
  export type ProviderAccount = {
9
52
  id: string;
10
53
  name: string;
11
- environment: string;
54
+ /**
55
+ * Workload STAGE — null for structural accounts (organisation/platform tiers
56
+ * carry no workload stage). Read the structural axis from `tier`, never from
57
+ * this field.
58
+ */
59
+ environment: string | null;
60
+ /**
61
+ * Structural TIER (organisation/platform/account). Optional for wire
62
+ * back-compat: old configs omit it and readers fall back to decoding
63
+ * `environment` via `accountTier()`. Producers (webapp buildOrgConfigPayload)
64
+ * emit it explicitly so the tier no longer rides on the environment string.
65
+ */
66
+ tier?: AccountTier;
12
67
  managed?: boolean;
13
68
  oidcRoleArn?: string;
14
69
  /**
@@ -26,6 +81,23 @@ export type ProviderAccount = {
26
81
  vaultLock?: VaultLockMode;
27
82
  /** Explicit acknowledgement that vaultLock: "compliance" is irreversible. */
28
83
  acknowledgeImmutableVaultLock?: boolean;
84
+ /**
85
+ * Account-level S3 Block Public Access posture. Absent ⇒ "off" — Fjall does
86
+ * not enforce account-level BPA (the posture layer flags exposed buckets
87
+ * instead). "enforced" sets all four account-level public-access flags true.
88
+ */
89
+ s3BlockPublicAccess?: S3BpaMode;
90
+ /**
91
+ * Management-events-trail lifecycle for the org-trail migration. Absent ⇒
92
+ * legacy per-account trail (auto-eligible); see TRAIL_LIFECYCLE_STATES.
93
+ */
94
+ trailLifecycle?: TrailLifecycleState;
95
+ /**
96
+ * Explicit acknowledgement that decommissioning the per-account trail
97
+ * discards its retained log history. Bucket deletion never proceeds without
98
+ * it — back up the bucket first if the history matters.
99
+ */
100
+ acknowledgeTrailHistoryLoss?: boolean;
29
101
  };
30
102
  export type Profile = {
31
103
  type: "sso" | "oidc";
@@ -74,17 +146,50 @@ export type RootConfig = z.infer<typeof RootConfigSchema>;
74
146
  export declare class Config {
75
147
  rootConfig: RootConfig;
76
148
  private configPath;
149
+ /**
150
+ * True when the config file was FOUND on disk but could not be read.
151
+ * saveConfig refuses to write in that state — the in-memory config never
152
+ * included the real file's contents, so writing would clobber them.
153
+ */
154
+ private loadFailed;
155
+ /**
156
+ * Top-level keys explicitly cleared this session (clearActiveTarget).
157
+ * The disk-preserving merge in saveConfig would otherwise resurrect them
158
+ * from the on-disk copy.
159
+ */
160
+ private readonly clearedKeys;
77
161
  constructor(rootConfig?: RootConfig, configPath?: string);
78
162
  /**
79
163
  * Find the config directory by walking up the directory tree.
80
164
  * Looks for fjall/fjall-config.json or direct fjall-config.json.
165
+ * Walks up from `startDir` when provided, otherwise from process.cwd().
81
166
  */
82
167
  private static findConfigDirectory;
83
168
  private static loadConfigFile;
84
- static loadConfig(): Config;
169
+ /**
170
+ * Load the config, walking up from `startDir` (default process.cwd()).
171
+ */
172
+ static loadConfig(startDir?: string): Config;
85
173
  private static formatZodError;
86
- saveConfig(): void;
87
- static getConfigDirectory(): string | null;
174
+ saveConfig(): Result<void, Error>;
175
+ /**
176
+ * Writability is checked at save time, not load time: POSIX rename replaces
177
+ * a read-only destination whenever the directory is writable, so without
178
+ * this guard the tmp-plus-rename write would silently replace a chmod-444
179
+ * config.
180
+ */
181
+ private static assertWritable;
182
+ /**
183
+ * Disk-preserving merge: re-reads the on-disk config immediately before
184
+ * writing so sibling top-level keys written by a concurrent process between
185
+ * this session's load and save survive. In-memory keys win at top-level-key
186
+ * granularity; keys explicitly cleared this session are removed even when
187
+ * the disk copy still carries them. Unreadable or invalid disk state falls
188
+ * back to the in-memory state with a warning rather than blocking the save.
189
+ */
190
+ private mergeWithDisk;
191
+ private static readDiskConfigForMerge;
192
+ static getConfigDirectory(startDir?: string): string | null;
88
193
  getActiveTarget(): string | undefined;
89
194
  setActiveTarget(name: string): void;
90
195
  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"],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,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 h}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,O=["compliance","governance","none"],$=["enforced","off"],_=["account","draining","org"],x="managementEvents",F="organisationManagementEvents",K=["active","draining","removed"],b="FjallTrailBucketName",k="FjallTrailKeyArn",L="OrganisationTrailBucketName",w=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(w).optional()}).strict(),T=m.keyof().options;function E(C,e,t){const n=e[t];n!==void 0&&(C[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 h(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 h(void 0)}mergeWithDisk(e){const t=s.readDiskConfigForMerge(e);if(t===void 0)return this.rootConfig;const n={...t};for(const o of T)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{x as ACCOUNT_TRAIL_NAME,K as ACCOUNT_TRAIL_STATES,s as Config,F as ORGANISATION_TRAIL_NAME,L as ORG_TRAIL_BUCKET_OUTPUT_KEY,m as RootConfigSchema,$ as S3_BPA_MODES,b as TRAIL_BUCKET_OUTPUT_KEY,k as TRAIL_KEY_ARN_OUTPUT_KEY,_ as TRAIL_LIFECYCLE_STATES,O as VAULT_LOCK_MODES};
@@ -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};
@@ -1,13 +1,18 @@
1
1
  /**
2
- * Standard environment constants shared across CLI, webapp, and deploy-core.
3
- *
4
- * "root" is implicit always derived from role === "organisation",
5
- * not user-selectable.
2
+ * Environment + account-axis constants shared across CLI, webapp, and
3
+ * deploy-core. Two independent axes live here: the workload STAGE
4
+ * ([[ACCOUNT_STAGES]]) and the structural TIER ([[ACCOUNT_TIERS]]). "root" is
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.
6
8
  */
7
- export declare const STANDARD_ENVIRONMENTS: readonly ["production", "staging", "development", "platform", "compliance"];
8
- export type StandardEnvironment = (typeof STANDARD_ENVIRONMENTS)[number];
9
- /** Type guard: checks whether a string is a valid standard environment. */
10
- export declare function isValidEnvironment(value: string): value is StandardEnvironment;
9
+ import { z } from "zod";
10
+ export declare const ACCOUNT_STAGES: readonly ["production", "staging", "development", "platform", "compliance"];
11
+ export type AccountStage = (typeof ACCOUNT_STAGES)[number];
12
+ /** Human-readable labels for each workload stage. */
13
+ export declare const ACCOUNT_STAGE_LABELS: Record<AccountStage, string>;
14
+ /** Type guard: checks whether a string is a user-selectable workload stage. */
15
+ export declare function isAccountStage(value: string): value is AccountStage;
11
16
  /**
12
17
  * Structural environments used for cascade account partitioning.
13
18
  * These are not user-selectable — "root" is implicit (management account),
@@ -18,18 +23,36 @@ export declare const STRUCTURAL_ENVIRONMENTS: {
18
23
  readonly PLATFORM: "platform";
19
24
  };
20
25
  /**
21
- * Wire-format environment list including the implicit "root" value.
22
- * Use this for schemas and validators that accept a system-assigned root
23
- * environment (e.g. quick-create when called after `fjall create org`,
24
- * and the public org-config API). User-facing pickers should still use
25
- * STANDARD_ENVIRONMENTS "root" is set by context, not chosen.
26
+ * Wire-tolerance vocabulary: the workload stages PLUS the structural "root"
27
+ * marker. Ingress schemas that face out-of-version CLIs or the org-config
28
+ * GET→PUT round-trip accept this superset, then decode "root" → null at
29
+ * PERSISTENCE via [[stageFromWireEnvironment]] (the DB never stores structural
30
+ * root). NOT a user-facing picker vocabulary pickers use ACCOUNT_STAGES.
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)".
26
37
  */
27
- export declare const STANDARD_ENVIRONMENTS_WITH_ROOT: readonly ["production", "staging", "development", "platform", "compliance", "root"];
28
- export type StandardEnvironmentWithRoot = (typeof STANDARD_ENVIRONMENTS_WITH_ROOT)[number];
29
- /** Human-readable labels for each standard environment. */
30
- export declare const ENVIRONMENT_LABELS: Record<StandardEnvironment, string>;
38
+ export declare const ACCOUNT_STAGES_WITH_ROOT: readonly ["production", "staging", "development", "platform", "compliance", "root"];
39
+ export type AccountStageWithRoot = (typeof ACCOUNT_STAGES_WITH_ROOT)[number];
31
40
  /** Returns the human-readable label for an environment, with capitalised fallback. */
32
41
  export declare function getEnvironmentLabel(env: string): string;
42
+ /**
43
+ * Structural TIER axis: an account's role in the organisation. Independent of
44
+ * the workload STAGE axis ([[ACCOUNT_STAGES]]) — a tier is never derived from a
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.
48
+ */
49
+ export declare const ACCOUNT_TIERS: readonly ["organisation", "platform", "account"];
50
+ export type AccountTier = (typeof ACCOUNT_TIERS)[number];
51
+ export declare const AccountTierSchema: z.ZodEnum<{
52
+ platform: "platform";
53
+ organisation: "organisation";
54
+ account: "account";
55
+ }>;
33
56
  /**
34
57
  * AWS account roles in the wire format used across the CLI, webapp API
35
58
  * responses, and the Prisma `AccountRole` enum. This is the SINGLE SOURCE
@@ -40,14 +63,38 @@ export declare function getEnvironmentLabel(env: string): string;
40
63
  * a new role here without updating Prisma fails the parity test; renaming
41
64
  * a value here propagates structurally to the webapp.
42
65
  *
43
- * Coupling: `environment` and `role` move together via
44
- * `environmentToRole`/`roleToEnvironment` in
45
- * `webapp/app/.server/utils/orgConfigHelpers.ts`. See
46
- * [[STRUCTURAL_ENVIRONMENTS]] for the env-side equivalent.
66
+ * Independence: `role` (TIER) and `environment` (STAGE) are independent axes.
67
+ * Inbound wire values that still carry a structural environment are decoded via
68
+ * [[environmentToTier]] / [[stageFromWireEnvironment]] — nothing derives one
69
+ * axis from the other on a write. See [[STRUCTURAL_ENVIRONMENTS]].
47
70
  */
48
71
  export declare const ACCOUNT_ROLES: {
49
72
  readonly ORGANISATION: "organisation";
50
73
  readonly PLATFORM: "platform";
51
74
  readonly ACCOUNT: "account";
52
75
  };
53
- export type AccountRole = (typeof ACCOUNT_ROLES)[keyof typeof ACCOUNT_ROLES];
76
+ /** Type guard: checks whether a string is a valid account tier. */
77
+ export declare function isAccountTier(value: string): value is AccountTier;
78
+ /**
79
+ * Decode an inbound wire `environment` to its structural TIER. The wire stays
80
+ * superset-tolerant: out-of-version CLIs and the post-`fjall create org`
81
+ * connect still submit "root"/"platform" as an environment. This is the only
82
+ * sanctioned environment→tier derivation — write paths must NOT re-derive a
83
+ * role from a stage. See decisions/2026-06-07-account-tier-vs-stage-separation.md.
84
+ */
85
+ export declare function environmentToTier(environment: string | null | undefined): AccountTier;
86
+ /**
87
+ * Decode an inbound wire `environment` to its workload STAGE, or null. "root"
88
+ * (the management account — no workloads), empty, null, or any unknown string
89
+ * yields null. "platform" is a real stage and passes through unchanged.
90
+ */
91
+ export declare function stageFromWireEnvironment(environment: string | null | undefined): AccountStage | null;
92
+ /**
93
+ * Canonical TIER accessor for deploy-core/CLI/MCP readers: prefer the explicit
94
+ * `tier` wire field, falling back to decoding a legacy structural environment.
95
+ * Replaces scattered `environment === STRUCTURAL_ENVIRONMENTS.*` comparisons.
96
+ */
97
+ export declare function accountTier(acc: {
98
+ tier?: AccountTier | null;
99
+ environment?: string | null;
100
+ }): AccountTier;
@@ -1 +1 @@
1
- const t=["production","staging","development","platform","compliance"];function n(o){return t.includes(o)}const e={ROOT:"root",PLATFORM:"platform"},p=[...t,e.ROOT],r={production:"Production",staging:"Staging",development:"Development",platform:"Platform",compliance:"Compliance"};function c(o){return n(o)?r[o]:o.charAt(0).toUpperCase()+o.slice(1)}const i={ORGANISATION:"organisation",PLATFORM:"platform",ACCOUNT:"account"};export{i as ACCOUNT_ROLES,r as ENVIRONMENT_LABELS,t as STANDARD_ENVIRONMENTS,p as STANDARD_ENVIRONMENTS_WITH_ROOT,e as STRUCTURAL_ENVIRONMENTS,c as getEnvironmentLabel,n as isValidEnvironment};
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";
@@ -6,12 +7,13 @@ export { singleton } from "./singleton.js";
6
7
  export { DANGEROUS_ENV_VARS, filterDangerousEnvVars, maskSensitiveOutput, parseShellArgs } from "./securityHelpers.js";
7
8
  export { sleep } from "./sleep.js";
8
9
  export { mapSettledWithConcurrency } from "./concurrency.js";
9
- export { STANDARD_ENVIRONMENTS, STANDARD_ENVIRONMENTS_WITH_ROOT, STRUCTURAL_ENVIRONMENTS, type StandardEnvironment, type StandardEnvironmentWithRoot, isValidEnvironment, ENVIRONMENT_LABELS, getEnvironmentLabel, ACCOUNT_ROLES, type AccountRole } from "./environments.js";
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
13
  export { abbreviateRegion } 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 i,toValidDatabaseName as n,toScreamingSnake as S,capitalise as N,getSafeZoneName as _,accountConstructKey as s,hasAsciiStableConstructKey as m}from"./caseConversion.js";import{findAccountNameCollision as A}from"./accountNameCollision.js";import{normaliseError as f,getErrorMessage as C,hasErrorCode as g,getErrorCode as T,getErrorStack as O,formatErrorString as I}from"./errorUtils.js";import{singleton as l}from"./singleton.js";import{DANGEROUS_ENV_VARS as c,filterDangerousEnvVars as V,maskSensitiveOutput as D,parseShellArgs as M}from"./securityHelpers.js";import{sleep as P}from"./sleep.js";import{mapSettledWithConcurrency as H}from"./concurrency.js";import{STANDARD_ENVIRONMENTS as b,STANDARD_ENVIRONMENTS_WITH_ROOT as h,STRUCTURAL_ENVIRONMENTS as L,isValidEnvironment as y,ENVIRONMENT_LABELS as G,getEnvironmentLabel as F,ACCOUNT_ROLES as K}from"./environments.js";import{RESOURCE_CATEGORIES as k,categoriseResource as W,getExpectedDuration as B,getFriendlyResourceType as Z}from"./resourceCategorisation.js";import{parseGitRemoteUrl as q}from"./gitRemoteParser.js";import{abbreviateRegion as z}from"./regions.js";import{SCOPE_VALUES as Q}from"./tokenScopes.js";import{deriveRegionsFromOrgConfig as $,deriveTargets as ee,deriveAllTargets as re,findTarget as oe,generateTargetName as te}from"./targets.js";import{buildAppConfigPath as ae}from"./appPath.js";import{findInfrastructurePaths as ne,findBoundaryPath as Se,isInfrastructureFile as Ne}from"./findInfrastructurePaths.js";import{inferContainerFromCandidates as se}from"./inferContainerFromCandidates.js";import{RESERVED_APP_NAMES as Re,isReservedAppName as Ae}from"./reservedAppNames.js";import{deriveContentHashTag as fe}from"./deriveContentHashTag.js";import{MIGRATION_SNAPSHOT_NAME_PREFIX as ge,EXPECTED_SCHEMA_VERSION_ENV as Te,EXPECTED_SCHEMA_VERSION_TOOL_ENV as Oe,EXPECTED_CH_SCHEMA_VERSION_ENV as Ie,SCHEMA_ADMIN_USER_ENV as xe,SCHEMA_ADMIN_PASSWORD_ENV as le,PRISMA_MIGRATION_DIR_RE as de,CLICKHOUSE_MIGRATION_SKIP_RE as ce}from"./migration/constants.js";export{K as ACCOUNT_ROLES,ce as CLICKHOUSE_MIGRATION_SKIP_RE,c as DANGEROUS_ENV_VARS,o as DNS_APEX,G as ENVIRONMENT_LABELS,Ie as EXPECTED_CH_SCHEMA_VERSION_ENV,Te as EXPECTED_SCHEMA_VERSION_ENV,Oe as EXPECTED_SCHEMA_VERSION_TOOL_ENV,ge as MIGRATION_SNAPSHOT_NAME_PREFIX,de as PRISMA_MIGRATION_DIR_RE,Re as RESERVED_APP_NAMES,k as RESOURCE_CATEGORIES,le as SCHEMA_ADMIN_PASSWORD_ENV,xe as SCHEMA_ADMIN_USER_ENV,Q as SCOPE_VALUES,b as STANDARD_ENVIRONMENTS,h as STANDARD_ENVIRONMENTS_WITH_ROOT,L as STRUCTURAL_ENVIRONMENTS,z as abbreviateRegion,s as accountConstructKey,ae as buildAppConfigPath,N as capitalise,W as categoriseResource,re as deriveAllTargets,fe as deriveContentHashTag,$ as deriveRegionsFromOrgConfig,ee as deriveTargets,V as filterDangerousEnvVars,A as findAccountNameCollision,Se as findBoundaryPath,ne as findInfrastructurePaths,oe as findTarget,I as formatErrorString,te as generateTargetName,t as getDomainExportNames,F as getEnvironmentLabel,T as getErrorCode,C as getErrorMessage,O as getErrorStack,B as getExpectedDuration,Z as getFriendlyResourceType,_ as getSafeZoneName,m as hasAsciiStableConstructKey,g as hasErrorCode,se as inferContainerFromCandidates,Ne as isInfrastructureFile,Ae as isReservedAppName,y as isValidEnvironment,H as mapSettledWithConcurrency,D as maskSensitiveOutput,f as normaliseError,q as parseGitRemoteUrl,M as parseShellArgs,l as singleton,P as sleep,i as toKebab,a as toPascalCase,S as toScreamingSnake,n 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 m,toScreamingSnake as s,capitalise as _,getSafeZoneName as A,accountConstructKey as C,hasAsciiStableConstructKey as p}from"./caseConversion.js";import{findAccountNameCollision as f}from"./accountNameCollision.js";import{normaliseError as R,getErrorMessage as c,hasErrorCode as g,getErrorCode as O,getErrorStack as x,formatErrorString as I}from"./errorUtils.js";import{singleton as l}from"./singleton.js";import{DANGEROUS_ENV_VARS as P,filterDangerousEnvVars as M,maskSensitiveOutput as V,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 G,STRUCTURAL_ENVIRONMENTS as b,ACCOUNT_STAGES as y,ACCOUNT_STAGE_LABELS as F,isAccountStage as K,ACCOUNT_TIERS as W,AccountTierSchema as X,isAccountTier as k,environmentToTier as B,stageFromWireEnvironment as Z,accountTier as j,getEnvironmentLabel as q,ACCOUNT_ROLES as w}from"./environments.js";import{RESOURCE_CATEGORIES as J,categoriseResource as Q,getExpectedDuration as Y,getFriendlyResourceType as $}from"./resourceCategorisation.js";import{parseGitRemoteUrl as re}from"./gitRemoteParser.js";import{abbreviateRegion as te}from"./regions.js";import{SCOPE_VALUES as ne}from"./tokenScopes.js";import{ConnectionWireSchema as ie,ConnectionsListResponseSchema as Se}from"./connectionsWire.js";import{deriveRegionsFromOrgConfig as se,deriveTargets as _e,deriveAllTargets as Ae,environmentOrTier as Ce,findTarget as pe,generateTargetName as Te}from"./targets.js";import{buildAppConfigPath as Ne}from"./appPath.js";import{findInfrastructurePaths as ce,findBoundaryPath as ge,isInfrastructureFile as Oe}from"./findInfrastructurePaths.js";import{inferContainerFromCandidates as Ie}from"./inferContainerFromCandidates.js";import{RESERVED_APP_NAMES as le,isReservedAppName as de}from"./reservedAppNames.js";import{deriveContentHashTag as Me}from"./deriveContentHashTag.js";import{MIGRATION_SNAPSHOT_NAME_PREFIX as Ue,EXPECTED_SCHEMA_VERSION_ENV as De,EXPECTED_SCHEMA_VERSION_TOOL_ENV as ve,EXPECTED_CH_SCHEMA_VERSION_ENV as he,SCHEMA_ADMIN_USER_ENV as He,SCHEMA_ADMIN_PASSWORD_ENV as Le,PRISMA_MIGRATION_DIR_RE as Ge,CLICKHOUSE_MIGRATION_SKIP_RE as be}from"./migration/constants.js";export{w as ACCOUNT_ROLES,y as ACCOUNT_STAGES,G as ACCOUNT_STAGES_WITH_ROOT,F as ACCOUNT_STAGE_LABELS,W as ACCOUNT_TIERS,X as AccountTierSchema,n as BACKUP_VAULT_NAME,be as CLICKHOUSE_MIGRATION_SKIP_RE,ie as ConnectionWireSchema,Se as ConnectionsListResponseSchema,P as DANGEROUS_ENV_VARS,o as DNS_APEX,he as EXPECTED_CH_SCHEMA_VERSION_ENV,De as EXPECTED_SCHEMA_VERSION_ENV,ve as EXPECTED_SCHEMA_VERSION_TOOL_ENV,Ue as MIGRATION_SNAPSHOT_NAME_PREFIX,Ge as PRISMA_MIGRATION_DIR_RE,le as RESERVED_APP_NAMES,J as RESOURCE_CATEGORIES,Le as SCHEMA_ADMIN_PASSWORD_ENV,He as SCHEMA_ADMIN_USER_ENV,ne as SCOPE_VALUES,b as STRUCTURAL_ENVIRONMENTS,te as abbreviateRegion,C as accountConstructKey,j as accountTier,Ne as buildAppConfigPath,_ as capitalise,Q as categoriseResource,Ae as deriveAllTargets,Me as deriveContentHashTag,se as deriveRegionsFromOrgConfig,_e as deriveTargets,Ce as environmentOrTier,B as environmentToTier,M as filterDangerousEnvVars,f as findAccountNameCollision,ge as findBoundaryPath,ce as findInfrastructurePaths,pe as findTarget,I as formatErrorString,Te as generateTargetName,t as getDomainExportNames,q as getEnvironmentLabel,O as getErrorCode,c as getErrorMessage,x as getErrorStack,Y as getExpectedDuration,$ as getFriendlyResourceType,A as getSafeZoneName,p as hasAsciiStableConstructKey,g as hasErrorCode,Ie as inferContainerFromCandidates,K as isAccountStage,k as isAccountTier,Oe as isInfrastructureFile,de as isReservedAppName,H as mapSettledWithConcurrency,V as maskSensitiveOutput,R as normaliseError,re as parseGitRemoteUrl,U as parseShellArgs,l as singleton,v as sleep,Z as stageFromWireEnvironment,S as toKebab,i as toPascalCase,s as toScreamingSnake,m 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;
@@ -173,7 +172,7 @@ export declare const RegistryActionSchema: z.ZodObject<{
173
172
  export type ScaffoldRegistryAction = z.infer<typeof RegistryActionSchema>;
174
173
  export declare const TargetAccountSchema: z.ZodObject<{
175
174
  name: z.ZodString;
176
- environment: z.ZodString;
175
+ environment: z.ZodNullable<z.ZodString>;
177
176
  source: z.ZodString;
178
177
  }, z.core.$strict>;
179
178
  export type TargetAccount = z.infer<typeof TargetAccountSchema>;
@@ -247,12 +246,12 @@ export declare const ScaffoldPlanSchema: z.ZodObject<{
247
246
  }, z.core.$strict>;
248
247
  targetAccount: z.ZodNullable<z.ZodObject<{
249
248
  name: z.ZodString;
250
- environment: z.ZodString;
249
+ environment: z.ZodNullable<z.ZodString>;
251
250
  source: z.ZodString;
252
251
  }, z.core.$strict>>;
253
252
  targetAccountCandidates: z.ZodArray<z.ZodObject<{
254
253
  name: z.ZodString;
255
- environment: z.ZodString;
254
+ environment: z.ZodNullable<z.ZodString>;
256
255
  source: z.ZodString;
257
256
  }, z.core.$strict>>;
258
257
  filesToWrite: z.ZodArray<z.ZodObject<{
@@ -1 +1 @@
1
- import{z as t}from"zod";const p=2,f={AUTHENTICATION_REQUIRED:"AUTHENTICATION_REQUIRED",VALIDATION_ERROR:"VALIDATION_ERROR",INTERNAL_ERROR:"INTERNAL_ERROR",INVALID_SUBCOMMAND:"INVALID_SUBCOMMAND",INVALID_FRAME:"INVALID_FRAME"},a=1e3,e=t.literal(p),l=t.enum(["completed","error","skipped"]),g=t.object({v:e,kind:t.literal("step.start"),stepId:t.string().min(1),name:t.string().min(1),index:t.number().int().min(0),total:t.number().int().min(1)}).strict(),u=t.object({v:e,kind:t.literal("step.complete"),stepId:t.string().min(1),name:t.string().min(1),status:l,index:t.number().int().min(0),total:t.number().int().min(1),errorMessage:t.string().optional()}).strict(),d=t.object({v:e,kind:t.literal("warning"),message:t.string().min(1)}).strict(),b=t.object({code:t.string().min(1),message:t.string().min(1),details:t.record(t.string(),t.unknown()).optional()}).strict(),h=t.object({v:e,kind:t.literal("error"),error:b}).strict(),R=t.object({v:e,kind:t.literal("result"),data:t.unknown()}).strict(),A=t.object({v:e,kind:t.literal("auth.browser.required"),url:t.string().url(),externalId:t.string().min(1),region:t.string().min(1),environment:t.string().min(1),reason:t.string().min(1)}).strict(),S=t.object({v:e,kind:t.literal("auth.browser.complete"),awsAccountId:t.string().min(1),accountName:t.string().min(1),roleArn:t.string().min(1)}).strict(),I=t.discriminatedUnion("kind",[g,u,d,h,R,A,S]),s=t.object({path:t.string().min(1),sizeBytes:t.number().int().nonnegative(),collisionMode:t.enum(["create","overwrite"])}).strict(),c=t.object({type:t.enum(["register-application","create-activity","auto-link-repository","writeback-config-path"]),description:t.string().min(1)}).strict(),x=t.enum(["tinkerer","lightweight","standard","resilient","enterprise"]),k=t.enum(["lightweight","standard","resilient","enterprise","none"]),m=t.object({name:t.string().min(1),environment:t.string().min(1),source:t.string().min(1)}).strict(),E=t.object({name:t.string().min(1),pattern:t.string().min(1).optional(),patternTier:x.optional(),patternDomain:t.string().min(1).optional(),network:k.optional(),template:t.string().min(1).optional(),docker:t.object({path:t.string().min(1).optional(),context:t.string().min(1).optional(),target:t.string().min(1).optional()}).strict().optional(),git:t.boolean().optional(),github:t.boolean().optional(),repoVisibility:t.enum(["private","public"]).optional(),intent:t.string().max(a).optional()}).strict(),T=t.object({planId:t.string().regex(/^[0-9a-f]{32}$/),normalisedPath:t.string().min(1),resolvedInputs:E,targetAccount:m.nullable(),targetAccountCandidates:t.array(m),filesToWrite:t.array(s),npmPackages:t.array(t.string().min(1)),registryActions:t.array(c),patternUnsupportedForDryRun:t.boolean().optional(),intent:t.string().max(a).optional(),framework:t.string().min(1).optional(),summary:t.string().min(1)}).strict(),N=t.object({filesToWrite:t.array(s),npmPackages:t.array(t.string().min(1)),registryActions:t.array(c),patternUnsupportedForDryRun:t.boolean().optional()}).strict(),j=t.object({path:t.string().min(1),name:t.string().min(1),mode:t.enum(["application","pattern"]),dependenciesInstalled:t.boolean().optional(),warning:t.string().optional(),dryRunArtefacts:N.optional()}).strict(),v=t.object({step:t.string().min(1),current:t.number().int().nonnegative(),total:t.number().int().positive().optional(),message:t.string().min(1)}).strict();function w(n){let o;try{o=JSON.parse(n)}catch(i){return{ok:!1,reason:"json",raw:n,error:i instanceof Error?i.message:String(i)}}const r=I.safeParse(o);return r.success?{ok:!0,frame:r.data}:{ok:!1,reason:"schema",raw:n,error:r.error.message}}export{S as AuthBrowserCompleteFrameSchema,A as AuthBrowserRequiredFrameSchema,N as DryRunArtefactsSchema,h as ErrorFrameSchema,s as FileToWriteSchema,a as INTENT_MAX_LENGTH,f as MCP_ERROR_CODES,p as MCP_PROTOCOL_VERSION,I as McpProtocolFrameSchema,c as RegistryActionSchema,E as ResolvedInputsSchema,R as ResultFrameSchema,T as ScaffoldPlanSchema,v as ScaffoldProgressFrameSchema,j as ScaffoldResultDataSchema,u as StepCompleteFrameSchema,g as StepStartFrameSchema,m as TargetAccountSchema,d as WarningFrameSchema,w as parseFrame};
1
+ import{z as t}from"zod";const l=2,f={AUTHENTICATION_REQUIRED:"AUTHENTICATION_REQUIRED",VALIDATION_ERROR:"VALIDATION_ERROR",INTERNAL_ERROR:"INTERNAL_ERROR",INVALID_SUBCOMMAND:"INVALID_SUBCOMMAND",INVALID_FRAME:"INVALID_FRAME"},a=1e3,e=t.literal(l),p=t.enum(["completed","error","skipped"]),g=t.object({v:e,kind:t.literal("step.start"),stepId:t.string().min(1),name:t.string().min(1),index:t.number().int().min(0),total:t.number().int().min(1)}).strict(),u=t.object({v:e,kind:t.literal("step.complete"),stepId:t.string().min(1),name:t.string().min(1),status:p,index:t.number().int().min(0),total:t.number().int().min(1),errorMessage:t.string().optional()}).strict(),d=t.object({v:e,kind:t.literal("warning"),message:t.string().min(1)}).strict(),b=t.object({code:t.string().min(1),message:t.string().min(1),details:t.record(t.string(),t.unknown()).optional()}).strict(),h=t.object({v:e,kind:t.literal("error"),error:b}).strict(),R=t.object({v:e,kind:t.literal("result"),data:t.unknown()}).strict(),A=t.object({v:e,kind:t.literal("auth.browser.required"),url:t.string().url(),externalId:t.string().min(1),region:t.string().min(1),environment:t.string().min(1),reason:t.string().min(1)}).strict(),S=t.object({v:e,kind:t.literal("auth.browser.complete"),awsAccountId:t.string().min(1),accountName:t.string().min(1),roleArn:t.string().min(1)}).strict(),I=t.discriminatedUnion("kind",[g,u,d,h,R,A,S]),s=t.object({path:t.string().min(1),sizeBytes:t.number().int().nonnegative(),collisionMode:t.enum(["create","overwrite"])}).strict(),c=t.object({type:t.enum(["register-application","create-activity","auto-link-repository","writeback-config-path"]),description:t.string().min(1)}).strict(),x=t.enum(["tinkerer","lightweight","standard","resilient","enterprise"]),k=t.enum(["lightweight","standard","resilient","enterprise","none"]),m=t.object({name:t.string().min(1),environment:t.string().min(1).nullable(),source:t.string().min(1)}).strict(),E=t.object({name:t.string().min(1),pattern:t.string().min(1).optional(),patternTier:x.optional(),patternDomain:t.string().min(1).optional(),network:k.optional(),template:t.string().min(1).optional(),docker:t.object({path:t.string().min(1).optional(),context:t.string().min(1).optional(),target:t.string().min(1).optional()}).strict().optional(),git:t.boolean().optional(),github:t.boolean().optional(),repoVisibility:t.enum(["private","public"]).optional(),intent:t.string().max(a).optional()}).strict(),T=t.object({planId:t.string().regex(/^[0-9a-f]{32}$/),normalisedPath:t.string().min(1),resolvedInputs:E,targetAccount:m.nullable(),targetAccountCandidates:t.array(m),filesToWrite:t.array(s),npmPackages:t.array(t.string().min(1)),registryActions:t.array(c),patternUnsupportedForDryRun:t.boolean().optional(),intent:t.string().max(a).optional(),framework:t.string().min(1).optional(),summary:t.string().min(1)}).strict(),N=t.object({filesToWrite:t.array(s),npmPackages:t.array(t.string().min(1)),registryActions:t.array(c),patternUnsupportedForDryRun:t.boolean().optional()}).strict(),j=t.object({path:t.string().min(1),name:t.string().min(1),mode:t.enum(["application","pattern"]),dependenciesInstalled:t.boolean().optional(),warning:t.string().optional(),dryRunArtefacts:N.optional()}).strict(),v=t.object({step:t.string().min(1),current:t.number().int().nonnegative(),total:t.number().int().positive().optional(),message:t.string().min(1)}).strict();function w(n){let o;try{o=JSON.parse(n)}catch(i){return{ok:!1,reason:"json",raw:n,error:i instanceof Error?i.message:String(i)}}const r=I.safeParse(o);return r.success?{ok:!0,frame:r.data}:{ok:!1,reason:"schema",raw:n,error:r.error.message}}export{S as AuthBrowserCompleteFrameSchema,A as AuthBrowserRequiredFrameSchema,N as DryRunArtefactsSchema,h as ErrorFrameSchema,s as FileToWriteSchema,a as INTENT_MAX_LENGTH,f as MCP_ERROR_CODES,l as MCP_PROTOCOL_VERSION,I as McpProtocolFrameSchema,c as RegistryActionSchema,E as ResolvedInputsSchema,R as ResultFrameSchema,T as ScaffoldPlanSchema,v as ScaffoldProgressFrameSchema,j as ScaffoldResultDataSchema,u as StepCompleteFrameSchema,g as StepStartFrameSchema,m as TargetAccountSchema,d as WarningFrameSchema,w as parseFrame};
package/dist/targets.d.ts CHANGED
@@ -4,6 +4,7 @@
4
4
  * A "target" is a deployment destination: an account + region combination
5
5
  * with a canonical name derived from org config at runtime.
6
6
  */
7
+ import { type AccountTier } from "./environments.js";
7
8
  /** Structural interface for the region fields on OrgConfig. */
8
9
  export interface OrgConfigRegions {
9
10
  primaryRegion?: string;
@@ -18,7 +19,9 @@ export declare function deriveRegionsFromOrgConfig(config: OrgConfigRegions): st
18
19
  export interface TargetAccount {
19
20
  name: string;
20
21
  id: string;
21
- environment: string;
22
+ /** Workload STAGE — null for structural accounts; read tier from `tier`. */
23
+ environment: string | null;
24
+ tier?: AccountTier;
22
25
  }
23
26
  export interface DerivedTarget {
24
27
  name: string;
@@ -27,6 +30,15 @@ export interface DerivedTarget {
27
30
  environment: string;
28
31
  region: string;
29
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;
30
42
  /**
31
43
  * Generate a canonical target name from account name and region.
32
44
  * e.g. ("Production-US", "us-east-1") → "production-us-use1"
package/dist/targets.js CHANGED
@@ -1 +1 @@
1
- import{abbreviateRegion as c}from"./regions.js";function s(e){const r=[e.primaryRegion,...e.secondaryRegions??[],e.disasterRecoveryRegion].filter(t=>t!==void 0);return[...new Set(r)]}function m(e,r){return`${e.toLowerCase()}-${c(r)}`}function u(e,r){const t=[];for(const n of e)if(n.environment!=="root")for(const o of r)t.push({name:m(n.name,o),accountName:n.name,accountId:n.id,environment:n.environment,region:o});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 f(e){return u(e.providerAccounts,s(e))}function p(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 g(e,r){return e.find(t=>t.name===r)}export{f as deriveAllTargets,s as deriveRegionsFromOrgConfig,u as deriveTargets,g as findTarget,m as generateTargetName,p 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.11.1",
3
+ "version": "2.13.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": "69823c3d7f2eacba419657464381119c5b5b5fd6"
120
+ "gitHead": "5b16c5731256628f829d4168c65cf165b3516f9a"
121
121
  }