From 3e2b732e95e411cdeffa9f54b6097b4d0a7113a8 Mon Sep 17 00:00:00 2001 From: Alano Terblanche <18033717+Benehiko@users.noreply.github.com> Date: Mon, 1 Jun 2026 10:00:22 +0200 Subject: [PATCH 1/2] fix(keychain): export ErrNoDefaultCollection sentinel The errNoDefaultCollection sentinel was package-private, so callers outside the keychain package could not use errors.Is to distinguish "no keychain infrastructure available" from other I/O errors. This matters for headless Linux deployments where the condition is expected and routine, and where an orchestration layer wants to fall back gracefully rather than resort to fragile error message comparisons. Export the sentinel as ErrNoDefaultCollection. It is declared in the cross-platform keychain.go (rather than the Linux-specific file) so that cross-platform callers can reference it on every platform, even though the condition is only ever produced on Linux. Co-Authored-By: Claude Opus 4.8 (1M context) --- store/keychain/keychain.go | 12 ++++++++++++ store/keychain/keychain_linux.go | 8 +------- store/keychain/keychain_linux_test.go | 2 +- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/store/keychain/keychain.go b/store/keychain/keychain.go index e472bb37..a6b632cf 100644 --- a/store/keychain/keychain.go +++ b/store/keychain/keychain.go @@ -25,6 +25,18 @@ import ( var _ store.Store = &keychainStore[store.Secret]{} +// ErrNoDefaultCollection is returned when the secret service has no usable +// default collection (no 'login' collection and no collection assigned to the +// 'default' alias). This typically happens on headless hosts where the keyring +// has not been initialized. +// +// It is exported so callers can use [errors.Is] to detect the absence of usable +// keychain infrastructure and fall back gracefully, rather than relying on +// fragile error message comparisons. This condition is only produced on Linux, +// but the sentinel is declared here (rather than in the Linux-specific file) so +// that cross-platform callers can reference it on every platform. +var ErrNoDefaultCollection = errors.New("no default keychain collection available") + type ( Option interface{ apply(any) error } optionFunc[K any] func(K) error diff --git a/store/keychain/keychain_linux.go b/store/keychain/keychain_linux.go index f40730ea..651bfb6e 100644 --- a/store/keychain/keychain_linux.go +++ b/store/keychain/keychain_linux.go @@ -62,12 +62,6 @@ const ( secretServiceIsCollectionLockedProperty = "org.freedesktop.Secret.Collection.Locked" ) -// errNoDefaultCollection is returned when the secret service has no usable -// default collection (no 'login' collection and no collection assigned to the -// 'default' alias). This typically happens on headless hosts where the keyring -// has not been initialized. -var errNoDefaultCollection = errors.New("no default keychain collection available") - // getDefaultCollection gets the secret service collection dbus object path. // // It prefers the loginKeychainObjectPath, since most users on X11 would have @@ -122,7 +116,7 @@ func resolveDefaultCollection(collections []dbus.ObjectPath, aliasPath dbus.Obje // The null path is syntactically valid (so IsValid above returns true) but // does not point at a real collection, so it must be rejected explicitly. if aliasPath == nullObjectPath { - return "", errNoDefaultCollection + return "", ErrNoDefaultCollection } return aliasPath, nil diff --git a/store/keychain/keychain_linux_test.go b/store/keychain/keychain_linux_test.go index 44fd29e7..d9aff5a7 100644 --- a/store/keychain/keychain_linux_test.go +++ b/store/keychain/keychain_linux_test.go @@ -53,7 +53,7 @@ func TestResolveDefaultCollection(t *testing.T) { // ReadAlias returns the null object path "/" collections: []dbus.ObjectPath{}, aliasPath: nullObjectPath, - wantErr: errNoDefaultCollection, + wantErr: ErrNoDefaultCollection, }, { name: "rejects syntactically invalid alias path", From 1d9e10beac10b5cc0edec411ecf3db82738e934b Mon Sep 17 00:00:00 2001 From: Alano Terblanche <18033717+Benehiko@users.noreply.github.com> Date: Mon, 1 Jun 2026 10:15:54 +0200 Subject: [PATCH 2/2] docs(keychain): note ErrNoDefaultCollection is Linux-specific Clarify in the doc comment that the condition is currently specific to the Linux keyring (freedesktop Secret Service) and never returned on macOS or Windows, which have no "default collection" concept. The sentinel still lives in the cross-platform file so platform-agnostic callers can reference it without build tags; on non-Linux platforms it simply never matches. Co-Authored-By: Claude Opus 4.8 (1M context) --- store/keychain/keychain.go | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/store/keychain/keychain.go b/store/keychain/keychain.go index a6b632cf..a13c2f96 100644 --- a/store/keychain/keychain.go +++ b/store/keychain/keychain.go @@ -30,11 +30,17 @@ var _ store.Store = &keychainStore[store.Secret]{} // 'default' alias). This typically happens on headless hosts where the keyring // has not been initialized. // +// NOTE: this condition is currently specific to the Linux keyring (the +// freedesktop Secret Service). macOS and Windows have no equivalent "default +// collection" concept, so the keychain store never returns this error on those +// platforms. The sentinel is nonetheless declared here, in the cross-platform +// file (rather than the Linux-specific one), so that platform-agnostic callers +// can reference it on every platform without build tags. On non-Linux platforms +// it simply never matches. +// // It is exported so callers can use [errors.Is] to detect the absence of usable // keychain infrastructure and fall back gracefully, rather than relying on -// fragile error message comparisons. This condition is only produced on Linux, -// but the sentinel is declared here (rather than in the Linux-specific file) so -// that cross-platform callers can reference it on every platform. +// fragile error message comparisons. var ErrNoDefaultCollection = errors.New("no default keychain collection available") type (