feat(#72): install ntfy on every node, drop --with-ntfy gating

ntfy is now part of the standard node install, just like Caddy. The
binary, /etc/ntfy/server.yml, and the Caddy push.<dnsZone> reverse-
proxy block are written unconditionally on every node, and the
ntfy.service starts as part of the standard service order.

Why uniform: ntfy listens on 127.0.0.1:NtfyListenPort only, reachable
exclusively via the local Caddy reverse-proxy block. Nodes that don't
serve a public push.* DNS entry just have an idle ntfy with no
inbound traffic — zero operational cost, zero attack surface change.
Removing the flag means no per-node toggling, no preference drift
between nodes, no "did we remember to set --with-ntfy" mistakes when
DNS topology changes (e.g. promoting a node to nameserver later).

Removed:
- NodePreferences.NtfyHost (yaml: ntfy_host)
- ProductionSetup.isNtfyHost field, SetNtfyHost, IsNtfyHost
- install/flags.go --with-ntfy + NtfyHost field
- upgrade/flags.go --with-ntfy + NtfyHost field + isFlagPassed helper
  (was only used for --with-ntfy tri-state semantics)
- upgrade/orchestrator.go preference-load and persist for ntfy
- upgrade/remote.go --with-ntfy forwarding

Phase 2 always calls InstallNtfy.
Phase 4 always calls EnableCaddyNtfyProxy + ConfigureNtfy.
Phase 5 always enables ntfy.service.
Phase 5b always starts ntfy.service.

VERSION bumped to 0.122.16.
This commit is contained in:
anonpenguin23 2026-05-14 11:51:08 +03:00
parent 8c37ef547e
commit 8b4abb7eef
9 changed files with 39 additions and 108 deletions

View File

@ -1 +1 @@
0.122.15
0.122.16

View File

@ -15,7 +15,6 @@ type Flags struct {
DryRun bool
SkipChecks bool
Nameserver bool // Make this node a nameserver (runs CoreDNS + Caddy)
NtfyHost bool // Host the self-hosted ntfy server on this node (feature #72)
JoinAddress string // HTTPS URL of existing node (e.g., https://node1.dbrs.space)
Token string // Invite token for joining (from orama invite)
ClusterSecret string // Deprecated: use --token instead
@ -65,7 +64,6 @@ func ParseFlags(args []string) (*Flags, error) {
fs.BoolVar(&flags.DryRun, "dry-run", false, "Show what would be done without making changes")
fs.BoolVar(&flags.SkipChecks, "skip-checks", false, "Skip minimum resource checks (RAM/CPU)")
fs.BoolVar(&flags.Nameserver, "nameserver", false, "Make this node a nameserver (runs CoreDNS + Caddy)")
fs.BoolVar(&flags.NtfyHost, "with-ntfy", false, "Host the self-hosted ntfy server (feature #72; usually colocated with --nameserver on devnet)")
// Cluster join flags
fs.StringVar(&flags.JoinAddress, "join", "", "Join existing cluster via HTTPS URL (e.g. https://node1.dbrs.space)")

View File

@ -46,7 +46,6 @@ func NewOrchestrator(flags *Flags) (*Orchestrator, error) {
setup := production.NewProductionSetup(oramaHome, os.Stdout, flags.Force, flags.SkipChecks)
setup.SetNameserver(flags.Nameserver)
setup.SetNtfyHost(flags.NtfyHost)
// Configure Anyone mode
if flags.AnyoneRelay && flags.AnyoneClient {

View File

@ -12,7 +12,6 @@ type Flags struct {
RestartServices bool
SkipChecks bool
Nameserver *bool // Pointer so we can detect if explicitly set vs default
NtfyHost *bool // Feature #72: nil = use saved preference; non-nil = explicit override
// Remote upgrade flags
Env string // Target environment for remote rolling upgrade
@ -51,8 +50,6 @@ func ParseFlags(args []string) (*Flags, error) {
// Nameserver flag - use pointer to detect if explicitly set
nameserver := fs.Bool("nameserver", false, "Make this node a nameserver (uses saved preference if not specified)")
// Ntfy host flag (feature #72) — same pattern, sticks via preferences.yaml.
ntfyHost := fs.Bool("with-ntfy", false, "Host the self-hosted ntfy server on this node (uses saved preference if not specified)")
// Anyone flags
fs.BoolVar(&flags.AnyoneClient, "anyone-client", false, "Install Anyone as client-only (SOCKS5 proxy on port 9050, no relay)")
@ -78,25 +75,7 @@ func ParseFlags(args []string) (*Flags, error) {
if *nameserver {
flags.Nameserver = nameserver
}
// Set ntfy_host only when explicitly passed (default false != "use saved").
// Without explicit set, the orchestrator reads the saved preference.
if isFlagPassed(fs, "with-ntfy") {
flags.NtfyHost = ntfyHost
}
return flags, nil
}
// isFlagPassed reports whether the named flag was explicitly set on
// the command line, not just defaulted. Used to distinguish "user
// didn't say anything; honor saved preference" from "user wrote
// --with-ntfy=false; turn it OFF".
func isFlagPassed(fs *flag.FlagSet, name string) bool {
passed := false
fs.Visit(func(f *flag.Flag) {
if f.Name == name {
passed = true
}
})
return passed
}

View File

@ -38,17 +38,8 @@ func NewOrchestrator(flags *Flags) *Orchestrator {
isNameserver = *flags.Nameserver
}
// Feature #72: ntfy-host preference survives upgrades the same way
// nameserver does — loaded from preferences.yaml unless the upgrade
// flags override it explicitly.
isNtfyHost := prefs.NtfyHost
if flags.NtfyHost != nil {
isNtfyHost = *flags.NtfyHost
}
setup := production.NewProductionSetup(oramaHome, os.Stdout, flags.Force, flags.SkipChecks)
setup.SetNameserver(isNameserver)
setup.SetNtfyHost(isNtfyHost)
// Configure Anyone mode (explicit flags > saved preferences > auto-detect)
// Explicit flags always win — they represent the user's current intent.
@ -220,15 +211,6 @@ func (o *Orchestrator) handleBranchPreferences() error {
fmt.Printf(" Nameserver mode: enabled (CoreDNS + Caddy)\n")
}
// If ntfy-host was explicitly provided, persist it (feature #72).
if o.flags.NtfyHost != nil {
prefs.NtfyHost = *o.flags.NtfyHost
prefsChanged = true
}
if o.setup.IsNtfyHost() {
fmt.Printf(" ntfy host: enabled (self-hosted ntfy on push.<dnsZone>)\n")
}
// Anyone client and relay are mutually exclusive — setting one clears the other.
if o.flags.AnyoneClient {
prefs.AnyoneClient = true

View File

@ -68,24 +68,16 @@ func (r *RemoteUpgrader) Execute() error {
}
// upgradeNode runs `orama node upgrade --restart` on a single remote node,
// forwarding the per-node flags the operator passed locally (--with-ntfy,
// --nameserver, --force, --skip-checks) so the remote orchestrator sees the
// same intent. Without this forwarding, the remote command would always use
// the saved preference, silently dropping operator overrides like
// `--with-ntfy` on the floor.
// forwarding the per-node flags the operator passed locally (--nameserver,
// --force, --skip-checks) so the remote orchestrator sees the same intent.
// Without this forwarding, the remote command would always use the saved
// preference, silently dropping operator overrides on the floor.
func (r *RemoteUpgrader) upgradeNode(node inspector.Node) error {
sudo := remotessh.SudoPrefix(node)
cmd := fmt.Sprintf("%sorama node upgrade --restart", sudo)
// Tri-state pointer flags: forward only when explicitly set locally.
// Tri-state pointer flag: forward only when explicitly set locally.
// nil = "honor saved preference on the remote" — don't pass anything.
if r.flags.NtfyHost != nil {
if *r.flags.NtfyHost {
cmd += " --with-ntfy"
} else {
cmd += " --with-ntfy=false"
}
}
if r.flags.Nameserver != nil {
if *r.flags.Nameserver {
cmd += " --nameserver"

View File

@ -38,7 +38,6 @@ type ProductionSetup struct {
skipOptionalDeps bool
skipResourceChecks bool
isNameserver bool // Whether this node is a nameserver (runs CoreDNS + Caddy)
isNtfyHost bool // Feature #72: whether this node hosts the self-hosted ntfy server
isAnyoneClient bool // Whether this node runs Anyone as client-only (SOCKS5 proxy)
anyoneRelayConfig *AnyoneRelayConfig // Configuration for Anyone relay mode
privChecker *PrivilegeChecker
@ -136,20 +135,6 @@ func (ps *ProductionSetup) IsNameserver() bool {
return ps.isNameserver
}
// SetNtfyHost flags this node as the host for the self-hosted ntfy
// server (feature #72). When set, Phase 2 installs ntfy and Phase 4
// generates /etc/ntfy/server.yml plus a Caddy reverse-proxy block for
// push.<dnsZone>. Requires isNameserver=true for devnet (ns1 also
// runs Caddy); production deployments may colocate or split.
func (ps *ProductionSetup) SetNtfyHost(isNtfy bool) {
ps.isNtfyHost = isNtfy
}
// IsNtfyHost returns whether this node hosts ntfy.
func (ps *ProductionSetup) IsNtfyHost() bool {
return ps.isNtfyHost
}
// SetAnyoneRelayConfig sets the Anyone relay configuration
func (ps *ProductionSetup) SetAnyoneRelayConfig(config *AnyoneRelayConfig) {
ps.anyoneRelayConfig = config
@ -359,12 +344,14 @@ func (ps *ProductionSetup) installFromSource() error {
ps.logf(" ⚠️ Caddy install warning: %v", err)
}
// Install ntfy only on nodes flagged as the ntfy host (feature #72).
// On devnet this is ns1; on production it can be a dedicated node.
if ps.isNtfyHost {
if err := ps.binaryInstaller.InstallNtfy(); err != nil {
ps.logf(" ⚠️ ntfy install warning: %v", err)
}
// Install ntfy on every node (feature #72). ntfy listens on
// 127.0.0.1:NtfyListenPort and is only reachable via the local
// Caddy reverse-proxy block, so it's safe to run cluster-wide:
// nodes that don't host a public push.* DNS entry simply have
// an idle ntfy with no inbound traffic. Uniform install means no
// per-node toggling and no surprises when DNS topology changes.
if err := ps.binaryInstaller.InstallNtfy(); err != nil {
ps.logf(" ⚠️ ntfy install warning: %v", err)
}
// These are pre-built binary downloads (not Go compilation), always run them
@ -725,20 +712,19 @@ func (ps *ProductionSetup) Phase4GenerateConfigs(peerAddresses []string, vpsIP s
email := "admin@" + caddyDomain
acmeEndpoint := "http://localhost:6001/v1/internal/acme"
// Self-hosted ntfy (feature #72): when this node hosts ntfy,
// (a) tell the Caddy installer to emit a push.<dnsZone> block
// pointing at the local ntfy listen port, and (b) write the
// ntfy server.yml. Both must happen BEFORE ConfigureCaddy is
// Self-hosted ntfy (feature #72): always emit the Caddy
// push.<dnsZone> reverse-proxy block and write
// /etc/ntfy/server.yml. Must happen BEFORE ConfigureCaddy is
// called below so the generated Caddyfile picks up the block.
if ps.isNtfyHost {
ntfyHost := "push." + dnsZone
ps.binaryInstaller.EnableCaddyNtfyProxy(ntfyHost)
ntfyBaseURL := "https://" + ntfyHost
if err := ps.binaryInstaller.ConfigureNtfy(ntfyBaseURL); err != nil {
ps.logf(" ⚠️ ntfy config warning: %v", err)
} else {
ps.logf(" ✓ ntfy config generated (base_url: %s)", ntfyBaseURL)
}
// ntfy is installed unconditionally on every node (see Phase 2)
// so the local 127.0.0.1:NtfyListenPort target always exists.
ntfyHost := "push." + dnsZone
ps.binaryInstaller.EnableCaddyNtfyProxy(ntfyHost)
ntfyBaseURL := "https://" + ntfyHost
if err := ps.binaryInstaller.ConfigureNtfy(ntfyBaseURL); err != nil {
ps.logf(" ⚠️ ntfy config warning: %v", err)
} else {
ps.logf(" ✓ ntfy config generated (base_url: %s)", ntfyBaseURL)
}
if err := ps.binaryInstaller.ConfigureCaddy(caddyDomain, email, acmeEndpoint, baseDomain); err != nil {
@ -899,12 +885,10 @@ func (ps *ProductionSetup) Phase5CreateSystemdServices(enableHTTPS bool) error {
if _, err := os.Stat("/usr/bin/caddy"); err == nil {
services = append(services, "caddy.service")
}
// Add ntfy when this node hosts the self-hosted ntfy server (#72).
// The unit file is written by installers/ntfy.go::writeSystemdUnit.
if ps.isNtfyHost {
if _, err := os.Stat("/usr/local/bin/ntfy"); err == nil {
services = append(services, "ntfy.service")
}
// Add ntfy on every node (#72). The unit file is written by
// installers/ntfy.go::writeSystemdUnit during Phase 2.
if _, err := os.Stat("/usr/local/bin/ntfy"); err == nil {
services = append(services, "ntfy.service")
}
for _, svc := range services {
if err := ps.serviceController.EnableService(svc); err != nil {
@ -982,16 +966,14 @@ func (ps *ProductionSetup) Phase5CreateSystemdServices(enableHTTPS bool) error {
}
}
// Start ntfy when this node hosts it (#72). Caddy must already be
// up (it terminates TLS for push.<dnsZone>), which the order
// above guarantees.
if ps.isNtfyHost {
if _, err := os.Stat("/usr/local/bin/ntfy"); err == nil {
if err := ps.serviceController.RestartService("ntfy.service"); err != nil {
ps.logf(" ⚠️ Failed to start ntfy.service: %v", err)
} else {
ps.logf(" - ntfy.service started")
}
// Start ntfy on every node (#72). Caddy must already be up (it
// terminates TLS for push.<dnsZone>), which the order above
// guarantees.
if _, err := os.Stat("/usr/local/bin/ntfy"); err == nil {
if err := ps.serviceController.RestartService("ntfy.service"); err != nil {
ps.logf(" ⚠️ Failed to start ntfy.service: %v", err)
} else {
ps.logf(" - ntfy.service started")
}
}

View File

@ -11,7 +11,6 @@ import (
type NodePreferences struct {
Branch string `yaml:"branch"`
Nameserver bool `yaml:"nameserver"`
NtfyHost bool `yaml:"ntfy_host"` // Feature #72: this node hosts self-hosted ntfy
AnyoneClient bool `yaml:"anyone_client"`
AnyoneRelay bool `yaml:"anyone_relay"`
AnyoneORPort int `yaml:"anyone_orport,omitempty"` // typically 9001

View File

@ -1,6 +1,6 @@
{
"name": "@debros/orama",
"version": "0.122.15",
"version": "0.122.16",
"description": "TypeScript SDK for Orama Network - Database, PubSub, Cache, Storage, Vault, and more",
"type": "module",
"main": "./dist/index.js",