@fjall/util 3.8.1 → 3.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.minified +1 -1
- package/dist/config.d.ts +47 -10
- package/dist/config.js +1 -1
- package/package.json +2 -2
package/dist/.minified
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
89 files minified at 2026-07-
|
|
1
|
+
89 files minified at 2026-07-20T22:26:24.729Z
|
package/dist/config.d.ts
CHANGED
|
@@ -144,6 +144,7 @@ export declare const DomainConfigSchema: z.ZodObject<{
|
|
|
144
144
|
}>;
|
|
145
145
|
parentDomain: z.ZodOptional<z.ZodString>;
|
|
146
146
|
account: z.ZodOptional<z.ZodString>;
|
|
147
|
+
region: z.ZodOptional<z.ZodString>;
|
|
147
148
|
}, z.core.$strict>;
|
|
148
149
|
export type DomainConfig = z.infer<typeof DomainConfigSchema>;
|
|
149
150
|
export declare const RootConfigSchema: z.ZodObject<{
|
|
@@ -156,6 +157,7 @@ export declare const RootConfigSchema: z.ZodObject<{
|
|
|
156
157
|
}>;
|
|
157
158
|
parentDomain: z.ZodOptional<z.ZodString>;
|
|
158
159
|
account: z.ZodOptional<z.ZodString>;
|
|
160
|
+
region: z.ZodOptional<z.ZodString>;
|
|
159
161
|
}, z.core.$strict>>>;
|
|
160
162
|
}, z.core.$strict>;
|
|
161
163
|
export type RootConfig = z.infer<typeof RootConfigSchema>;
|
|
@@ -180,17 +182,21 @@ export declare const RootConfigReadSchema: z.ZodObject<{
|
|
|
180
182
|
}>;
|
|
181
183
|
parentDomain: z.ZodOptional<z.ZodString>;
|
|
182
184
|
account: z.ZodOptional<z.ZodString>;
|
|
185
|
+
region: z.ZodOptional<z.ZodString>;
|
|
183
186
|
}, z.core.$strict>>>;
|
|
184
187
|
}, z.core.$strip>;
|
|
185
188
|
export type RootConfigRead = z.infer<typeof RootConfigReadSchema>;
|
|
186
189
|
/**
|
|
187
|
-
* Canonical serialiser for the root fjall-config.json
|
|
190
|
+
* Canonical serialiser for the root fjall-config.json - the single source of
|
|
188
191
|
* truth for the file's on-disk shape. Both `Config.saveConfig` and the webapp
|
|
189
192
|
* scaffold MUST route through this rather than hand-rolling the JSON, so the
|
|
190
193
|
* two can never drift (a scaffold emitting a key the loader later rejects is
|
|
191
|
-
* exactly the bug this prevents). The `RootConfig` input type
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
+
* exactly the bug this prevents). The `RootConfig` input type keeps OUR keys
|
|
195
|
+
* honest at compile time, but the emitted JSON is deliberately NOT
|
|
196
|
+
* strict-validated: `mergeWithDisk` routes top-level keys another fjall
|
|
197
|
+
* version wrote through the save verbatim (see its doc), so a merged save may
|
|
198
|
+
* carry keys RootConfigSchema does not recognise. The round-trip test asserts
|
|
199
|
+
* only that the emitted DEFAULT validates against RootConfigSchema.
|
|
194
200
|
*/
|
|
195
201
|
export declare function serialiseRootConfig(config?: RootConfig): string;
|
|
196
202
|
/**
|
|
@@ -214,6 +220,19 @@ export declare class Config {
|
|
|
214
220
|
* from the on-disk copy.
|
|
215
221
|
*/
|
|
216
222
|
private readonly clearedKeys;
|
|
223
|
+
/**
|
|
224
|
+
* Deep copy of the state this instance LOADED from disk (empty for a
|
|
225
|
+
* programmatically-constructed Config, whose entire state is session
|
|
226
|
+
* intent). saveConfig diffs the live state against this snapshot so it
|
|
227
|
+
* only re-asserts what this session actually changed - an untouched key
|
|
228
|
+
* (or an untouched domains[] entry) keeps whatever a concurrent process
|
|
229
|
+
* wrote to disk between load and save. This is the structural fix for
|
|
230
|
+
* the activeTarget lost-update incident: previously every save re-wrote
|
|
231
|
+
* the whole load-time snapshot at top-level-key granularity, clobbering
|
|
232
|
+
* a concurrent `fjall target set` and erasing concurrently-registered
|
|
233
|
+
* sibling domain entries.
|
|
234
|
+
*/
|
|
235
|
+
private loadedSnapshot;
|
|
217
236
|
constructor(rootConfig?: RootConfig, configPath?: string);
|
|
218
237
|
/**
|
|
219
238
|
* Find the config directory by walking up the directory tree.
|
|
@@ -236,14 +255,32 @@ export declare class Config {
|
|
|
236
255
|
*/
|
|
237
256
|
private static assertWritable;
|
|
238
257
|
/**
|
|
239
|
-
*
|
|
240
|
-
* writing
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
258
|
+
* Session-intent merge: re-reads the on-disk config immediately before
|
|
259
|
+
* writing and asserts ONLY what this session actually changed relative to
|
|
260
|
+
* its load-time snapshot (see loadedSnapshot). An untouched key keeps the
|
|
261
|
+
* disk's current value, the `domains` key merges PER ENTRY (keyed by name)
|
|
262
|
+
* so concurrently-registered siblings survive, keys this version does not
|
|
263
|
+
* recognise ride through verbatim, and keys explicitly cleared this
|
|
264
|
+
* session are removed even when the disk copy still carries them.
|
|
265
|
+
* Unreadable or invalid disk state falls back to the in-memory state with
|
|
266
|
+
* a warning rather than blocking the save.
|
|
245
267
|
*/
|
|
246
268
|
private mergeWithDisk;
|
|
269
|
+
/**
|
|
270
|
+
* Did this session change `key` relative to its load-time snapshot?
|
|
271
|
+
* JSON comparison is exact for today's scalar keys; a future nested
|
|
272
|
+
* top-level key needs its own per-entry merge (as `domains` has).
|
|
273
|
+
*/
|
|
274
|
+
private sessionChangedKey;
|
|
275
|
+
/**
|
|
276
|
+
* Per-entry merge of the `domains` key. Disk order is the base: an entry
|
|
277
|
+
* this session changed is asserted, an entry this session removed (it was
|
|
278
|
+
* in the load snapshot, it is gone from live state) drops out, and any
|
|
279
|
+
* entry this session never touched keeps the DISK version - including
|
|
280
|
+
* entries a concurrent process registered after this session loaded.
|
|
281
|
+
* Session additions append after the disk entries.
|
|
282
|
+
*/
|
|
283
|
+
private mergeDomains;
|
|
247
284
|
private static readDiskConfigForMerge;
|
|
248
285
|
static getConfigDirectory(startDir?: string): string | null;
|
|
249
286
|
/**
|
package/dist/config.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var
|
|
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};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fjall/util",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.9.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": "
|
|
137
|
+
"gitHead": "7cef1c5c3c09bb333888e65ede3d92b2d88c3c63"
|
|
138
138
|
}
|