@fjall/util 3.9.0 → 3.10.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
- 89 files minified at 2026-07-20T22:26:24.729Z
1
+ 89 files minified at 2026-07-22T07:34:05.207Z
package/dist/config.d.ts CHANGED
@@ -214,6 +214,13 @@ export declare class Config {
214
214
  * included the real file's contents, so writing would clobber them.
215
215
  */
216
216
  private loadFailed;
217
+ /**
218
+ * True when the file's JSON parsed but failed even the tolerant
219
+ * RootConfigReadSchema (a recognised key such as `domains` is malformed),
220
+ * so the WHOLE config fell back to empty and any recorded domain pins
221
+ * vanished from memory.
222
+ */
223
+ private parseDegraded;
217
224
  /**
218
225
  * Top-level keys explicitly cleared this session (clearActiveTarget).
219
226
  * The disk-preserving merge in saveConfig would otherwise resurrect them
@@ -289,6 +296,16 @@ export declare class Config {
289
296
  * signal error paths branch on.
290
297
  */
291
298
  getConfigPath(): string | null;
299
+ /**
300
+ * True when a fjall-config.json EXISTS on disk but its contents are not
301
+ * represented in this instance: the file could not be read (loadFailed),
302
+ * or its recognised keys were malformed and the tolerant read fell back
303
+ * to empty (parseDegraded). Callers that enforce recorded truth - the
304
+ * deploy account pin - must fail closed in this state rather than treat
305
+ * a missing entry as the absence of a pin. False for a config that simply
306
+ * does not exist.
307
+ */
308
+ isContentUnavailable(): boolean;
292
309
  getActiveTarget(): string | undefined;
293
310
  setActiveTarget(name: string): void;
294
311
  clearActiveTarget(): void;
package/dist/config.js CHANGED
@@ -1 +1 @@
1
- var x=Object.defineProperty;var p=(l,e)=>x(l,"name",{value:e,configurable:!0});import*as s from"fs";import*as g from"path";import{z as f}from"zod";import{failure as y,success as O}from"./docker/result.js";import{getErrorMessage as S}from"./errorUtils.js";import{logger as T}from"./logger.js";import{maskSensitiveOutput as C}from"./securityHelpers.js";const $=10,w="fjall-config.json",U=["compliance","governance","none"],J=["enforced","off"],B=["centralised","off"],P=["account","draining","org"],G="managementEvents",W="organisationManagementEvents",Y=["active","draining","removed"],Z="FjallTrailBucketName",H="FjallTrailKeyArn",q="OrganisationTrailBucketName",A=f.object({name:f.string(),type:f.enum(["apex","delegated"]),parentDomain:f.string().optional(),account:f.string().optional(),region:f.string().optional()}).strict(),D=f.object({activeTarget:f.string().optional(),domains:f.array(A).optional()}).strict(),j=f.object({activeTarget:f.string().optional(),domains:f.array(A).optional()});function F(l={}){return JSON.stringify(l,null,2)}p(F,"serialiseRootConfig");const _=D.keyof().options;function N(l,e,n){const t=e[n];t!==void 0&&(l[n]=t)}p(N,"copyDefinedKey");function k(l,e){if(e===void 0)return!1;const n=new Set([...Object.keys(l),...Object.keys(e)]);for(const t of n)if(l[t]!==e[t])return!1;return!0}p(k,"domainEntriesEqual");class d{static{p(this,"Config")}rootConfig;configPath=null;loadFailed=!1;clearedKeys=new Set;loadedSnapshot={};constructor(e,n){this.rootConfig=e??{},this.configPath=n??null}static findConfigDirectory(e){let n=e!==void 0&&e!==""?e:process.cwd();for(let t=0;t<$;t++){const r=g.join(n,"fjall"),o=g.join(r,w);if(s.existsSync(o))return r;const i=g.join(n,w);if(s.existsSync(i))return n;const c=g.dirname(n);if(c===n)break;n=c}return null}static loadConfigFile(e){try{return s.accessSync(e,s.constants.R_OK),s.readFileSync(e,{encoding:"utf8"})}catch(n){return T.warn("Config",`Config file at ${e} could not be read; using defaults`,{file:e,error:C(S(n))}),null}}static loadConfig(e){const n=d.findConfigDirectory(e);if(!n)return new d;const t=g.join(n,w),r=d.loadConfigFile(t);if(r===null){const c=new d(void 0,t);return c.loadFailed=!0,c}let o;if(r!==""){let c;try{c=JSON.parse(r)}catch(m){throw d.formatZodError(m,w)}const u=D.safeParse(c);if(u.success)o=u.data;else{const m=j.safeParse(c);o=m.success?m.data:{},T.warn("Config","fjall-config.json contains keys this version does not recognise; they were ignored (only activeTarget and domains are read). If this is an old config, regenerate it with `fjall create ...` or re-run `fjall connect`.",{file:t})}}const i=new d(o,t);return i.loadedSnapshot=structuredClone(i.rootConfig),i}static formatZodError(e,n){if(e instanceof f.ZodError&&e.issues.length>0){const o=e.issues.map(i=>`${i.path.join(".")}: ${i.message}`).join("; ");return new Error(`Failed to parse ${n}: ${o}`)}const r=(e instanceof Error?e.message:String(e)).replace(/\n/g," ").substring(0,500);return new Error(`Failed to parse ${n}: ${r}`)}saveConfig(){let e=this.configPath;if(!e){const i=d.findConfigDirectory()||g.join(process.cwd(),"fjall");e=g.join(i,w)}if(this.loadFailed)return y(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 n=g.dirname(e);try{s.mkdirSync(n,{recursive:!0})}catch(i){return y(new Error(`Cannot create config directory ${n}: ${C(S(i))}`))}const t=d.assertWritable(e,n);if(!t.success)return t;const r=F(this.mergeWithDisk(e)),o=`${e}.tmp-${process.pid}`;try{s.writeFileSync(o,r,{mode:384}),s.renameSync(o,e)}catch(i){return y(new Error(`Failed to save ${e}: ${C(S(i))}`))}return O(void 0)}static assertWritable(e,n){if(s.existsSync(e))try{s.accessSync(e,s.constants.W_OK)}catch{return y(new Error(`Cannot save ${e}: the file is read-only. Make it writable (e.g. chmod u+w ${e}) and retry.`))}try{s.accessSync(n,s.constants.W_OK)}catch{return y(new Error(`Cannot save ${e}: the directory ${n} is not writable. Make it writable (e.g. chmod u+w ${n}) and retry.`))}return O(void 0)}mergeWithDisk(e){const n=d.readDiskConfigForMerge(e);if(n===void 0)return this.rootConfig;const t={...n.foreign,...n.known};for(const o of _)o!=="domains"&&this.sessionChangedKey(o)&&(this.rootConfig[o]===void 0?delete t[o]:N(t,this.rootConfig,o));const r=this.mergeDomains(n.known.domains);r!==void 0?t.domains=r:delete t.domains;for(const o of this.clearedKeys)delete t[o];return t}sessionChangedKey(e){return JSON.stringify(this.rootConfig[e])!==JSON.stringify(this.loadedSnapshot[e])}mergeDomains(e){const n=this.rootConfig.domains,t=this.loadedSnapshot.domains??[];if(n===void 0&&t.length===0)return e;const r=p(a=>a.toLowerCase(),"norm"),o=n??[],i=new Map(t.map(a=>[r(a.name),a])),c=new Map(o.map(a=>[r(a.name),a])),u=[],m=new Set;for(const a of e??[]){const h=r(a.name);m.add(h);const E=c.get(h),v=i.get(h);if(E!==void 0&&!k(E,v)){u.push(E);continue}E===void 0&&v!==void 0||u.push(a)}for(const a of o){const h=r(a.name);m.has(h)||k(a,i.get(h))||u.push(a)}return u}static readDiskConfigForMerge(e){if(!s.existsSync(e))return;let n;try{n=JSON.parse(s.readFileSync(e,{encoding:"utf8"}))}catch(o){T.warn("Config",`Could not re-read ${e} before saving; writing in-memory state without merging`,{file:e,error:C(S(o))});return}const t=j.safeParse(n);if(!t.success){T.warn("Config",`On-disk ${e} failed validation before saving; writing in-memory state without merging`,{file:e,error:C(t.error.message)});return}const r={};if(typeof n=="object"&&n!==null){const o=_;for(const[i,c]of Object.entries(n))o.includes(i)||(r[i]=c)}return{known:t.data,foreign:r}}static getConfigDirectory(e){return d.findConfigDirectory(e)}getConfigPath(){return this.configPath}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(n=>n.name.toLowerCase()===e.toLowerCase())}removeDomain(e){if(!this.rootConfig.domains)return!1;const n=this.rootConfig.domains.findIndex(t=>t.name.toLowerCase()===e.toLowerCase());return n===-1?!1:(this.rootConfig.domains.splice(n,1),!0)}}export{G as ACCOUNT_TRAIL_NAME,Y as ACCOUNT_TRAIL_STATES,d as Config,A as DomainConfigSchema,W as ORGANISATION_TRAIL_NAME,q as ORG_TRAIL_BUCKET_OUTPUT_KEY,B as ROOT_ACCESS_MANAGEMENT_MODES,w as ROOT_CONFIG_FILENAME,j as RootConfigReadSchema,D as RootConfigSchema,J as S3_BPA_MODES,Z as TRAIL_BUCKET_OUTPUT_KEY,H as TRAIL_KEY_ARN_OUTPUT_KEY,P as TRAIL_LIFECYCLE_STATES,U as VAULT_LOCK_MODES,F as serialiseRootConfig};
1
+ var k=Object.defineProperty;var h=(g,e)=>k(g,"name",{value:e,configurable:!0});import*as a from"fs";import*as u from"path";import{z as c}from"zod";import{failure as y,success as O}from"./docker/result.js";import{getErrorMessage as T}from"./errorUtils.js";import{logger as C}from"./logger.js";import{maskSensitiveOutput as w}from"./securityHelpers.js";const F=10,E="fjall-config.json",U=["compliance","governance","none"],J=["enforced","off"],B=["centralised","off"],P=["account","draining","org"],G="managementEvents",W="organisationManagementEvents",Y=["active","draining","removed"],Z="FjallTrailBucketName",H="FjallTrailKeyArn",q="OrganisationTrailBucketName",D=c.object({name:c.string(),type:c.enum(["apex","delegated"]),parentDomain:c.string().optional(),account:c.string().optional(),region:c.string().optional()}).strict(),j=c.object({activeTarget:c.string().optional(),domains:c.array(D).optional()}).strict(),A=c.object({activeTarget:c.string().optional(),domains:c.array(D).optional()});function $(g={}){return JSON.stringify(g,null,2)}h($,"serialiseRootConfig");const x=j.keyof().options;function N(g,e,n){const t=e[n];t!==void 0&&(g[n]=t)}h(N,"copyDefinedKey");function _(g,e){if(e===void 0)return!1;const n=new Set([...Object.keys(g),...Object.keys(e)]);for(const t of n)if(g[t]!==e[t])return!1;return!0}h(_,"domainEntriesEqual");class l{static{h(this,"Config")}rootConfig;configPath=null;loadFailed=!1;parseDegraded=!1;clearedKeys=new Set;loadedSnapshot={};constructor(e,n){this.rootConfig=e??{},this.configPath=n??null}static findConfigDirectory(e){let n=e!==void 0&&e!==""?e:process.cwd();for(let t=0;t<F;t++){const r=u.join(n,"fjall"),o=u.join(r,E);if(a.existsSync(o))return r;const s=u.join(n,E);if(a.existsSync(s))return n;const f=u.dirname(n);if(f===n)break;n=f}return null}static loadConfigFile(e){try{return a.accessSync(e,a.constants.R_OK),a.readFileSync(e,{encoding:"utf8"})}catch(n){return C.warn("Config",`Config file at ${e} could not be read; using defaults`,{file:e,error:w(T(n))}),null}}static loadConfig(e){const n=l.findConfigDirectory(e);if(!n)return new l;const t=u.join(n,E),r=l.loadConfigFile(t);if(r===null){const d=new l(void 0,t);return d.loadFailed=!0,d}let o,s=!1;if(r!==""){let d;try{d=JSON.parse(r)}catch(i){throw l.formatZodError(i,E)}const p=j.safeParse(d);if(p.success)o=p.data;else{const i=A.safeParse(d);o=i.success?i.data:{},s=!i.success,i.success?C.warn("Config","fjall-config.json contains keys this version does not recognise; they were ignored (only activeTarget and domains are read). If this is an old config, regenerate it with `fjall create ...` or re-run `fjall connect`.",{file:t}):C.warn("Config","fjall-config.json has a malformed activeTarget or domains value, so its contents were ignored for this run. Fix the file (or regenerate it with `fjall create ...` / `fjall connect`).",{file:t})}}const f=new l(o,t);return f.parseDegraded=s,f.loadedSnapshot=structuredClone(f.rootConfig),f}static formatZodError(e,n){if(e instanceof c.ZodError&&e.issues.length>0){const o=e.issues.map(s=>`${s.path.join(".")}: ${s.message}`).join("; ");return new Error(`Failed to parse ${n}: ${o}`)}const r=(e instanceof Error?e.message:String(e)).replace(/\n/g," ").substring(0,500);return new Error(`Failed to parse ${n}: ${r}`)}saveConfig(){let e=this.configPath;if(!e){const s=l.findConfigDirectory()||u.join(process.cwd(),"fjall");e=u.join(s,E)}if(this.loadFailed)return y(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 n=u.dirname(e);try{a.mkdirSync(n,{recursive:!0})}catch(s){return y(new Error(`Cannot create config directory ${n}: ${w(T(s))}`))}const t=l.assertWritable(e,n);if(!t.success)return t;const r=$(this.mergeWithDisk(e)),o=`${e}.tmp-${process.pid}`;try{a.writeFileSync(o,r,{mode:384}),a.renameSync(o,e)}catch(s){return y(new Error(`Failed to save ${e}: ${w(T(s))}`))}return O(void 0)}static assertWritable(e,n){if(a.existsSync(e))try{a.accessSync(e,a.constants.W_OK)}catch{return y(new Error(`Cannot save ${e}: the file is read-only. Make it writable (e.g. chmod u+w ${e}) and retry.`))}try{a.accessSync(n,a.constants.W_OK)}catch{return y(new Error(`Cannot save ${e}: the directory ${n} is not writable. Make it writable (e.g. chmod u+w ${n}) and retry.`))}return O(void 0)}mergeWithDisk(e){const n=l.readDiskConfigForMerge(e);if(n===void 0)return this.rootConfig;const t={...n.foreign,...n.known};for(const o of x)o!=="domains"&&this.sessionChangedKey(o)&&(this.rootConfig[o]===void 0?delete t[o]:N(t,this.rootConfig,o));const r=this.mergeDomains(n.known.domains);r!==void 0?t.domains=r:delete t.domains;for(const o of this.clearedKeys)delete t[o];return t}sessionChangedKey(e){return JSON.stringify(this.rootConfig[e])!==JSON.stringify(this.loadedSnapshot[e])}mergeDomains(e){const n=this.rootConfig.domains,t=this.loadedSnapshot.domains??[];if(n===void 0&&t.length===0)return e;const r=h(i=>i.toLowerCase(),"norm"),o=n??[],s=new Map(t.map(i=>[r(i.name),i])),f=new Map(o.map(i=>[r(i.name),i])),d=[],p=new Set;for(const i of e??[]){const m=r(i.name);p.add(m);const S=f.get(m),v=s.get(m);if(S!==void 0&&!_(S,v)){d.push(S);continue}S===void 0&&v!==void 0||d.push(i)}for(const i of o){const m=r(i.name);p.has(m)||_(i,s.get(m))||d.push(i)}return d}static readDiskConfigForMerge(e){if(!a.existsSync(e))return;let n;try{n=JSON.parse(a.readFileSync(e,{encoding:"utf8"}))}catch(o){C.warn("Config",`Could not re-read ${e} before saving; writing in-memory state without merging`,{file:e,error:w(T(o))});return}const t=A.safeParse(n);if(!t.success){C.warn("Config",`On-disk ${e} failed validation before saving; writing in-memory state without merging`,{file:e,error:w(t.error.message)});return}const r={};if(typeof n=="object"&&n!==null){const o=x;for(const[s,f]of Object.entries(n))o.includes(s)||(r[s]=f)}return{known:t.data,foreign:r}}static getConfigDirectory(e){return l.findConfigDirectory(e)}getConfigPath(){return this.configPath}isContentUnavailable(){return this.loadFailed||this.parseDegraded}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(n=>n.name.toLowerCase()===e.toLowerCase())}removeDomain(e){if(!this.rootConfig.domains)return!1;const n=this.rootConfig.domains.findIndex(t=>t.name.toLowerCase()===e.toLowerCase());return n===-1?!1:(this.rootConfig.domains.splice(n,1),!0)}}export{G as ACCOUNT_TRAIL_NAME,Y as ACCOUNT_TRAIL_STATES,l as Config,D as DomainConfigSchema,W as ORGANISATION_TRAIL_NAME,q as ORG_TRAIL_BUCKET_OUTPUT_KEY,B as ROOT_ACCESS_MANAGEMENT_MODES,E as ROOT_CONFIG_FILENAME,A as RootConfigReadSchema,j as RootConfigSchema,J as S3_BPA_MODES,Z as TRAIL_BUCKET_OUTPUT_KEY,H as TRAIL_KEY_ARN_OUTPUT_KEY,P as TRAIL_LIFECYCLE_STATES,U as VAULT_LOCK_MODES,$ as serialiseRootConfig};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fjall/util",
3
- "version": "3.9.0",
3
+ "version": "3.10.0",
4
4
  "description": "Common utility methods",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -134,5 +134,5 @@
134
134
  "engines": {
135
135
  "node": ">=22.0.0"
136
136
  },
137
- "gitHead": "7cef1c5c3c09bb333888e65ede3d92b2d88c3c63"
137
+ "gitHead": "ce4e8471194f7e3e5e7df66844bf9387503983f6"
138
138
  }