package main // Shared data model + JSON plumbing for the Go port of the NexusGate panel. // // Compatibility contract with the Node implementation: // - The database is one JSON object (see Data). Every record type mirrors // the JS object shapes field by field; fields JS may leave absent use // pointer+omitempty only where "absent" and "null" are semantically // identical in the JS code (all reads are truthiness-based). Where the // JS create paths write an explicit null, the Go field has NO omitempty // so a nil pointer re-marshals as null. // - Unknown fields (written by other panel versions) are captured per // record in Extra and re-emitted verbatim on marshal, so a load/save // round trip never drops data. // - All JSON output goes through marshalJSON (HTML escaping disabled) to // match JSON.stringify. import ( "bytes" "crypto/rand" "encoding/json" "fmt" "reflect" "strings" "sync" "time" ) // ---------- errors ---------- // HTTPError mirrors a JS Error carrying statusCode. type HTTPError struct { Status int Message string } func (e *HTTPError) Error() string { return e.Message } func httpErr(status int, format string, args ...any) *HTTPError { return &HTTPError{Status: status, Message: fmt.Sprintf(format, args...)} } // ---------- time ---------- // NowISO is JS `new Date().toISOString()`: UTC, millisecond precision, Z. func NowISO() string { return ISOFromTime(time.Now()) } func ISOFromTime(t time.Time) string { return t.UTC().Format("2006-01-02T15:04:05.000Z") } // ParseISO accepts the forms JS `new Date(value)` accepts for the inputs // this panel produces/consumes: RFC3339 (with optional fractional seconds // and offsets) and date-only "2006-01-02" (interpreted as UTC midnight). func ParseISO(s string) (time.Time, bool) { s = strings.TrimSpace(s) if s == "" { return time.Time{}, false } for _, layout := range []string{ time.RFC3339Nano, "2006-01-02T15:04:05.000Z0700", "2006-01-02T15:04:05Z0700", "2006-01-02T15:04Z0700", "2006-01-02", } { if t, err := time.Parse(layout, s); err == nil { return t, true } } return time.Time{}, false } // ParseISOMs is JS Date.parse: milliseconds since epoch; ok=false is NaN // (comparisons against NaN are always false in the JS code paths). func ParseISOMs(s string) (int64, bool) { t, ok := ParseISO(s) if !ok { return 0, false } return t.UnixMilli(), true } // ---------- ids ---------- func NewUUID() string { var b [16]byte if _, err := rand.Read(b[:]); err != nil { panic(err) } b[6] = (b[6] & 0x0f) | 0x40 b[8] = (b[8] & 0x3f) | 0x80 return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16]) } // NewID mirrors JS `id(prefix)` => `${prefix}_${crypto.randomUUID()}`. func NewID(prefix string) string { return prefix + "_" + NewUUID() } func StrPtr(s string) *string { return &s } func BoolPtr(b bool) *bool { return &b } // boolVal is JS truthiness for an optional boolean field. func boolVal(p *bool) bool { return p != nil && *p } func IntPtr(v int) *int { return &v } // ---------- JSON helpers ---------- // marshalJSON is JSON.stringify: compact, no HTML escaping. func marshalJSON(v any) ([]byte, error) { var buf bytes.Buffer enc := json.NewEncoder(&buf) enc.SetEscapeHTML(false) if err := enc.Encode(v); err != nil { return nil, err } return bytes.TrimRight(buf.Bytes(), "\n"), nil } // unmarshalJSONUseNumber decodes with UseNumber so generic `any` fields // (activity detail, job payloads from agents) keep integer literals exact. func unmarshalJSONUseNumber(b []byte, v any) error { dec := json.NewDecoder(bytes.NewReader(b)) dec.UseNumber() return dec.Decode(v) } // cloneJSONValue is structuredClone for JSON-shaped values. func cloneJSONValue[T any](v T) T { raw, err := marshalJSON(v) if err != nil { panic(err) } var out T if err := unmarshalJSONUseNumber(raw, &out); err != nil { panic(err) } return out } // ---------- unknown-field passthrough ---------- type extraFields map[string]json.RawMessage var knownKeyCache sync.Map // reflect.Type -> map[string]struct{} func knownJSONKeys(t reflect.Type) map[string]struct{} { if cached, ok := knownKeyCache.Load(t); ok { return cached.(map[string]struct{}) } keys := map[string]struct{}{} for i := 0; i < t.NumField(); i++ { f := t.Field(i) if f.PkgPath != "" { // unexported continue } tag := f.Tag.Get("json") if tag == "-" { continue } name := strings.Split(tag, ",")[0] if name == "" { name = f.Name } keys[name] = struct{}{} } knownKeyCache.Store(t, keys) return keys } func captureExtra(raw []byte, sample any) extraFields { var all map[string]json.RawMessage if err := json.Unmarshal(raw, &all); err != nil || len(all) == 0 { return nil } known := knownJSONKeys(reflect.TypeOf(sample)) var extra extraFields for k, v := range all { if _, ok := known[k]; !ok { if extra == nil { extra = extraFields{} } extra[k] = v } } return extra } // mergeMarshal marshals a record and re-attaches its unknown fields. func mergeMarshal(sample any, extra extraFields) ([]byte, error) { base, err := marshalJSON(sample) if err != nil || len(extra) == 0 { return base, err } var merged map[string]json.RawMessage if err := json.Unmarshal(base, &merged); err != nil { return nil, err } for k, v := range extra { if _, ok := merged[k]; !ok { merged[k] = v } } return marshalJSON(merged) } // recordJSON implements the UnmarshalJSON half shared by every record type. func recordJSON[T any](raw []byte, dst *T, extra *extraFields) error { var plain T if err := unmarshalJSONUseNumber(raw, &plain); err != nil { return err } *dst = plain *extra = captureExtra(raw, plain) return nil } // ---------- database ---------- type Data struct { SchemaVersion int `json:"schemaVersion"` CreatedAt string `json:"createdAt"` Settings Settings `json:"settings"` Users []*User `json:"users"` Servers []*Server `json:"servers"` Agents []*Agent `json:"agents"` EnrollmentTokens []*EnrollmentToken `json:"enrollmentTokens"` Customers []*Customer `json:"customers"` Nodes []*Node `json:"nodes"` NodeClients []*NodeClient `json:"nodeClients"` Jobs []*Job `json:"jobs"` Activity []*Activity `json:"activity"` Observations []*Observation `json:"observations"` SubscriptionAccess []*SubscriptionAccess `json:"subscriptionAccess"` SubscriptionClients []*SubscriptionClient `json:"subscriptionClients"` KeyStore map[string]*KeyEntry `json:"keyStore"` SubSourceCache map[string]*SubSourceCacheEntry `json:"subSourceCache,omitempty"` Extra extraFields `json:"-"` } func (d *Data) UnmarshalJSON(raw []byte) error { type plain Data var p plain var extra extraFields if err := recordJSON(raw, &p, &extra); err != nil { return err } *d = Data(p) d.Extra = extra return nil } func (d Data) MarshalJSON() ([]byte, error) { type plain Data return mergeMarshal(plain(d), d.Extra) } // Clone is structuredClone(this.data). Since round-3 item 22 it is a // reflective deep copy: the old JSON marshal/unmarshal round trip cost // a full serialize + parse of the whole database (with a per-record // map dance for unknown fields) on EVERY store read — twice per admin // request — and dominated panel latency on the router's CPU. The data // graph is plain exported structs/slices/maps/strings/numbers, which // reflection copies exactly; TestCloneEquivalence proves byte-identical // output against cloneViaJSON on the real seed database. func (d *Data) Clone() *Data { if d == nil { return nil } return cloneReflectValue(reflect.ValueOf(d)).Interface().(*Data) } // cloneReflectValue deep-copies v. Structs are shallow-copied first (so // any unexported field keeps its value) and every exported field is // then replaced by its deep copy; []byte (incl. json.RawMessage) is // copied verbatim; scalars pass through. func cloneReflectValue(v reflect.Value) reflect.Value { if !v.IsValid() { return v } switch v.Kind() { case reflect.Pointer: if v.IsNil() { return v } out := reflect.New(v.Type().Elem()) out.Elem().Set(cloneReflectValue(v.Elem())) return out case reflect.Interface: if v.IsNil() { return v } return reflect.ValueOf(cloneReflectValue(v.Elem()).Interface()) case reflect.Slice: if v.IsNil() { return v } if v.Type().Elem().Kind() == reflect.Uint8 { out := reflect.MakeSlice(v.Type(), v.Len(), v.Len()) reflect.Copy(out, v) return out } out := reflect.MakeSlice(v.Type(), v.Len(), v.Len()) for i := 0; i < v.Len(); i++ { out.Index(i).Set(cloneReflectValue(v.Index(i))) } return out case reflect.Map: if v.IsNil() { return v } out := reflect.MakeMapWithSize(v.Type(), v.Len()) iter := v.MapRange() for iter.Next() { out.SetMapIndex(cloneReflectValue(iter.Key()), cloneReflectValue(iter.Value())) } return out case reflect.Struct: out := reflect.New(v.Type()).Elem() out.Set(v) for i := 0; i < v.NumField(); i++ { if v.Type().Field(i).PkgPath != "" { continue // unexported: kept from the shallow copy } out.Field(i).Set(cloneReflectValue(v.Field(i))) } return out default: return v } } // cloneViaJSON is the original Clone implementation: a full JSON // marshal/unmarshal round trip. Kept for the equivalence test and the // item-22 benchmarks that quantify what the reflective Clone replaced. func (d *Data) cloneViaJSON() *Data { raw, err := marshalJSON(d) if err != nil { panic(err) } var out Data if err := unmarshalJSONUseNumber(raw, &out); err != nil { panic(err) } return &out } type Settings struct { BrandName string `json:"brandName"` SessionHours float64 `json:"sessionHours"` ObservationTtlMinutes float64 `json:"observationTtlMinutes"` ActivityRetentionDays float64 `json:"activityRetentionDays"` CompletedJobRetentionDays float64 `json:"completedJobRetentionDays"` ExternalSubRefreshMinutes float64 `json:"externalSubRefreshMinutes,omitempty"` AllowPrivateSubSources bool `json:"allowPrivateSubSources,omitempty"` // Depleting-soon thresholds for the customers summary, aligned with // 3x-ui's expireDiff/trafficDiff settings (same names, units and // factory default of 0 = nobody counts as depleting soon). ExpireDiff float64 `json:"expireDiff,omitempty"` TrafficDiff float64 `json:"trafficDiff,omitempty"` Extra extraFields `json:"-"` } func (s *Settings) UnmarshalJSON(raw []byte) error { type plain Settings var p plain var extra extraFields if err := recordJSON(raw, &p, &extra); err != nil { return err } *s = Settings(p) s.Extra = extra return nil } func (s Settings) MarshalJSON() ([]byte, error) { type plain Settings return mergeMarshal(plain(s), s.Extra) } type User struct { ID string `json:"id"` Username string `json:"username"` PasswordHash string `json:"passwordHash"` Role string `json:"role"` Status string `json:"status"` CreatedAt string `json:"createdAt"` UpdatedAt *string `json:"updatedAt,omitempty"` Extra extraFields `json:"-"` } func (u *User) UnmarshalJSON(raw []byte) error { type plain User var p plain var extra extraFields if err := recordJSON(raw, &p, &extra); err != nil { return err } *u = User(p) u.Extra = extra return nil } func (u User) MarshalJSON() ([]byte, error) { type plain User return mergeMarshal(plain(u), u.Extra) } type EngineState struct { Status string `json:"status"` Detail string `json:"detail"` SingBoxInstalled bool `json:"singBoxInstalled"` Certificates []string `json:"certificates"` SingBoxStatus string `json:"singBoxStatus"` SingBoxVersion string `json:"singBoxVersion,omitempty"` At string `json:"at"` Extra extraFields `json:"-"` } func (e *EngineState) UnmarshalJSON(raw []byte) error { type plain EngineState var p plain var extra extraFields if err := recordJSON(raw, &p, &extra); err != nil { return err } *e = EngineState(p) e.Extra = extra return nil } func (e EngineState) MarshalJSON() ([]byte, error) { type plain EngineState return mergeMarshal(plain(e), e.Extra) } type ServerUsage struct { LastReportAt string `json:"lastReportAt"` SampleCount int `json:"sampleCount"` Error *string `json:"error"` Extra extraFields `json:"-"` } func (u *ServerUsage) UnmarshalJSON(raw []byte) error { type plain ServerUsage var p plain var extra extraFields if err := recordJSON(raw, &p, &extra); err != nil { return err } *u = ServerUsage(p) u.Extra = extra return nil } func (u ServerUsage) MarshalJSON() ([]byte, error) { type plain ServerUsage return mergeMarshal(plain(u), u.Extra) } type Server struct { ID string `json:"id"` Name string `json:"name"` Role string `json:"role"` Region string `json:"region"` PublicAddress string `json:"publicAddress"` PublicAddressV6 string `json:"publicAddressV6"` TLSDomain string `json:"tlsDomain"` PortRangeStart int `json:"portRangeStart"` PortRangeEnd int `json:"portRangeEnd"` Labels []string `json:"labels"` Status string `json:"status"` Enabled *bool `json:"enabled,omitempty"` CreatedAt string `json:"createdAt"` UpdatedAt string `json:"updatedAt"` LastSeenAt *string `json:"lastSeenAt,omitempty"` System json.RawMessage `json:"system,omitempty"` Engine *EngineState `json:"engine,omitempty"` EnginePresent bool `json:"-"` Usage *ServerUsage `json:"usage,omitempty"` Extra extraFields `json:"-"` } func (s *Server) UnmarshalJSON(raw []byte) error { type plain Server var p plain var extra extraFields if err := recordJSON(raw, &p, &extra); err != nil { return err } *s = Server(p) s.Extra = extra var probe map[string]json.RawMessage if json.Unmarshal(raw, &probe) == nil { _, s.EnginePresent = probe["engine"] } return nil } func (s Server) MarshalJSON() ([]byte, error) { type plain Server base, err := mergeMarshal(plain(s), s.Extra) if err != nil || s.Engine != nil || !s.EnginePresent { return base, err } // JS assigns server.engine = ... || null on heartbeat: once the key // exists it stays present with value null. var merged map[string]json.RawMessage if err := json.Unmarshal(base, &merged); err != nil { return nil, err } merged["engine"] = json.RawMessage("null") return marshalJSON(merged) } type Agent struct { ID string `json:"id"` ServerID string `json:"serverId"` KeyHash string `json:"keyHash"` KeyFingerprint *string `json:"keyFingerprint,omitempty"` Status string `json:"status"` Hostname string `json:"hostname"` Version string `json:"version"` CreatedAt string `json:"createdAt"` LastSeenAt string `json:"lastSeenAt"` Extra extraFields `json:"-"` } func (a *Agent) UnmarshalJSON(raw []byte) error { type plain Agent var p plain var extra extraFields if err := recordJSON(raw, &p, &extra); err != nil { return err } *a = Agent(p) a.Extra = extra return nil } func (a Agent) MarshalJSON() ([]byte, error) { type plain Agent return mergeMarshal(plain(a), a.Extra) } type EnrollmentToken struct { ID string `json:"id"` ServerID string `json:"serverId"` TokenHash string `json:"tokenHash"` CreatedAt string `json:"createdAt"` ExpiresAt string `json:"expiresAt"` UsedAt *string `json:"usedAt"` Extra extraFields `json:"-"` } func (t *EnrollmentToken) UnmarshalJSON(raw []byte) error { type plain EnrollmentToken var p plain var extra extraFields if err := recordJSON(raw, &p, &extra); err != nil { return err } *t = EnrollmentToken(p) t.Extra = extra return nil } func (t EnrollmentToken) MarshalJSON() ([]byte, error) { type plain EnrollmentToken return mergeMarshal(plain(t), t.Extra) } type UsageCounters struct { Uplink int64 `json:"uplink"` Downlink int64 `json:"downlink"` Epoch *string `json:"epoch"` Extra extraFields `json:"-"` } func (c *UsageCounters) UnmarshalJSON(raw []byte) error { type plain UsageCounters var p plain var extra extraFields if err := recordJSON(raw, &p, &extra); err != nil { return err } *c = UsageCounters(p) c.Extra = extra return nil } func (c UsageCounters) MarshalJSON() ([]byte, error) { type plain UsageCounters return mergeMarshal(plain(c), c.Extra) } type UsageSlot struct { UsageCounters *UsageCounters `json:"usageCounters,omitempty"` RetiredUsageEpochs []string `json:"retiredUsageEpochs,omitempty"` LastSampleAt *string `json:"lastSampleAt,omitempty"` LastActiveAt *string `json:"lastActiveAt,omitempty"` SpeedUpBps *float64 `json:"speedUpBps,omitempty"` SpeedDownBps *float64 `json:"speedDownBps,omitempty"` SpeedAt *string `json:"speedAt,omitempty"` Extra extraFields `json:"-"` } func (s *UsageSlot) UnmarshalJSON(raw []byte) error { type plain UsageSlot var p plain var extra extraFields if err := recordJSON(raw, &p, &extra); err != nil { return err } *s = UsageSlot(p) s.Extra = extra return nil } func (s UsageSlot) MarshalJSON() ([]byte, error) { type plain UsageSlot return mergeMarshal(plain(s), s.Extra) } type Customer struct { ID string `json:"id"` SubscriptionToken string `json:"subscriptionToken"` Name string `json:"name"` Group string `json:"group"` Status string `json:"status"` SuspendReason *string `json:"suspendReason,omitempty"` SuspendReasonPresent bool `json:"-"` TrafficLimitBytes float64 `json:"trafficLimitBytes"` UsedBytes int64 `json:"usedBytes"` UsedUplinkBytes int64 `json:"usedUplinkBytes"` UsedDownlinkBytes int64 `json:"usedDownlinkBytes"` TrafficResetDay int `json:"trafficResetDay,omitempty"` TrafficResetPeriod string `json:"trafficResetPeriod,omitempty"` TrafficResetAt *string `json:"trafficResetAt,omitempty"` ExpiresAt *string `json:"expiresAt"` IPLimit int64 `json:"ipLimit"` DeviceLimit int64 `json:"deviceLimit"` Tags []string `json:"tags"` Notes string `json:"notes"` CreatedAt string `json:"createdAt"` UpdatedAt string `json:"updatedAt"` ServerUsage map[string]*UsageSlot `json:"serverUsage,omitempty"` LastUsageAt *string `json:"lastUsageAt,omitempty"` LastOnlineAt *string `json:"lastOnlineAt,omitempty"` // The customer's single credential set, shared by all of their node // bindings (round-2 item 2). Populated by the store-load migration for // legacy records and at creation for new ones. Credentials *Credentials `json:"credentials,omitempty"` ExternalSources []ExtSource `json:"externalSources,omitempty"` ManualLinks []ManualLink `json:"manualLinks,omitempty"` Extra extraFields `json:"-"` } // ExtSource is one external subscription source on a customer // (SUB-AGGREGATION-DESIGN.md). UserAgent is optional. type ExtSource struct { ID string `json:"id"` URL string `json:"url"` Remark string `json:"remark"` Enabled bool `json:"enabled"` UserAgent string `json:"userAgent,omitempty"` } // ManualLink is one hand-added node link on a customer. type ManualLink struct { ID string `json:"id"` URI string `json:"uri"` Remark string `json:"remark"` Enabled bool `json:"enabled"` } // SubCacheItem is one parsed entry of a fetched external source. type SubCacheItem struct { URI string `json:"uri"` Name string `json:"name"` } // SubSourceCacheEntry is the fetch cache for one external source, // stored top-level in data.subSourceCache keyed by source id. type SubSourceCacheEntry struct { FetchedAt *string `json:"fetchedAt"` OkAt *string `json:"okAt"` FailCount int `json:"failCount"` LastError *string `json:"lastError"` DroppedCount int `json:"droppedCount"` Items []SubCacheItem `json:"items"` } func (c *Customer) UnmarshalJSON(raw []byte) error { type plain Customer var p plain var extra extraFields if err := recordJSON(raw, &p, &extra); err != nil { return err } *c = Customer(p) c.Extra = extra var probe map[string]json.RawMessage if json.Unmarshal(raw, &probe) == nil { _, c.SuspendReasonPresent = probe["suspendReason"] } return nil } func (c Customer) MarshalJSON() ([]byte, error) { type plain Customer base, err := mergeMarshal(plain(c), c.Extra) if err != nil || c.SuspendReason != nil || !c.SuspendReasonPresent { return base, err } // JS writes suspendReason: null explicitly on resume paths. var merged map[string]json.RawMessage if err := json.Unmarshal(base, &merged); err != nil { return nil, err } merged["suspendReason"] = json.RawMessage("null") return marshalJSON(merged) } type RealityConfig struct { KeyID string `json:"keyId"` ServerName string `json:"serverName"` DestPort int `json:"destPort"` Extra extraFields `json:"-"` } func (r *RealityConfig) UnmarshalJSON(raw []byte) error { type plain RealityConfig var p plain var extra extraFields if err := recordJSON(raw, &p, &extra); err != nil { return err } *r = RealityConfig(p) r.Extra = extra return nil } func (r RealityConfig) MarshalJSON() ([]byte, error) { type plain RealityConfig return mergeMarshal(plain(r), r.Extra) } type ExitCredentials struct { ClientID string `json:"clientId"` Password string `json:"password"` SocksUser string `json:"socksUser"` Extra extraFields `json:"-"` } func (c *ExitCredentials) UnmarshalJSON(raw []byte) error { type plain ExitCredentials var p plain var extra extraFields if err := recordJSON(raw, &p, &extra); err != nil { return err } *c = ExitCredentials(p) c.Extra = extra return nil } func (c ExitCredentials) MarshalJSON() ([]byte, error) { type plain ExitCredentials return mergeMarshal(plain(c), c.Extra) } type MeteredTraffic struct { Uplink int64 `json:"uplink"` Downlink int64 `json:"downlink"` Extra extraFields `json:"-"` } func (m *MeteredTraffic) UnmarshalJSON(raw []byte) error { type plain MeteredTraffic var p plain var extra extraFields if err := recordJSON(raw, &p, &extra); err != nil { return err } *m = MeteredTraffic(p) m.Extra = extra return nil } func (m MeteredTraffic) MarshalJSON() ([]byte, error) { type plain MeteredTraffic return mergeMarshal(plain(m), m.Extra) } type Node struct { ID string `json:"id"` Name string `json:"name"` ServerID string `json:"serverId"` ExitServerID *string `json:"exitServerId"` Protocol string `json:"protocol"` Topology string `json:"topology"` ExitProtocol *string `json:"exitProtocol"` // ExitMode selects the forward exit target: "" / "managed" is a panel // server (ExitServerID); "manual" is an unmanaged landing endpoint // described by ExitAddress + ExitPort + ExitProtocol + ExitCredentials // (round-2 item 8). Manual exits have no exit resource and are always // ExitReady — the panel only builds the entry-side outbound. ExitMode string `json:"exitMode,omitempty"` ExitAddress string `json:"exitAddress,omitempty"` NetworkMode string `json:"networkMode"` RealityServerName string `json:"realityServerName"` RealityDestPort int `json:"realityDestPort"` PortMode string `json:"portMode"` Port int `json:"port"` ExitPortMode *string `json:"exitPortMode"` ExitPort *int `json:"exitPort"` Status string `json:"status"` EntryReady bool `json:"entryReady"` ExitReady bool `json:"exitReady"` ResourceID string `json:"resourceId"` ExitResourceID string `json:"exitResourceId"` Artifacts map[string]json.RawMessage `json:"artifacts"` Error *string `json:"error"` PendingDelete bool `json:"pendingDelete"` AutoRemoving *bool `json:"autoRemoving,omitempty"` WSPath *string `json:"wsPath,omitempty"` SSServerKey *string `json:"ssServerKey,omitempty"` // SocksUser/SocksPassword are the node-level fixed account of a // socks5-auth inbound (round-3 item 21, the 3x-ui inbound model): // the inbound's only user, carried by every subscriber's link for // this node. Empty on legacy records until the store-load migration // (MigrateSocksNodeCredentials) mints a pair. SocksUser string `json:"socksUser,omitempty"` SocksPassword string `json:"socksPassword,omitempty"` // RebuildPending marks a node whose binding-class changes are being // coalesced (round-3 item 23): the flusher applies ONE rebuild with // the final state once the fixed window (from RebuildPendingAt, the // FIRST pending change) elapses. Persisted so a panel restart inside // the window cannot strand the node un-deployed. RebuildPending bool `json:"rebuildPending,omitempty"` RebuildPendingAt string `json:"rebuildPendingAt,omitempty"` RebuildPendingExit bool `json:"rebuildPendingExit,omitempty"` Reality *RealityConfig `json:"reality,omitempty"` ExitCredentials *ExitCredentials `json:"exitCredentials,omitempty"` UsageCounters *UsageCounters `json:"usageCounters"` RetiredUsageEpochs []string `json:"retiredUsageEpochs"` MeteredTraffic *MeteredTraffic `json:"meteredTraffic"` LastUsageAt *string `json:"lastUsageAt"` SortIndex int `json:"sortIndex"` SpeedUpBps *float64 `json:"speedUpBps,omitempty"` SpeedDownBps *float64 `json:"speedDownBps,omitempty"` SpeedAt *string `json:"speedAt,omitempty"` LastActiveAt *string `json:"lastActiveAt,omitempty"` LastSampleAt *string `json:"lastSampleAt,omitempty"` CreatedAt string `json:"createdAt"` UpdatedAt string `json:"updatedAt"` Extra extraFields `json:"-"` } func (n *Node) UnmarshalJSON(raw []byte) error { type plain Node var p plain var extra extraFields if err := recordJSON(raw, &p, &extra); err != nil { return err } *n = Node(p) n.Extra = extra return nil } func (n Node) MarshalJSON() ([]byte, error) { type plain Node return mergeMarshal(plain(n), n.Extra) } type Credentials struct { ClientID string `json:"clientId"` RelayPassword string `json:"relayPassword"` ShortID string `json:"shortId"` Extra extraFields `json:"-"` } func (c *Credentials) UnmarshalJSON(raw []byte) error { type plain Credentials var p plain var extra extraFields if err := recordJSON(raw, &p, &extra); err != nil { return err } *c = Credentials(p) c.Extra = extra return nil } func (c Credentials) MarshalJSON() ([]byte, error) { type plain Credentials return mergeMarshal(plain(c), c.Extra) } type NodeClient struct { ID string `json:"id"` NodeID string `json:"nodeId"` CustomerID string `json:"customerId"` // Deprecated: credentials live on the Customer (round-2 item 2). This // field is only a migration input — the store-load migration converges // it into Customer.Credentials and clears it, so it disappears from // disk on the next save. Builders receive resolved copies instead. Credentials *Credentials `json:"credentials,omitempty"` Status string `json:"status"` CreatedAt string `json:"createdAt"` UpdatedAt string `json:"updatedAt"` Extra extraFields `json:"-"` } func (n *NodeClient) UnmarshalJSON(raw []byte) error { type plain NodeClient var p plain var extra extraFields if err := recordJSON(raw, &p, &extra); err != nil { return err } *n = NodeClient(p) n.Extra = extra return nil } func (n NodeClient) MarshalJSON() ([]byte, error) { type plain NodeClient return mergeMarshal(plain(n), n.Extra) } // KeyPair is a Reality keypair as shipped in job payload keyMaterial. type KeyPair struct { PrivateKey string `json:"privateKey"` PublicKey string `json:"publicKey"` } // KeyEntry is a keyStore record (KeyPair + updatedAt). type KeyEntry struct { PrivateKey string `json:"privateKey"` PublicKey string `json:"publicKey"` UpdatedAt string `json:"updatedAt"` Extra extraFields `json:"-"` } func (k *KeyEntry) UnmarshalJSON(raw []byte) error { type plain KeyEntry var p plain var extra extraFields if err := recordJSON(raw, &p, &extra); err != nil { return err } *k = KeyEntry(p) k.Extra = extra return nil } func (k KeyEntry) MarshalJSON() ([]byte, error) { type plain KeyEntry return mergeMarshal(plain(k), k.Extra) } type JobPayload struct { Resource json.RawMessage `json:"resource,omitempty"` Kind string `json:"kind,omitempty"` KeyMaterial map[string]KeyPair `json:"keyMaterial,omitempty"` ResourceID string `json:"resourceId,omitempty"` Extra extraFields `json:"-"` } func (p *JobPayload) UnmarshalJSON(raw []byte) error { type plain JobPayload var q plain var extra extraFields if err := recordJSON(raw, &q, &extra); err != nil { return err } *p = JobPayload(q) p.Extra = extra return nil } func (p JobPayload) MarshalJSON() ([]byte, error) { type plain JobPayload return mergeMarshal(plain(p), p.Extra) } // KindOf mirrors JS `payload.kind || payload.resource?.meta?.kind`. func (p *JobPayload) KindOf() string { if p == nil { return "" } if p.Kind != "" { return p.Kind } if len(p.Resource) > 0 { var probe struct { Meta struct { Kind string `json:"kind"` } `json:"meta"` } if err := json.Unmarshal(p.Resource, &probe); err == nil { return probe.Meta.Kind } } return "" } type Job struct { ID string `json:"id"` ServerID string `json:"serverId"` NodeID string `json:"nodeId"` Action string `json:"action"` Payload *JobPayload `json:"payload"` Status string `json:"status"` Attempts int `json:"attempts"` CreatedAt string `json:"createdAt"` UpdatedAt string `json:"updatedAt"` Error *string `json:"error"` LeaseUntil *string `json:"leaseUntil"` StartedAt *string `json:"startedAt,omitempty"` CompletedAt *string `json:"completedAt,omitempty"` Extra extraFields `json:"-"` } func (j *Job) UnmarshalJSON(raw []byte) error { type plain Job var p plain var extra extraFields if err := recordJSON(raw, &p, &extra); err != nil { return err } *j = Job(p) j.Extra = extra return nil } func (j Job) MarshalJSON() ([]byte, error) { type plain Job return mergeMarshal(plain(j), j.Extra) } type Activity struct { ID string `json:"id"` At string `json:"at"` Actor string `json:"actor"` Action string `json:"action"` Target string `json:"target"` Detail map[string]any `json:"detail"` Extra extraFields `json:"-"` } func (a *Activity) UnmarshalJSON(raw []byte) error { type plain Activity var p plain var extra extraFields if err := recordJSON(raw, &p, &extra); err != nil { return err } *a = Activity(p) a.Extra = extra return nil } func (a Activity) MarshalJSON() ([]byte, error) { type plain Activity return mergeMarshal(plain(a), a.Extra) } type Observation struct { ID string `json:"id"` CustomerID string `json:"customerId"` IP string `json:"ip"` FirstSeen string `json:"firstSeenAt"` LastSeenAt string `json:"lastSeenAt"` Extra extraFields `json:"-"` } func (o *Observation) UnmarshalJSON(raw []byte) error { type plain Observation var p plain var extra extraFields if err := recordJSON(raw, &p, &extra); err != nil { return err } *o = Observation(p) o.Extra = extra return nil } func (o Observation) MarshalJSON() ([]byte, error) { type plain Observation return mergeMarshal(plain(o), o.Extra) } type SubscriptionAccess struct { ID string `json:"id"` CustomerID string `json:"customerId"` At string `json:"at"` IP string `json:"ip"` UserAgent string `json:"userAgent"` IdentityType string `json:"identityType"` ClientKey string `json:"clientKey"` Format string `json:"format"` Status int `json:"status"` Reason string `json:"reason"` Bytes int64 `json:"bytes"` Extra extraFields `json:"-"` } func (a *SubscriptionAccess) UnmarshalJSON(raw []byte) error { type plain SubscriptionAccess var p plain var extra extraFields if err := recordJSON(raw, &p, &extra); err != nil { return err } *a = SubscriptionAccess(p) a.Extra = extra return nil } func (a SubscriptionAccess) MarshalJSON() ([]byte, error) { type plain SubscriptionAccess return mergeMarshal(plain(a), a.Extra) } type SubscriptionClient struct { CustomerID string `json:"customerId"` ClientKey string `json:"clientKey"` IdentityType string `json:"identityType"` FirstSeenAt string `json:"firstSeenAt"` LastSeenAt string `json:"lastSeenAt"` Extra extraFields `json:"-"` } func (c *SubscriptionClient) UnmarshalJSON(raw []byte) error { type plain SubscriptionClient var p plain var extra extraFields if err := recordJSON(raw, &p, &extra); err != nil { return err } *c = SubscriptionClient(p) c.Extra = extra return nil } func (c SubscriptionClient) MarshalJSON() ([]byte, error) { type plain SubscriptionClient return mergeMarshal(plain(c), c.Extra) }