Add aviso-key: prints one private key
Stdout carries the key alone, newline-terminated, so a shell substitution can capture it into a secret store without parsing. The public half is derived at boot; there is no second value to keep.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2 files changed,
+61
−0
cmd/aviso-key/main.go+33 −0cmd/aviso-key/main_test.go+28 −0
diff --git a/cmd/aviso-key/main.go b/cmd/aviso-key/main.go| new file mode 100644 |
| index 0000000..fbb0897 |
| --- /dev/null |
| +++ b/cmd/aviso-key/main.go |
| @@ -0,0 +1,33 @@ |
| +// Command aviso-key prints one VAPID private key, and nothing else, so |
| +// it can be captured straight into a secret store: |
| +// |
| +// APP_VAPID_PRIVATE_KEY="$(go run amadan.net/rastrillo/aviso/cmd/aviso-key)" |
| +// |
| +// The public key is derived by aviso.New; there is no second value to |
| +// keep. Run it once per app: rotating the key means every browser |
| +// re-enrols on its next visit. |
| +package main |
| + |
| +import ( |
| + "fmt" |
| + "io" |
| + "os" |
| + |
| + "amadan.net/rastrillo/aviso" |
| +) |
| + |
| +func run(w io.Writer) error { |
| + k, err := aviso.GenerateKey() |
| + if err != nil { |
| + return err |
| + } |
| + _, err = fmt.Fprintln(w, k) |
| + return err |
| +} |
| + |
| +func main() { |
| + if err := run(os.Stdout); err != nil { |
| + fmt.Fprintln(os.Stderr, "aviso-key:", err) |
| + os.Exit(1) |
| + } |
| +} |
diff --git a/cmd/aviso-key/main_test.go b/cmd/aviso-key/main_test.go| new file mode 100644 |
| index 0000000..e56aad1 |
| --- /dev/null |
| +++ b/cmd/aviso-key/main_test.go |
| @@ -0,0 +1,28 @@ |
| +package main |
| + |
| +import ( |
| + "bytes" |
| + "encoding/base64" |
| + "strings" |
| + "testing" |
| +) |
| + |
| +// One line, one key, nothing else: a shell substitution captures it |
| +// without parsing. |
| +func TestRunPrintsOnePrivateKey(t *testing.T) { |
| + var out bytes.Buffer |
| + if err := run(&out); err != nil { |
| + t.Fatal(err) |
| + } |
| + if !strings.HasSuffix(out.String(), "\n") || strings.Count(out.String(), "\n") != 1 { |
| + t.Fatalf("want exactly one newline-terminated line, got %q", out.String()) |
| + } |
| + s := strings.TrimSpace(out.String()) |
| + raw, err := base64.RawURLEncoding.DecodeString(s) |
| + if err != nil || len(raw) != 32 { |
| + t.Fatalf("not a 32-byte base64url key: %q", s) |
| + } |
| + if strings.ContainsAny(s, "=+/") { |
| + t.Fatalf("not unpadded base64url: %q", s) |
| + } |
| +} |