let lib = import ; secretsConfig = (import ../validation/secrets.nix).getSecretsConfig (import ../config/secrets.nix); isSopsEncrypted = filePath: let content = builtins.readFile filePath; in lib.hasInfix "\nsops:" content || lib.hasInfix "\n\"sops\":" content || lib.hasInfix "\"sops\":" content || builtins.substring 0 5 content == "sops:"; extractJsonKeys = filePath: let content = builtins.readFile (builtins.toString filePath); parsed = builtins.fromJSON content; recurseKeys = prefix: obj: if builtins.isAttrs obj then lib.concatMap (name: if name == "sops" then [] else let newPrefix = if prefix == "" then name else "${prefix}.${name}"; in recurseKeys newPrefix obj.${name} ) (builtins.attrNames obj) else [ prefix ]; in recurseKeys "" parsed; getRuntimePath = path: "/run/secrets/${builtins.concatStringsSep "_" path}"; defaultMetadata = { path = null; owner = null; group = null; mode = null; }; # Step 1: Convert path/string leaves to a normalized attrset shape. normalizeLeaf = path: node: if builtins.isString node || builtins.isPath node then { file = node; keys = null; metadata = defaultMetadata; } else if builtins.isAttrs node && node ? file then { file = node.file; keys = node.keys or null; metadata = { path = node.path or null; owner = node.owner or null; group = node.group or null; mode = node.mode or null; }; } else throw "Invalid secret leaf at ${builtins.concatStringsSep "." path}: must be string, path, or attrset with 'file'"; # Step 2: Flatten normalized leaf structure into entries. flattenTree = path: node: if builtins.isAttrs node && !(node ? file) then lib.concatMap (name: flattenTree (path ++ [ name ]) node.${name} ) (builtins.attrNames node) else let normalized = normalizeLeaf path node; in [ { inherit path; file = normalized.file; keys = normalized.keys; metadata = normalized.metadata; } ]; entries = flattenTree [] secretsConfig; # Step 3a: Look for keys when type is JSON and keys are not explicitly provided. enrichedEntries = map (entry: let isJson = builtins.match ".*\\.json$" (builtins.toString entry.file) != null; extractedKeys = if isJson && entry.keys == null then extractJsonKeys entry.file else null; in entry // { keys = if extractedKeys != null then extractedKeys else entry.keys; } ) entries; isReady = entry: if !builtins.pathExists entry.file then false else if builtins.match ".*\\.(ya?ml|json)$" (builtins.toString entry.file) != null then true else isSopsEncrypted entry.file; readyEntries = builtins.filter isReady enrichedEntries; missingEntries = builtins.filter (entry: !(isReady entry)) enrichedEntries; # Step 3b: Expand key lists to per-secret entries and build sops.secrets attrset. mkSopsSecrets = sourceEntries: let expanded = lib.concatMap (entry: if entry.keys != null && entry.keys != [] then map (key: entry // { singleKey = key; }) entry.keys else [ (entry // { singleKey = null; }) ] ) sourceEntries; mkEntry = entry: let normalizedKeyName = if entry.singleKey != null then builtins.replaceStrings [ "." ] [ "_" ] entry.singleKey else null; secretName = if entry.singleKey != null then builtins.concatStringsSep "_" (entry.path ++ [ normalizedKeyName ]) else builtins.concatStringsSep "_" entry.path; isJson = builtins.match ".*\\.json$" (builtins.toString entry.file) != null; isYaml = builtins.match ".*\\.ya?ml$" (builtins.toString entry.file) != null; sopsKey = if entry.singleKey == null then null else builtins.replaceStrings [ "." ] [ "/" ] entry.singleKey; in { name = secretName; value = { sopsFile = entry.file; # Compatibility: current sops-install-secrets key traversal expects # YAML map shape for nested key extraction. JSON documents are valid # YAML, so parse .json via "yaml" to support nested keys. format = if isJson || isYaml then "yaml" else "binary"; path = if entry.metadata.path != null then entry.metadata.path else getRuntimePath (entry.path ++ lib.optional (normalizedKeyName != null) normalizedKeyName); owner = if entry.metadata.owner != null then entry.metadata.owner else "root"; group = if entry.metadata.group != null then entry.metadata.group else "root"; mode = if entry.metadata.mode != null then entry.metadata.mode else "0400"; } // lib.optionalAttrs (sopsKey != null) { key = sopsKey; }; }; in builtins.listToAttrs (map mkEntry expanded); in { source = secretsConfig; byName = mkSopsSecrets readyEntries; missing = missingEntries; }