feat: share env files as encrypted links
CI / Checks (pull_request) Failing after 10s
PR title / Conventional title (pull_request) Successful in 0s

S → f on a repo, an env file or a key in one copies a
lazysecrets://share link holding the env files, encrypted (age) with
a secret's value: whoever can read that secret can open it. The key is
share.key in the repo's .lazysecrets.yaml, else in the config, else
picked when sharing; on Azure the link pins its version. Opening one
says what it holds, decrypts when asked, and saves the files in the
clone of the repo (or a folder chosen), replacing existing ones only
when asked. lazysecrets share prints a link from the command line.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
kebandClaude Opus 5.5 committed 2026-10-13 11:59:39 +02:00
1 parent 1a5e40a012
commit 4b2f685921
26 files changed
+1541 -98

No files matched your search

+27 -6
View File
@@ -453,11 +453,8 @@ func (p *Provider) Value(ctx context.Context, o provider.Object) (provider.Secre
}
switch o.Type {
case provider.TypeSecret:
resp, err := vc.secrets.GetSecret(ctx, o.Name, o.Version, nil)
if err != nil {
return provider.SecretValue{}, cleanError(err)
}
return provider.NewSecretValue([]byte(deref(resp.Value))), nil
v, _, err := p.VersionedValue(ctx, o)
return v, err
case provider.TypeKey:
resp, err := vc.keys.GetKey(ctx, o.Name, o.Version, nil)
if err != nil {
@@ -481,7 +478,31 @@ func (p *Provider) Value(ctx context.Context, o provider.Object) (provider.Secre
return provider.SecretValue{}, provider.ErrUnsupported
}
var _ provider.Locator = (*Provider)(nil)
// VersionedValue is Value, with the version read.
func (p *Provider) VersionedValue(ctx context.Context, o provider.Object) (provider.SecretValue, string, error) {
if o.Type != provider.TypeSecret {
v, err := p.Value(ctx, o)
return v, o.Version, err
}
vc, err := p.clients(o.Container.Detail)
if err != nil {
return provider.SecretValue{}, "", err
}
resp, err := vc.secrets.GetSecret(ctx, o.Name, o.Version, nil)
if err != nil {
return provider.SecretValue{}, "", cleanError(err)
}
version := o.Version
if resp.ID != nil {
version = resp.ID.Version()
}
return provider.NewSecretValue([]byte(deref(resp.Value))), version, nil
}
var (
_ provider.Locator = (*Provider)(nil)
_ provider.Versioned = (*Provider)(nil)
)
// validHost guards the Resource Graph query against odd input.
var validHost = regexp.MustCompile(`^[a-z0-9.-]+$`)
+1 -1
View File
@@ -35,7 +35,7 @@ func Repos() *Provider {
// commit are what git says of it: "tracked", "ignored" or "untracked".
file := func(src provider.Source, path, method, git, commit string, kv ...string) {
sops := method != ""
detail := "env · plain text"
detail := provider.DetailPlainEnv
if sops {
detail = "sops · " + method
}
+11
View File
@@ -167,6 +167,13 @@ type Current interface {
Current(ctx context.Context) (Source, bool)
}
// Versioned is implemented by providers whose secrets have versions: it
// is Value, also returning the version read (the current one when
// o.Version is empty), so a link can pin it.
type Versioned interface {
VersionedValue(ctx context.Context, o Object) (SecretValue, string, error)
}
// Locator is implemented by providers that can find where a container lives,
// to open links. address is a vault host (kv.vault.azure.net) or a
// workspace host. It returns the ID of the source holding it.
@@ -220,6 +227,10 @@ func (d *Denied) Error() string {
func (d *Denied) Unwrap() error { return d.Err }
// DetailPlainEnv is the Detail of a container that is a plain (not
// encrypted) env file in a repo.
const DetailPlainEnv = "env · plain text"
// ContainerDetails is implemented by providers that can say more about a
// container than its listing does: how a file is encrypted and what git
// knows of it. ErrUnsupported means there's nothing more for c.
+37 -6
View File
@@ -311,8 +311,8 @@ func (p *Provider) Containers(ctx context.Context, src provider.Source) ([]provi
c.Detail = "sops · " + strings.Join(meta.methods, ", ")
case isSops:
return // a SOPS file where .sops.yaml says SOPS files don't go
case isEnv(f):
c.Detail = "env · plain text"
case IsEnv(f):
c.Detail = provider.DetailPlainEnv
default:
return
}
@@ -464,6 +464,16 @@ func (p *Provider) Locate(_ context.Context, host string) (string, bool, error)
return "", false, nil
}
// Root is the root of the git repo dir is in, and the repo's ID: its
// remote without scheme or .git (a repo link's host), else the root.
func Root(dir string) (root, id string, err error) {
root, err = gitRoot(dir)
if err != nil {
return "", "", fmt.Errorf("%s isn't in a git repository", dir)
}
return root, info(root, "").id, nil
}
// --- git ---
func gitRoot(dir string) (string, error) {
@@ -568,6 +578,27 @@ func normalizeRemote(remote string) (string, string) {
type lazysecretsFile struct {
Include []string `yaml:"include"`
Exclude []string `yaml:"exclude"`
Share struct {
// Key is the link to the secret that encrypts the repo's
// shared env files (package share).
Key string `yaml:"key"`
} `yaml:"share"`
}
// ShareKey is the share.key of the repo's .lazysecrets.yaml, if any.
func ShareKey(root string) (string, error) {
b, err := os.ReadFile(filepath.Join(root, ".lazysecrets.yaml"))
if errors.Is(err, os.ErrNotExist) {
return "", nil
}
if err != nil {
return "", err
}
var f lazysecretsFile
if err := yaml.Unmarshal(b, &f); err != nil {
return "", fmt.Errorf("%s/.lazysecrets.yaml: %w", root, err)
}
return f.Share.Key, nil
}
// selector says which files to look at: user (the include/exclude of the
@@ -618,7 +649,7 @@ func envFiles(root string, tracked []string) []string {
dirs := map[string]bool{".": true}
for _, f := range tracked {
isTracked[f] = true
if isEnv(f) {
if IsEnv(f) {
out = append(out, f)
}
if d := path.Dir(f); !dirs[d] && !inPackages(f) {
@@ -632,7 +663,7 @@ func envFiles(root string, tracked []string) []string {
}
for _, e := range entries {
f := path.Join(d, e.Name())
if e.Type().IsRegular() && !isTracked[f] && isEnv(f) {
if e.Type().IsRegular() && !isTracked[f] && IsEnv(f) {
out = append(out, f)
}
}
@@ -663,10 +694,10 @@ func inPackages(f string) bool {
return false
}
// isEnv reports whether a file is named like an env file with real values:
// IsEnv reports whether a file is named like an env file with real values:
// .env, .env.local, prod.env; not .env.example and the like, nor one in
// dependencies.
func isEnv(f string) bool {
func IsEnv(f string) bool {
if inPackages(f) {
return false
}