review: internal/registry package
closed(1) internal/registry/doc.go, internal/registry/options.go
- All ENV vars start with “STALWARTSYNC” which does not match the name of the service. They would be more intuitively named “STALWART_URL”, “STALWART_USERNAME”, etc.
(2) Architectural design
What is the difference between internal/registry and internal/jmap since both expose a way to construct a jmap.Client? This is a confusing API design because it’s not clear in what scenarios the caller should go through the registry package to get a client and why the registry package owns the upstream URL and secrets. The jmap.Client also takes an endpoint, so that configuration is duplicated in multiple places but the only differences become apparent by reading the doc strings about whether it should include the /jmap path or not. In what scenario does the caller go straight to jmap.New to get a client vs. registry.Options{}.Client()?
The design of the client construction is not idiomatic Go code. It would not be normal to call registry.Options{}.Client() to get a functional client. This implies that the Options struct may be mutated and the client reconstructed every time instead of being created once through a New method or similar. Wouldn’t a more idiomatic API be registry.NewClient(o registry.Options) (*jmap.Client, error)? But still it’s not clear why the registry has it’s own client constructor function for a jmap.Client while jmap also has jmap.New with subtle differences in how the endpoints are constructed. If the intent is to create a JMAP client with all appropriate methods to interact with the API, then registry should be combined with the JMAP package to make a cleaner API surface that surfaces one jmap.Client constructor function that is provided with all required configuration (endpoint, secret files, etc.) Internally it can expose a basic jmap.new() function if necessary to hide some of the implementation details.
Generally, internal packages should not reach out to the environment to configure themselves. If some configuration value is required, it should be an argument to the constructor function and the service is configured from the outside by the caller. The caller reads ENV vars or parses a config value in a single place, then configures all required services with that information. If every client reads its own configuration in different ways, it makes the overall configuration of the service very difficult to track. If a registry.NewClient needs a URL, username, and secret to function, then it declares those as part of it’s NewClient function and the caller is responsible for getting those values from the environment or other configuration location. Internal packages should not have side effects, they should be as pure as possible. It should look like registry.NewClient(endpoint, user, secret). Some other top-level config.go should be getting these from the environment, parsing a config file, and reading the secrets from disk, then failing fast when required config values are absent. This happens in the main() or close to it in the call stack on startup, not deep in the internal packages.
2 Comments
Task 4a: Merge jmap+registry into
internal/stalwart— one client, pure constructorsDecisions locked with owner
internal/stalwart; constructorNewClient(endpoint, username, secret) (*Client, error)validating non-empty values + parseable endpoint URLSTALWART_URL,STALWART_USERNAME,STALWART_SECRET_FILE) moves out of the library — interim home is the integration-test helper_test.go; permanent home is future cmd/ config nearmain()Final API surface
File mapping
authenticator)git.kilimanjaro.io/sovrn/internal/stalwart; symbol renames registry.X→stalwart.X, jmap.X→stalwart.X; signatures take *stalwart.ClientBug-item checklist (acceptance)
Tasks
R1 implementer → commit
refactor(stalwart): merge jmap+registry into single client packageR2 spec reviewer (checklist vs actual tree) R3 quality reviewer (merged surface coherence, doc quality, zero-mutation test semantics preserved) R4 completion comments on both bugs; left open for owner verification.Implemented — 2026-08-25 (change xzmvnpzr)
All review points addressed by merging
internal/jmap+internal/registryinto a single packageinternal/stalwart(plan: comment dd7435c).stalwart.NewClient(endpoint, username, secret) (*Client, error)— validates endpoint (parse/scheme/host//jmap-suffix) + non-empty username/secret. NoOptionsstruct; noOptions{}.Client()pattern.FromEnvand allos.Getenvreads removed from library code (grep-verified zero outside_test.go). Config assembly belongs nearmain(); interim env reading (STALWART_URL/STALWART_USERNAME/STALWART_SECRET_FILE) will live in the Task 8 integration-test helper.Client(Get/Query/QueryEquals/Set/Destroy/VerifySchema);serverBaseunexported; error prefixes normalized tostalwart:.NewRequestalways includes core capability (+dedup); all errcheck violations fixed.gofmt/go vet/go test -race ./internal/...green;golangci-lint run ./internal/...= 0 issues.Left open for your verification per protocol.