feat: share.keyRules, how long and varied a share key must be
CI / Checks (pull_request) Successful in 39s
PR title / Conventional title (pull_request) Successful in 2s

Sharing refuses a key shorter than minLength characters (default 32,
never under 16) or of fewer than minClasses kinds of lowercase,
uppercase, digits and symbols (default 2, so random hex passes),
saying so without showing it. Opening doesn't check.

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 12:16:12 +02:00
1 parent b46991c7c2
commit 0bfa2a3141
13 files changed
+155 -20

No files matched your search

+2 -1
View File
@@ -317,7 +317,7 @@ Flags:
if shareArgs != nil { if shareArgs != nil {
ctx, cancel := context.WithTimeout(context.Background(), *timeout) ctx, cancel := context.WithTimeout(context.Background(), *timeout)
defer cancel() defer cancel()
return shareFiles(ctx, providers, st, cfg.Share.Key, shareArgs, os.Stdout) return shareFiles(ctx, providers, st, cfg.Share.Key, shareRules(cfg), shareArgs, os.Stdout)
} }
// Links opened elsewhere (lazysecrets launch) can come here. // Links opened elsewhere (lazysecrets launch) can come here.
@@ -391,6 +391,7 @@ Flags:
Changelog: lazysecrets.Changelog, Changelog: lazysecrets.Changelog,
Dir: placeDir, Dir: placeDir,
ShareKey: cfg.Share.Key, ShareKey: cfg.Share.Key,
ShareRules: shareRules(cfg),
Open: open, Open: open,
Styles: &theme, Styles: &theme,
DefaultFg: defaultFg, DefaultFg: defaultFg,
+8 -2
View File
@@ -9,6 +9,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"git.kebcloud.no/keb/lazysecrets/internal/config"
"git.kebcloud.no/keb/lazysecrets/internal/provider" "git.kebcloud.no/keb/lazysecrets/internal/provider"
"git.kebcloud.no/keb/lazysecrets/internal/provider/repo" "git.kebcloud.no/keb/lazysecrets/internal/provider/repo"
"git.kebcloud.no/keb/lazysecrets/internal/share" "git.kebcloud.no/keb/lazysecrets/internal/share"
@@ -19,7 +20,7 @@ import (
// the --key secret's value, else the repo's or the config's share.key: // the --key secret's value, else the repo's or the config's share.key:
// //
// lazysecrets share .env apps/api/.env // lazysecrets share .env apps/api/.env
func shareFiles(ctx context.Context, providers []provider.Provider, st *store.Store, general string, args []string, out io.Writer) error { func shareFiles(ctx context.Context, providers []provider.Provider, st *store.Store, general string, rules share.Rules, args []string, out io.Writer) error {
fs := flag.NewFlagSet("share", flag.ContinueOnError) fs := flag.NewFlagSet("share", flag.ContinueOnError)
keyLink := fs.String("key", "", "the secret to encrypt with: its lazysecrets:// link, Key Vault URL or reference (default: share.key)") keyLink := fs.String("key", "", "the secret to encrypt with: its lazysecrets:// link, Key Vault URL or reference (default: share.key)")
if err := fs.Parse(args); err != nil { if err := fs.Parse(args); err != nil {
@@ -78,10 +79,15 @@ func shareFiles(ctx context.Context, providers []provider.Provider, st *store.St
if err != nil { if err != nil {
return fmt.Errorf("reading the key %s: %w", key, err) return fmt.Errorf("reading the key %s: %w", key, err)
} }
link, err := share.Seal(share.Header{Key: share.Pin(key, version), Repo: id}, []byte(v.Reveal()), files) link, err := share.Seal(share.Header{Key: share.Pin(key, version), Repo: id}, []byte(v.Reveal()), files, rules)
if err != nil { if err != nil {
return err return err
} }
_, err = fmt.Fprintln(out, link) _, err = fmt.Fprintln(out, link)
return err return err
} }
// shareRules are the config's share.keyRules.
func shareRules(cfg config.Config) share.Rules {
return share.Rules{MinLength: cfg.Share.KeyRules.MinLength, MinClasses: cfg.Share.KeyRules.MinClasses}
}
+5 -2
View File
@@ -16,6 +16,9 @@ import (
"git.kebcloud.no/keb/lazysecrets/internal/store" "git.kebcloud.no/keb/lazysecrets/internal/store"
) )
// demoRules take the demo key, made up to be remembered.
var demoRules = share.Rules{MinLength: 16, MinClasses: 1}
func TestShareFiles(t *testing.T) { func TestShareFiles(t *testing.T) {
root, _ := filepath.EvalSymlinks(t.TempDir()) root, _ := filepath.EvalSymlinks(t.TempDir())
for _, args := range [][]string{{"init", "-q"}, {"remote", "add", "origin", "git@git.example.com:team/app.git"}} { for _, args := range [][]string{{"init", "-q"}, {"remote", "add", "origin", "git@git.example.com:team/app.git"}} {
@@ -31,7 +34,7 @@ func TestShareFiles(t *testing.T) {
key := "lazysecrets://azure/kv-app-prod.vault.azure.net/secrets/db-password" key := "lazysecrets://azure/kv-app-prod.vault.azure.net/secrets/db-password"
var out bytes.Buffer var out bytes.Buffer
if err := shareFiles(ctx, providers, st, key, []string{filepath.Join(root, ".env")}, &out); err != nil { if err := shareFiles(ctx, providers, st, key, demoRules, []string{filepath.Join(root, ".env")}, &out); err != nil {
t.Fatal(err) t.Fatal(err)
} }
r, err := ref.Parse(strings.TrimSpace(out.String())) r, err := ref.Parse(strings.TrimSpace(out.String()))
@@ -57,7 +60,7 @@ func TestShareFiles(t *testing.T) {
{key, nil, "usage"}, {key, nil, "usage"},
{"", []string{"--key", "lazysecrets://azure/kv-app-prod.vault.azure.net", filepath.Join(root, ".env")}, "isn't a secret"}, {"", []string{"--key", "lazysecrets://azure/kv-app-prod.vault.azure.net", filepath.Join(root, ".env")}, "isn't a secret"},
} { } {
if err := shareFiles(ctx, providers, st, c.general, c.args, &out); err == nil || !strings.Contains(err.Error(), c.want) { if err := shareFiles(ctx, providers, st, c.general, demoRules, c.args, &out); err == nil || !strings.Contains(err.Error(), c.want) {
t.Errorf("%v: %v, want %q", c.args, err, c.want) t.Errorf("%v: %v, want %q", c.args, err, c.want)
} }
} }
+3
View File
@@ -37,6 +37,9 @@ providers:
exclude: ["**/testdata/**"] exclude: ["**/testdata/**"]
share: # sharing env files as encrypted links share: # sharing env files as encrypted links
key: https://kv-team.vault.azure.net/secrets/env-share # or its lazysecrets:// link; for repos naming none key: https://kv-team.vault.azure.net/secrets/env-share # or its lazysecrets:// link; for repos naming none
keyRules: # what a key's value must be
minLength: 32 # characters, 16 at least
minClasses: 2 # kinds: lowercase, uppercase, digits, symbols
``` ```
More, each on its own page: [repos](repos.md) (`providers.repos`), More, each on its own page: [repos](repos.md) (`providers.repos`),
+14 -2
View File
@@ -42,8 +42,7 @@ lazysecrets://share/AQ…
The files are encrypted with a secret's value, so whoever can read that The files are encrypted with a secret's value, so whoever can read that
secret (in Key Vault, Databricks or a repo's SOPS file) can open the link, secret (in Key Vault, Databricks or a repo's SOPS file) can open the link,
and nobody else: who may open it is up to the vault's access, as for and nobody else: who may open it is up to the vault's access, as for
everything else. Make a secret for it once, random and at least 16 everything else. Make a secret for it once, random:
characters:
```sh ```sh
az keyvault secret set --vault-name kv-team --name env-share --value "$(openssl rand -base64 32)" az keyvault secret set --vault-name kv-team --name env-share --value "$(openssl rand -base64 32)"
@@ -63,6 +62,19 @@ With neither, sharing asks which secret, in [Go to](keys.md#go-to): it
lists only secrets, and `enter` picks one (pasting a reference picks it lists only secrets, and `enter` picks one (pasting a reference picks it
too). too).
Anyone with a link can try to guess the key, so sharing refuses one that
isn't long and varied enough, saying how (never showing it). By default
that's 32 characters of two kinds; your config can ask for more, or less:
```yaml
share:
keyRules:
minLength: 32 # characters; 16 at least
minClasses: 2 # kinds of character: lowercase, uppercase, digits, symbols
```
Opening a link doesn't check them.
On the command line, `lazysecrets share [--key <secret>] <env file>...` On the command line, `lazysecrets share [--key <secret>] <env file>...`
prints the link. prints the link.
+16
View File
@@ -90,6 +90,16 @@ type Share struct {
// lazysecrets:// link, Key Vault URL or reference), for repos whose // lazysecrets:// link, Key Vault URL or reference), for repos whose
// .lazysecrets.yaml doesn't name one. // .lazysecrets.yaml doesn't name one.
Key string `yaml:"key"` Key string `yaml:"key"`
// KeyRules are what a key's value must be to share with it.
KeyRules KeyRules `yaml:"keyRules"`
}
// KeyRules default to 32 characters of 2 kinds (share.DefaultRules).
type KeyRules struct {
MinLength int `yaml:"minLength"` // at least 16
// MinClasses is how many kinds of character: lowercase, uppercase,
// digits, symbols.
MinClasses int `yaml:"minClasses"`
} }
type Providers struct { type Providers struct {
@@ -236,6 +246,12 @@ func (c Config) validate(path string) error {
return fmt.Errorf("%s: share.key: %q is not a secret's lazysecrets:// link, Key Vault URL or reference", path, k) return fmt.Errorf("%s: share.key: %q is not a secret's lazysecrets:// link, Key Vault URL or reference", path, k)
} }
} }
if n := c.Share.KeyRules.MinLength; n != 0 && n < 16 {
return fmt.Errorf("%s: share.keyRules.minLength: %d is under 16", path, n)
}
if n := c.Share.KeyRules.MinClasses; n < 0 || n > 4 {
return fmt.Errorf("%s: share.keyRules.minClasses: %d is not between 1 and 4", path, n)
}
for i, v := range c.Providers.Azure.Vaults { for i, v := range c.Providers.Azure.Vaults {
if !strings.HasPrefix(v, "https://") { if !strings.HasPrefix(v, "https://") {
return fmt.Errorf("%s: providers.azure.vaults[%d]: %q is not an https:// vault URL", path, i, v) return fmt.Errorf("%s: providers.azure.vaults[%d]: %q is not an https:// vault URL", path, i, v)
+3
View File
@@ -72,6 +72,9 @@ func TestLoadRejectsMistakes(t *testing.T) {
"view": "sourcesView: tree\n", "view": "sourcesView: tree\n",
"releases": "releases: [git.example.com/team/lazysecrets]\n", "releases": "releases: [git.example.com/team/lazysecrets]\n",
"http": "releases: [http://git.example.com/team/lazysecrets]\n", "http": "releases: [http://git.example.com/team/lazysecrets]\n",
"share key": "share:\n key: https://kv.vault.azure.net/\n",
"short keys": "share:\n keyRules:\n minLength: 8\n",
"classes": "share:\n keyRules:\n minClasses: 5\n",
} { } {
if _, err := Load(write(t, content)); err == nil { if _, err := Load(write(t, content)); err == nil {
t.Errorf("%s: no error", name) t.Errorf("%s: no error", name)
+59
View File
@@ -0,0 +1,59 @@
package share
import (
"fmt"
"unicode"
)
// Rules are what a key's value must be to share with it: random enough
// that guessing it is out of reach, since anyone with a link can try.
type Rules struct {
MinLength int // characters
MinClasses int // kinds of character: lowercase, uppercase, digits, symbols
}
// DefaultRules take a random base64 or hex value (openssl rand -base64
// 32, or -hex 32) and refuse passwords made up to be remembered.
var DefaultRules = Rules{MinLength: 32, MinClasses: 2}
// Check reports what key lacks, without showing it.
func (r Rules) Check(key []byte) error {
r = r.withDefaults()
s := string(key)
var lower, upper, digit, symbol bool
for _, c := range s {
switch {
case unicode.IsLower(c):
lower = true
case unicode.IsUpper(c):
upper = true
case unicode.IsDigit(c):
digit = true
default:
symbol = true
}
}
kinds := 0
for _, has := range []bool{lower, upper, digit, symbol} {
if has {
kinds++
}
}
n := len([]rune(s))
if n >= r.MinLength && kinds >= r.MinClasses {
return nil
}
return fmt.Errorf("the key's value is %d characters of %d kinds, and share.keyRules wants at least %d characters of %d kinds (lowercase, uppercase, digits, symbols): use a random one, like openssl rand -base64 32 makes",
n, kinds, r.MinLength, r.MinClasses)
}
func (r Rules) withDefaults() Rules {
if r.MinLength == 0 {
r.MinLength = DefaultRules.MinLength
}
if r.MinClasses == 0 {
r.MinClasses = DefaultRules.MinClasses
}
r.MinLength = max(r.MinLength, MinKey)
return r
}
+6 -5
View File
@@ -40,7 +40,7 @@ const (
// MaxLink keeps a link short enough for chat and tickets (Teams takes // MaxLink keeps a link short enough for chat and tickets (Teams takes
// about 28 KB) and for passing it to a terminal window. // about 28 KB) and for passing it to a terminal window.
MaxLink = 16 << 10 MaxLink = 16 << 10
// MinKey is the shortest secret value accepted as a key. // MinKey is the shortest key the rules can ask for.
MinKey = 16 MinKey = 16
// maxFiles caps what opening a link unpacks. // maxFiles caps what opening a link unpacks.
maxFiles = 1 << 20 maxFiles = 1 << 20
@@ -85,13 +85,14 @@ type file struct {
Data string `json:"data"` Data string `json:"data"`
} }
// Seal encrypts files with key, the value of h.Key, into a share link. // Seal encrypts files with key, the value of h.Key, into a share link,
func Seal(h Header, key []byte, files []File) (ref.Ref, error) { // if key keeps to rules.
func Seal(h Header, key []byte, files []File, rules Rules) (ref.Ref, error) {
if _, err := ParseKey(h.Key.String()); err != nil { if _, err := ParseKey(h.Key.String()); err != nil {
return ref.Ref{}, fmt.Errorf("the key: %w", err) return ref.Ref{}, fmt.Errorf("the key: %w", err)
} }
if len(key) < MinKey { if err := rules.Check(key); err != nil {
return ref.Ref{}, fmt.Errorf("the key's value is %d characters: use a random one of at least %d", len(key), MinKey) return ref.Ref{}, err
} }
if len(files) == 0 { if len(files) == 0 {
return ref.Ref{}, errors.New("no files to share") return ref.Ref{}, errors.New("no files to share")
+27 -4
View File
@@ -23,7 +23,7 @@ func TestSealOpen(t *testing.T) {
{Path: ".env", Data: []byte("DB_PASSWORD=hunter2\n")}, {Path: ".env", Data: []byte("DB_PASSWORD=hunter2\n")},
{Path: "apps/api/.env.local", Data: []byte("TOKEN=\"a b\" # quoted\n")}, {Path: "apps/api/.env.local", Data: []byte("TOKEN=\"a b\" # quoted\n")},
} }
link, err := Seal(Header{Key: key, Repo: "git.example.com/team/app"}, pass, files) link, err := Seal(Header{Key: key, Repo: "git.example.com/team/app"}, pass, files, Rules{})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -62,7 +62,7 @@ func TestSealRefuses(t *testing.T) {
files []File files []File
want string want string
}{ }{
"short key": {Header{Key: key}, []byte("short"), ok, "at least 16"}, "short key": {Header{Key: key}, []byte("short"), ok, "keyRules wants"},
"vault as key": {Header{Key: ref.Ref{Kind: provider.KindAzure, Host: "kv.vault.azure.net"}}, pass, ok, "isn't a secret"}, "vault as key": {Header{Key: ref.Ref{Kind: provider.KindAzure, Host: "kv.vault.azure.net"}}, pass, ok, "isn't a secret"},
"no files": {Header{Key: key}, pass, nil, "no files"}, "no files": {Header{Key: key}, pass, nil, "no files"},
"not env": {Header{Key: key}, pass, []File{{Path: "main.go", Data: []byte("x")}}, "isn't an env file"}, "not env": {Header{Key: key}, pass, []File{{Path: "main.go", Data: []byte("x")}}, "isn't an env file"},
@@ -70,7 +70,7 @@ func TestSealRefuses(t *testing.T) {
"binary": {Header{Key: key}, pass, []File{{Path: ".env", Data: []byte{0xff, 0xfe}}}, "isn't text"}, "binary": {Header{Key: key}, pass, []File{{Path: ".env", Data: []byte{0xff, 0xfe}}}, "isn't text"},
"too big": {Header{Key: key}, pass, []File{{Path: ".env", Data: []byte(random(2 * MaxLink))}}, "more than the 16 KB"}, "too big": {Header{Key: key}, pass, []File{{Path: ".env", Data: []byte(random(2 * MaxLink))}}, "more than the 16 KB"},
} { } {
if _, err := Seal(c.h, c.pass, c.files); err == nil || !strings.Contains(err.Error(), c.want) { if _, err := Seal(c.h, c.pass, c.files, Rules{}); err == nil || !strings.Contains(err.Error(), c.want) {
t.Errorf("%s: %v, want %q", name, err, c.want) t.Errorf("%s: %v, want %q", name, err, c.want)
} }
} }
@@ -89,8 +89,31 @@ func random(n int) string {
return b.String() return b.String()
} }
func TestRules(t *testing.T) {
for _, c := range []struct {
r Rules
key string
pass bool
}{
{Rules{}, "q2Hd8bT4Lw1sZp9Kf6Vx3Nc7Rm0Ya5Ue", true}, // base64-like, 32
{Rules{}, "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", true}, // hex
{Rules{}, "correct-horse-battery-staple", false}, // 28: short
{Rules{}, strings.Repeat("a", 40), false}, // one kind
{Rules{MinLength: 16, MinClasses: 1}, strings.Repeat("a", 16), true},
{Rules{MinLength: 4}, "abc123", false}, // never under MinKey
{Rules{MinClasses: 4}, "q2Hd8bT4Lw1sZp9Kf6Vx3Nc7Rm0Ya5Ue", false},
{Rules{MinClasses: 4}, "q2Hd8bT4Lw1sZp9Kf6Vx3Nc7Rm0Ya5U+", true},
} {
if err := c.r.Check([]byte(c.key)); (err == nil) != c.pass {
t.Errorf("%+v %q: %v", c.r, c.key, err)
} else if err != nil && strings.Contains(err.Error(), c.key) {
t.Errorf("the error shows the key: %v", err)
}
}
}
func TestReadGarbled(t *testing.T) { func TestReadGarbled(t *testing.T) {
link, err := Seal(Header{Key: key}, pass, []File{{Path: ".env", Data: []byte("A=1")}}) link, err := Seal(Header{Key: key}, pass, []File{{Path: ".env", Data: []byte("A=1")}}, Rules{})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
+5 -1
View File
@@ -24,6 +24,7 @@ import (
"git.kebcloud.no/keb/lazysecrets/internal/commands" "git.kebcloud.no/keb/lazysecrets/internal/commands"
"git.kebcloud.no/keb/lazysecrets/internal/provider" "git.kebcloud.no/keb/lazysecrets/internal/provider"
"git.kebcloud.no/keb/lazysecrets/internal/ref" "git.kebcloud.no/keb/lazysecrets/internal/ref"
"git.kebcloud.no/keb/lazysecrets/internal/share"
"git.kebcloud.no/keb/lazysecrets/internal/store" "git.kebcloud.no/keb/lazysecrets/internal/store"
"git.kebcloud.no/keb/lazysecrets/internal/ui/icons" "git.kebcloud.no/keb/lazysecrets/internal/ui/icons"
"git.kebcloud.no/keb/lazysecrets/internal/ui/keys" "git.kebcloud.no/keb/lazysecrets/internal/ui/keys"
@@ -84,6 +85,8 @@ type Options struct {
// ShareKey is the config's share.key: the secret that encrypts shared // ShareKey is the config's share.key: the secret that encrypts shared
// env files, for repos whose .lazysecrets.yaml names none. // env files, for repos whose .lazysecrets.yaml names none.
ShareKey string ShareKey string
// ShareRules are what a key's value must be to share with it.
ShareRules share.Rules
// Dir is the folder lazysecrets runs in: it goes back to where it was // Dir is the folder lazysecrets runs in: it goes back to where it was
// left there, unless it was started on a link. "" doesn't. // left there, unless it was started on a link. "" doesn't.
Dir string Dir string
@@ -274,6 +277,7 @@ type Model struct {
open *ref.Ref // reference to open at startup open *ref.Ref // reference to open at startup
shareKey string // the config's share.key shareKey string // the config's share.key
shareRules share.Rules // and share.keyRules
// A lazysecrets:// link found on the clipboard, offered in the footer. // A lazysecrets:// link found on the clipboard, offered in the footer.
dismissedLink string // the last link offered: not offered again dismissedLink string // the last link offered: not offered again
@@ -460,7 +464,7 @@ func New(providers []provider.Provider, opts Options) Model {
m.spinning = true m.spinning = true
m.cachePath, m.placeDir, m.sourcesAt = opts.CacheFile, opts.Dir, map[string]time.Time{} m.cachePath, m.placeDir, m.sourcesAt = opts.CacheFile, opts.Dir, map[string]time.Time{}
m.shareKey = opts.ShareKey m.shareKey, m.shareRules = opts.ShareKey, opts.ShareRules
m.configFiles, m.statePath, m.changelog = opts.ConfigFiles, opts.StatePath, changelogFields(opts.Changelog) m.configFiles, m.statePath, m.changelog = opts.ConfigFiles, opts.StatePath, changelogFields(opts.Changelog)
if m.cachePath != "" { if m.cachePath != "" {
m.loadCacheFile() m.loadCacheFile()
+2 -2
View File
@@ -158,14 +158,14 @@ func (m *Model) pickPasted(text string) tea.Cmd {
// seal reads the key and encrypts the files with it. // seal reads the key and encrypts the files with it.
func (m *Model) seal(s sharing) tea.Cmd { func (m *Model) seal(s sharing) tea.Cmd {
read := m.keyReader() read, rules := m.keyReader(), m.shareRules
m.flash = flash{text: "encrypting with " + s.key.Name + "…"} m.flash = flash{text: "encrypting with " + s.key.Name + "…"}
return func() tea.Msg { return func() tea.Msg {
v, version, err := read(s.key) v, version, err := read(s.key)
if err != nil { if err != nil {
return sealedMsg{err: fmt.Errorf("reading the key %s: %w", s.key.Name, err)} return sealedMsg{err: fmt.Errorf("reading the key %s: %w", s.key.Name, err)}
} }
link, err := share.Seal(share.Header{Key: share.Pin(s.key, version), Repo: s.repo.Detail}, []byte(v.Reveal()), s.files) link, err := share.Seal(share.Header{Key: share.Pin(s.key, version), Repo: s.repo.Detail}, []byte(v.Reveal()), s.files, rules)
return sealedMsg{link: link, n: len(s.files), err: err} return sealedMsg{link: link, n: len(s.files), err: err}
} }
} }
+5 -1
View File
@@ -12,12 +12,16 @@ import (
"git.kebcloud.no/keb/lazysecrets/internal/provider/fake" "git.kebcloud.no/keb/lazysecrets/internal/provider/fake"
"git.kebcloud.no/keb/lazysecrets/internal/provider/repo" "git.kebcloud.no/keb/lazysecrets/internal/provider/repo"
"git.kebcloud.no/keb/lazysecrets/internal/share"
) )
// scryptWait is how long the driver waits for encrypting and decrypting, // scryptWait is how long the driver waits for encrypting and decrypting,
// which take about a second on purpose. // which take about a second on purpose.
const scryptWait = 20 * time.Second const scryptWait = 20 * time.Second
// demoRules take the demo key, made up to be remembered.
var demoRules = share.Rules{MinLength: 16, MinClasses: 1}
// cloneWith makes a clone of git.example.com/team/app holding files. // cloneWith makes a clone of git.example.com/team/app holding files.
func cloneWith(t *testing.T, files map[string]string) string { func cloneWith(t *testing.T, files map[string]string) string {
t.Helper() t.Helper()
@@ -35,7 +39,7 @@ func cloneWith(t *testing.T, files map[string]string) string {
func shareFrom(t *testing.T, root string, paste bool) string { func shareFrom(t *testing.T, root string, paste bool) string {
t.Helper() t.Helper()
clip := &fakeClipboard{} clip := &fakeClipboard{}
d := newDriverOpts(t, Options{Clipboard: clip}, fake.Azure(0), repo.New(repo.Settings{Dir: root})) d := newDriverOpts(t, Options{Clipboard: clip, ShareRules: demoRules}, fake.Azure(0), repo.New(repo.Settings{Dir: root}))
d.wait = scryptWait d.wait = scryptWait
if src, ok := d.m.selectedSource(); !ok || src.ID != root { if src, ok := d.m.selectedSource(); !ok || src.ID != root {
t.Fatalf("selected %v, want the repo", src.ID) t.Fatalf("selected %v, want the repo", src.ID)