@fjall/util 21.1.0 → 22.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,7 +9,7 @@
9
9
  "package.json",
10
10
  "tsconfig.json"
11
11
  ],
12
- "hash": "3cd4016b6ba53a403e092a115897c084755ae5791ecbb7812d771635c0e9bd67",
12
+ "hash": "a9867245fbea585c7d8da8754f180acad069f576bad9c76c7c4d85783af73356",
13
13
  "files": {
14
14
  "src/async/concurrency.ts": "3c7ebe0222a720f9",
15
15
  "src/async/singleton.ts": "7bcc21b0fb920561",
@@ -33,7 +33,7 @@
33
33
  "src/cdkToolchain.ts": "a6b996c60b87d644",
34
34
  "src/cfn/physicalNameProperties.ts": "35d3c6f28079c593",
35
35
  "src/clickhouse.ts": "655c74cdba292c29",
36
- "src/config.ts": "2c4174dabed881ed",
36
+ "src/config.ts": "64887717f4eba63f",
37
37
  "src/configPaths.ts": "afcc2e4ceb6652a0",
38
38
  "src/constructMap.ts": "b81fc8e90015467f",
39
39
  "src/deploy/approvalTokenOutput.ts": "4be7b52825d88a37",
@@ -119,15 +119,15 @@
119
119
  "src/repo/scanLocalRepository.ts": "c7bc76201622a7a5",
120
120
  "src/repo/scanTypes.ts": "1fda805a7d8de93b",
121
121
  "src/resourceCategorisation.ts": "a140d1b3cb38be33",
122
- "src/scaffold/ensureGitignore.ts": "230253a19bbf092c",
122
+ "src/scaffold/ensureGitignore.ts": "178a2c3b2c18bc83",
123
123
  "src/scaffold/gitignore.ts": "87c66797a4bd05f2",
124
124
  "src/secrets.ts": "50258d463e4e67c3",
125
- "src/securityHelpers.ts": "81ba6146cf6fcefb",
125
+ "src/securityHelpers.ts": "1e68e1330c6da971",
126
126
  "src/targets.ts": "126280f0a5762e21",
127
127
  "../scripts/minify-dist.mjs": "6b2e4b0df8aec601",
128
- "package.json": "da75ad74e9acae9c",
128
+ "package.json": "f5adff33f9349701",
129
129
  "tsconfig.json": "db759831ad95f396"
130
130
  },
131
- "outputHash": "6286cd5069962e57b14e7a8b535b6862c7e8d9618cc4d0395239e2fb5013ef8d",
131
+ "outputHash": "df5b803de1956d9c166a800379ebf8baaf249c9df6ba359fda62299f13aeeac3",
132
132
  "outputFileCount": 235
133
133
  }
package/dist/.minified CHANGED
@@ -1 +1 @@
1
- 117 files minified at 2026-08-28T12:40:07.188Z
1
+ 117 files minified at 2026-08-30T00:18:14.876Z
package/dist/config.d.ts CHANGED
@@ -147,6 +147,25 @@ export declare const DomainConfigSchema: z.ZodObject<{
147
147
  region: z.ZodOptional<z.ZodString>;
148
148
  }, z.core.$strict>;
149
149
  export type DomainConfig = z.infer<typeof DomainConfigSchema>;
