package secrets import ( "context" "fmt" "strings" "sync" infisical "github.com/infisical/go-sdk" ) // InfisicalBackend reads secrets from an Infisical instance via machine identity auth. type InfisicalBackend struct { client infisical.InfisicalClientInterface cfg InfisicalConfig mu sync.Mutex connected bool } // InfisicalConfig holds connection parameters for an Infisical instance. type InfisicalConfig struct { SiteURL string // Infisical server URL (cloud or self-hosted) ClientID string // Machine identity client ID ClientSecret string // Machine identity client secret ProjectID string // Infisical project/workspace ID SecretPath string // Path prefix for secrets (e.g., "/") Env string // Environment slug (e.g., "dev", "prod") } // NewInfisicalBackend creates an Infisical backend. Connects lazily on first Get. func NewInfisicalBackend(cfg InfisicalConfig) *InfisicalBackend { autoRefresh := true return &InfisicalBackend{ cfg: cfg, client: infisical.NewInfisicalClient(context.Background(), infisical.Config{ SiteUrl: cfg.SiteURL, AutoTokenRefresh: &autoRefresh, CacheExpiryInSeconds: 0, // no caching — live reads over localhost }), } } func (b *InfisicalBackend) Name() string { return "infisical" } func (b *InfisicalBackend) connect() error { b.mu.Lock() defer b.mu.Unlock() if b.connected { return nil } clientID := b.cfg.ClientID clientSecret := b.cfg.ClientSecret if clientID == "" || clientSecret == "" { return fmt.Errorf("%w: OIKOS_INFISICAL_CLIENT_ID and OIKOS_INFISICAL_CLIENT_SECRET not set", ErrBackendUnavailable) } _, err := b.client.Auth().UniversalAuthLogin(clientID, clientSecret) if err != nil { return fmt.Errorf("%w: infisical auth: %w", ErrBackendUnavailable, err) } b.connected = true return nil } func (b *InfisicalBackend) Get(ctx context.Context, key string) (string, error) { if err := b.connect(); err != nil { return "", err } opts := infisical.ListSecretsOptions{ ProjectID: b.cfg.ProjectID, Environment: b.cfg.Env, SecretPath: b.cfg.SecretPath, } secrets, err := b.client.Secrets().List(opts) if err != nil { return "", fmt.Errorf("infisical list: %w", err) } // Strip path prefix for matching lookupKey := strings.TrimPrefix(key, b.cfg.SecretPath) for _, s := range secrets { if s.SecretKey == lookupKey { return s.SecretValue, nil } } return "", ErrNotFound } func (b *InfisicalBackend) List(ctx context.Context) ([]string, error) { if err := b.connect(); err != nil { return nil, err } opts := infisical.ListSecretsOptions{ ProjectID: b.cfg.ProjectID, Environment: b.cfg.Env, SecretPath: b.cfg.SecretPath, } secrets, err := b.client.Secrets().List(opts) if err != nil { return nil, fmt.Errorf("infisical list: %w", err) } keys := make([]string, len(secrets)) for i, s := range secrets { keys[i] = s.SecretKey } return keys, nil } func (b *InfisicalBackend) Set(ctx context.Context, key string, value string) error { if err := b.connect(); err != nil { return err } // Try create first (idempotent — upserts); fall back to update on conflict. _, err := b.client.Secrets().Create(infisical.CreateSecretOptions{ SecretKey: key, SecretValue: value, Environment: b.cfg.Env, SecretPath: b.cfg.SecretPath, ProjectID: b.cfg.ProjectID, Type: "shared", }) if err != nil { // Create failed (key may already exist) — update instead. _, err = b.client.Secrets().Update(infisical.UpdateSecretOptions{ SecretKey: key, NewSecretValue: value, Environment: b.cfg.Env, SecretPath: b.cfg.SecretPath, ProjectID: b.cfg.ProjectID, Type: "shared", }) } return err }