import fs from 'node:fs' import crypto from 'node:crypto' const sourcePath='contracts/configuration/kbx.configuration.json' const text=fs.readFileSync(sourcePath,'utf8') const source=JSON.parse(text) const sha=crypto.createHash('sha256').update(text).digest('hex') const seenKeys=new Set(),seenEnv=new Set() for(const s of source.settings??[]){ if(seenKeys.has(s.key))throw new Error(`duplicate configuration key ${s.key}`) if(seenEnv.has(s.env))throw new Error(`duplicate environment variable ${s.env}`) seenKeys.add(s.key);seenEnv.add(s.env) } const seenProfiles=new Set() for(const e of source.environments??[]){if(seenProfiles.has(e.id))throw new Error(`duplicate environment profile ${e.id}`);seenProfiles.add(e.id)} const manifest={ schemaVersion:source.schemaVersion, contractVersion:source.contractVersion, sourceSha256:sha, principles:source.principles, sourcePrecedence:source.sourcePrecedence, settingCount:source.settings.length, environmentCount:source.environments.length, settings:source.settings, environments:source.environments } fs.mkdirSync('generated',{recursive:true}) fs.writeFileSync('generated/configuration-manifest.json',JSON.stringify(manifest,null,2)+'\n') const settingCatalog=Object.fromEntries(source.settings.map(s=>[s.key,s])) const envCatalog=Object.fromEntries(source.environments.map(e=>[e.id,e])) fs.mkdirSync('packages/kbx-contracts/src/generated',{recursive:true}) fs.writeFileSync('packages/kbx-contracts/src/generated/configurationCatalog.ts',`// generated from ${sourcePath}; do not edit.\nimport type { KbxConfigurationSettingDefinition, KbxEnvironmentProfileDefinition } from '../configuration'\nexport const kbxConfigurationSourceSha256='${sha}' as const\nexport const kbxConfigurationCatalog=${JSON.stringify(settingCatalog,null,2)} as const satisfies Record\nexport const kbxEnvironmentProfiles=${JSON.stringify(envCatalog,null,2)} as const satisfies Record\nexport type KbxConfigurationKey=keyof typeof kbxConfigurationCatalog\nexport type KbxEnvironmentProfile=keyof typeof kbxEnvironmentProfiles\n`) const esc=s=>String(s).replaceAll('\\','\\\\').replaceAll('"','\\"') const csDefault=v=>v===undefined?'null':`"${esc(String(v))}"` const csAllowed=v=>`new string[]{${(v??[]).map(x=>`"${esc(x)}"`).join(',')}}` const csNullableInt=v=>v===undefined?'null':String(v) const csSettings=source.settings.map(s=>` ["${esc(s.key)}"] = new("${esc(s.key)}","${esc(s.env)}","${esc(s.category)}","${esc(s.type)}",${s.secret?'true':'false'},${s.restartRequired?'true':'false'},${csDefault(s.default)},${csAllowed(s.allowedValues)},${csAllowed(s.requiredIn)},${s.requiredWhen?`"${esc(s.requiredWhen.key)}"`:'null'},${s.requiredWhen?`"${esc(String(s.requiredWhen.equals))}"`:'null'},${csNullableInt(s.minimum)},${csNullableInt(s.maximum)},${csAllowed(s.allowedSources)})`).join(',\n') const csProfiles=source.environments.map(e=>` ["${esc(e.id)}"] = new("${esc(e.id)}",${e.artifactPromotion?'true':'false'},"${esc(e.migrationStrategy)}","${esc(e.providerNetwork)}",${e.requireHttps?'true':'false'},${e.providerEndpointOverrideAllowed?'true':'false'},${csAllowed(e.requiredGates)})`).join(',\n') fs.mkdirSync('backend/Shared/Configuration/Generated',{recursive:true}) fs.writeFileSync('backend/Shared/Configuration/Generated/KbxConfigurationCatalog.g.cs',`// generated from ${sourcePath}; do not edit.\nnamespace Kbx.Shared.Configuration.Generated;\npublic sealed record KbxConfigurationSettingDefinition(string Key,string Env,string Category,string Type,bool Secret,bool RestartRequired,string? DefaultValue,string[] AllowedValues,string[] RequiredIn,string? RequiredWhenKey,string? RequiredWhenEquals,int? Minimum,int? Maximum,string[] AllowedSources);\npublic sealed record KbxEnvironmentProfileDefinition(string Id,bool ArtifactPromotion,string MigrationStrategy,string ProviderNetwork,bool RequireHttps,bool ProviderEndpointOverrideAllowed,string[] RequiredGates);\npublic static class KbxConfigurationCatalog {\n public const string SourceSha256="${sha}";\n public static readonly IReadOnlyDictionary Settings=new Dictionary(StringComparer.Ordinal) {\n${csSettings}\n };\n public static readonly IReadOnlyDictionary Environments=new Dictionary(StringComparer.Ordinal) {\n${csProfiles}\n };\n}\n`) // Generate a secret-free env template. Secret values are intentionally blank. const envLines=['# Generated KBX environment variable template. Do not commit real secret values.'] for(const s of source.settings){ const suffix=s.secret?' # secret: inject from environment/secret store':'' const value=s.secret?'':(s.default===undefined?'':String(s.default)) envLines.push(`${s.env}=${value}${suffix}`) } fs.mkdirSync('deploy/kbx',{recursive:true}) fs.writeFileSync('deploy/kbx/.env.kbx.example',envLines.join('\n')+'\n') const publicDefaults={} for(const s of source.settings){if(!s.secret&&s.default!==undefined)publicDefaults[s.key]=s.default} fs.writeFileSync('deploy/kbx/nonsecret-defaults.generated.json',JSON.stringify(publicDefaults,null,2)+'\n') fs.writeFileSync('deploy/kbx/environment-matrix.generated.json',JSON.stringify({sourceSha256:sha,environments:source.environments},null,2)+'\n') fs.mkdirSync('deploy/kbx/environments',{recursive:true}) const profileOverrides={ Development:{'Kbx:Runtime:Environment':'Development','Kbx:Database:MigrationsMode':'startup-apply','Kbx:Security:RequireHttps':false}, Test:{'Kbx:Runtime:Environment':'Test','Kbx:Database:MigrationsMode':'startup-apply','Kbx:Security:RequireHttps':false,'ExternalProviders:Krx:Enabled':false,'ExternalProviders:OpenDart:Enabled':false,'ExternalProviders:Kis:Enabled':false}, Staging:{'Kbx:Runtime:Environment':'Staging','Kbx:Database:MigrationsMode':'predeploy','Kbx:Security:RequireHttps':true}, Production:{'Kbx:Runtime:Environment':'Production','Kbx:Database:MigrationsMode':'predeploy','Kbx:Security:RequireHttps':true,'ExternalProviders:Kis:Environment':'production'} } for(const e of source.environments){ const lines=[`# Generated ${e.id} example. Secret values intentionally blank.`] const overrides=profileOverrides[e.id]??{} for(const s of source.settings){ let value=s.secret?'':(Object.hasOwn(overrides,s.key)?String(overrides[s.key]):(s.default===undefined?'':String(s.default))) lines.push(`${s.env}=${value}${s.secret?' # secret':''}`) } fs.writeFileSync(`deploy/kbx/environments/${e.id.toLowerCase()}.env.example`,lines.join('\n')+'\n') } fs.writeFileSync('deploy/kbx/release-artifact.contract.json',JSON.stringify({ schemaVersion:'1.0',configurationSourceSha256:sha,immutableArtifact:true,environmentSpecificConfigurationExternal:true, promotionOrder:['Staging','Production'], requiredEvidence:['generated/release-impact.json','generated/configuration-manifest.json','generated/test-scenario-manifest.json','generated/api-manifest.json','migration-dry-run-report','configuration-validation-report'] },null,2)+'\n') const md=['# KBX Configuration Reference','',`Source SHA-256: \`${sha}\``,'',`Settings: **${source.settings.length}** ยท Environments: **${source.environments.length}**`,'','| Key | Env | Category | Type | Secret | Restart |','|---|---|---|---|---:|---:|'] for(const s of source.settings)md.push(`| \`${s.key}\` | \`${s.env}\` | ${s.category} | ${s.type} | ${s.secret?'yes':'no'} | ${s.restartRequired?'yes':'no'} |`) md.push('','## Environment profiles','') for(const e of source.environments)md.push(`### ${e.id}\n- Migration: \`${e.migrationStrategy}\`\n- Provider network: \`${e.providerNetwork}\`\n- HTTPS required: \`${e.requireHttps}\`\n- Endpoint override allowed: \`${e.providerEndpointOverrideAllowed}\`\n- Gates: ${e.requiredGates.map(x=>`\`${x}\``).join(', ')}`) fs.writeFileSync('docs/configuration-reference.generated.md',md.join('\n')+'\n') console.log(`configuration contracts generated: settings=${source.settings.length}, environments=${source.environments.length}, SHA ${sha.slice(0,12)}`)