150
+ /**
151
+ * STRICT wire contract for the `organisation` key — the project's binding to
152
+ * ONE Fjall organisation.
153
+ *
154
+ * This is the ONE organisation fact that belongs in fjall-config.json, and it
155
+ * is not a mirror of API state: it is a project ASSERTION, written once by
156
+ * `fjall org bind` from stated intent, that says which organisation this
157
+ * source tree deploys into. Everything else about an organisation (accounts,
158
+ * regions, tiers) is mutable API state that a committed copy would misreport,
159
+ * which is why it stays out of this file.
160
+ *
161
+ * `id` is the only field ever compared. `name` is display-only, so renaming an
162
+ * organisation neither trips the guard nor dirties a checkout.
163
+ */
164
+ export declare const OrganisationBindingSchema: z.ZodObject<{
165
+ id: z.ZodString;
166
+ name: z.ZodOptional<z.ZodString>;
167
+ }, z.core.$strict>;
168
+ export type OrganisationBinding = z.infer<typeof OrganisationBindingSchema>;
150
169
  export declare const RootConfigSchema: z.ZodObject<{
151
170
  activeTarget: z.ZodOptional<z.ZodString>;
152
171
  domains: z.ZodOptional<z.ZodArray<z.ZodObject<{
@@ -164,6 +183,10 @@ export declare const RootConfigSchema: z.ZodObject<{
164
183
  legacyOwnerId: z.ZodOptional<z.ZodString>;
165
184
  legacySgDescription: z.ZodOptional<z.ZodString>;
166
185
  }, z.core.$strict>>>>>;
186
+ organisation: z.ZodOptional<z.ZodObject<{
187
+ id: z.ZodString;
188
+ name: z.ZodOptional<z.ZodString>;
189
+ }, z.core.$strict>>;
167
190
  }, z.core.$strict>;
168
191
  export type RootConfig = z.infer<typeof RootConfigSchema>;
169
192
  /**
@@ -194,6 +217,10 @@ export declare const RootConfigReadSchema: z.ZodObject<{
194
217
  legacyOwnerId: z.ZodOptional<z.ZodString>;
195
218
  legacySgDescription: z.ZodOptional<z.ZodString>;
196
219
  }, z.core.$strict>>>>>;
220
+ organisation: z.ZodOptional<z.ZodObject<{
221
+ id: z.ZodString;
222
+ name: z.ZodOptional<z.ZodString>;
223
+ }, z.core.$strict>>;
197
224
  }, z.core.$strip>;
198
225
  export type RootConfigRead = z.infer<typeof RootConfigReadSchema>;
199
226
  /**
@@ -356,4 +383,7 @@ export declare class Config {
356
383
  addDomain(domain: DomainConfig): void;
357
384
  getDomain(name: string): DomainConfig | undefined;
358
385
  removeDomain(name: string): boolean;
386
+ getOrganisation(): OrganisationBinding | undefined;
387
+ setOrganisation(binding: OrganisationBinding): void;
388
+ clearOrganisation(): void;
359
389
  }
package/dist/config.js CHANGED
@@ -1,2 +1,2 @@
1
- var F=Object.defineProperty;var p=(u,e)=>F(u,"name",{value:e,configurable:!0});import*as a from"fs";import*as d from"path";import{z as l}from"zod";import{CapacityIdentityConfigSchema as A}from"./capacityIdentity.js";import{failure as m,success as w}from"./docker/result.js";import{getErrorMessage as E}from"./errorUtils.js";import{logger as O}from"./logger.js";import{maskSensitiveOutput as y}from"./securityHelpers.js";import{ensureGitignoreEntries as I}from"./scaffold/ensureGitignore.js";import{FJALL_MANAGED_STATE_DIR_ENTRIES as D,FJALL_PROJECT_GITIGNORE_HEADER as R,LOCAL_CONFIG_GITIGNORE_ENTRY as b,LOCAL_STATE_GITIGNORE_MARKER as $}from"./scaffold/gitignore.js";import{LOCAL_CONFIG_DIRNAME as x,LOCAL_CONFIG_FILENAME as M,ROOT_CONFIG_FILENAME as T}from"./configPaths.js";import{LOCAL_CONFIG_DIRNAME as de,LOCAL_CONFIG_FILENAME as ge,ROOT_CONFIG_FILENAME as ue}from"./configPaths.js";const G=10,Q=["compliance","governance","none"],ee=["enforced","off"],te=["centralised","off"],ne=["account","draining","org"],oe="managementEvents",re="organisationManagementEvents",ie=["active","draining","removed"],se="FjallTrailBucketName",ae="FjallTrailKeyArn",ce="OrganisationTrailBucketName",j=l.object({name:l.string(),type:l.enum(["apex","delegated"]),parentDomain:l.string().optional(),account:l.string().optional(),region:l.string().optional()}).strict(),L=l.object({activeTarget:l.string().optional(),domains:l.array(j).optional(),capacityIdentity:A.optional()}).strict(),_=l.object({activeTarget:l.string().optional(),domains:l.array(j).optional(),capacityIdentity:A.optional()}),P=l.object({activeTarget:l.string().optional()});function K(u={}){return JSON.stringify(u,null,2)}p(K,"serialiseRootConfig");const N=L.keyof().options;function J(u,e,t){const n=e[t];n!==void 0&&(u[t]=n)}p(J,"copyDefinedKey");function k(u,e){if(e===void 0)return!1;const t=new Set([...Object.keys(u),...Object.keys(e)]);for(const n of t)if(u[n]!==e[n])return!1;return!0}p(k,"domainEntriesEqual");class f{static{p(this,"Config")}rootConfig;configPath=null;loadFailed=!1;parseDegraded=!1;loadedSnapshot={};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<G;n++){const o=d.join(t,"fjall"),i=d.join(o,T);if(a.existsSync(i))return o;const r=d.join(t,T);if(a.existsSync(r))return t;const c=d.dirname(t);if(c===t)break;t=c}return null}static loadConfigFile(e){try{return a.accessSync(e,a.constants.R_OK),a.readFileSync(e,{encoding:"utf8"})}catch(t){return O.warn("Config",`Config file at ${e} could not be read; using defaults`,{file:e,error:y(E(t))}),null}}static loadConfig(e){const t=f.findConfigDirectory(e);if(!t)return new f;const n=d.join(t,T),o=f.loadConfigFile(n);if(o===null){const g=new f(void 0,n);return g.loadFailed=!0,g}let i,r=!1;if(o!==""){let g;try{g=JSON.parse(o)}catch(s){throw f.formatZodError(s,T)}const C=L.safeParse(g);if(C.success)i=C.data;else{const s=_.safeParse(g);i=s.success?s.data:{},r=!s.success,s.success?O.warn("Config","fjall-config.json contains keys this version does not recognise (only activeTarget, domains and capacityIdentity are read). They were most likely written by a newer fjall version \u2014 leave them in place; saves preserve them verbatim.",{file:n}):O.warn("Config","fjall-config.json has a malformed activeTarget, domains or capacityIdentity value, so its contents were ignored for this run \u2014 any capacity-identity pins it holds will NOT apply (a pinned stack deploys unpinned and is blocked by the rename gate). Fix the file by hand; do NOT regenerate it, which would discard the pins.",{file:n})}}const c=new f(i,n);return c.parseDegraded=r,c.loadedSnapshot=structuredClone(c.rootConfig),c}static formatZodError(e,t){if(e instanceof l.ZodError&&e.issues.length>0){const i=e.issues.map(r=>`${r.path.join(".")}: ${r.message}`).join("; ");return new Error(`Failed to parse ${t}: ${i}`)}const o=(e instanceof Error?e.message:String(e)).replace(/\n/g," ").substring(0,500);return new Error(`Failed to parse ${t}: ${o}`)}saveConfig(){const e=d.join(this.resolveProjectDirectory()??d.join(process.cwd(),"fjall"),T);if(this.loadFailed)return m(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=d.dirname(e);try{a.mkdirSync(t,{recursive:!0})}catch(c){return m(new Error(`Cannot create config directory ${t}: ${y(E(c))}`))}const n=f.assertWritable(e,t);if(!n.success)return n;const o=this.mergeWithDisk(e);if(!o.success)return o;const i=K(o.data),r=`${e}.tmp-${process.pid}`;try{a.writeFileSync(r,i,{mode:384}),a.renameSync(r,e)}catch(c){return m(new Error(`Failed to save ${e}: ${y(E(c))}`))}return this.configPath=e,w(void 0)}static assertWritable(e,t){if(a.existsSync(e))try{a.accessSync(e,a.constants.W_OK)}catch{return m(new Error(`Cannot save ${e}: the file is read-only. Make it writable (e.g. chmod u+w ${e}) and retry.`))}try{a.accessSync(t,a.constants.W_OK)}catch{return m(new Error(`Cannot save ${e}: the directory ${t} is not writable. Make it writable (e.g. chmod u+w ${t}) and retry.`))}return w(void 0)}mergeWithDisk(e){const t=f.readDiskConfigForMerge(e);if(t.kind==="unmergeable")return m(new Error(`Refusing to save ${e}: ${t.reason}. Writing now would keep only this session's state and drop everything else the file carries (including keys written by other fjall versions). Fix or remove the file and retry.`));if(t.kind==="absent")return w(this.rootConfig);const n=t.disk,o={...n.foreign,...n.known};for(const r of N)r!=="domains"&&this.sessionChangedKey(r)&&(this.rootConfig[r]===void 0?delete o[r]:J(o,this.rootConfig,r));const i=this.mergeDomains(n.known.domains);return i!==void 0?o.domains=i:delete o.domains,w(o)}sessionChangedKey(e){return JSON.stringify(this.rootConfig[e])!==JSON.stringify(this.loadedSnapshot[e])}mergeDomains(e){const t=this.rootConfig.domains,n=this.loadedSnapshot.domains??[];if(t===void 0&&n.length===0)return e;const o=p(s=>s.toLowerCase(),"norm"),i=t??[],r=new Map(n.map(s=>[o(s.name),s])),c=new Map(i.map(s=>[o(s.name),s])),g=[],C=new Set;for(const s of e??[]){const h=o(s.name);C.add(h);const v=c.get(h),S=r.get(h);if(v!==void 0&&!k(v,S)){g.push(v);continue}v===void 0&&S!==void 0||g.push(s)}for(const s of i){const h=o(s.name);C.has(h)||k(s,r.get(h))||g.push(s)}return g}static readDiskConfigForMerge(e){if(!a.existsSync(e))return{kind:"absent"};let t;try{const i=a.readFileSync(e,{encoding:"utf8"});if(i==="")return{kind:"absent"};t=JSON.parse(i)}catch(i){return{kind:"unmergeable",reason:`the existing file could not be read or parsed as JSON (${y(E(i))})`}}const n=_.safeParse(t);if(!n.success){const i=n.error.issues.map(r=>`${r.path.join(".")}: ${r.message}`).join("; ");return{kind:"unmergeable",reason:`a recognised key in the existing file is malformed (${y(i)})`}}const o={};if(typeof t=="object"&&t!==null){const i=N;for(const[r,c]of Object.entries(t))i.includes(r)||(o[r]=c)}return{kind:"ok",disk:{known:n.data,foreign:o}}}static getConfigDirectory(e){return f.findConfigDirectory(e)}getConfigPath(){return this.configPath}isContentUnavailable(){return this.loadFailed||this.parseDegraded}static warnedLegacyActiveTarget=!1;localConfigCache;resolveProjectDirectory(){return this.configPath!==null?d.dirname(this.configPath):f.findConfigDirectory()}localConfigFile(){const e=this.resolveProjectDirectory();return e===null?null:d.join(e,x,M)}loadLocalConfig(){if(this.localConfigCache===void 0){const e=this.localConfigFile();if(e===null)return this.localConfigCache={};try{const t=a.readFileSync(e,{encoding:"utf8"}),n=P.safeParse(JSON.parse(t));this.localConfigCache=n.success?n.data:{}}catch{this.localConfigCache={}}}return this.localConfigCache}writeLocalConfig(e){const t=this.localConfigFile();if(t===null)return m(new Error("Cannot persist local state: no fjall-config.json was found from this directory, so there is no project to anchor `.fjall/local.json` to. Run this from inside a fjall project, or run `fjall create account` / `fjall create app` first."));try{a.mkdirSync(d.dirname(t),{recursive:!0});const n=`${t}.tmp-${process.pid}`;a.writeFileSync(n,JSON.stringify(e,null,2)+`
2
- `,{encoding:"utf8"}),a.renameSync(n,t),I(d.dirname(d.dirname(t)),[b,...D],{header:R,marker:$})}catch(n){return m(new Error(`Failed to write ${t}: ${y(E(n))}`))}return this.localConfigCache=e,w(void 0)}static warnLegacyActiveTargetOnce(){f.warnedLegacyActiveTarget||(f.warnedLegacyActiveTarget=!0,O.warn("Config","`activeTarget` in fjall-config.json is deprecated and ignored: a committed target must never re-aim a deploy or CI pipeline. Run `fjall target set <name>` to store it locally in .fjall/local.json, then remove `activeTarget` from the committed fjall-config.json."))}getActiveTarget(){const e=this.loadLocalConfig().activeTarget;if(e!==void 0)return e;this.rootConfig.activeTarget!==void 0&&f.warnLegacyActiveTargetOnce()}setActiveTarget(e){const t=this.loadLocalConfig();return t.activeTarget=e,this.writeLocalConfig(t)}clearActiveTarget(){const e=this.loadLocalConfig();return delete e.activeTarget,this.writeLocalConfig(e)}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{oe as ACCOUNT_TRAIL_NAME,ie as ACCOUNT_TRAIL_STATES,f as Config,j as DomainConfigSchema,de as LOCAL_CONFIG_DIRNAME,ge as LOCAL_CONFIG_FILENAME,re as ORGANISATION_TRAIL_NAME,ce as ORG_TRAIL_BUCKET_OUTPUT_KEY,te as ROOT_ACCESS_MANAGEMENT_MODES,ue as ROOT_CONFIG_FILENAME,_ as RootConfigReadSchema,L as RootConfigSchema,ee as S3_BPA_MODES,se as TRAIL_BUCKET_OUTPUT_KEY,ae as TRAIL_KEY_ARN_OUTPUT_KEY,ne as TRAIL_LIFECYCLE_STATES,Q as VAULT_LOCK_MODES,K as serialiseRootConfig};
1
+ var I=Object.defineProperty;var p=(u,e)=>I(u,"name",{value:e,configurable:!0});import*as a from"fs";import*as d from"path";import{z as c}from"zod";import{CapacityIdentityConfigSchema as A}from"./capacityIdentity.js";import{failure as m,success as w}from"./docker/result.js";import{getErrorMessage as E}from"./errorUtils.js";import{logger as v}from"./logger.js";import{maskSensitiveOutput as C}from"./securityHelpers.js";import{ensureGitignoreEntries as D}from"./scaffold/ensureGitignore.js";import{FJALL_MANAGED_STATE_DIR_ENTRIES as R,FJALL_PROJECT_GITIGNORE_HEADER as b,LOCAL_CONFIG_GITIGNORE_ENTRY as x,LOCAL_STATE_GITIGNORE_MARKER as $}from"./scaffold/gitignore.js";import{LOCAL_CONFIG_DIRNAME as M,LOCAL_CONFIG_FILENAME as G,ROOT_CONFIG_FILENAME as T}from"./configPaths.js";import{LOCAL_CONFIG_DIRNAME as ge,LOCAL_CONFIG_FILENAME as ue,ROOT_CONFIG_FILENAME as me}from"./configPaths.js";const P=10,ee=["compliance","governance","none"],te=["enforced","off"],ne=["centralised","off"],oe=["account","draining","org"],ie="managementEvents",re="organisationManagementEvents",se=["active","draining","removed"],ae="FjallTrailBucketName",ce="FjallTrailKeyArn",le="OrganisationTrailBucketName",j=c.object({name:c.string(),type:c.enum(["apex","delegated"]),parentDomain:c.string().optional(),account:c.string().optional(),region:c.string().optional()}).strict(),L=c.object({id:c.string().min(1),name:c.string().min(1).optional()}).strict(),_=c.object({activeTarget:c.string().optional(),domains:c.array(j).optional(),capacityIdentity:A.optional(),organisation:L.optional()}).strict(),N=c.object({activeTarget:c.string().optional(),domains:c.array(j).optional(),capacityIdentity:A.optional(),organisation:L.optional()}),K=c.object({activeTarget:c.string().optional()});function J(u={}){return JSON.stringify(u,null,2)}p(J,"serialiseRootConfig");const k=_.keyof().options;function U(u,e,t){const n=e[t];n!==void 0&&(u[t]=n)}p(U,"copyDefinedKey");function F(u,e){if(e===void 0)return!1;const t=new Set([...Object.keys(u),...Object.keys(e)]);for(const n of t)if(u[n]!==e[n])return!1;return!0}p(F,"domainEntriesEqual");class f{static{p(this,"Config")}rootConfig;configPath=null;loadFailed=!1;parseDegraded=!1;loadedSnapshot={};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<P;n++){const o=d.join(t,"fjall"),r=d.join(o,T);if(a.existsSync(r))return o;const i=d.join(t,T);if(a.existsSync(i))return t;const l=d.dirname(t);if(l===t)break;t=l}return null}static loadConfigFile(e){try{return a.accessSync(e,a.constants.R_OK),a.readFileSync(e,{encoding:"utf8"})}catch(t){return v.warn("Config",`Config file at ${e} could not be read; using defaults`,{file:e,error:C(E(t))}),null}}static loadConfig(e){const t=f.findConfigDirectory(e);if(!t)return new f;const n=d.join(t,T),o=f.loadConfigFile(n);if(o===null){const g=new f(void 0,n);return g.loadFailed=!0,g}let r,i=!1;if(o!==""){let g;try{g=JSON.parse(o)}catch(s){throw f.formatZodError(s,T)}const y=_.safeParse(g);if(y.success)r=y.data;else{const s=N.safeParse(g);r=s.success?s.data:{},i=!s.success,s.success?v.warn("Config","fjall-config.json contains keys this version does not recognise (only activeTarget, domains and capacityIdentity are read). They were most likely written by a newer fjall version \u2014 leave them in place; saves preserve them verbatim.",{file:n}):v.warn("Config","fjall-config.json has a malformed activeTarget, domains or capacityIdentity value, so its contents were ignored for this run \u2014 any capacity-identity pins it holds will NOT apply (a pinned stack deploys unpinned and is blocked by the rename gate). Fix the file by hand; do NOT regenerate it, which would discard the pins.",{file:n})}}const l=new f(r,n);return l.parseDegraded=i,l.loadedSnapshot=structuredClone(l.rootConfig),l}static formatZodError(e,t){if(e instanceof c.ZodError&&e.issues.length>0){const r=e.issues.map(i=>`${i.path.join(".")}: ${i.message}`).join("; ");return new Error(`Failed to parse ${t}: ${r}`)}const o=(e instanceof Error?e.message:String(e)).replace(/\n/g," ").substring(0,500);return new Error(`Failed to parse ${t}: ${o}`)}saveConfig(){const e=d.join(this.resolveProjectDirectory()??d.join(process.cwd(),"fjall"),T);if(this.loadFailed)return m(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=d.dirname(e);try{a.mkdirSync(t,{recursive:!0})}catch(l){return m(new Error(`Cannot create config directory ${t}: ${C(E(l))}`))}const n=f.assertWritable(e,t);if(!n.success)return n;const o=this.mergeWithDisk(e);if(!o.success)return o;const r=J(o.data),i=`${e}.tmp-${process.pid}`;try{a.writeFileSync(i,r,{mode:384}),a.renameSync(i,e)}catch(l){return m(new Error(`Failed to save ${e}: ${C(E(l))}`))}return this.configPath=e,w(void 0)}static assertWritable(e,t){if(a.existsSync(e))try{a.accessSync(e,a.constants.W_OK)}catch{return m(new Error(`Cannot save ${e}: the file is read-only. Make it writable (e.g. chmod u+w ${e}) and retry.`))}try{a.accessSync(t,a.constants.W_OK)}catch{return m(new Error(`Cannot save ${e}: the directory ${t} is not writable. Make it writable (e.g. chmod u+w ${t}) and retry.`))}return w(void 0)}mergeWithDisk(e){const t=f.readDiskConfigForMerge(e);if(t.kind==="unmergeable")return m(new Error(`Refusing to save ${e}: ${t.reason}. Writing now would keep only this session's state and drop everything else the file carries (including keys written by other fjall versions). Fix or remove the file and retry.`));if(t.kind==="absent")return w(this.rootConfig);const n=t.disk,o={...n.foreign,...n.known};for(const i of k)i!=="domains"&&this.sessionChangedKey(i)&&(this.rootConfig[i]===void 0?delete o[i]:U(o,this.rootConfig,i));const r=this.mergeDomains(n.known.domains);return r!==void 0?o.domains=r:delete o.domains,w(o)}sessionChangedKey(e){return JSON.stringify(this.rootConfig[e])!==JSON.stringify(this.loadedSnapshot[e])}mergeDomains(e){const t=this.rootConfig.domains,n=this.loadedSnapshot.domains??[];if(t===void 0&&n.length===0)return e;const o=p(s=>s.toLowerCase(),"norm"),r=t??[],i=new Map(n.map(s=>[o(s.name),s])),l=new Map(r.map(s=>[o(s.name),s])),g=[],y=new Set;for(const s of e??[]){const h=o(s.name);y.add(h);const O=l.get(h),S=i.get(h);if(O!==void 0&&!F(O,S)){g.push(O);continue}O===void 0&&S!==void 0||g.push(s)}for(const s of r){const h=o(s.name);y.has(h)||F(s,i.get(h))||g.push(s)}return g}static readDiskConfigForMerge(e){if(!a.existsSync(e))return{kind:"absent"};let t;try{const r=a.readFileSync(e,{encoding:"utf8"});if(r==="")return{kind:"absent"};t=JSON.parse(r)}catch(r){return{kind:"unmergeable",reason:`the existing file could not be read or parsed as JSON (${C(E(r))})`}}const n=N.safeParse(t);if(!n.success){const r=n.error.issues.map(i=>`${i.path.join(".")}: ${i.message}`).join("; ");return{kind:"unmergeable",reason:`a recognised key in the existing file is malformed (${C(r)})`}}const o={};if(typeof t=="object"&&t!==null){const r=k;for(const[i,l]of Object.entries(t))r.includes(i)||(o[i]=l)}return{kind:"ok",disk:{known:n.data,foreign:o}}}static getConfigDirectory(e){return f.findConfigDirectory(e)}getConfigPath(){return this.configPath}isContentUnavailable(){return this.loadFailed||this.parseDegraded}static warnedLegacyActiveTarget=!1;localConfigCache;resolveProjectDirectory(){return this.configPath!==null?d.dirname(this.configPath):f.findConfigDirectory()}localConfigFile(){const e=this.resolveProjectDirectory();return e===null?null:d.join(e,M,G)}loadLocalConfig(){if(this.localConfigCache===void 0){const e=this.localConfigFile();if(e===null)return this.localConfigCache={};try{const t=a.readFileSync(e,{encoding:"utf8"}),n=K.safeParse(JSON.parse(t));this.localConfigCache=n.success?n.data:{}}catch{this.localConfigCache={}}}return this.localConfigCache}writeLocalConfig(e){const t=this.localConfigFile();if(t===null)return m(new Error("Cannot persist local state: no fjall-config.json was found from this directory, so there is no project to anchor `.fjall/local.json` to. Run this from inside a fjall project, or run `fjall create account` / `fjall create app` first."));try{a.mkdirSync(d.dirname(t),{recursive:!0});const n=`${t}.tmp-${process.pid}`;a.writeFileSync(n,JSON.stringify(e,null,2)+`
2
+ `,{encoding:"utf8"}),a.renameSync(n,t),D(d.dirname(d.dirname(t)),[x,...R],{header:b,marker:$})}catch(n){return m(new Error(`Failed to write ${t}: ${C(E(n))}`))}return this.localConfigCache=e,w(void 0)}static warnLegacyActiveTargetOnce(){f.warnedLegacyActiveTarget||(f.warnedLegacyActiveTarget=!0,v.warn("Config","`activeTarget` in fjall-config.json is deprecated and ignored: a committed target must never re-aim a deploy or CI pipeline. Run `fjall target set <name>` to store it locally in .fjall/local.json, then remove `activeTarget` from the committed fjall-config.json."))}getActiveTarget(){const e=this.loadLocalConfig().activeTarget;if(e!==void 0)return e;this.rootConfig.activeTarget!==void 0&&f.warnLegacyActiveTargetOnce()}setActiveTarget(e){const t=this.loadLocalConfig();return t.activeTarget=e,this.writeLocalConfig(t)}clearActiveTarget(){const e=this.loadLocalConfig();return delete e.activeTarget,this.writeLocalConfig(e)}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)}getOrganisation(){return this.rootConfig.organisation}setOrganisation(e){this.rootConfig.organisation=e}clearOrganisation(){delete this.rootConfig.organisation}}export{ie as ACCOUNT_TRAIL_NAME,se as ACCOUNT_TRAIL_STATES,f as Config,j as DomainConfigSchema,ge as LOCAL_CONFIG_DIRNAME,ue as LOCAL_CONFIG_FILENAME,re as ORGANISATION_TRAIL_NAME,le as ORG_TRAIL_BUCKET_OUTPUT_KEY,L as OrganisationBindingSchema,ne as ROOT_ACCESS_MANAGEMENT_MODES,me as ROOT_CONFIG_FILENAME,N as RootConfigReadSchema,_ as RootConfigSchema,te as S3_BPA_MODES,ae as TRAIL_BUCKET_OUTPUT_KEY,ce as TRAIL_KEY_ARN_OUTPUT_KEY,oe as TRAIL_LIFECYCLE_STATES,ee as VAULT_LOCK_MODES,J as serialiseRootConfig};
@@ -1,18 +1,26 @@
1
1
  /**
2
2
  * Idempotently guarantee that a `.gitignore` in `directory` carries `entries`.
3
3
  *
4
- * Absent file → written with `header` followed by the entries. Existing file →
5
- * only the missing entries are appended, under `marker` so the addition is
6
- * attributable. An entry already present anywhere in the file (at any
7
- * indentation) is left alone, so repeat calls are no-ops.
4
+ * Absent file → written with `header` followed by the entries, unless the
5
+ * caller passes `createIfMissing: false`. Existing file only the missing
6
+ * entries are appended, under `marker` so the addition is attributable. An
7
+ * entry already present anywhere in the file (at any indentation) is left
8
+ * alone, so repeat calls are no-ops.
9
+ *
10
+ * `createIfMissing: false` is for migration callers, which append globs to a
11
+ * checkout that already ignores something. Creating a `.gitignore` where the
12
+ * author never had one is a write into their repository that they did not
13
+ * ask for, and it is not what a migration means.
8
14
  *
9
15
  * Best-effort by design: this maintains repository hygiene, never correctness.
10
16
  * The caller's real work — writing config or recording deploy state — must not
11
17
  * fail because a `.gitignore` could not be written (read-only checkout, no
12
18
  * repository at all, permissions). Returns whether the file now provably
13
- * carries every entry, for callers that want to assert or log.
19
+ * carries every entry, for callers that want to assert or log — so a skipped
20
+ * creation reports `false`, because no file carries anything.
14
21
  */
15
22
  export declare function ensureGitignoreEntries(directory: string, entries: readonly string[], options: {
16
23
  header?: string;
17
24
  marker: string;
25
+ createIfMissing?: boolean;
18
26
  }): boolean;
@@ -1,9 +1,9 @@
1
- var d=Object.defineProperty;var u=(o,e)=>d(o,"name",{value:e,configurable:!0});import i from"fs";import g from"path";function l(o,e,s){const t=g.join(o,".gitignore");try{const r=i.existsSync(t)?i.readFileSync(t,"utf8"):void 0;if(r===void 0){const n=s.header??"";return i.writeFileSync(t,`${n}${e.join(`
1
+ var d=Object.defineProperty;var o=(s,n)=>d(s,"name",{value:n,configurable:!0});import i from"fs";import g from"path";function p(s,n,f){const t=g.join(s,".gitignore");try{const r=i.existsSync(t)?i.readFileSync(t,"utf8"):void 0;if(r===void 0){if(f.createIfMissing===!1)return!1;const e=f.header??"";return i.writeFileSync(t,`${e}${n.join(`
2
2
  `)}
3
- `,{encoding:"utf8"}),!0}const a=new Set(r.split(`
4
- `).map(n=>n.trim())),c=e.filter(n=>!a.has(n));if(c.length===0)return!0;const f=r===""||r.endsWith(`
3
+ `,{encoding:"utf8"}),!0}const c=new Set(r.split(`
4
+ `).map(e=>e.trim())),a=n.filter(e=>!c.has(e));if(a.length===0)return!0;const u=r===""||r.endsWith(`
5
5
  `)?"":`
6
- `;return i.appendFileSync(t,`${f}${s.marker}
7
- ${c.join(`
6
+ `;return i.appendFileSync(t,`${u}${f.marker}
7
+ ${a.join(`
8
8
  `)}
9
- `,"utf8"),!0}catch{return!1}}u(l,"ensureGitignoreEntries");export{l as ensureGitignoreEntries};
9
+ `,"utf8"),!0}catch{return!1}}o(p,"ensureGitignoreEntries");export{p as ensureGitignoreEntries};
@@ -1 +1 @@
1
- var g=Object.defineProperty;var A=(t,a)=>g(t,"name",{value:a,configurable:!0});const R=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 u(t){return Object.fromEntries(Object.entries(t).filter(([a])=>!R.has(a.toUpperCase())))}A(u,"filterDangerousEnvVars");const L=/fjall_(?:ak|dk)_[A-Z2-7]{16}\.[A-Z2-7]{40}/,S=/(fjall_(?:ak|dk)_)[A-Z2-7]{16}\.[A-Z2-7]{40}/g,D=/\bfj_[0-9a-f]{8}_[0-9a-f]{48}\b/gi,s="***",i="[REDACTED]",o=`-----BEGIN ${i} PRIVATE KEY-----...-----END ${i} PRIVATE KEY-----`,l=String.raw`(?:next|continuation|pagination|page|forward|backward)+token`,p=String.raw`(?!ghu_|ghs_|ghp_|gho_|github_pat_|(?:sk|rk)_(?:live|test)_|whsec_|fjall_(?:ak|dk)_|fj_[0-9a-f]{8}_|AKIA|ASIA|%[a-zA-Z])`,I=new RegExp(String.raw`(?<!"${l}":\s*")`+String.raw`(?<="[a-zA-Z0-9_-]*(?:password|passwd|secret|api[_-]?key|token|auth|credential)":\s*")`+p+String.raw`[^"]+`,"gi"),N=new RegExp(String.raw`(?<!${l}\s*[=:]\s*["']?)`+String.raw`(?<=(?<!/)(?:password|passwd|secret|api[_-]?key|token|auth|credential)\s*[=:]\s*["']?)`+p+String.raw`[^\s"']+`,"gi");function f(t){return t.replace(/-----BEGIN [A-Z ]*PRIVATE KEY-----(?:[^"\\]|\\[\\nrt"])*?-----END [A-Z ]*PRIVATE KEY-----/g,()=>o).replace(/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,()=>o).replace(/(\w+:\/\/[^:]+:)[^@]+(@)/gi,(a,e,_)=>`${e}${s}${_}`).replace(/(?<![a-zA-Z])(password|passwd|secret|api[_-]?key|token|auth|credential)([=:])["']?[^\s"']+/gi,(a,e,_)=>`${e}${_}${s}`).replace(I,s).replace(N,s).replace(/\b(ghu_|ghs_|ghp_|gho_|github_pat_)[A-Za-z0-9_]+/g,(a,e)=>`${e}${s}`).replace(/\b(sk|rk)_(live|test)_[A-Za-z0-9]{8,}/g,(a,e,_)=>`${e}_${_}_${s}`).replace(/\bwhsec_[A-Za-z0-9]{8,}/g,()=>`whsec_${s}`).replace(/(?<=Authorization:\s*Bearer\s+)[A-Za-z0-9._~+/=-]+/gi,s).replace(/\b(AKIA|ASIA)[A-Z0-9]{12,}\b/g,(a,e)=>`${e}${s}`).replace(/(?<=aws_secret_access_key\s*=\s*["']?|SecretAccessKey[=:]\s*|"(aws)?secretAccessKey":\s*"|"aws_secret_access_key":\s*")[A-Za-z0-9/+=]{40,}/gi,s).replace(/(?<="(aws)?sessionToken":\s*"|"aws_session_token":\s*")[^"]+/gi,s).replace(/(?<=aws_session_token\s*[=:]\s*["']?|SessionToken[=:]\s*["']?)[^\s"']+/gi,s).replace(/(?<="(internal[Aa]piKey|fjallCallbackToken)":\s*")[^"]+/g,s).replace(S,(a,e)=>`${e}${s}`).replace(D,()=>`fj_${s}`)}A(f,"maskSensitiveOutput");function w(t){const a=[];let e="",_=!1,E=!1,c=!1,n=!1;for(const r of t){if(c){e+=r,c=!1;continue}if(r==="\\"&&!_){c=!0;continue}if(r==="'"&&!E){_=!_,n=!0;continue}if(r==='"'&&!_){E=!E,n=!0;continue}if(r===" "&&!_&&!E){(e||n)&&(a.push(e),e="",n=!1);continue}e+=r}if(_||E)throw new Error(`Unbalanced ${_?"single":"double"} quote in command: ${t.slice(0,80)}`);if(c)throw new Error(`Trailing backslash in command: ${t.slice(0,80)}`);return(e||n)&&a.push(e),a}A(w,"parseShellArgs");export{R as DANGEROUS_ENV_VARS,s as MASKED_VALUE_MARKER,i as REDACTED_KEY_SENTINEL,L as SCOPED_TOKEN_REGEX,u as filterDangerousEnvVars,f as maskSensitiveOutput,w as parseShellArgs};
1
+ var p=Object.defineProperty;var c=(t,_)=>p(t,"name",{value:_,configurable:!0});const D=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 u(t){return Object.fromEntries(Object.entries(t).filter(([_])=>!D.has(_.toUpperCase())))}c(u,"filterDangerousEnvVars");const O=/fjall_(?:ak|dk)_[A-Z2-7]{16}\.[A-Z2-7]{40}/,I=/(fjall_(?:ak|dk)_)[A-Z2-7]{16}\.[A-Z2-7]{40}/g,N=/\bfj_[0-9a-f]{8}_[0-9a-f]{48}\b/gi,s="***",o="[REDACTED]",l=`-----BEGIN ${o} PRIVATE KEY-----...-----END ${o} PRIVATE KEY-----`,g=String.raw`(?:next|continuation|pagination|page|forward|backward)+token`,R=String.raw`(?!ghu_|ghs_|ghp_|gho_|github_pat_|(?:sk|rk)_(?:live|test)_|whsec_|fjall_(?:ak|dk)_|fj_[0-9a-f]{8}_|AKIA|ASIA|%[a-zA-Z])`,i=String.raw`password|passwd|secret|api[_-]?key|token|auth|credential`,S=String.raw`arn:aws[a-z-]*:[a-z0-9-]+:[a-z0-9-]*:[0-9]*:`,T=new RegExp(String.raw`(?<!"${g}":\s*")`+String.raw`(?<="[a-zA-Z0-9_-]*(?:${i})":\s*")`+R+String.raw`[^"]+`,"gi"),L=new RegExp(String.raw`(?<!${g}\s*[=:]\s*["']?)`+String.raw`(?<!${S}(?:${i})\s*[=:]\s*["']?)`+String.raw`(?<=(?<!/)(?:${i})\s*[=:]\s*["']?)`+R+String.raw`[^\s"']+`,"gi"),w=new RegExp(String.raw`(?<!${S})(?<![a-zA-Z])(${i})([=:])["']?[^\s"']+`,"gi");function $(t){return t.replace(/-----BEGIN [A-Z ]*PRIVATE KEY-----(?:[^"\\]|\\[\\nrt"])*?-----END [A-Z ]*PRIVATE KEY-----/g,()=>l).replace(/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,()=>l).replace(/(\w+:\/\/[^:]+:)[^@]+(@)/gi,(_,e,a)=>`${e}${s}${a}`).replace(w,(_,e,a)=>`${e}${a}${s}`).replace(T,s).replace(L,s).replace(/\b(ghu_|ghs_|ghp_|gho_|github_pat_)[A-Za-z0-9_]+/g,(_,e)=>`${e}${s}`).replace(/\b(sk|rk)_(live|test)_[A-Za-z0-9]{8,}/g,(_,e,a)=>`${e}_${a}_${s}`).replace(/\bwhsec_[A-Za-z0-9]{8,}/g,()=>`whsec_${s}`).replace(/(?<=Authorization:\s*Bearer\s+)[A-Za-z0-9._~+/=-]+/gi,s).replace(/\b(AKIA|ASIA)[A-Z0-9]{12,}\b/g,(_,e)=>`${e}${s}`).replace(/(?<=aws_secret_access_key\s*=\s*["']?|SecretAccessKey[=:]\s*|"(aws)?secretAccessKey":\s*"|"aws_secret_access_key":\s*")[A-Za-z0-9/+=]{40,}/gi,s).replace(/(?<="(aws)?sessionToken":\s*"|"aws_session_token":\s*")[^"]+/gi,s).replace(/(?<=aws_session_token\s*[=:]\s*["']?|SessionToken[=:]\s*["']?)[^\s"']+/gi,s).replace(/(?<="(internal[Aa]piKey|fjallCallbackToken)":\s*")[^"]+/g,s).replace(I,(_,e)=>`${e}${s}`).replace(N,()=>`fj_${s}`)}c($,"maskSensitiveOutput");function h(t){const _=[];let e="",a=!1,r=!1,A=!1,n=!1;for(const E of t){if(A){e+=E,A=!1;continue}if(E==="\\"&&!a){A=!0;continue}if(E==="'"&&!r){a=!a,n=!0;continue}if(E==='"'&&!a){r=!r,n=!0;continue}if(E===" "&&!a&&!r){(e||n)&&(_.push(e),e="",n=!1);continue}e+=E}if(a||r)throw new Error(`Unbalanced ${a?"single":"double"} quote in command: ${t.slice(0,80)}`);if(A)throw new Error(`Trailing backslash in command: ${t.slice(0,80)}`);return(e||n)&&_.push(e),_}c(h,"parseShellArgs");export{D as DANGEROUS_ENV_VARS,s as MASKED_VALUE_MARKER,o as REDACTED_KEY_SENTINEL,O as SCOPED_TOKEN_REGEX,u as filterDangerousEnvVars,$ as maskSensitiveOutput,h as parseShellArgs};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fjall/util",
3
- "version": "21.1.0",
3
+ "version": "22.0.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/fjall-tech/fjall.git",