# SPDX-License-Identifier: LSL-1.0 # Copyright (c) 2026 Markus Maiwald # Stewardship: Self Sovereign Society Foundation # # This file is part of the Nexus Sovereign Core. # See legal/LICENSE_SOVEREIGN.md for license terms. ## NPR Recipe Format Handler (.npr) ## ## This module implements the NPR (Nexus Package Recipe) format for source-level ## package definitions. NPR files are plain-text KDL format files that are ## Git-friendly and contain build instructions, dependencies, and metadata. ## ## Format: .npr (Nexus Package Recipe, plain text KDL) ## - Human-readable KDL format for version control ## - Build instruction templates and dependency specifications ## - Ed25519 digital signatures for recipe integrity ## - Integration with build system and dependency resolution import std/[os, json, times, strutils, sequtils, tables, options] import ./types_fixed import ./formats import ./cas type NprError* = object of NimPakError recipeName*: string RecipeValidationResult* = object valid*: bool errors*: seq[ValidationError] warnings*: seq[string] # ============================================================================= # NPR Recipe Creation and Management # ============================================================================= proc createNprRecipe*(metadata: Fragment, buildInstructions: BuildTemplate): NprRecipe = ## Factory method to create NPR recipe with proper defaults NprRecipe( metadata: metadata, buildInstructions: buildInstructions, signature: none(Signature), format: NprRecipe, cryptoAlgorithms: CryptoAlgorithms( hashAlgorithm: "BLAKE2b", signatureAlgorithm: "Ed25519", version: "1.0" ) ) proc createBuildTemplate*(system: BuildSystemType, configureArgs: seq[string] = @[], buildArgs: seq[string] = @[], installArgs: seq[string] = @[], environment: Table[string, string] = initTable[string, string]()): BuildTemplate = ## Factory method to create build template with sensible defaults BuildTemplate( system: system, configureArgs: configureArgs, buildArgs: buildArgs, installArgs: installArgs, environment: environment ) # ============================================================================= # KDL Serialization for NPR Format # ============================================================================= proc escapeKdlString(s: string): string = ## Escape special characters in KDL strings result = "\"" for c in s: case c: of '"': result.add("\\\"") of '\\': result.add("\\\\") of '\n': result.add("\\n") of '\r': result.add("\\r") of '\t': result.add("\\t") else: result.add(c) result.add("\"") proc formatKdlBoolean(b: bool): string = ## Format boolean for KDL if b: "true" else: "false" proc formatKdlArray(items: seq[string]): string = ## Format string array for KDL if items.len == 0: return "" result = "" for i, item in items: if i > 0: result.add(" ") result.add(escapeKdlString(item)) proc formatKdlTable(table: Table[string, string]): string = ## Format table as KDL key-value pairs result = "" for key, value in table: result.add(" " & escapeKdlString(key) & " " & escapeKdlString(value) & "\n") proc toHex(b: byte): string = ## Convert byte to hex string const hexChars = "0123456789abcdef" result = $hexChars[b shr 4] & $hexChars[b and 0x0F] proc serializeNprToKdl*(recipe: NprRecipe): string = ## Serialize NPR recipe to KDL format with comprehensive metadata ## Plain-text format optimized for Git version control and human readability result = "recipe " & escapeKdlString(recipe.metadata.id.name) & " {\n" result.add(" version " & escapeKdlString(recipe.metadata.id.version) & "\n") result.add(" stream " & escapeKdlString($recipe.metadata.id.stream) & "\n") result.add(" format " & escapeKdlString($recipe.format) & "\n") result.add("\n") # Source information for fetching and building result.add(" source {\n") result.add(" method " & escapeKdlString($recipe.metadata.source.sourceMethod) & "\n") result.add(" url " & escapeKdlString(recipe.metadata.source.url) & "\n") result.add(" hash " & escapeKdlString(recipe.metadata.source.hash) & "\n") result.add(" hash-algorithm " & escapeKdlString(recipe.metadata.source.hashAlgorithm) & "\n") result.add(" timestamp " & escapeKdlString($recipe.metadata.source.timestamp) & "\n") result.add(" }\n\n") # Build system configuration and instructions result.add(" build {\n") result.add(" system " & escapeKdlString($recipe.buildInstructions.system) & "\n") if recipe.buildInstructions.configureArgs.len > 0: result.add(" configure-args " & formatKdlArray(recipe.buildInstructions.configureArgs) & "\n") if recipe.buildInstructions.buildArgs.len > 0: result.add(" build-args " & formatKdlArray(recipe.buildInstructions.buildArgs) & "\n") if recipe.buildInstructions.installArgs.len > 0: result.add(" install-args " & formatKdlArray(recipe.buildInstructions.installArgs) & "\n") if recipe.buildInstructions.environment.len > 0: result.add(" environment {\n") result.add(formatKdlTable(recipe.buildInstructions.environment)) result.add(" }\n") result.add(" }\n\n") # Package metadata result.add(" metadata {\n") result.add(" description " & escapeKdlString(recipe.metadata.metadata.description) & "\n") result.add(" license " & escapeKdlString(recipe.metadata.metadata.license) & "\n") result.add(" maintainer " & escapeKdlString(recipe.metadata.metadata.maintainer) & "\n") if recipe.metadata.metadata.tags.len > 0: result.add(" tags " & formatKdlArray(recipe.metadata.metadata.tags) & "\n") result.add(" }\n\n") # Runtime profile requirements result.add(" runtime {\n") result.add(" libc " & escapeKdlString($recipe.metadata.metadata.runtime.libc) & "\n") result.add(" allocator " & escapeKdlString($recipe.metadata.metadata.runtime.allocator) & "\n") result.add(" systemd-aware " & formatKdlBoolean(recipe.metadata.metadata.runtime.systemdAware) & "\n") result.add(" reproducible " & formatKdlBoolean(recipe.metadata.metadata.runtime.reproducible) & "\n") if recipe.metadata.metadata.runtime.tags.len > 0: result.add(" tags " & formatKdlArray(recipe.metadata.metadata.runtime.tags) & "\n") result.add(" }\n\n") # Dependencies with version constraints if recipe.metadata.dependencies.len > 0: result.add(" dependencies {\n") for dep in recipe.metadata.dependencies: result.add(" " & escapeKdlString(dep.name) & " " & escapeKdlString(dep.version) & " stream=" & escapeKdlString($dep.stream) & "\n") result.add(" }\n\n") # ACUL compliance requirements result.add(" acul {\n") result.add(" required " & formatKdlBoolean(recipe.metadata.acul.required) & "\n") if recipe.metadata.acul.membership.len > 0: result.add(" membership " & escapeKdlString(recipe.metadata.acul.membership) & "\n") if recipe.metadata.acul.attribution.len > 0: result.add(" attribution " & escapeKdlString(recipe.metadata.acul.attribution) & "\n") if recipe.metadata.acul.buildLog.len > 0: result.add(" build-log " & escapeKdlString(recipe.metadata.acul.buildLog) & "\n") result.add(" }\n\n") # Cryptographic integrity and signature result.add(" integrity {\n") result.add(" algorithm " & escapeKdlString(recipe.cryptoAlgorithms.hashAlgorithm) & "\n") result.add(" signature-algorithm " & escapeKdlString(recipe.cryptoAlgorithms.signatureAlgorithm) & "\n") result.add(" version " & escapeKdlString(recipe.cryptoAlgorithms.version) & "\n") if recipe.signature.isSome: let sig = recipe.signature.get() result.add(" signature " & escapeKdlString(sig.signature.mapIt(it.toHex()).join("")) & "\n") result.add(" key-id " & escapeKdlString(sig.keyId) & "\n") result.add(" }\n") result.add("}\n") proc deserializeNprFromKdl*(kdlContent: string): Result[NprRecipe, NprError] = ## Deserialize NPR recipe from KDL format ## TODO: Implement proper KDL parsing when kdl library is available ## For now, return an error indicating this is not yet implemented return err[NprRecipe, NprError](NprError( code: InvalidMetadata, msg: "KDL deserialization not yet implemented - waiting for kdl library", recipeName: "unknown" )) # ============================================================================= # Recipe Validation # ============================================================================= proc validateNprRecipe*(recipe: NprRecipe): RecipeValidationResult = ## Validate NPR recipe format and content var result = RecipeValidationResult(valid: true, errors: @[], warnings: @[]) # Validate basic metadata if recipe.metadata.id.name.len == 0: result.errors.add(ValidationError( field: "metadata.id.name", message: "Recipe name cannot be empty", suggestions: @["Provide a valid recipe name"] )) result.valid = false if recipe.metadata.id.version.len == 0: result.errors.add(ValidationError( field: "metadata.id.version", message: "Recipe version cannot be empty", suggestions: @["Provide a valid version string"] )) result.valid = false # Validate source information if recipe.metadata.source.url.len == 0: result.errors.add(ValidationError( field: "metadata.source.url", message: "Source URL cannot be empty", suggestions: @["Provide a valid source URL"] )) result.valid = false if recipe.metadata.source.hash.len == 0: result.errors.add(ValidationError( field: "metadata.source.hash", message: "Source hash cannot be empty", suggestions: @["Calculate and provide source hash"] )) result.valid = false # Validate build system configuration if recipe.buildInstructions.system == Custom and recipe.buildInstructions.configureArgs.len == 0: result.warnings.add("Custom build system without configure arguments may require manual intervention") # Validate build system specific requirements case recipe.buildInstructions.system: of CMake: if "CMAKE_BUILD_TYPE" notin recipe.buildInstructions.environment: result.warnings.add("CMake build without CMAKE_BUILD_TYPE may use debug configuration") of Autotools: if recipe.buildInstructions.configureArgs.len == 0: result.warnings.add("Autotools build without configure arguments may use default configuration") of Cargo: if "CARGO_BUILD_TARGET" notin recipe.buildInstructions.environment: result.warnings.add("Cargo build without explicit target may not be reproducible") else: discard # Validate dependencies for i, dep in recipe.metadata.dependencies: if dep.name.len == 0: result.errors.add(ValidationError( field: "dependencies[" & $i & "].name", message: "Dependency name cannot be empty", suggestions: @["Provide valid dependency name"] )) result.valid = false if dep.version.len == 0: result.errors.add(ValidationError( field: "dependencies[" & $i & "].version", message: "Dependency version cannot be empty", suggestions: @["Provide valid dependency version"] )) result.valid = false # Validate cryptographic algorithms if not isQuantumResistant(recipe.cryptoAlgorithms): result.warnings.add("Using non-quantum-resistant algorithms: " & recipe.cryptoAlgorithms.hashAlgorithm & "/" & recipe.cryptoAlgorithms.signatureAlgorithm) return result # ============================================================================= # Recipe File Operations # ============================================================================= proc saveNprRecipe*(recipe: NprRecipe, filePath: string): Result[void, NprError] = ## Save NPR recipe to file in KDL format try: let kdlContent = serializeNprToKdl(recipe) # Ensure the file has the correct .npr extension let finalPath = if filePath.endsWith(".npr"): filePath else: filePath & ".npr" # Ensure parent directory exists let parentDir = finalPath.parentDir() if not dirExists(parentDir): createDir(parentDir) writeFile(finalPath, kdlContent) return ok[void, NprError]() except IOError as e: return err[void, NprError](NprError( code: FileWriteError, msg: "Failed to save NPR recipe: " & e.msg, recipeName: recipe.metadata.id.name )) proc loadNprRecipe*(filePath: string): Result[NprRecipe, NprError] = ## Load NPR recipe from file try: if not fileExists(filePath): return err[NprRecipe, NprError](NprError( code: PackageNotFound, msg: "NPR recipe file not found: " & filePath, recipeName: "unknown" )) let kdlContent = readFile(filePath) return deserializeNprFromKdl(kdlContent) except IOError as e: return err[NprRecipe, NprError](NprError( code: FileReadError, msg: "Failed to load NPR recipe: " & e.msg, recipeName: "unknown" )) # ============================================================================= # Recipe Digital Signatures # ============================================================================= proc signNprRecipe*(recipe: var NprRecipe, keyId: string, privateKey: seq[byte]): Result[void, NprError] = ## Sign NPR recipe with Ed25519 private key ## Creates a comprehensive signature payload including all critical recipe metadata try: # Create comprehensive signature payload from recipe metadata and build instructions let payload = recipe.metadata.id.name & recipe.metadata.id.version & $recipe.metadata.id.stream & recipe.metadata.source.hash & $recipe.buildInstructions.system & recipe.buildInstructions.configureArgs.join(" ") & recipe.buildInstructions.buildArgs.join(" ") & recipe.buildInstructions.installArgs.join(" ") # TODO: Implement actual Ed25519 signing when crypto library is available # For now, create a deterministic placeholder signature based on payload let payloadHash = calculateBlake2b(payload.toOpenArrayByte(0, payload.len - 1).toSeq()) let placeholderSig = payloadHash[0..63].toOpenArrayByte(0, 63).toSeq() # 64 bytes like Ed25519 let signature = Signature( keyId: keyId, algorithm: recipe.cryptoAlgorithms.signatureAlgorithm, signature: placeholderSig ) recipe.signature = some(signature) return ok[void, NprError]() except Exception as e: return err[void, NprError](NprError( code: UnknownError, msg: "Failed to sign recipe: " & e.msg, recipeName: recipe.metadata.id.name )) proc verifyNprSignature*(recipe: NprRecipe, publicKey: seq[byte]): Result[bool, NprError] = ## Verify NPR recipe signature ## TODO: Implement proper Ed25519 verification when crypto library is available if recipe.signature.isNone: return ok[bool, NprError](false) # No signature to verify try: let sig = recipe.signature.get() # TODO: Implement actual Ed25519 verification # For now, just check if signature exists and has correct length let isValid = sig.signature.len == 64 and sig.keyId.len > 0 return ok[bool, NprError](isValid) except Exception as e: return err[bool, NprError](NprError( code: UnknownError, msg: "Failed to verify signature: " & e.msg, recipeName: recipe.metadata.id.name )) # ============================================================================= # Build System Integration # ============================================================================= proc getBuildSystemDefaults*(system: BuildSystemType): BuildTemplate = ## Get default build template for a build system case system: of CMake: return createBuildTemplate( system = CMake, configureArgs = @["-DCMAKE_BUILD_TYPE=Release"], buildArgs = @["--build", ".", "--parallel"], installArgs = @["--install", "."], environment = {"CMAKE_BUILD_TYPE": "Release"}.toTable() ) of Autotools: return createBuildTemplate( system = Autotools, configureArgs = @["--prefix=/usr"], buildArgs = @["-j$(nproc)"], installArgs = @["install"], environment = initTable[string, string]() ) of Meson: return createBuildTemplate( system = Meson, configureArgs = @["setup", "builddir", "--buildtype=release"], buildArgs = @["-C", "builddir"], installArgs = @["install", "-C", "builddir"], environment = initTable[string, string]() ) of Cargo: return createBuildTemplate( system = Cargo, configureArgs = @[], buildArgs = @["build", "--release"], installArgs = @["install", "--path", "."], environment = {"CARGO_BUILD_TARGET": "x86_64-unknown-linux-musl"}.toTable() ) of NimBuild: return createBuildTemplate( system = NimBuild, configureArgs = @[], buildArgs = @["c", "-d:release", "--mm:orc"], installArgs = @[], environment = {"NIM_BUILD_TYPE": "release"}.toTable() ) of Custom: return createBuildTemplate( system = Custom, configureArgs = @[], buildArgs = @[], installArgs = @[], environment = initTable[string, string]() ) proc validateBuildInstructions*(instructions: BuildTemplate): seq[string] = ## Validate build instructions and return warnings var warnings: seq[string] = @[] case instructions.system: of CMake: if "-DCMAKE_BUILD_TYPE" notin instructions.configureArgs.join(" "): warnings.add("CMake build without explicit build type") of Autotools: if "--prefix" notin instructions.configureArgs.join(" "): warnings.add("Autotools build without explicit prefix") of Cargo: if "--release" notin instructions.buildArgs.join(" "): warnings.add("Cargo build without release flag") else: discard return warnings # ============================================================================= # Utility Functions # ============================================================================= proc getNprInfo*(recipe: NprRecipe): string = ## Get human-readable recipe information result = "NPR Recipe: " & recipe.metadata.id.name & " v" & recipe.metadata.id.version & "\n" result.add("Stream: " & $recipe.metadata.id.stream & "\n") result.add("Build System: " & $recipe.buildInstructions.system & "\n") result.add("Dependencies: " & $recipe.metadata.dependencies.len & "\n") result.add("Source: " & recipe.metadata.source.url & "\n") if recipe.signature.isSome: result.add("Signed: Yes (Key: " & recipe.signature.get().keyId & ")\n") else: result.add("Signed: No\n") proc calculateBlake2b*(data: seq[byte]): string = ## Calculate BLAKE2b hash - imported from CAS module cas.calculateBlake2b(data) proc calculateBlake3*(data: seq[byte]): string = ## Calculate BLAKE3 hash - imported from CAS module cas.calculateBlake3(data)