feat: share env files as encrypted links #54

Merged
keb merged 25 commits from share-files into main 2026-10-13 14:55:22 +02:00
5 changed files with 34 additions and 10 deletions
Showing only changes of commit b46991c7c2 - Show all commits

No files matched your search

+1
View File
@@ -40,6 +40,7 @@ type option struct {
group string // the source row it's in (search) group string // the source row it's in (search)
kind provider.Kind // the kind of provider it's from (search) kind provider.Kind // the kind of provider it's from (search)
hits []int // characters of the label the filter matched (Go to) hits []int // characters of the label the filter matched (Go to)
plain bool // a key in a plain env file: no key to encrypt with (Go to)
run func(m *Model) tea.Cmd run func(m *Model) tea.Cmd
// header makes this a section title: not selectable, and hidden // header makes this a section title: not selectable, and hidden
// while a filter narrows the list. // while a filter narrows the list.
+4 -2
View File
@@ -61,13 +61,14 @@ func (m *Model) searchEntries() (sources, items []option) {
var es []entry var es []entry
// key is the store key, so ctrl+f stars the entry; mark shows how // key is the store key, so ctrl+f stars the entry; mark shows how
// much is loaded, reload (ctrl+r) loads it again. // much is loaded, reload (ctrl+r) loads it again.
add := func(depth int, row string, kind provider.Kind, path []string, account, key, mark string, run, reload func(*Model) tea.Cmd) { add := func(depth int, row string, kind provider.Kind, path []string, account, key, mark string, run, reload func(*Model) tea.Cmd) *option {
n := m.store.Node(key) n := m.store.Node(key)
sort := strings.ToLower(strings.Join(path, "/")) + " " + account sort := strings.ToLower(strings.Join(path, "/")) + " " + account
es = append(es, entry{depth, sort, es = append(es, entry{depth, sort,
option{label: m.pathLabel(kind, path, account), note: n.Note, star: key, option{label: m.pathLabel(kind, path, account), note: n.Note, star: key,
starAccount: providerOf(row), starred: n.StarredFor(providerOf(row)), starAccount: providerOf(row), starred: n.StarredFor(providerOf(row)),
mark: mark, run: run, reload: reload, id: sort, group: row, kind: kind}}) mark: mark, run: run, reload: reload, id: sort, group: row, kind: kind}})
return &es[len(es)-1].opt
} }
for _, p := range m.providers { for _, p := range m.providers {
for _, src := range m.sourceResults[p.ID()].sources { for _, src := range m.sourceResults[p.ID()].sources {
@@ -98,10 +99,11 @@ func (m *Model) searchEntries() (sources, items []option) {
typ := strings.TrimSuffix(strings.ToLower(pluralLabels[t]), "s") // "key" typ := strings.TrimSuffix(strings.ToLower(pluralLabels[t]), "s") // "key"
suffix = strings.Trim(typ+" · "+suffix, " ·") suffix = strings.Trim(typ+" · "+suffix, " ·")
} }
add(2, row, c.Source.Kind, []string{src.Name, c.Name, o.Name}, suffix, objectKey(o), " ", func(m *Model) tea.Cmd { e := add(2, row, c.Source.Kind, []string{src.Name, c.Name, o.Name}, suffix, objectKey(o), " ", func(m *Model) tea.Cmd {
m.startNav(r, actGo, row) m.startNav(r, actGo, row)
return nil return nil
}, reload) }, reload)
e.plain = c.Detail == provider.DetailPlainEnv
} }
} }
} }
+12 -7
View File
@@ -57,7 +57,8 @@ type searchView struct {
// secrets only, and enter gives the selected one's link to pick // secrets only, and enter gives the selected one's link to pick
// instead of going there. // instead of going there.
pick func(m *Model, r ref.Ref) tea.Cmd pick func(m *Model, r ref.Ref) tea.Cmd
pickTitle, pickNote string pickTitle string
pickLines []string // what picking does, above the input
// loaded are the scopes a search has loaded since the popup opened: // loaded are the scopes a search has loaded since the popup opened:
// typing loads the scope once, so lists that fail aren't asked for // typing loads the scope once, so lists that fail aren't asked for
@@ -749,7 +750,14 @@ func (m Model) searchViewBox() string {
lists = panes.BoxCounted(m.st, title(resultsTitle, true), "", counter(v.res, len(v.shown)), right, resW, listH, focused) lists = panes.BoxCounted(m.st, title(resultsTitle, true), "", counter(v.res, len(v.shown)), right, resW, listH, focused)
} }
body := []string{" " + v.input.View(), ""} var body []string
if v.pick != nil {
for _, l := range v.pickLines {
body = append(body, " "+l)
}
body = append(body, "")
}
body = append(body, " "+v.input.View(), "")
for _, l := range strings.Split(lists, "\n") { for _, l := range strings.Split(lists, "\n") {
body = append(body, " "+l) body = append(body, " "+l)
} }
@@ -761,9 +769,6 @@ func (m Model) searchViewBox() string {
} else { } else {
body = append(body, "") // the same height on every tab body = append(body, "") // the same height on every tab
} }
if v.pick != nil && v.pickNote != "" {
body = append(body, " "+m.st.Muted.Render(v.pickNote))
}
km := m.keys km := m.keys
var foot string var foot string
switch { switch {
@@ -808,10 +813,10 @@ func (v *searchView) selectLabel() string {
} }
// pickable is the link of a Go to entry that a picker can pick: a // pickable is the link of a Go to entry that a picker can pick: a
// secret (not a key or certificate). // secret (not a key or certificate, nor a key in a plain env file).
func pickable(o option) (ref.Ref, bool) { func pickable(o option) (ref.Ref, bool) {
r, err := ref.Parse(o.star) r, err := ref.Parse(o.star)
if err != nil || r.IsShare() || r.IsSource() || r.IsContainer() || (r.Type != "" && r.Type != provider.TypeSecret) { if err != nil || o.plain || r.IsShare() || r.IsSource() || r.IsContainer() || (r.Type != "" && r.Type != provider.TypeSecret) {
return ref.Ref{}, false return ref.Ref{}, false
} }
return r, true return r, true
+9 -1
View File
@@ -131,7 +131,15 @@ func (m *Model) pickShareKey(s sharing) tea.Cmd {
return nil return nil
} }
m.search.pickTitle = "Encrypt with which secret?" m.search.pickTitle = "Encrypt with which secret?"
m.search.pickNote = "Whoever can read it can open the link. share.key sets a default." what := s.files[0].Path
if len(s.files) > 1 {
what = plural(len(s.files), "env file")
}
m.search.pickLines = []string{
"Sharing " + what + " from " + s.repo.Name + " as an encrypted link.",
"Pick the secret to encrypt with: whoever can read it can open the link.",
m.st.Muted.Render("Its value isn't shown or put in the link. share.key sets a default."),
}
m.fillSearch(false) m.fillSearch(false)
return tea.Batch(cmd, m.searchLoads()) return tea.Batch(cmd, m.searchLoads())
} }
+8
View File
@@ -47,6 +47,14 @@ func shareFrom(t *testing.T, root string, paste bool) string {
if v == nil || v.pick == nil { if v == nil || v.pick == nil {
t.Fatalf("no secret picker: %s", d.m.flash.text) t.Fatalf("no secret picker: %s", d.m.flash.text)
} }
for _, o := range v.shown {
if o.plain || strings.Contains(o.label, ".env") {
t.Errorf("a plain env value offered as the key: %s", o.label)
}
}
if !strings.Contains(d.screen(), "as an encrypted link") {
t.Errorf("doesn't say what picking does:\n%s", d.screen())
}
if paste { if paste {
// A reference pasted is picked as it is. // A reference pasted is picked as it is.
d.send(tea.PasteMsg{Content: "https://" + kv + "/secrets/db-password"}) d.send(tea.PasteMsg{Content: "https://" + kv + "/secrets/db-password"})