orama/core/pkg/push/credentials/registry.go
anonpenguin23 07638354d2 feat(#72): full-privacy push — self-hosted ntfy + APNs-direct provider
Migration 028: namespace_push_credentials
- Per-(namespace, provider) AES-256-GCM encrypted credential blob.
- Generic schema — apns/ntfy/expo/future plug in with zero migration.
- Separated from migration 026's namespace_push_config (preferences vs
  credentials, different access patterns).

pkg/push/credentials
- Manager + Registry + RQLite store; HKDF purpose "namespace-push-credentials"
  via pkg/secrets. Provider Validator interface for per-provider schema.

pkg/push/providers/apns
- Apple Push Notification service direct provider (no Expo proxy).
- Validator + dispatcher; credentials are p8 signing key + key_id + team_id.

pkg/push/providers/ntfy/credentials.go
- ntfy credential schema (auth_token + default topic). Used both with
  the public ntfy.sh and our self-hosted instance.

pkg/environments/production/installers/ntfy.go
- Self-hosted ntfy server installer. Binary, system user, hardened
  /etc/ntfy/server.yml, systemd unit. Listens on 127.0.0.1:NtfyListenPort
  only — Caddy is the only public path.

pkg/environments/production/installers/caddy.go
- Emit reverse_proxy block for push.<dnsZone> -> 127.0.0.1:NtfyListenPort
  when operator enables ntfy on a node.

CLI: install/upgrade orchestrators learn a new "ntfy" install/preserve
phase; flag gating in install/flags.go + upgrade/flags.go.

Gateway handlers/push/credentials_handler.go
- GET/PUT/DELETE /v1/namespace/push-credentials/{provider}.
- PUT validates against provider Validator before encrypting and storing.
- GET returns a redacted view (booleans + non-secret fields only).

Push manager: provider resolution now also consults
namespace_push_credentials before falling back to YAML defaults.

Docs: core/docs/PUSH_NOTIFICATIONS.md walks through end-to-end setup.

VERSION bumped to 0.122.14.
2026-05-14 10:48:00 +03:00

89 lines
3.0 KiB
Go

package credentials
import "sync"
// registry is the package-level map of provider name → Validator.
//
// Provider packages (pkg/push/providers/apns, .../ntfy, .../fcm, …)
// export a Validator implementation; the gateway dependency wiring
// calls Register at startup for each provider it wants to support on
// this gateway. Anyone-can-register-anything is intentional — operators
// who want to disable a provider simply don't register its Validator,
// and PUT/GET for that provider return 400 ErrUnknownProvider.
//
// Safe for concurrent reads; mutations should happen at gateway
// startup before request serving begins.
var (
registryMu sync.RWMutex
registry = map[string]Validator{}
)
// Register makes a Validator available for the provider name. Calling
// Register with the same name twice replaces the previous one — useful
// in tests; in production it indicates a wiring bug and is logged by
// the gateway startup path.
//
// Panics if v is nil or v.Provider() is empty: these are programmer
// errors that should fail loud at gateway startup, not mysteriously at
// first PUT.
func Register(v Validator) {
if v == nil {
panic("credentials: Register called with nil Validator")
}
name := v.Provider()
if name == "" {
panic("credentials: Validator.Provider() returned empty string")
}
registryMu.Lock()
defer registryMu.Unlock()
registry[name] = v
}
// LookupValidator returns the Validator for provider, or (nil, false)
// if no Validator is registered. Used by the PUT/GET handlers to
// reject unknown providers with a 400 + clear error.
func LookupValidator(provider string) (Validator, bool) {
registryMu.RLock()
defer registryMu.RUnlock()
v, ok := registry[provider]
return v, ok
}
// RegisteredProviders returns the names of all currently-registered
// providers. Used by the "what providers does this gateway support"
// summary endpoint and by tests. Order is unspecified.
func RegisteredProviders() []string {
registryMu.RLock()
defer registryMu.RUnlock()
out := make([]string, 0, len(registry))
for name := range registry {
out = append(out, name)
}
return out
}
// resetRegistry clears the registry. Used internally by the package's
// own tests; the exported ResetRegistryForTest wrapper makes it
// callable from tests in OTHER packages (which can't reach
// package-internal symbols).
//
// Not safe to call while requests are in flight; intended for test
// setup/teardown ONLY.
func resetRegistry() {
registryMu.Lock()
defer registryMu.Unlock()
registry = map[string]Validator{}
}
// ResetRegistryForTest clears the global Validator registry. Tests in
// other packages (e.g. the HTTP handler tests) that register
// Validators should defer this so they don't leak state into other
// tests in the same binary.
//
// Exposed as a regular exported function (not _test.go-gated) because
// test files in other packages cannot reach _test.go-only exports of
// THIS package. Safe to call at runtime but pointless outside tests.
func ResetRegistryForTest() {
resetRegistry()
}