package secrets import ( "errors" "fmt" "os" "path/filepath" "strings" secretcontracts "git.cin.su/m/Assistant/contracts/secrets" ) var ( ErrEmptyCredentialRef = errors.New("empty credential ref") ErrPathOutsideRoot = errors.New("secret path outside protected root") ErrEmptySecret = errors.New("empty secret") ) // FileResolver resolves CredentialRef metadata against a protected local file // backend. It is a bootstrap mechanism, not canonical configuration storage. type FileResolver struct { root string } func NewFileResolver(root string) (*FileResolver, error) { root = strings.TrimSpace(root) if root == "" { return nil, fmt.Errorf("secret backend root: %w", ErrPathOutsideRoot) } cleanRoot, err := filepath.Abs(filepath.Clean(root)) if err != nil { return nil, fmt.Errorf("secret backend root: %w", err) } return &FileResolver{root: cleanRoot}, nil } func (r *FileResolver) ResolveFile(ref secretcontracts.SecretFileRef) (string, error) { if strings.TrimSpace(string(ref.CredentialRef)) == "" { return "", ErrEmptyCredentialRef } cleanPath, err := filepath.Abs(filepath.Clean(strings.TrimSpace(ref.Path))) if err != nil { return "", err } if !insideRoot(r.root, cleanPath) { return "", ErrPathOutsideRoot } content, err := os.ReadFile(cleanPath) if err != nil { return "", err } secret := strings.TrimSpace(string(content)) if secret == "" { return "", ErrEmptySecret } return secret, nil } func insideRoot(root, path string) bool { rel, err := filepath.Rel(root, path) if err != nil { return false } return rel == "." || (!strings.HasPrefix(rel, "..") && !filepath.IsAbs(rel)) }