Files
jarl/internal/api/types.go
T
adamandClaude Opus 5.5 4cc2ab2287 Zone page: links to the same zone in the other views
The header says "also in VIEW" for every zone of the same name in
another view, linked or not, in the view's colour, keeping the open tab
where the other zone has it. The zone API gains in_views.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 15:45:41 +01:00

646 lines
23 KiB
Go

// Package api defines the JSON types of Jarl's HTTP API, shared by the
// server and the Go client used by the CLI.
package api
import (
"encoding/json"
"time"
"jarl/internal/dnssec"
"jarl/internal/model"
"jarl/internal/nscheck"
"jarl/internal/porkbun"
"jarl/internal/version"
)
// Version is the API path prefix.
const Prefix = "/api/v1"
// Zone is a zone as returned by the API.
type Zone struct {
model.Zone
Display string `json:"display_name"`
View string `json:"view,omitempty"`
Records int `json:"record_count"`
// DisabledRecords is how many of the records are disabled.
DisabledRecords int `json:"disabled_count,omitempty"`
ZoneFile string `json:"zone_file,omitempty"` // the file named loads
RName string `json:"rname_email,omitempty"`
// Registrar names the registrar Jarl can manage the domain at
// ("porkbun"), when one is configured for it.
Registrar string `json:"registrar,omitempty"`
// LinkedWith are the views of the zones this one is linked with
// (docs/views.md#linked-zones).
LinkedWith []string `json:"linked_with,omitempty"`
// Links are the zones this one is linked with, with their views.
Links []ZoneLink `json:"links,omitempty"`
// InViews are the zones of this name in the other views (any type),
// in view order: for going straight to one.
InViews []ZoneLink `json:"in_views,omitempty"`
// OtherViews are the other views with a primary zone of this name
// (linked or not): what GET /zones/{zone}/compare compares.
OtherViews []string `json:"other_views,omitempty"`
// Prefix is the network a reverse zone answers for; PTRMissing how
// many of its view's A and AAAA addresses in it have no PTR record
// (POST /zones/{zone}/fill-ptr adds them).
Prefix string `json:"prefix,omitempty"`
PTRMissing int `json:"ptr_missing,omitempty"`
// Pending is set when listing with a changeset: add, change or delete.
Pending string `json:"pending,omitempty"`
}
// Record is a record as returned by the API.
type Record struct {
model.Record
Zone string `json:"zone"`
View string `json:"view,omitempty"`
RelativeName string `json:"relative_name"`
EffectiveTTL uint32 `json:"effective_ttl"`
// Linked is "shared" or "local" for a record of a linked zone ("" otherwise).
Linked string `json:"linked,omitempty"`
// Text is the decoded value of TXT and SPF records.
Text *string `json:"text,omitempty"`
// Pending is set when listing with a changeset: add, change, delete,
// disable or enable.
Pending string `json:"pending,omitempty"`
}
// View is a view with its zone count.
type View struct {
model.View
Zones int `json:"zone_count"`
Color string `json:"color,omitempty"` // web UI colour, see model.ViewColors
}
// ViewColor sets the colour the web UI shows a view in.
type ViewColor struct {
Color string `json:"color"`
}
// ACL is an ACL with the objects that use it.
type ACL struct {
model.ACL
UsedBy []string `json:"used_by"`
}
// TSIGKey is a key; Secret is only filled when asked for and allowed.
type TSIGKey struct {
model.TSIGKey
UsedBy []string `json:"used_by"`
}
// Option is an option with display helpers.
type Option struct {
model.Option
Where string `json:"where"` // e.g. "view internal"
Description string `json:"description,omitempty"`
Status string `json:"status,omitempty"`
// Formatted is the value laid out over several lines, as in
// named.conf, when it has a block (for display and editing; Value is
// the same on one line).
Formatted string `json:"formatted,omitempty"`
}
// User is a user with formatted grants.
type User struct {
model.User
GrantStrings []string `json:"grant_strings"`
// TOTPRequired says that totp.require in jarl.yaml makes two-factor
// login compulsory for the user.
TOTPRequired bool `json:"totp_required,omitempty"`
}
// Token is an API token. Secret is only set when the token is created.
type Token struct {
model.Token
GrantStrings []string `json:"grant_strings,omitempty"`
Secret string `json:"token,omitempty"`
}
// Whoami describes the authenticated principal.
type Whoami struct {
User User `json:"user"`
Method string `json:"method"`
Token *Token `json:"token,omitempty"`
CSRFToken string `json:"csrf_token,omitempty"`
Permissions []string `json:"permissions"`
Admin bool `json:"admin"`
Version string `json:"version"`
// TOTP is the user's two-factor login (sessions only).
TOTP *TOTPStatus `json:"totp,omitempty"`
// TOTPSetupRequired: the user must set up two-factor login before
// doing anything else (totp.require).
TOTPSetupRequired bool `json:"totp_setup_required,omitempty"`
// Build is what the server was built from (release or commit).
Build version.Build `json:"build"`
}
// LoginChallenge answers a correct password of a user with two-factor
// login: send a code with the challenge to POST /auth/login/totp.
type LoginChallenge struct {
TOTPRequired bool `json:"totp_required"`
Challenge string `json:"challenge"`
}
// LoginTOTP is the second login step.
type LoginTOTP struct {
Challenge string `json:"challenge"`
// Code is the current code from the authenticator app, or a recovery
// code.
Code string `json:"code"`
}
// TOTPStatus is a user's two-factor login (GET /me/totp).
type TOTPStatus struct {
Enabled bool `json:"enabled"`
EnabledAt *time.Time `json:"enabled_at,omitempty"`
Required bool `json:"required"`
RecoveryCodesLeft int `json:"recovery_codes_left"`
// Policy is totp.require: none, admins or all.
Policy string `json:"policy"`
}
// TOTPSetupRequest starts setting up two-factor login.
type TOTPSetupRequest struct {
Password string `json:"password"`
}
// TOTPSetup is a new secret to put into an authenticator app: QR is the
// QR code of URI, as an image data: URI.
type TOTPSetup struct {
Secret string `json:"secret"`
URI string `json:"uri"`
QR string `json:"qr"`
Issuer string `json:"issuer"`
Account string `json:"account"`
}
// TOTPCode confirms a setup.
type TOTPCode struct {
Code string `json:"code"`
}
// TOTPDisable turns two-factor login off.
type TOTPDisable struct {
Password string `json:"password"`
Code string `json:"code"`
}
// RecoveryCodes are new recovery codes, shown once.
type RecoveryCodes struct {
Codes []string `json:"recovery_codes"`
}
// LoginRequest logs in.
type LoginRequest struct {
Username string `json:"username"`
Password string `json:"password"`
}
// ErrorBody is the error response.
type ErrorBody struct {
Error ErrorInfo `json:"error"`
}
// ErrorInfo describes an error.
type ErrorInfo struct {
Code string `json:"code"`
Message string `json:"message"`
Op int `json:"op,omitempty"`
Problems json.RawMessage `json:"problems,omitempty"`
Check json.RawMessage `json:"check,omitempty"`
Result json.RawMessage `json:"result,omitempty"`
}
// ChangeRequest is a request in a batch (POST /apply or changeset ops).
type ChangeRequest struct {
Op string `json:"op"`
ID int64 `json:"id,omitempty"`
BaseVersion int `json:"base_version,omitempty"`
Data json.RawMessage `json:"data,omitempty"`
}
// BatchRequest applies (or stages) several requests at once.
type BatchRequest struct {
Description string `json:"description,omitempty"`
Ops []ChangeRequest `json:"ops"`
DryRun bool `json:"dry_run,omitempty"`
Force bool `json:"force,omitempty"`
// AcceptRisk confirms a change that opens the server up
// (docs/exposure.md).
AcceptRisk bool `json:"accept_risk,omitempty"`
// Apply (for POST /changesets) applies the new changeset at once.
Apply bool `json:"apply,omitempty"`
}
// MutationResponse is returned by endpoints that change the configuration.
type MutationResponse struct {
Applied bool `json:"applied"`
Staged bool `json:"staged,omitempty"`
DryRun bool `json:"dry_run,omitempty"`
ChangesetID int64 `json:"changeset_id,omitempty"`
Result json.RawMessage `json:"result,omitempty"`
Changeset json.RawMessage `json:"changeset,omitempty"`
Added []model.Op `json:"added,omitempty"`
Warnings []string `json:"warnings,omitempty"`
Problems json.RawMessage `json:"problems,omitempty"`
Object json.RawMessage `json:"object,omitempty"`
}
// Changeset is a changeset as returned by the API.
type Changeset struct {
model.Changeset
Undoable bool `json:"undoable"`
}
// StatusResponse is GET /status.
type StatusResponse struct {
Version string `json:"version"`
Build version.Build `json:"build"`
Engine json.RawMessage `json:"engine"`
Time time.Time `json:"time"`
// ExpiringTokens are tokens that expire within TokenExpiryWarning or
// expired less than that long ago (not revoked): every user's for
// administrators, otherwise the caller's own.
ExpiringTokens []Token `json:"expiring_tokens"`
// Backup is the state of database backups (administrators only).
Backup *BackupStatus `json:"backup,omitempty"`
// TOTP is about two-factor login across users (administrators only).
TOTP *TOTPOverview `json:"totp,omitempty"`
// BlocklistErrors are blocklists whose last update failed (for those
// who may see blocking).
BlocklistErrors []BlocklistError `json:"blocklist_errors,omitempty"`
// BlockingPausedUntil is set while blocking is paused for a time,
// BlockingPausedIndefinitely while it is paused until resumed.
BlockingPausedUntil *time.Time `json:"blocking_paused_until,omitempty"`
BlockingPausedIndefinitely bool `json:"blocking_paused_indefinitely,omitempty"`
// Certificates sums up the TLS certificates Jarl manages
// (administrators only, when there are any).
Certificates *CertSummary `json:"certificates,omitempty"`
// EncryptedDNS is the last check of DNS over TLS and HTTPS, when it
// is set up (for those who may see options).
EncryptedDNS *EncDNSSummary `json:"encrypted_dns,omitempty"`
// Storage is the size of Jarl's databases and how full the disks Jarl
// uses are (administrators only).
Storage *Storage `json:"storage,omitempty"`
}
// Storage is the size of Jarl's databases and the disks Jarl uses.
type Storage struct {
Databases []Database `json:"databases"`
Total int64 `json:"total"` // the databases' bytes
Disks []Disk `json:"disks"`
}
// Database is one of Jarl's databases: its file with its -wal and -shm.
type Database struct {
Label string `json:"label"`
Path string `json:"path"`
Bytes int64 `json:"bytes"`
// Rows is how many answers the query log holds (statistics only),
// and QueryLogRetention how long it keeps them ("7d").
QueryLogRows *int64 `json:"query_log_rows,omitempty"`
QueryLogRetention string `json:"query_log_retention,omitempty"`
}
// Disk is a file system Jarl uses: what of Jarl's it holds, its size,
// what is free, and how much of it the databases take.
type Disk struct {
Path string `json:"path"`
Holds []string `json:"holds"`
Total int64 `json:"total"`
Free int64 `json:"free"`
Databases int64 `json:"databases"`
}
// CertSummary is what the dashboard reports about certificates.
type CertSummary struct {
Total int `json:"total"`
// Problems are the certificates that aren't fine: failing, expiring,
// expired (not ones still being obtained for the first time).
Problems []CertProblem `json:"problems"`
// Next is the certificate that expires first, and when.
Next string `json:"next,omitempty"`
NextAfter *time.Time `json:"next_after,omitempty"`
// Serving is the certificate jarld serves HTTPS with ("" for none or tls.cert_file).
Serving string `json:"serving,omitempty"`
}
// CertProblem is a certificate that needs attention.
type CertProblem struct {
Name string `json:"name"`
State string `json:"state"`
Message string `json:"message"`
}
// BlocklistError is a blocklist whose last update failed. UpdatedAt is
// the version named keeps using (nil: none yet).
type BlocklistError struct {
Name string `json:"name"`
Error string `json:"error"`
UpdatedAt *time.Time `json:"updated_at,omitempty"`
}
// TOTPOverview is what the dashboard reports about two-factor login.
type TOTPOverview struct {
// Policy is totp.require: none, admins or all.
Policy string `json:"policy"`
// Missing are enabled users who must use two-factor login and
// haven't set it up (they can do nothing else until they do).
Missing []string `json:"missing"`
// SharedAdmin is set when an enabled administrator called "admin"
// (the usual first account) exists next to other enabled
// administrators: it is probably shared, and worth disabling.
SharedAdmin string `json:"shared_admin,omitempty"`
}
// TokenExpiresHeader is sent with responses to a token that expires
// soon (model.TokenExpiryWarning), holding its expiry time (RFC 3339). The
// CLI warns its user when it sees it.
const TokenExpiresHeader = "Jarl-Token-Expires"
// BackupStatus describes the newest database backup, as recorded by
// 'jarld backup'.
type BackupStatus struct {
LastAt *time.Time `json:"last_at,omitempty"`
LastFile string `json:"last_file,omitempty"`
LastSize int64 `json:"last_size,omitempty"`
LastError string `json:"last_error,omitempty"`
// LastErrorAt is when the newest failed backup ran (only reported
// when it is newer than the last success).
LastErrorAt *time.Time `json:"last_error_at,omitempty"`
Dir string `json:"dir"`
// WarnAfter (seconds) is how old the newest backup may get before
// Stale is set (0: never).
WarnAfter int64 `json:"warn_after"`
Stale bool `json:"stale"`
}
// SearchResult is one search hit.
type SearchResult struct {
Kind string `json:"kind"` // zone, record, acl, view, key
ID int64 `json:"id"`
ZoneID int64 `json:"zone_id,omitempty"`
Label string `json:"label"`
Detail string `json:"detail,omitempty"`
}
// PasswordChange changes a password.
type PasswordChange struct {
Current string `json:"current_password,omitempty"`
Password string `json:"password"`
}
// UserInput creates or updates a user.
type UserInput struct {
Username *string `json:"username,omitempty"`
Password *string `json:"password,omitempty"`
Admin *bool `json:"admin,omitempty"`
Grants *[]model.Grant `json:"grants,omitempty"`
// GrantStrings are grants in compact form ("records:write zones=x").
GrantStrings *[]string `json:"grant_strings,omitempty"`
Role *string `json:"role,omitempty"` // a role template name
Disabled *bool `json:"disabled,omitempty"`
Comment *string `json:"comment,omitempty"`
}
// TokenInput creates a token.
type TokenInput struct {
Name string `json:"name"`
Grants *[]model.Grant `json:"grants,omitempty"`
GrantStrings *[]string `json:"grant_strings,omitempty"`
Role string `json:"role,omitempty"`
ExpiresIn string `json:"expires_in,omitempty"` // e.g. "90d", "720h"
ExpiresAt *time.Time `json:"expires_at,omitempty"`
}
// TokenRotation gives a token a new secret (POST /tokens/{id}/rotate).
// Every field is optional.
type TokenRotation struct {
// Grace keeps the old secret working this long (e.g. "1h", "2d"; at
// most 7d), so clients can be updated without an outage. Default:
// the old secret stops at once.
Grace string `json:"grace,omitempty"`
// The new expiry. Default: a token that expires gets its lifetime
// again, counted from now; one that doesn't, still doesn't.
ExpiresIn string `json:"expires_in,omitempty"`
ExpiresAt *time.Time `json:"expires_at,omitempty"`
NoExpiry bool `json:"no_expiry,omitempty"`
}
// ZoneImport imports zone file text into a zone.
type ZoneImport struct {
Text string `json:"text"`
// Mode "merge" adds missing records; "replace" makes the zone equal
// to the file (deleting other records).
Mode string `json:"mode"`
DryRun bool `json:"dry_run,omitempty"`
// UseSOA also takes the SOA timers and default TTL from the file.
UseSOA bool `json:"use_soa,omitempty"`
}
// SerialsResponse is GET /zones/{zone}/serials.
type SerialsResponse struct {
Zone string `json:"zone"`
View string `json:"view,omitempty"`
Type string `json:"type"`
// Serial is what this server serves: named's loaded serial, or for a
// primary zone Jarl's when named can't be asked (SerialSource says
// which).
Serial *uint32 `json:"serial"`
SerialSource string `json:"serial_source,omitempty"`
Servers []nscheck.Result `json:"servers"`
Notes []string `json:"notes"`
CheckedAt time.Time `json:"checked_at"`
}
// DelegationResponse is GET /zones/{zone}/delegation.
type DelegationResponse struct {
*nscheck.DelegationReport
View string `json:"view,omitempty"`
Notes []string `json:"notes"`
CheckedAt time.Time `json:"checked_at"`
}
// DNSSECResponse is GET /zones/{zone}/dnssec.
type DNSSECResponse struct {
Zone string `json:"zone"`
View string `json:"view,omitempty"`
Type string `json:"type"`
*dnssec.Status
// DSAt says where the zone's DS record goes: "parent_zone" (a primary
// zone in Jarl, below), "registrar" (the zone is a registered domain),
// "parent" (a subdomain whose parent zone is hosted elsewhere),
// "reverse" (whoever delegates the addresses) or "none" (nothing on
// the internet delegates the zone, so it can't have one).
DSAt string `json:"ds_at"`
// DSPlace completes "add the DS record …" ("at your registrar"); ""
// for "none".
DSPlace string `json:"ds_place,omitempty"`
// DSNote explains DSAt "none".
DSNote string `json:"ds_note,omitempty"`
// ParentZone is set when the parent zone is a primary zone in Jarl
// (in the zone's own view, else preferably a public one): the DS
// record then goes there, as a record.
ParentZone string `json:"parent_zone,omitempty"`
ParentZoneID int64 `json:"parent_zone_id,omitempty"`
ParentView string `json:"parent_view,omitempty"`
// ParentDS is the DS records the parent zone in Jarl has for the
// zone (record data, e.g. "51126 13 2 FFBE...").
ParentDS []string `json:"parent_ds,omitempty"`
// Registrar names the registrar Jarl can publish the DS at
// ("porkbun"), when one is configured in jarl.yaml.
Registrar string `json:"registrar,omitempty"`
}
// CheckDSResponse is POST /zones/{zone}/dnssec/check-ds: what the parent
// zone has, and what named was told.
type CheckDSResponse struct {
Parent *nscheck.ParentDSReport `json:"parent"`
Marked []string `json:"marked"`
Notes []string `json:"notes"`
Status *dnssec.Status `json:"status"`
}
// DNSSECWipe is POST /zones/{zone}/dnssec/wipe.
type DNSSECWipe struct {
// Force wipes although the parent zone's DS records couldn't be
// checked, or named still counts a removed DS as cached.
Force bool `json:"force,omitempty"`
DryRun bool `json:"dry_run,omitempty"`
}
// DNSSECWipeResponse is the result of a wipe.
type DNSSECWipeResponse struct {
Removed []string `json:"removed"`
DryRun bool `json:"dry_run,omitempty"`
ChangesetID int64 `json:"changeset_id,omitempty"`
Parent *nscheck.ParentDSReport `json:"parent,omitempty"`
}
// RegistrarResponse is GET /zones/{zone}/registrar: the domain's
// delegation at the registrar, next to what the zone says.
type RegistrarResponse struct {
Registrar string `json:"registrar"` // "porkbun"
Domain string `json:"domain"`
// NS are the name servers at the registry, JarlNS those in the
// zone's NS records (both without trailing dots).
NS []string `json:"ns"`
JarlNS []string `json:"jarl_ns"`
NSMatch bool `json:"ns_match"`
// Glue covers every glue record at the registry and every name server
// inside the zone that needs one.
Glue []GlueStatus `json:"glue"`
// DS are the DS records at the registry.
DS []RegistrarDS `json:"ds"`
Findings []nscheck.Finding `json:"findings"`
// Error is set when the registrar couldn't be asked; the rest is
// then empty.
Error string `json:"error,omitempty"`
}
// GlueStatus compares a glue record at the registry with the zone.
type GlueStatus struct {
Host string `json:"host"`
Registry []string `json:"registry"` // addresses at the registry (none: no glue record)
Jarl []string `json:"jarl"` // the zone's A and AAAA records for the host
// Needed: a name server in the zone's NS records that is inside the
// domain, so resolvers need its glue. InUse: in the registry's NS.
Needed bool `json:"needed"`
InUse bool `json:"in_use"`
// Action is what syncing the glue does: "create", "update" or "".
Action string `json:"action,omitempty"`
}
// RegistrarDS is a DS record at the registry.
type RegistrarDS struct {
porkbun.DS
// Matches: it points at a key named publishes for the zone.
Matches bool `json:"matches"`
}
// RegistrarChange asks for a change at the registrar.
type RegistrarChange struct {
// NS (set-ns) are the name servers to set (default: the zone's NS
// records).
NS []string `json:"ns,omitempty"`
// KeyTag (publish DS) picks the key (default: every key that needs
// one).
KeyTag uint16 `json:"key_tag,omitempty"`
// Force skips the checks that the change won't break the domain.
Force bool `json:"force,omitempty"`
DryRun bool `json:"dry_run,omitempty"`
}
// RegistrarChangeResponse is the result of a change at the registrar.
type RegistrarChangeResponse struct {
// Done lists what was changed (or would be, in a dry run).
Done []string `json:"done"`
DryRun bool `json:"dry_run,omitempty"`
Checks []nscheck.Finding `json:"checks"`
Warnings []string `json:"warnings"`
}
// Blocklist is a blocklist as returned by the API (docs/blocking.md).
type Blocklist struct {
model.Blocklist
// Views are the views it applies to; AutoViews is set when that is
// "every view that offers recursion" (view_ids is empty).
Views []string `json:"views"`
AutoViews bool `json:"auto_views,omitempty"`
// State is ok, waiting (not downloaded yet), error (the last update
// failed; the previous version stays in use), disabled, or idle (it
// applies to no view).
State string `json:"state"`
Status *model.BlocklistStatus `json:"status,omitempty"`
// MemoryMB estimates what named needs for the list, in all its views.
MemoryMB int `json:"memory_mb,omitempty"`
Pending string `json:"pending,omitempty"`
// Step is the name of the step the list starts at ("normal" or one
// above it).
Step string `json:"step"`
}
// BlockStep is a blocking step above normal as returned by the API, with
// what uses it.
type BlockStep struct {
model.BlockStep
Lists []string `json:"lists"`
Groups []string `json:"groups"`
Pending string `json:"pending,omitempty"`
}
// ClientGroup is a client group as returned by the API.
type ClientGroup struct {
model.ClientGroup
Step string `json:"step"`
Views []string `json:"views"`
AutoViews bool `json:"auto_views,omitempty"`
Pending string `json:"pending,omitempty"`
}
// BlockRule is a block or allow rule as returned by the API.
type BlockRule struct {
model.BlockRule
Views []string `json:"views"`
AutoViews bool `json:"auto_views,omitempty"`
Pending string `json:"pending,omitempty"`
}
// ZoneLink is a zone a zone is linked with.
type ZoneLink struct {
ID int64 `json:"id"`
View string `json:"view"`
// Type and Linked are set in Zone.InViews.
Type string `json:"type,omitempty"`
Linked bool `json:"linked,omitempty"`
}
// EncDNSSummary is what the dashboard says about encrypted DNS.
type EncDNSSummary struct {
Checked time.Time `json:"checked"`
OK bool `json:"ok"`
Problems []string `json:"problems"`
Warnings []string `json:"warnings"`
}