@fjall/util 28.1.1 → 28.3.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/.build-source-hash +9 -7
- package/dist/.minified +1 -1
- package/dist/aws/index.d.ts +1 -1
- package/dist/aws/index.js +1 -1
- package/dist/aws/infraTags.d.ts +16 -0
- package/dist/aws/infraTags.js +1 -1
- package/dist/config.d.ts +21 -128
- package/dist/config.js +2 -2
- package/dist/rootConfigMerge.d.ts +90 -0
- package/dist/rootConfigMerge.js +1 -0
- package/dist/rootConfigSchema.d.ts +136 -0
- package/dist/rootConfigSchema.js +1 -0
- package/package.json +1 -1
package/dist/.build-source-hash
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"package.json",
|
|
10
10
|
"tsconfig.json"
|
|
11
11
|
],
|
|
12
|
-
"hash": "
|
|
12
|
+
"hash": "1b353654390a51d91d44defda603bd6d8cb96cbe24c177ce4b61593d7c6b14f1",
|
|
13
13
|
"files": {
|
|
14
14
|
"src/async/concurrency.ts": "3c7ebe0222a720f9",
|
|
15
15
|
"src/async/singleton.ts": "7bcc21b0fb920561",
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
"src/aws/driftSuspects.ts": "5400dc05ebd6c977",
|
|
23
23
|
"src/aws/errors.ts": "5addfa6bb2d1456a",
|
|
24
24
|
"src/aws/failureAnalysisCategories.ts": "85ab1632d8069273",
|
|
25
|
-
"src/aws/index.ts": "
|
|
26
|
-
"src/aws/infraTags.ts": "
|
|
25
|
+
"src/aws/index.ts": "ce3da231274defdf",
|
|
26
|
+
"src/aws/infraTags.ts": "ba9442248b8a0ef4",
|
|
27
27
|
"src/aws/ipamTags.ts": "ac19ecd6c36b3bc2",
|
|
28
28
|
"src/aws/maskFailureAnalysis.ts": "ae11250412cd6fd8",
|
|
29
29
|
"src/aws/monitoringRole.ts": "70c8b5947dc9138d",
|
|
@@ -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": "
|
|
36
|
+
"src/config.ts": "6e15344c1d4a79fe",
|
|
37
37
|
"src/configPaths.ts": "afcc2e4ceb6652a0",
|
|
38
38
|
"src/constructMap.ts": "b81fc8e90015467f",
|
|
39
39
|
"src/deploy/approvalTokenOutput.ts": "4be7b52825d88a37",
|
|
@@ -124,6 +124,8 @@
|
|
|
124
124
|
"src/repo/scanLocalRepository.ts": "c7bc76201622a7a5",
|
|
125
125
|
"src/repo/scanTypes.ts": "1fda805a7d8de93b",
|
|
126
126
|
"src/resourceCategorisation.ts": "a140d1b3cb38be33",
|
|
127
|
+
"src/rootConfigMerge.ts": "e5085a5ae802647d",
|
|
128
|
+
"src/rootConfigSchema.ts": "5751173d999c0690",
|
|
127
129
|
"src/scaffold/ensureGitignore.ts": "178a2c3b2c18bc83",
|
|
128
130
|
"src/scaffold/gitignore.ts": "87c66797a4bd05f2",
|
|
129
131
|
"src/secrets.ts": "50258d463e4e67c3",
|
|
@@ -131,9 +133,9 @@
|
|
|
131
133
|
"src/staticSiteOutput.ts": "cc81ed32acc597f1",
|
|
132
134
|
"src/targets.ts": "126280f0a5762e21",
|
|
133
135
|
"../scripts/minify-dist.mjs": "6b2e4b0df8aec601",
|
|
134
|
-
"package.json": "
|
|
136
|
+
"package.json": "afe9001baeb8e135",
|
|
135
137
|
"tsconfig.json": "db759831ad95f396"
|
|
136
138
|
},
|
|
137
|
-
"outputHash": "
|
|
138
|
-
"outputFileCount":
|
|
139
|
+
"outputHash": "34f0f73f7350ef0563940fe5ab533b1f82cff72bd8fa95adcf3c064ea765ff28",
|
|
140
|
+
"outputFileCount": 251
|
|
139
141
|
}
|
package/dist/.minified
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
125 files minified at 2026-09-02T05:34:30.992Z
|
package/dist/aws/index.d.ts
CHANGED
|
@@ -6,6 +6,6 @@ export { FAILURE_ANALYSIS_CATEGORIES, type FailureAnalysisCategory } from "./fai
|
|
|
6
6
|
export { maskFailureAnalysis } from "./maskFailureAnalysis.js";
|
|
7
7
|
export { classifyDriftSuspectEvents, findCurrentOperationStart, isDriftSuspectReason, isOperationOpeningEvent, type DriftSuspectEvent } from "./driftSuspects.js";
|
|
8
8
|
export { IPAM_OPERATIONS_POOL_TAG_KEY, formatIpamPairTagValue } from "./ipamTags.js";
|
|
9
|
-
export { SDK_PRE_EMPTY_TAG_KEY } from "./infraTags.js";
|
|
9
|
+
export { KMS_RETENTION_TAG_KEY, SDK_PRE_EMPTY_TAG_KEY } from "./infraTags.js";
|
|
10
10
|
export { COST_ALLOCATION_TAGS, DEFAULT_COST_ALLOCATION_ENVIRONMENT, DEFAULT_COST_ALLOCATION_OWNER, ecrCreateRepositoryTags, rdsPreMigrationSnapshotTags, type AwsSdkTag, type EcrCreateTag } from "./costAllocationTags.js";
|
|
11
11
|
export { ACCOUNT_MONITORING_ROLE_NAME } from "./monitoringRole.js";
|
package/dist/aws/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{AWSError as e,NoRolesFoundError as t,InvalidCredentialsError as
|
|
1
|
+
import{AWSError as e,NoRolesFoundError as t,InvalidCredentialsError as E,SSOTokenExpiredError as s,MissingRegionError as T,ProfileNotFoundError as _,CommandError as i,isAWSError as O,isNoRolesFoundError as A,isSSOUnauthorizedError as a}from"./errors.js";import{STACK_NOT_FOUND_PATTERN as S,CDK_NO_STACKS_MATCH as N,isResourceEvent as p}from"./cloudformationTypes.js";import{countResourceProgress as m,formatElapsed as C}from"./deployProgress.js";import{CloudFormationFailureAnalyser as u}from"./CloudFormationFailureAnalyser.js";import{FAILURE_ANALYSIS_CATEGORIES as l}from"./failureAnalysisCategories.js";import{maskFailureAnalysis as x}from"./maskFailureAnalysis.js";import{classifyDriftSuspectEvents as F,findCurrentOperationStart as P,isDriftSuspectReason as M,isOperationOpeningEvent as g}from"./driftSuspects.js";import{IPAM_OPERATIONS_POOL_TAG_KEY as c,formatIpamPairTagValue as D}from"./ipamTags.js";import{KMS_RETENTION_TAG_KEY as U,SDK_PRE_EMPTY_TAG_KEY as Y}from"./infraTags.js";import{COST_ALLOCATION_TAGS as y,DEFAULT_COST_ALLOCATION_ENVIRONMENT as W,DEFAULT_COST_ALLOCATION_OWNER as h,ecrCreateRepositoryTags as k,rdsPreMigrationSnapshotTags as V}from"./costAllocationTags.js";import{ACCOUNT_MONITORING_ROLE_NAME as H}from"./monitoringRole.js";export{H as ACCOUNT_MONITORING_ROLE_NAME,e as AWSError,N as CDK_NO_STACKS_MATCH,y as COST_ALLOCATION_TAGS,u as CloudFormationFailureAnalyser,i as CommandError,W as DEFAULT_COST_ALLOCATION_ENVIRONMENT,h as DEFAULT_COST_ALLOCATION_OWNER,l as FAILURE_ANALYSIS_CATEGORIES,c as IPAM_OPERATIONS_POOL_TAG_KEY,E as InvalidCredentialsError,U as KMS_RETENTION_TAG_KEY,T as MissingRegionError,t as NoRolesFoundError,_ as ProfileNotFoundError,Y as SDK_PRE_EMPTY_TAG_KEY,s as SSOTokenExpiredError,S as STACK_NOT_FOUND_PATTERN,F as classifyDriftSuspectEvents,m as countResourceProgress,k as ecrCreateRepositoryTags,P as findCurrentOperationStart,C as formatElapsed,D as formatIpamPairTagValue,O as isAWSError,M as isDriftSuspectReason,A as isNoRolesFoundError,g as isOperationOpeningEvent,p as isResourceEvent,a as isSSOUnauthorizedError,x as maskFailureAnalysis,V as rdsPreMigrationSnapshotTags};
|
package/dist/aws/infraTags.d.ts
CHANGED
|
@@ -16,3 +16,19 @@
|
|
|
16
16
|
* guards against casual renames.
|
|
17
17
|
*/
|
|
18
18
|
export declare const SDK_PRE_EMPTY_TAG_KEY = "fjall:sdk-pre-empty";
|
|
19
|
+
/**
|
|
20
|
+
* The KMS retention marker tag, shared by producer and consumer:
|
|
21
|
+
*
|
|
22
|
+
* - Producer (`@fjall/components-infrastructure`): `CustomerManagedKey`
|
|
23
|
+
* (`secrets/kms.ts`) tags every key whose resolved removal policy is
|
|
24
|
+
* RETAIN, valued with the `protects` scope that justified the retention.
|
|
25
|
+
* - Consumer (webapp posture): the `KMS_KEY_ORPHAN` tier-policy row reads it
|
|
26
|
+
* to tell a key retained on purpose from one that was forgotten (tier-aware
|
|
27
|
+
* alert policy design 2026-09-01 §8 K4). The row waits until deployed keys
|
|
28
|
+
* carry the tag.
|
|
29
|
+
*
|
|
30
|
+
* The literal is deployed infrastructure state: a rename would strip every
|
|
31
|
+
* retained key of its marker until the next deploy. The pin test in
|
|
32
|
+
* `__tests__/infraTags.test.ts` guards against casual renames.
|
|
33
|
+
*/
|
|
34
|
+
export declare const KMS_RETENTION_TAG_KEY = "fjall:kms:retention";
|
package/dist/aws/infraTags.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
const
|
|
1
|
+
const t="fjall:sdk-pre-empty",_="fjall:kms:retention";export{_ as KMS_RETENTION_TAG_KEY,t as SDK_PRE_EMPTY_TAG_KEY};
|
package/dist/config.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
1
|
import { type Result } from "./docker/result.js";
|
|
3
2
|
import { type AccountTier } from "./environments.js";
|
|
3
|
+
import { type DomainConfig, type OrganisationBinding, type RootConfig } from "./rootConfigSchema.js";
|
|
4
|
+
export { DomainConfigSchema, OrganisationBindingSchema, ROOT_CONFIG_KEYS, RootConfigReadSchema, RootConfigSchema, serialiseRootConfig, type DomainConfig, type OrganisationBinding, type RootConfig, type RootConfigRead } from "./rootConfigSchema.js";
|
|
5
|
+
export { mergeRootConfig, mergeRootConfigContent, readRootConfigForMerge, type RootConfigDiskState, type RootConfigMergeRead, type RootConfigMergeResult } from "./rootConfigMerge.js";
|
|
4
6
|
export { LOCAL_CONFIG_DIRNAME, LOCAL_CONFIG_FILENAME, ROOT_CONFIG_FILENAME } from "./configPaths.js";
|
|
5
7
|
/**
|
|
6
8
|
* The Fjall root config filename. Single source of truth — the repo-root marker
|
|
@@ -138,115 +140,6 @@ export type SSOSession = {
|
|
|
138
140
|
ssoRegion: string;
|
|
139
141
|
ssoStartUrl: string;
|
|
140
142
|
};
|
|
141
|
-
/**
|
|
142
|
-
* STRICT wire contract for one `domains[]` entry in fjall-config.json —
|
|
143
|
-
* unknown keys are rejected, never silently carried. This is the write-path
|
|
144
|
-
* schema shared with the webapp's domain-topology resolver; a scaffold or
|
|
145
|
-
* saver emitting a key this schema rejects is exactly the drift it prevents.
|
|
146
|
-
*/
|
|
147
|
-
export declare const DomainConfigSchema: z.ZodObject<{
|
|
148
|
-
name: z.ZodString;
|
|
149
|
-
type: z.ZodEnum<{
|
|
150
|
-
apex: "apex";
|
|
151
|
-
delegated: "delegated";
|
|
152
|
-
}>;
|
|
153
|
-
parentDomain: z.ZodOptional<z.ZodString>;
|
|
154
|
-
account: z.ZodOptional<z.ZodString>;
|
|
155
|
-
region: z.ZodOptional<z.ZodString>;
|
|
156
|
-
}, z.core.$strict>;
|
|
157
|
-
export type DomainConfig = z.infer<typeof DomainConfigSchema>;
|
|
158
|
-
/**
|
|
159
|
-
* STRICT wire contract for the `organisation` key — the project's binding to
|
|
160
|
-
* ONE Fjall organisation.
|
|
161
|
-
*
|
|
162
|
-
* This is the ONE organisation fact that belongs in fjall-config.json, and it
|
|
163
|
-
* is not a mirror of API state: it is a project ASSERTION that says which
|
|
164
|
-
* organisation this source tree deploys into. Two writers, both recording
|
|
165
|
-
* stated intent: `fjall create organisation` writes it at scaffold time (the
|
|
166
|
-
* organisation being created IS the command's subject — the project is born
|
|
167
|
-
* bound), and `fjall org bind` records or deliberately re-records it for
|
|
168
|
-
* projects that predate birth binding or genuinely move. Everything else
|
|
169
|
-
* about an organisation (accounts, regions, tiers) is mutable API state that
|
|
170
|
-
* a committed copy would misreport, which is why it stays out of this file.
|
|
171
|
-
*
|
|
172
|
-
* `id` is the only field ever compared. `name` is display-only, so renaming an
|
|
173
|
-
* organisation neither trips the guard nor dirties a checkout.
|
|
174
|
-
*/
|
|
175
|
-
export declare const OrganisationBindingSchema: z.ZodObject<{
|
|
176
|
-
id: z.ZodString;
|
|
177
|
-
name: z.ZodOptional<z.ZodString>;
|
|
178
|
-
}, z.core.$strict>;
|
|
179
|
-
export type OrganisationBinding = z.infer<typeof OrganisationBindingSchema>;
|
|
180
|
-
export declare const RootConfigSchema: z.ZodObject<{
|
|
181
|
-
activeTarget: z.ZodOptional<z.ZodString>;
|
|
182
|
-
domains: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
183
|
-
name: z.ZodString;
|
|
184
|
-
type: z.ZodEnum<{
|
|
185
|
-
apex: "apex";
|
|
186
|
-
delegated: "delegated";
|
|
187
|
-
}>;
|
|
188
|
-
parentDomain: z.ZodOptional<z.ZodString>;
|
|
189
|
-
account: z.ZodOptional<z.ZodString>;
|
|
190
|
-
region: z.ZodOptional<z.ZodString>;
|
|
191
|
-
}, z.core.$strict>>>;
|
|
192
|
-
capacityIdentity: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
193
|
-
legacyAnchor: z.ZodString;
|
|
194
|
-
legacyOwnerId: z.ZodOptional<z.ZodString>;
|
|
195
|
-
legacySgDescription: z.ZodOptional<z.ZodString>;
|
|
196
|
-
}, z.core.$strict>>>>>;
|
|
197
|
-
organisation: z.ZodOptional<z.ZodObject<{
|
|
198
|
-
id: z.ZodString;
|
|
199
|
-
name: z.ZodOptional<z.ZodString>;
|
|
200
|
-
}, z.core.$strict>>;
|
|
201
|
-
}, z.core.$strict>;
|
|
202
|
-
export type RootConfig = z.infer<typeof RootConfigSchema>;
|
|
203
|
-
/**
|
|
204
|
-
* Tolerant READ schema for the deploy-time load path. Intentionally NOT
|
|
205
|
-
* `.strict()`: a fjall-config.json written by a different fjall version, or a
|
|
206
|
-
* legacy pre-config-split file, may carry top-level keys this version does not
|
|
207
|
-
* know (`version`, `services`, `providerAccounts`, `generatorVersion`, …).
|
|
208
|
-
* Stripping them and reading only the recognised keys keeps `fjall deploy` from
|
|
209
|
-
* hard-failing on an otherwise-harmless older file. The strict RootConfigSchema
|
|
210
|
-
* remains the contract for the WRITE/save path — never use this schema to
|
|
211
|
-
* validate what you are about to write. Domain entries stay nested-strict
|
|
212
|
-
* (DomainConfigSchema): tolerance applies to unknown TOP-LEVEL keys only.
|
|
213
|
-
*/
|
|
214
|
-
export declare const RootConfigReadSchema: z.ZodObject<{
|
|
215
|
-
activeTarget: z.ZodOptional<z.ZodString>;
|
|
216
|
-
domains: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
217
|
-
name: z.ZodString;
|
|
218
|
-
type: z.ZodEnum<{
|
|
219
|
-
apex: "apex";
|
|
220
|
-
delegated: "delegated";
|
|
221
|
-
}>;
|
|
222
|
-
parentDomain: z.ZodOptional<z.ZodString>;
|
|
223
|
-
account: z.ZodOptional<z.ZodString>;
|
|
224
|
-
region: z.ZodOptional<z.ZodString>;
|
|
225
|
-
}, z.core.$strict>>>;
|
|
226
|
-
capacityIdentity: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
227
|
-
legacyAnchor: z.ZodString;
|
|
228
|
-
legacyOwnerId: z.ZodOptional<z.ZodString>;
|
|
229
|
-
legacySgDescription: z.ZodOptional<z.ZodString>;
|
|
230
|
-
}, z.core.$strict>>>>>;
|
|
231
|
-
organisation: z.ZodOptional<z.ZodObject<{
|
|
232
|
-
id: z.ZodString;
|
|
233
|
-
name: z.ZodOptional<z.ZodString>;
|
|
234
|
-
}, z.core.$strict>>;
|
|
235
|
-
}, z.core.$strip>;
|
|
236
|
-
export type RootConfigRead = z.infer<typeof RootConfigReadSchema>;
|
|
237
|
-
/**
|
|
238
|
-
* Canonical serialiser for the root fjall-config.json - the single source of
|
|
239
|
-
* truth for the file's on-disk shape. Both `Config.saveConfig` and the webapp
|
|
240
|
-
* scaffold MUST route through this rather than hand-rolling the JSON, so the
|
|
241
|
-
* two can never drift (a scaffold emitting a key the loader later rejects is
|
|
242
|
-
* exactly the bug this prevents). The `RootConfig` input type keeps OUR keys
|
|
243
|
-
* honest at compile time, but the emitted JSON is deliberately NOT
|
|
244
|
-
* strict-validated: `mergeWithDisk` routes top-level keys another fjall
|
|
245
|
-
* version wrote through the save verbatim (see its doc), so a merged save may
|
|
246
|
-
* carry keys RootConfigSchema does not recognise. The round-trip test asserts
|
|
247
|
-
* only that the emitted DEFAULT validates against RootConfigSchema.
|
|
248
|
-
*/
|
|
249
|
-
export declare function serialiseRootConfig(config?: RootConfig): string;
|
|
250
143
|
/**
|
|
251
144
|
* Config class for loading and saving the root fjall-config.json.
|
|
252
145
|
* Single config file at fjall/fjall-config.json (or fjall-config.json at cwd).
|
|
@@ -282,6 +175,18 @@ export declare class Config {
|
|
|
282
175
|
* sibling domain entries.
|
|
283
176
|
*/
|
|
284
177
|
private loadedSnapshot;
|
|
178
|
+
/**
|
|
179
|
+
* The directory `loadConfig` was asked to walk up from, remembered so an
|
|
180
|
+
* UNBOUND instance stays coherent with its origin: later resolutions
|
|
181
|
+
* (`resolveProjectDirectory`'s fallback walk, `saveConfig`'s create-a-new-
|
|
182
|
+
* project default) anchor here instead of re-walking from process.cwd().
|
|
183
|
+
* Without this, `loadConfig(scaffoldRoot)` under `create app --into`
|
|
184
|
+
* followed by a save could materialise the config in a DIFFERENT project —
|
|
185
|
+
* whichever one happened to contain the shell's cwd. Undefined for
|
|
186
|
+
* programmatically-constructed instances and no-arg loads (cwd semantics
|
|
187
|
+
* unchanged).
|
|
188
|
+
*/
|
|
189
|
+
private searchStartDir;
|
|
285
190
|
constructor(rootConfig?: RootConfig, configPath?: string);
|
|
286
191
|
/**
|
|
287
192
|
* Find the config directory by walking up the directory tree.
|
|
@@ -306,11 +211,9 @@ export declare class Config {
|
|
|
306
211
|
/**
|
|
307
212
|
* Session-intent merge: re-reads the on-disk config immediately before
|
|
308
213
|
* writing and asserts ONLY what this session actually changed relative to
|
|
309
|
-
* its load-time snapshot (see loadedSnapshot).
|
|
310
|
-
*
|
|
311
|
-
* so
|
|
312
|
-
* recognise ride through verbatim, and keys explicitly cleared this
|
|
313
|
-
* session are removed even when the disk copy still carries them.
|
|
214
|
+
* its load-time snapshot (see loadedSnapshot). The rules live in
|
|
215
|
+
* `mergeRootConfig` (`./rootConfigMerge.ts`), shared with the webapp
|
|
216
|
+
* scaffold so the file's two writers cannot drift apart.
|
|
314
217
|
* An absent file (first-ever save) writes the in-memory state as-is; an
|
|
315
218
|
* unreadable or invalid existing file REFUSES the save — the in-memory
|
|
316
219
|
* state came through the tolerant stripping read, so writing it unmerged
|
|
@@ -318,20 +221,10 @@ export declare class Config {
|
|
|
318
221
|
*/
|
|
319
222
|
private mergeWithDisk;
|
|
320
223
|
/**
|
|
321
|
-
*
|
|
322
|
-
*
|
|
323
|
-
*
|
|
324
|
-
*/
|
|
325
|
-
private sessionChangedKey;
|
|
326
|
-
/**
|
|
327
|
-
* Per-entry merge of the `domains` key. Disk order is the base: an entry
|
|
328
|
-
* this session changed is asserted, an entry this session removed (it was
|
|
329
|
-
* in the load snapshot, it is gone from live state) drops out, and any
|
|
330
|
-
* entry this session never touched keeps the DISK version - including
|
|
331
|
-
* entries a concurrent process registered after this session loaded.
|
|
332
|
-
* Session additions append after the disk entries.
|
|
224
|
+
* Filesystem half of the pre-save re-read: an absent file needs no merge, an
|
|
225
|
+
* unreadable one refuses the save, and the bytes of anything else are
|
|
226
|
+
* classified by `readRootConfigForMerge`.
|
|
333
227
|
*/
|
|
334
|
-
private mergeDomains;
|
|
335
228
|
private static readDiskConfigForMerge;
|
|
336
229
|
static getConfigDirectory(startDir?: string): string | null;
|
|
337
230
|
/**
|
package/dist/config.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var
|
|
2
|
-
`,{encoding:"utf8"}),
|
|
1
|
+
var O=Object.defineProperty;var T=(w,e)=>O(w,"name",{value:e,configurable:!0});import*as o from"fs";import*as i from"path";import{z as y}from"zod";import{failure as d,success as u}from"./docker/result.js";import{getErrorMessage as h}from"./errorUtils.js";import{logger as p}from"./logger.js";import{maskSensitiveOutput as m}from"./securityHelpers.js";import{mergeRootConfig as S,readRootConfigForMerge as A}from"./rootConfigMerge.js";import{RootConfigReadSchema as v,RootConfigSchema as L,serialiseRootConfig as R}from"./rootConfigSchema.js";import{DomainConfigSchema as ae,OrganisationBindingSchema as se,ROOT_CONFIG_KEYS as ce,RootConfigReadSchema as le,RootConfigSchema as fe,serialiseRootConfig as de}from"./rootConfigSchema.js";import{mergeRootConfig as ue,mergeRootConfigContent as he,readRootConfigForMerge as me}from"./rootConfigMerge.js";import{ensureGitignoreEntries as _}from"./scaffold/ensureGitignore.js";import{FJALL_MANAGED_STATE_DIR_ENTRIES as F,FJALL_PROJECT_GITIGNORE_HEADER as j,LOCAL_CONFIG_GITIGNORE_ENTRY as D,LOCAL_STATE_GITIGNORE_MARKER as I}from"./scaffold/gitignore.js";import{LOCAL_CONFIG_DIRNAME as N,LOCAL_CONFIG_FILENAME as $,ROOT_CONFIG_FILENAME as C}from"./configPaths.js";import{LOCAL_CONFIG_DIRNAME as pe,LOCAL_CONFIG_FILENAME as ye,ROOT_CONFIG_FILENAME as Ee}from"./configPaths.js";const M=10,Z=["compliance","governance","none"],z=["enforced","off"],V=["centralised","off"],X=["account","draining","org"],q="managementEvents",Q="organisationManagementEvents",ee=["active","draining","removed"],te="FjallTrailBucketName",re="FjallTrailKeyArn",oe="OrganisationTrailBucketName",b=y.object({activeTarget:y.string().optional()});class n{static{T(this,"Config")}rootConfig;configPath=null;loadFailed=!1;parseDegraded=!1;loadedSnapshot={};searchStartDir;constructor(e,t){this.rootConfig=e??{},this.configPath=t??null}static findConfigDirectory(e){let t=e!==void 0&&e!==""?e:process.cwd();for(let r=0;r<M;r++){const a=i.join(t,"fjall"),f=i.join(a,C);if(o.existsSync(f))return a;const c=i.join(t,C);if(o.existsSync(c))return t;const s=i.dirname(t);if(s===t)break;t=s}return null}static loadConfigFile(e){try{return o.accessSync(e,o.constants.R_OK),o.readFileSync(e,{encoding:"utf8"})}catch(t){return p.warn("Config",`Config file at ${e} could not be read; using defaults`,{file:e,error:m(h(t))}),null}}static loadConfig(e){const t=n.findConfigDirectory(e);if(!t){const l=new n;return l.searchStartDir=e,l}const r=i.join(t,C),a=n.loadConfigFile(r);if(a===null){const l=new n(void 0,r);return l.searchStartDir=e,l.loadFailed=!0,l}let f,c=!1;if(a!==""){let l;try{l=JSON.parse(a)}catch(g){throw n.formatZodError(g,C)}const E=L.safeParse(l);if(E.success)f=E.data;else{const g=v.safeParse(l);f=g.success?g.data:{},c=!g.success,g.success?p.warn("Config","fjall-config.json contains keys this version does not recognise (only activeTarget, domains, capacityIdentity and organisation are read). They were most likely written by a newer fjall version \u2014 leave them in place; saves preserve them verbatim.",{file:r}):p.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:r})}}const s=new n(f,r);return s.searchStartDir=e,s.parseDegraded=c,s.loadedSnapshot=structuredClone(s.rootConfig),s}static formatZodError(e,t){if(e instanceof y.ZodError&&e.issues.length>0){const f=e.issues.map(c=>`${c.path.join(".")}: ${c.message}`).join("; ");return new Error(`Failed to parse ${t}: ${f}`)}const a=(e instanceof Error?e.message:String(e)).replace(/\n/g," ").substring(0,500);return new Error(`Failed to parse ${t}: ${a}`)}saveConfig(){const e=i.join(this.resolveProjectDirectory()??i.join(this.searchStartDir??process.cwd(),"fjall"),C);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=i.dirname(e);try{o.mkdirSync(t,{recursive:!0})}catch(s){return d(new Error(`Cannot create config directory ${t}: ${m(h(s))}`))}const r=n.assertWritable(e,t);if(!r.success)return r;const a=this.mergeWithDisk(e);if(!a.success)return a;const f=R(a.data),c=`${e}.tmp-${process.pid}`;try{o.writeFileSync(c,f,{mode:384}),o.renameSync(c,e)}catch(s){return d(new Error(`Failed to save ${e}: ${m(h(s))}`))}return this.configPath=e,u(void 0)}static assertWritable(e,t){if(o.existsSync(e))try{o.accessSync(e,o.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{o.accessSync(t,o.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 u(void 0)}mergeWithDisk(e){const t=n.readDiskConfigForMerge(e);return t.kind==="unmergeable"?d(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.`)):t.kind==="absent"?u(this.rootConfig):u(S(t.disk,this.rootConfig,this.loadedSnapshot))}static readDiskConfigForMerge(e){if(!o.existsSync(e))return{kind:"absent"};let t;try{t=o.readFileSync(e,{encoding:"utf8"})}catch(r){return{kind:"unmergeable",reason:`the existing file could not be read or parsed as JSON (${m(h(r))})`}}return A(t)}static getConfigDirectory(e){return n.findConfigDirectory(e)}getConfigPath(){return this.configPath}isContentUnavailable(){return this.loadFailed||this.parseDegraded}static warnedLegacyActiveTarget=!1;localConfigCache;resolveProjectDirectory(){return this.configPath!==null?i.dirname(this.configPath):n.findConfigDirectory(this.searchStartDir)}localConfigFile(){const e=this.resolveProjectDirectory();return e===null?null:i.join(e,N,$)}loadLocalConfig(){if(this.localConfigCache===void 0){const e=this.localConfigFile();if(e===null)return this.localConfigCache={};try{const t=o.readFileSync(e,{encoding:"utf8"}),r=b.safeParse(JSON.parse(t));this.localConfigCache=r.success?r.data:{}}catch{this.localConfigCache={}}}return this.localConfigCache}writeLocalConfig(e){const t=this.localConfigFile();if(t===null)return d(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{o.mkdirSync(i.dirname(t),{recursive:!0});const r=`${t}.tmp-${process.pid}`;o.writeFileSync(r,JSON.stringify(e,null,2)+`
|
|
2
|
+
`,{encoding:"utf8"}),o.renameSync(r,t),_(i.dirname(i.dirname(t)),[D,...F],{header:j,marker:I})}catch(r){return d(new Error(`Failed to write ${t}: ${m(h(r))}`))}return this.localConfigCache=e,u(void 0)}static warnLegacyActiveTargetOnce(){n.warnedLegacyActiveTarget||(n.warnedLegacyActiveTarget=!0,p.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&&n.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(r=>r.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{q as ACCOUNT_TRAIL_NAME,ee as ACCOUNT_TRAIL_STATES,n as Config,ae as DomainConfigSchema,pe as LOCAL_CONFIG_DIRNAME,ye as LOCAL_CONFIG_FILENAME,Q as ORGANISATION_TRAIL_NAME,oe as ORG_TRAIL_BUCKET_OUTPUT_KEY,se as OrganisationBindingSchema,V as ROOT_ACCESS_MANAGEMENT_MODES,Ee as ROOT_CONFIG_FILENAME,ce as ROOT_CONFIG_KEYS,le as RootConfigReadSchema,fe as RootConfigSchema,z as S3_BPA_MODES,te as TRAIL_BUCKET_OUTPUT_KEY,re as TRAIL_KEY_ARN_OUTPUT_KEY,X as TRAIL_LIFECYCLE_STATES,Z as VAULT_LOCK_MODES,ue as mergeRootConfig,he as mergeRootConfigContent,me as readRootConfigForMerge,de as serialiseRootConfig};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { type RootConfig, type RootConfigRead } from "./rootConfigSchema.js";
|
|
2
|
+
/**
|
|
3
|
+
* The root `fjall-config.json` merge, as pure functions.
|
|
4
|
+
*
|
|
5
|
+
* The file has two writers — the CLI's `Config.saveConfig` and the webapp
|
|
6
|
+
* scaffold — and a merge each writer implements separately drifts silently: a
|
|
7
|
+
* rule tightened on one side ships green through the other's typecheck, lint
|
|
8
|
+
* and tests. Everything here is bytes-and-values only (no `fs`, no `path`, no
|
|
9
|
+
* logger) so both writers can call the SAME implementation from their own
|
|
10
|
+
* I/O boundary.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* On-disk state re-read immediately before a save: the recognised keys
|
|
14
|
+
* (tolerantly parsed) plus any top-level keys this fjall version does not
|
|
15
|
+
* recognise, carried through the save verbatim so another version's config
|
|
16
|
+
* survives ours.
|
|
17
|
+
*/
|
|
18
|
+
export interface RootConfigDiskState {
|
|
19
|
+
known: RootConfigRead;
|
|
20
|
+
foreign: Record<string, unknown>;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Outcome of classifying the existing file. `absent` (no file, or a zero-byte
|
|
24
|
+
* one) lets a first-ever save proceed unmerged; `unmergeable` (corrupt JSON, a
|
|
25
|
+
* non-object JSON value, or a malformed recognised key) REFUSES the save — the
|
|
26
|
+
* caller's in-memory state came through the tolerant stripping read, so writing
|
|
27
|
+
* it unmerged would silently delete every key the merge could not see (e.g. a
|
|
28
|
+
* newer version's `capacityIdentity` pins).
|
|
29
|
+
*
|
|
30
|
+
* `reason` is pre-masked at construction — every arm in `readRootConfigForMerge`
|
|
31
|
+
* (this file) routes echoed file content through `maskSensitiveOutput` or is a
|
|
32
|
+
* static literal, and the CLI's file-read arm (`Config.readDiskConfigForMerge`,
|
|
33
|
+
* `util/src/config.ts`) masks the same field — so consumers may surface it to
|
|
34
|
+
* any sink directly without re-masking.
|
|
35
|
+
*/
|
|
36
|
+
export type RootConfigMergeRead = {
|
|
37
|
+
kind: "absent";
|
|
38
|
+
} | {
|
|
39
|
+
kind: "unmergeable";
|
|
40
|
+
reason: string;
|
|
41
|
+
} | {
|
|
42
|
+
kind: "ok";
|
|
43
|
+
disk: RootConfigDiskState;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Classify the BYTES of an existing fjall-config.json for a merge. `undefined`
|
|
47
|
+
* and `""` are both `absent` — loader parity: `Config.loadConfig` treats a
|
|
48
|
+
* zero-byte file as a valid empty config, so there is nothing to preserve and
|
|
49
|
+
* nothing to refuse over.
|
|
50
|
+
*
|
|
51
|
+
* Reasons are masked at construction (JSON parse errors and zod issue text
|
|
52
|
+
* both echo file content), so callers may surface them to any sink directly.
|
|
53
|
+
*/
|
|
54
|
+
export declare function readRootConfigForMerge(raw: string | undefined): RootConfigMergeRead;
|
|
55
|
+
/**
|
|
56
|
+
* Session-intent merge: start from the freshly-read disk state and assert ONLY
|
|
57
|
+
* what the session actually changed relative to its load-time `snapshot`. An
|
|
58
|
+
* untouched key keeps the disk's current value, the `domains` key merges PER
|
|
59
|
+
* ENTRY (keyed by name) so concurrently-registered siblings survive, keys this
|
|
60
|
+
* version does not recognise ride through verbatim, and keys the session
|
|
61
|
+
* explicitly cleared are removed even when the disk copy still carries them.
|
|
62
|
+
*
|
|
63
|
+
* The returned value deliberately carries the foreign keys at runtime -
|
|
64
|
+
* preserving another version's config is the point, not schema drift on OUR
|
|
65
|
+
* keys.
|
|
66
|
+
*/
|
|
67
|
+
export declare function mergeRootConfig(disk: RootConfigDiskState, live: RootConfig, snapshot: RootConfig): RootConfig;
|
|
68
|
+
/**
|
|
69
|
+
* `reason` carries `RootConfigMergeRead.reason` verbatim and is therefore
|
|
70
|
+
* pre-masked at construction (see that type).
|
|
71
|
+
*/
|
|
72
|
+
export type RootConfigMergeResult = {
|
|
73
|
+
kind: "ok";
|
|
74
|
+
content: string;
|
|
75
|
+
} | {
|
|
76
|
+
kind: "unmergeable";
|
|
77
|
+
reason: string;
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* Bytes-in → bytes-out composition of the two functions above: classify the
|
|
81
|
+
* existing file, merge the session's intent into it, and serialise. An absent
|
|
82
|
+
* (or zero-byte) file writes `live` as-is.
|
|
83
|
+
*
|
|
84
|
+
* `snapshot` defaults to `{}` — the shape of a writer whose whole state IS
|
|
85
|
+
* session intent, such as the webapp scaffold asserting exactly
|
|
86
|
+
* `{ organisation }` against a config it never loaded. A writer that loaded
|
|
87
|
+
* the file first MUST pass the state it loaded, or every key it read back
|
|
88
|
+
* would be re-asserted over whatever a concurrent process has since written.
|
|
89
|
+
*/
|
|
90
|
+
export declare function mergeRootConfigContent(existing: string | undefined, live: RootConfig, snapshot?: RootConfig): RootConfigMergeResult;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var O=Object.defineProperty;var f=(o,e)=>O(o,"name",{value:e,configurable:!0});import{getErrorMessage as C}from"./errorUtils.js";import{ROOT_CONFIG_KEYS as l,RootConfigReadSchema as S,copyDefinedKey as E,domainEntriesEqual as k,serialiseRootConfig as y}from"./rootConfigSchema.js";import{maskSensitiveOutput as h}from"./securityHelpers.js";function N(o){if(o===void 0||o==="")return{kind:"absent"};let e;try{e=JSON.parse(o)}catch(n){return{kind:"unmergeable",reason:`the existing file could not be read or parsed as JSON (${h(C(n))})`}}if(typeof e!="object"||e===null||Array.isArray(e))return{kind:"unmergeable",reason:"it does not contain a JSON object"};const r=S.safeParse(e);if(!r.success){const n=r.error.issues.map(a=>`${a.path.join(".")}: ${a.message}`).join("; ");return{kind:"unmergeable",reason:`a recognised key in the existing file is malformed (${h(n)})`}}const t={},s=l;for(const[n,a]of Object.entries(e))s.includes(n)||(t[n]=a);return{kind:"ok",disk:{known:r.data,foreign:t}}}f(N,"readRootConfigForMerge");function R(o,e,r){return JSON.stringify(o[r])!==JSON.stringify(e[r])}f(R,"sessionChangedKey");function J(o,e,r){const t=e.domains,s=r.domains??[];if(t===void 0&&s.length===0)return o;const n=f(i=>i.toLowerCase(),"norm"),a=t??[],m=new Map(s.map(i=>[n(i.name),i])),b=new Map(a.map(i=>[n(i.name),i])),c=[],g=new Set;for(const i of o??[]){const d=n(i.name);g.add(d);const u=b.get(d),p=m.get(d);if(u!==void 0&&!k(u,p)){c.push(u);continue}u===void 0&&p!==void 0||c.push(i)}for(const i of a){const d=n(i.name);g.has(d)||k(i,m.get(d))||c.push(i)}return c}f(J,"mergeDomains");function j(o,e,r){const t={...o.foreign,...o.known};for(const n of l)n!=="domains"&&R(e,r,n)&&(e[n]===void 0?delete t[n]:E(t,e,n));const s=J(o.known.domains,e,r);return s!==void 0?t.domains=s:delete t.domains,t}f(j,"mergeRootConfig");function $(o,e,r={}){const t=N(o);return t.kind==="unmergeable"?{kind:"unmergeable",reason:t.reason}:t.kind==="absent"?{kind:"ok",content:y(e)}:{kind:"ok",content:y(j(t.disk,e,r))}}f($,"mergeRootConfigContent");export{j as mergeRootConfig,$ as mergeRootConfigContent,N as readRootConfigForMerge};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Wire contract for the root `fjall-config.json` — the schemas, the canonical
|
|
4
|
+
* serialiser, and the shape helpers the merge needs.
|
|
5
|
+
*
|
|
6
|
+
* Split out of `config.ts` so `rootConfigMerge.ts` stays a pure module: the
|
|
7
|
+
* merge needs these schemas but must not drag `config.ts`'s fs/path/logger
|
|
8
|
+
* graph in with them, because the webapp imports the merge from its own I/O
|
|
9
|
+
* boundary. `config.ts` re-exports the schemas, their companion types, the
|
|
10
|
+
* serialiser and ROOT_CONFIG_KEYS, so `@fjall/util/config` is unchanged for
|
|
11
|
+
* consumers; `RootConfigKey`, `copyDefinedKey` and `domainEntriesEqual` are
|
|
12
|
+
* merge-internal and deliberately not re-exported.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* STRICT wire contract for one `domains[]` entry in fjall-config.json —
|
|
16
|
+
* unknown keys are rejected, never silently carried. This is the write-path
|
|
17
|
+
* schema shared with the webapp's domain-topology resolver; a scaffold or
|
|
18
|
+
* saver emitting a key this schema rejects is exactly the drift it prevents.
|
|
19
|
+
*/
|
|
20
|
+
export declare const DomainConfigSchema: z.ZodObject<{
|
|
21
|
+
name: z.ZodString;
|
|
22
|
+
type: z.ZodEnum<{
|
|
23
|
+
apex: "apex";
|
|
24
|
+
delegated: "delegated";
|
|
25
|
+
}>;
|
|
26
|
+
parentDomain: z.ZodOptional<z.ZodString>;
|
|
27
|
+
account: z.ZodOptional<z.ZodString>;
|
|
28
|
+
region: z.ZodOptional<z.ZodString>;
|
|
29
|
+
}, z.core.$strict>;
|
|
30
|
+
export type DomainConfig = z.infer<typeof DomainConfigSchema>;
|
|
31
|
+
/**
|
|
32
|
+
* STRICT wire contract for the `organisation` key — the project's binding to
|
|
33
|
+
* ONE Fjall organisation.
|
|
34
|
+
*
|
|
35
|
+
* This is the ONE organisation fact that belongs in fjall-config.json, and it
|
|
36
|
+
* is not a mirror of API state: it is a project ASSERTION that says which
|
|
37
|
+
* organisation this source tree deploys into. Two writers, both recording
|
|
38
|
+
* stated intent: `fjall create organisation` writes it at scaffold time (the
|
|
39
|
+
* organisation being created IS the command's subject — the project is born
|
|
40
|
+
* bound), and `fjall org bind` records or deliberately re-records it for
|
|
41
|
+
* projects that predate birth binding or genuinely move. Everything else
|
|
42
|
+
* about an organisation (accounts, regions, tiers) is mutable API state that
|
|
43
|
+
* a committed copy would misreport, which is why it stays out of this file.
|
|
44
|
+
*
|
|
45
|
+
* `id` is the only field ever compared. `name` is display-only, so renaming an
|
|
46
|
+
* organisation neither trips the guard nor dirties a checkout.
|
|
47
|
+
*/
|
|
48
|
+
export declare const OrganisationBindingSchema: z.ZodObject<{
|
|
49
|
+
id: z.ZodString;
|
|
50
|
+
name: z.ZodOptional<z.ZodString>;
|
|
51
|
+
}, z.core.$strict>;
|
|
52
|
+
export type OrganisationBinding = z.infer<typeof OrganisationBindingSchema>;
|
|
53
|
+
export declare const RootConfigSchema: z.ZodObject<{
|
|
54
|
+
activeTarget: z.ZodOptional<z.ZodString>;
|
|
55
|
+
domains: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
56
|
+
name: z.ZodString;
|
|
57
|
+
type: z.ZodEnum<{
|
|
58
|
+
apex: "apex";
|
|
59
|
+
delegated: "delegated";
|
|
60
|
+
}>;
|
|
61
|
+
parentDomain: z.ZodOptional<z.ZodString>;
|
|
62
|
+
account: z.ZodOptional<z.ZodString>;
|
|
63
|
+
region: z.ZodOptional<z.ZodString>;
|
|
64
|
+
}, z.core.$strict>>>;
|
|
65
|
+
capacityIdentity: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
66
|
+
legacyAnchor: z.ZodString;
|
|
67
|
+
legacyOwnerId: z.ZodOptional<z.ZodString>;
|
|
68
|
+
legacySgDescription: z.ZodOptional<z.ZodString>;
|
|
69
|
+
}, z.core.$strict>>>>>;
|
|
70
|
+
organisation: z.ZodOptional<z.ZodObject<{
|
|
71
|
+
id: z.ZodString;
|
|
72
|
+
name: z.ZodOptional<z.ZodString>;
|
|
73
|
+
}, z.core.$strict>>;
|
|
74
|
+
}, z.core.$strict>;
|
|
75
|
+
export type RootConfig = z.infer<typeof RootConfigSchema>;
|
|
76
|
+
/**
|
|
77
|
+
* Tolerant READ schema for the deploy-time load path. Intentionally NOT
|
|
78
|
+
* `.strict()`: a fjall-config.json written by a different fjall version, or a
|
|
79
|
+
* legacy pre-config-split file, may carry top-level keys this version does not
|
|
80
|
+
* know (`version`, `services`, `providerAccounts`, `generatorVersion`, …).
|
|
81
|
+
* Stripping them and reading only the recognised keys keeps `fjall deploy` from
|
|
82
|
+
* hard-failing on an otherwise-harmless older file. The strict RootConfigSchema
|
|
83
|
+
* remains the contract for the WRITE/save path — never use this schema to
|
|
84
|
+
* validate what you are about to write. Domain entries stay nested-strict
|
|
85
|
+
* (DomainConfigSchema): tolerance applies to unknown TOP-LEVEL keys only.
|
|
86
|
+
*/
|
|
87
|
+
export declare const RootConfigReadSchema: z.ZodObject<{
|
|
88
|
+
activeTarget: z.ZodOptional<z.ZodString>;
|
|
89
|
+
domains: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
90
|
+
name: z.ZodString;
|
|
91
|
+
type: z.ZodEnum<{
|
|
92
|
+
apex: "apex";
|
|
93
|
+
delegated: "delegated";
|
|
94
|
+
}>;
|
|
95
|
+
parentDomain: z.ZodOptional<z.ZodString>;
|
|
96
|
+
account: z.ZodOptional<z.ZodString>;
|
|
97
|
+
region: z.ZodOptional<z.ZodString>;
|
|
98
|
+
}, z.core.$strict>>>;
|
|
99
|
+
capacityIdentity: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
100
|
+
legacyAnchor: z.ZodString;
|
|
101
|
+
legacyOwnerId: z.ZodOptional<z.ZodString>;
|
|
102
|
+
legacySgDescription: z.ZodOptional<z.ZodString>;
|
|
103
|
+
}, z.core.$strict>>>>>;
|
|
104
|
+
organisation: z.ZodOptional<z.ZodObject<{
|
|
105
|
+
id: z.ZodString;
|
|
106
|
+
name: z.ZodOptional<z.ZodString>;
|
|
107
|
+
}, z.core.$strict>>;
|
|
108
|
+
}, z.core.$strip>;
|
|
109
|
+
export type RootConfigRead = z.infer<typeof RootConfigReadSchema>;
|
|
110
|
+
/**
|
|
111
|
+
* Canonical serialiser for the root fjall-config.json - the single source of
|
|
112
|
+
* truth for the file's on-disk shape. Both `Config.saveConfig` and the webapp
|
|
113
|
+
* scaffold MUST route through this rather than hand-rolling the JSON, so the
|
|
114
|
+
* two can never drift (a scaffold emitting a key the loader later rejects is
|
|
115
|
+
* exactly the bug this prevents). The `RootConfig` input type keeps OUR keys
|
|
116
|
+
* honest at compile time, but the emitted JSON is deliberately NOT
|
|
117
|
+
* strict-validated: `mergeRootConfig` routes top-level keys another fjall
|
|
118
|
+
* version wrote through the save verbatim (see its doc), so a merged save may
|
|
119
|
+
* carry keys RootConfigSchema does not recognise. The round-trip test asserts
|
|
120
|
+
* only that the emitted DEFAULT validates against RootConfigSchema.
|
|
121
|
+
*/
|
|
122
|
+
export declare function serialiseRootConfig(config?: RootConfig): string;
|
|
123
|
+
export type RootConfigKey = keyof RootConfig;
|
|
124
|
+
/**
|
|
125
|
+
* Top-level keys this version recognises, derived from the strict write
|
|
126
|
+
* schema so a key added there cannot silently start being classified as
|
|
127
|
+
* another version's foreign key by the merge.
|
|
128
|
+
*/
|
|
129
|
+
export declare const ROOT_CONFIG_KEYS: readonly RootConfigKey[];
|
|
130
|
+
export declare function copyDefinedKey<K extends RootConfigKey>(target: RootConfig, source: RootConfig, key: K): void;
|
|
131
|
+
/**
|
|
132
|
+
* Exact equality for one `domains[]` entry. DomainConfigSchema is flat and
|
|
133
|
+
* strict (every value a primitive string), so a key-union shallow compare is
|
|
134
|
+
* a complete equality - revisit if the schema ever grows a nested key.
|
|
135
|
+
*/
|
|
136
|
+
export declare function domainEntriesEqual(a: DomainConfig, b: DomainConfig | undefined): boolean;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var p=Object.defineProperty;var a=(o,n)=>p(o,"name",{value:n,configurable:!0});import{z as t}from"zod";import{CapacityIdentityConfigSchema as r}from"./capacityIdentity.js";const c=t.object({name:t.string(),type:t.enum(["apex","delegated"]),parentDomain:t.string().optional(),account:t.string().optional(),region:t.string().optional()}).strict(),s=t.object({id:t.string().min(1),name:t.string().min(1).optional()}).strict(),f=t.object({activeTarget:t.string().optional(),domains:t.array(c).optional(),capacityIdentity:r.optional(),organisation:s.optional()}).strict(),d=t.object({activeTarget:t.string().optional(),domains:t.array(c).optional(),capacityIdentity:r.optional(),organisation:s.optional()});function y(o={}){return JSON.stringify(o,null,2)}a(y,"serialiseRootConfig");const u=f.keyof().options;function x(o,n,e){const i=n[e];i!==void 0&&(o[e]=i)}a(x,"copyDefinedKey");function S(o,n){if(n===void 0)return!1;const e=new Set([...Object.keys(o),...Object.keys(n)]);for(const i of e)if(o[i]!==n[i])return!1;return!0}a(S,"domainEntriesEqual");export{c as DomainConfigSchema,s as OrganisationBindingSchema,u as ROOT_CONFIG_KEYS,d as RootConfigReadSchema,f as RootConfigSchema,x as copyDefinedKey,S as domainEntriesEqual,y as serialiseRootConfig};
|