// Copyright (C) 2026 Dcntrlized // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published // by the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . // Package instance holds the instance's public identity: a small, self- // contained file at the data root that answers "who am I" for the hub app's // public profile (hub-app theme M1) and, later, federation attestation. package instance import ( "encoding/json" "fmt" "os" "path/filepath" ) // Identity is the instance's user-visible self-description. Name falls back to // DCNTRLIZED_INSTANCE_NAME and then the hostname when instance.json is absent; // Title and Description are optional and only ever come from the file. type Identity struct { Name string `json:"name"` Title string `json:"title,omitempty"` Description string `json:"description,omitempty"` } // Path returns the identity file location for the given data root. func Path(dataRoot string) string { return filepath.Join(dataRoot, "instance.json") } // Load resolves the instance identity: instance.json when present, else // DCNTRLIZED_INSTANCE_NAME, else os.Hostname(). Read-on-demand by design — // no daemon state, so an edited file shows up on the next request. func Load(dataRoot string) Identity { id := defaultIdentity() if data, err := os.ReadFile(Path(dataRoot)); err == nil { var fromFile Identity if json.Unmarshal(data, &fromFile) == nil && fromFile.Name != "" { if fromFile.Title != "" { id.Title = fromFile.Title } if fromFile.Description != "" { id.Description = fromFile.Description } id.Name = fromFile.Name } } return id } // Save atomically writes the identity file (tmp + rename), chmod 600 like the // rest of Maestro's file stores. The owner-gated edit endpoint that uses this // lands with the federation theme; the helper exists so both callers share one // write path. func Save(dataRoot string, id Identity) error { if id.Name == "" { return fmt.Errorf("instance name must not be empty") } data, err := json.MarshalIndent(id, "", " ") if err != nil { return fmt.Errorf("marshal instance identity: %w", err) } path := Path(dataRoot) tmp := path + ".tmp" if err := os.WriteFile(tmp, append(data, '\n'), 0o600); err != nil { return fmt.Errorf("write instance identity: %w", err) } if err := os.Rename(tmp, path); err != nil { return fmt.Errorf("replace instance identity: %w", err) } return nil } func defaultIdentity() Identity { name := os.Getenv("DCNTRLIZED_INSTANCE_NAME") if name == "" { if host, err := os.Hostname(); err == nil { name = host } } return Identity{Name: name} }