npm

package
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 1, 2026 License: MIT Imports: 19 Imported by: 0

Documentation

Overview

Package npm implements a minimal, dependency-free API client for Nginx Proxy Manager and NPMplus.

The two flavours authenticate differently:

NPM      POST /api/tokens -> {"token":"<jwt>"} -> Authorization: Bearer <jwt>
NPMplus  POST /api/tokens -> Set-Cookie: npm_token=...; HttpOnly

The client detects the mode from the login response and transparently re-authenticates when the session expires or the API answers 401.

Index

Constants

View Source
const (
	SchemeAuto = "auto"
	SchemeKeep = "$scheme"
)

Redirection schemes that keep the scheme of the request.

View Source
const (
	MetaManagedBy = "managed_by"
	MetaContainer = "managed_container"
	MetaIndex     = "managed_index"
	// MetaContainerID pins the resource to a container id as well as a name,
	// so a renamed container is still recognised as the owner.
	MetaContainerID = "managed_container_id"
	// MetaInstance names the sync instance that created the resource. Several
	// instances can drive one NPM without treating each other's hosts as
	// orphans.
	MetaInstance = "managed_instance"
	MetaPrefix   = "managed_prefix"
	// MetaDirectory is the UI group of a resource. NPMplus keeps it in the
	// meta object rather than in a column of its own, which is why grouping
	// works through the same field this tool already writes its ownership
	// markers into. Upstream nginx-proxy-manager stores the key too, but has
	// no grouping in its UI, so it is treated as an NPMplus-only setting.
	MetaDirectory  = "directory"
	ManagedByValue = "npmplus-docker-sync"
	CertificateNew = "new"
)

Meta keys written into every resource this tool owns.

View Source
const (
	AccessListPublic = "public"
	AccessListCustom = "custom"
	AccessListGlobal = "global"
)

Access list types. NPMplus replaced NPM's single `access_list_id` with a list of ids plus an explicit type; "public" means no access list at all and "global" (locations only) means "inherit from the proxy host".

View Source
const (
	LocationPrefix = ""
	LocationExact  = "= "
	LocationRegex  = "~ "
	LocationIRegex = "~* "
	LocationPrefer = "^~ "
	LocationNamed  = "@"
)

Location modifiers. NPMplus stores the nginx prefix of a custom location verbatim, including the trailing space it needs in the generated config.

View Source
const MaxDescriptionLength = 255

MaxDescriptionLength is the limit NPMplus puts on `npmplus_description`.

View Source
const MaxDirectoryLength = 255

MaxDirectoryLength is the limit NPMplus puts on a group name: it truncates anything longer when it moves the value out of the meta object into a column, so a longer name would not survive an upgrade intact.

View Source
const RedthManagedByValue = "npm-docker-sync"

RedthManagedByValue is the ownership marker of Redth/npm-docker-sync, the project whose label syntax this tool adopted.

It is deliberately *not* treated as our own: both tools can run against the same NPM instance, and silently taking over the other's hosts would have them delete each other's work. Migration is explicit (MIGRATE_FROM_REDTH).

Variables

Kinds is the canonical processing order. Proxy hosts come first because they are by far the most common resource.

View Source
var LegacyManagedByValues = []string{}

LegacyManagedByValues are ownership markers written by earlier releases of *this* tool. They are still recognised so an upgrade adopts (and re-stamps) existing resources instead of orphaning them.

Functions

func Conform

func Conform(r Resource, dialect Dialect)

Conform brings a desired resource in line with what the server can store and return: NPMplus-only settings go against upstream NPM, retired switches go against an NPMplus that no longer has them, and the resource records whether its ownership marker can be written. Call it once the dialect is known, before fingerprinting - a field the server drops would otherwise differ from the live resource on every single run.

func Diff

func Diff(current, desired Resource) string

Diff renders the configuration fields in which two resources differ, as "field: live → desired".

It exists so a dry-run says what would actually change, and so an edit made in the NPM UI is named in the log line that overwrites it - "would update" alone leaves the operator guessing.

func Downgraded

func Downgraded(r Resource, d Dialect) []string

Downgraded returns the labels whose value the server can only store in a weaker form: a proxy protocol v2 on an NPMplus release that only knows "on".

func Inert

func Inert(r Resource) []string

Inert returns the labels of the proxy host switches that do nothing for its scheme or in their combination and are therefore left out of the request (Location.cleanForwardOptions). The caller reports those a label set explicitly.

func IsManaged

func IsManaged(m Meta) bool

IsManaged reports whether the meta object carries the current ownership marker or one of its predecessors.

func IsNotFound

func IsNotFound(err error) bool

IsNotFound reports whether err is a 404 from the API.

func IsRedth

func IsRedth(m Meta) bool

IsRedth reports whether the resource was created by Redth/npm-docker-sync.

func NormalizeDomains

func NormalizeDomains(domains []string) []string

NormalizeDomains lowercases, trims and sorts domain names so two resources can be compared deterministically.

func OwnedByInstance

func OwnedByInstance(m Meta, instance string) bool

OwnedByInstance reports whether a resource of ours belongs to this sync instance. A resource without an instance stamp was written by a release that did not have them yet and is adopted once; a resource carrying another instance's id belongs to that instance and is never touched.

func Owner

func Owner(m Meta) string

Owner returns the tool named in the ownership marker, "" for a resource created by hand.

func StripNPMplus

func StripNPMplus(r Resource)

StripNPMplus removes every NPMplus-only setting from a resource, so the desired state matches what upstream nginx-proxy-manager can store and return. Call it once the flavour is known, before fingerprinting.

func Unavailable

func Unavailable(kind Kind, dialect Dialect) []string

Unavailable returns the labels of the switches this server does not offer for the kind: NPMplus retired them (websockets and HTTP/2 are always on there), upstream NPM before 2.12 has no trust_forwarded_proto. The caller reports those a label set explicitly.

func UnsupportedByNPM

func UnsupportedByNPM(r Resource) []string

UnsupportedByNPM returns the NPMplus-only fields the resource uses. Against upstream nginx-proxy-manager they are silently left out of the request, and the reconcile loop reports them once instead of failing the write.

func XFrameOptionsValue

func XFrameOptionsValue(value string) string

XFrameOptionsValue returns the spelling NPMplus expects for a lower case x-frame-options value.

Types

type APIError

type APIError struct {
	Status  int
	Method  string
	Path    string
	Message string
	// Body is the raw response body (truncated), kept for diagnostics when
	// the API returns something other than its usual error envelope.
	Body string
}

APIError is returned for non-2xx API responses.

func (*APIError) Error

func (e *APIError) Error() string

Error implements error.

type AccessList

type AccessList struct {
	ID   int    `json:"id"`
	Name string `json:"name"`
}

AccessList mirrors `/api/nginx/access-lists`. Only the identity is needed: labels reference a list by name and the sync resolves it into an id.

type AuthMode

type AuthMode int

AuthMode describes how the client authenticates against the API.

const (
	// AuthUnknown means no successful login has happened yet.
	AuthUnknown AuthMode = iota
	// AuthCookie is used by NPMplus (httpOnly session cookies).
	AuthCookie
	// AuthBearer is used by upstream NPM (JWT in the Authorization header).
	AuthBearer
)

func (AuthMode) String

func (m AuthMode) String() string

String implements fmt.Stringer.

type Certificate

type Certificate struct {
	ID          int      `json:"id"`
	Provider    string   `json:"provider"`
	NiceName    string   `json:"nice_name"`
	DomainNames []string `json:"domain_names"`
	ExpiresOn   string   `json:"expires_on"`
	// IsDeleted is NPM's soft-delete marker, serialised as 0/1.
	IsDeleted Flag `json:"is_deleted"`
}

Certificate mirrors the `/api/nginx/certificates` resource.

NPMplus stores client CAs in the same collection (provider "mtls"); they are not server certificates, which is why the provider is decoded too.

type CertificateID

type CertificateID struct {
	ID  int
	New bool
}

CertificateID models NPM's polymorphic certificate_id field: responses return a number, while create/update payloads may carry the string "new" to request a fresh Let's Encrypt certificate.

func CertificateRef

func CertificateRef(id int) CertificateID

CertificateRef returns a CertificateID pointing at an existing certificate.

func NewCertificate

func NewCertificate() CertificateID

NewCertificate returns a CertificateID requesting a new LE certificate.

func (CertificateID) IsZero

func (c CertificateID) IsZero() bool

IsZero reports whether no certificate is attached at all.

func (CertificateID) MarshalJSON

func (c CertificateID) MarshalJSON() ([]byte, error)

MarshalJSON emits either a number or the literal string "new".

func (CertificateID) String

func (c CertificateID) String() string

String implements fmt.Stringer.

func (*CertificateID) UnmarshalJSON

func (c *CertificateID) UnmarshalJSON(data []byte) error

UnmarshalJSON accepts numbers, numeric strings, "new", null and "".

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client talks to the NPM/NPMplus REST API. It is safe for concurrent use.

func New

func New(baseURL, identity, secret string, opts ...Option) (*Client, error)

New creates a client for the given base URL (e.g. http://npm:81).

func (*Client) AuthMode

func (c *Client) AuthMode() AuthMode

AuthMode reports the detected authentication mode.

func (*Client) BaseURL

func (c *Client) BaseURL() string

BaseURL returns the configured API base URL.

func (*Client) Create

func (c *Client) Create(ctx context.Context, resource Resource) (Resource, error)

Create adds a resource and returns the version NPM stored.

Only the flavour specific request payload is sent, never the response model: both APIs reject unknown properties outright.

func (*Client) CreateDeadHost

func (c *Client) CreateDeadHost(ctx context.Context, host *DeadHost) (*DeadHost, error)

CreateDeadHost creates a new 404 host.

func (*Client) CreateProxyHost

func (c *Client) CreateProxyHost(ctx context.Context, host *ProxyHost) (*ProxyHost, error)

CreateProxyHost creates a new proxy host.

func (*Client) CreateRedirectionHost

func (c *Client) CreateRedirectionHost(ctx context.Context, host *RedirectionHost) (*RedirectionHost, error)

CreateRedirectionHost creates a new redirection host.

func (*Client) CreateStream

func (c *Client) CreateStream(ctx context.Context, stream *Stream) (*Stream, error)

CreateStream creates a new stream.

func (*Client) Delete

func (c *Client) Delete(ctx context.Context, kind Kind, id int) error

Delete removes a resource of the given kind.

func (*Client) DeleteDeadHost

func (c *Client) DeleteDeadHost(ctx context.Context, id int) error

DeleteDeadHost removes a 404 host.

func (*Client) DeleteProxyHost

func (c *Client) DeleteProxyHost(ctx context.Context, id int) error

DeleteProxyHost removes a proxy host.

func (*Client) DeleteRedirectionHost

func (c *Client) DeleteRedirectionHost(ctx context.Context, id int) error

DeleteRedirectionHost removes a redirection host.

func (*Client) DeleteStream

func (c *Client) DeleteStream(ctx context.Context, id int) error

DeleteStream removes a stream.

func (*Client) DetectFlavour

func (c *Client) DetectFlavour(ctx context.Context) (Flavour, error)

DetectFlavour probes the server and remembers the result. A pinned flavour is returned unchanged without any request.

Two signals are used, strongest first:

  1. An existing proxy host: NPMplus serialises `npmplus_access_list_ids`, upstream NPM serialises `access_list_id`. This is authoritative because it is the very field the write payloads disagree about.
  2. GET /api/: both report a `version`, but in different shapes - upstream NPM an object ({major, minor, revision}), NPMplus a string ("2026-07-24-r1-a30a954-2.15.1"). Older NPMplus versions reported none at all, which stays inconclusive and lands on the default below.

When neither is conclusive the detection falls back to NPMplus, which is what this tool is named after, and says so in the log.

func (*Client) Dialect

func (c *Client) Dialect() Dialect

Dialect is what the payload builders write for: the detected flavour plus the server version.

func (*Client) Flavour

func (c *Client) Flavour() Flavour

Flavour reports the dialect in use. It is FlavourAuto until either WithFlavour pinned one or DetectFlavour resolved one.

func (*Client) List

func (c *Client) List(ctx context.Context, kind Kind) ([]Resource, error)

List returns every resource of the given kind.

func (*Client) ListAccessLists

func (c *Client) ListAccessLists(ctx context.Context) ([]AccessList, error)

ListAccessLists returns the access lists stored in NPM, so labels can name them instead of pinning an id that differs between instances.

func (*Client) ListCertificates

func (c *Client) ListCertificates(ctx context.Context) ([]Certificate, error)

ListCertificates returns the certificates stored in NPM.

func (*Client) ListDeadHosts

func (c *Client) ListDeadHosts(ctx context.Context) ([]DeadHost, error)

ListDeadHosts returns every 404 host known to NPM.

func (*Client) ListProxyHosts

func (c *Client) ListProxyHosts(ctx context.Context) ([]ProxyHost, error)

ListProxyHosts returns every proxy host known to NPM.

func (*Client) ListRedirectionHosts

func (c *Client) ListRedirectionHosts(ctx context.Context) ([]RedirectionHost, error)

ListRedirectionHosts returns every redirection host known to NPM.

func (*Client) ListStreams

func (c *Client) ListStreams(ctx context.Context) ([]Stream, error)

ListStreams returns every TCP/UDP stream known to NPM.

func (*Client) Login

func (c *Client) Login(ctx context.Context) error

Login authenticates against POST /api/tokens and detects whether the server uses cookies (NPMplus) or a bearer token (NPM).

func (*Client) Ping

func (c *Client) Ping(ctx context.Context) error

Ping performs a cheap authenticated request; used by the readiness probe.

func (*Client) Recheck

func (c *Client) Recheck(ctx context.Context) error

Recheck notices a server upgrade: when GET /api/ reports a different version than before, the dialect is probed again. It is cheap enough to run on every periodic resync.

func (*Client) ServerVersion

func (c *Client) ServerVersion() string

ServerVersion returns the version last reported by the API, "" when the server does not report one (NPMplus).

func (*Client) SetEnabled

func (c *Client) SetEnabled(ctx context.Context, kind Kind, id int, enabled bool) error

SetEnabled switches a resource on or off. Both flavours refuse `enabled` in a create/update body and expose dedicated endpoints instead, which answer 400 "Host is already enabled" when the state already matches - that is treated as success.

func (*Client) Update

func (c *Client) Update(ctx context.Context, id int, resource Resource) (Resource, error)

Update replaces an existing resource.

func (*Client) UpdateDeadHost

func (c *Client) UpdateDeadHost(ctx context.Context, id int, host *DeadHost) (*DeadHost, error)

UpdateDeadHost updates an existing 404 host.

func (*Client) UpdateOwnCredentials

func (c *Client) UpdateOwnCredentials(ctx context.Context, email, currentSecret, newSecret string) error

UpdateOwnCredentials changes the e-mail address and the password of the logged-in user.

Both projects ship a default account that has to be rotated before the API is usable; this is what the integration harness uses to do that on images which do not seed the first user from the environment.

func (*Client) UpdateProxyHost

func (c *Client) UpdateProxyHost(ctx context.Context, id int, host *ProxyHost) (*ProxyHost, error)

UpdateProxyHost updates an existing proxy host.

func (*Client) UpdateRedirectionHost

func (c *Client) UpdateRedirectionHost(ctx context.Context, id int, host *RedirectionHost) (*RedirectionHost, error)

UpdateRedirectionHost updates an existing redirection host.

func (*Client) UpdateStream

func (c *Client) UpdateStream(ctx context.Context, id int, stream *Stream) (*Stream, error)

UpdateStream updates an existing stream.

type DeadHost

type DeadHost struct {
	ID          int    `json:"id,omitempty"`
	CreatedOn   string `json:"created_on,omitempty"`
	ModifiedOn  string `json:"modified_on,omitempty"`
	OwnerUserID int    `json:"owner_user_id,omitempty"`

	DomainNames    []string      `json:"domain_names"`
	CertificateID  CertificateID `json:"certificate_id"`
	SSLForced      Flag          `json:"ssl_forced"`
	HSTSEnabled    Flag          `json:"hsts_enabled"`
	HSTSSubdomains Flag          `json:"hsts_subdomains"`
	HTTP2Support   Flag          `json:"http2_support"`
	HTTP3Support   Flag          `json:"npmplus_http3_support"`
	AdvancedConfig string        `json:"advanced_config"`
	Enabled        Flag          `json:"enabled"`
	Meta           Meta          `json:"meta"`

	// Directory is NPMplus' `npmplus_directory` column. It is moved into the
	// meta object when the resource is decoded and never kept here.
	Directory string `json:"npmplus_directory,omitempty"`
	// OwnerMarker records the ownership marker in the advanced configuration;
	// see marker.go. Not an API field.
	OwnerMarker Flag `json:"ownership_marker,omitempty"`
}

DeadHost mirrors `/api/nginx/dead-hosts`, the "404 host" in the NPM UI.

func (*DeadHost) AdoptServerState

func (h *DeadHost) AdoptServerState(current Resource)

AdoptServerState implements Resource.

func (*DeadHost) Describe

func (h *DeadHost) Describe() string

Describe implements Resource.

func (*DeadHost) Domains

func (h *DeadHost) Domains() []string

Domains returns the normalised domain names of the host.

func (*DeadHost) Fingerprint

func (h *DeadHost) Fingerprint() string

Fingerprint implements Resource.

func (*DeadHost) IsEnabled

func (h *DeadHost) IsEnabled() bool

IsEnabled implements Resource.

func (*DeadHost) Kind

func (h *DeadHost) Kind() Kind

Kind implements Resource.

func (*DeadHost) OwnerMarked

func (h *DeadHost) OwnerMarked() bool

OwnerMarked implements Resource.

func (*DeadHost) Payload

func (h *DeadHost) Payload(dialect Dialect) (any, error)

Payload implements Resource.

func (*DeadHost) ResourceCertificate

func (h *DeadHost) ResourceCertificate() CertificateID

ResourceCertificate implements Resource.

func (*DeadHost) ResourceID

func (h *DeadHost) ResourceID() int

ResourceID implements Resource.

func (*DeadHost) ResourceKey

func (h *DeadHost) ResourceKey() string

ResourceKey implements Resource.

func (*DeadHost) ResourceMeta

func (h *DeadHost) ResourceMeta() Meta

ResourceMeta implements Resource.

func (*DeadHost) SetEnabled

func (h *DeadHost) SetEnabled(enabled bool)

SetEnabled implements Resource.

func (*DeadHost) SetResourceID

func (h *DeadHost) SetResourceID(id int)

SetResourceID implements Resource.

func (*DeadHost) UnmarshalJSON

func (h *DeadHost) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes the resource and lifts the ownership marker and the group column into the meta object.

type Dialect

type Dialect struct {
	Flavour Flavour
	// Version is upstream NPM's release; NPMplus reports none we can compare.
	Version Version
	// Schema is what the server says it accepts (GET /api/schema). nil when
	// it serves none, in which case Accepts falls back to built-in knowledge.
	Schema WriteSchema
	// MetaRetired records an NPMplus release that moved the meta object into
	// columns. Only consulted without a schema.
	MetaRetired bool
}

Dialect is everything a request body has to be written for. Both servers validate with `additionalProperties: false` and have gained and lost fields over their releases: a field that NPM 2.12 expects makes 2.11 reject the whole payload, and a `meta` object that every NPMplus release accepted makes the ones after its meta-to-columns migration reject it.

func (Dialect) Accepts

func (d Dialect) Accepts(kind Kind, field string) bool

Accepts reports whether a create/update body of the kind may carry the field. The schema read from the server decides when there is one; without it the answer comes from what is known about the flavour and its releases.

func (Dialect) AcceptsValue

func (d Dialect) AcceptsValue(kind Kind, field, value string) bool

AcceptsValue reports whether the server allows the value for a field that is restricted to a list of values.

func (Dialect) CarriesMarker

func (d Dialect) CarriesMarker(kind Kind) bool

CarriesMarker reports whether resources of the kind can hold the ownership marker on this server. Every kind can, except upstream NPM's streams, which have no advanced configuration; those keep their owner in the meta object.

func (Dialect) KeepsOwnership

func (d Dialect) KeepsOwnership(kind Kind) bool

KeepsOwnership reports whether the server can store who owns a resource of the kind at all. Without it nothing could ever be recognised as ours again.

func (Dialect) SupportsStreamCertificate

func (d Dialect) SupportsStreamCertificate() bool

SupportsStreamCertificate reports whether streams accept `certificate_id`. Upstream NPM added stream certificates in 2.12; NPMplus has always had them.

func (Dialect) SupportsTrustForwardedProto

func (d Dialect) SupportsTrustForwardedProto() bool

SupportsTrustForwardedProto reports whether proxy hosts accept `trust_forwarded_proto`. Upstream NPM added it in 2.12; NPMplus retired it together with the meta object.

func (Dialect) TypeOf

func (d Dialect) TypeOf(kind Kind, field string) string

TypeOf reports the JSON type the server expects for a field, "" when it is unknown or not a single type.

type Flag

type Flag bool

Flag models NPM's "enabled" field which is a number in some versions and a boolean in others.

func (Flag) MarshalJSON

func (f Flag) MarshalJSON() ([]byte, error)

MarshalJSON always writes a boolean, which every NPM version accepts.

func (*Flag) UnmarshalJSON

func (f *Flag) UnmarshalJSON(data []byte) error

UnmarshalJSON accepts true/false, 1/0 and "1"/"0".

type Flavour

type Flavour string

Flavour is the API dialect of the server this client talks to.

NPM and NPMplus share the same endpoints but validate request bodies against JSON schemas with `additionalProperties: false`, and those schemas have drifted apart: NPMplus dropped `enabled` and `access_list_id` from every write payload, replaced the latter with `npmplus_access_list_ids` plus `npmplus_access_list_type`, and types the stream ports as strings. Sending the wrong dialect fails with

400 data must NOT have additional properties

so the dialect has to be known before the first write.

const (
	FlavourAuto    Flavour = "auto"
	FlavourNPMplus Flavour = "npmplus"
	FlavourNPM     Flavour = "npm"
)

The supported dialects. FlavourAuto asks the client to probe the server.

func ParseFlavour

func ParseFlavour(raw string) (Flavour, error)

ParseFlavour resolves the spellings accepted in configuration.

func (Flavour) String

func (f Flavour) String() string

String implements fmt.Stringer.

func (Flavour) Valid

func (f Flavour) Valid() bool

Valid reports whether f is one of the known values.

type Kind

type Kind string

Kind enumerates the NPM resource types this tool manages.

const (
	KindProxy    Kind = "proxy"
	KindRedirect Kind = "redirect"
	KindStream   Kind = "stream"
	KindDead     Kind = "dead"
)

The four routing resources of Nginx Proxy Manager / NPMplus.

func (Kind) Label

func (k Kind) Label() string

Label returns a human readable name used in logs and errors.

func (Kind) Path

func (k Kind) Path() string

Path returns the API collection path of the kind.

func (Kind) String

func (k Kind) String() string

String implements fmt.Stringer.

func (Kind) Valid

func (k Kind) Valid() bool

Valid reports whether the kind is one of the supported resources.

type Location

type Location struct {
	Path           string `json:"path"`
	LocationType   string `json:"location_type"`
	AdvancedConfig string `json:"advanced_config"`
	LocationConfig string `json:"npmplus_location_config,omitempty"`
	ForwardScheme  string `json:"forward_scheme"`
	ForwardHost    string `json:"forward_host"`
	ForwardPort    int    `json:"forward_port"`

	AccessListIDs  []int  `json:"npmplus_access_list_ids,omitempty"`
	AccessListType string `json:"npmplus_access_list_type,omitempty"`
	// AccessListNames are names that still have to be resolved into ids. They
	// never reach the API.
	AccessListNames []string `json:"-"`
	// AccessLists is the expanded relation; see ProxyHost.AccessLists.
	AccessLists []accessListRef `json:"access_lists,omitempty"`

	// Enabled is nil when the server did not report it; a location without
	// the flag is enabled.
	Enabled                  *bool  `json:"npmplus_enabled,omitempty"`
	NoIndex                  Flag   `json:"npmplus_noindex"`
	DisableCrowdsecAppsec    Flag   `json:"npmplus_crowdsec_appsec"`
	DisableRequestBuffering  Flag   `json:"npmplus_proxy_request_buffering"`
	DisableResponseBuffering Flag   `json:"npmplus_proxy_response_buffering"`
	UpstreamCompression      Flag   `json:"npmplus_upstream_compression"`
	FancyIndex               Flag   `json:"npmplus_fancyindex"`
	XFrameOptions            string `json:"npmplus_x_frame_options,omitempty"`
	AuthRequest              string `json:"npmplus_auth_request,omitempty"`
	AuthRequestUpstream      string `json:"npmplus_auth_request_upstream,omitempty"`
}

Location is an optional custom location block of a proxy host.

The npmplus_* fields are unknown to upstream NPM; the per-flavour payload structs decide which of them are sent. Three of them are inverted in the API - `npmplus_crowdsec_appsec: true` *disables* the AppSec component - so the model spells them out as Disable* and the label parser negates them.

func (Location) IsEnabled

func (l Location) IsEnabled() bool

IsEnabled reports whether the location is active.

type Meta

type Meta map[string]any

Meta is the free-form meta object attached to every NPM resource. NPM keeps its Let's Encrypt settings here; we add our own ownership markers.

func AdoptMeta

func AdoptMeta(desired, live Meta) Meta

AdoptMeta carries the foreign bookkeeping of a resource we are taking over into the meta we are about to write, so a migration is not a data loss.

func (Meta) ClearDirectory

func (m Meta) ClearDirectory()

ClearDirectory removes the group from the meta object, leaving the resource as if it had never been grouped by this tool.

func (Meta) Container

func (m Meta) Container() string

Container returns the container name recorded in the marker, if any.

func (Meta) ContainerID

func (m Meta) ContainerID() string

ContainerID returns the container id recorded in the marker, if any.

func (Meta) Directory

func (m Meta) Directory() string

Directory returns the UI group of the resource, "" for an ungrouped one. NPMplus trims the stored value before it groups by it, so this does too.

func (Meta) Instance

func (m Meta) Instance() string

Instance returns the sync instance that created the resource. An empty string means the resource predates instance stamping.

func (Meta) ManagedBy

func (m Meta) ManagedBy(value string) bool

ManagedBy reports whether the resource carries our ownership marker.

func (Meta) Prefix

func (m Meta) Prefix() string

Prefix returns the label namespace the resource was created from.

func (Meta) SetDirectory

func (m Meta) SetDirectory(group string)

SetDirectory records the UI group. An empty name is kept as an empty value rather than dropped: "explicitly ungrouped" has to be distinguishable from "no group configured", which is what makes clearing one possible at all.

type Option

type Option func(*Client)

Option customises the client.

func WithClock

func WithClock(now func() time.Time) Option

WithClock injects a time source (tests).

func WithFlavour

func WithFlavour(f Flavour) Option

WithFlavour pins the dialect instead of probing for it. Useful when the API sits behind a proxy that rewrites responses, or to keep a known-good configuration stable across upgrades.

func WithHTTPClient

func WithHTTPClient(hc *http.Client) Option

WithHTTPClient replaces the underlying HTTP client. The client must have a cookie jar for NPMplus support; New installs one if it is missing.

func WithInsecureSkipVerify

func WithInsecureSkipVerify(skip bool) Option

WithInsecureSkipVerify disables TLS verification. Only use it for NPM instances behind a self-signed certificate on a trusted network.

func WithLogger

func WithLogger(l *slog.Logger) Option

WithLogger sets the structured logger.

func WithPayloadLogging

func WithPayloadLogging(enabled bool) Option

WithPayloadLogging mirrors complete request bodies into the debug log. Values of credential-looking meta keys are redacted, but the bodies still contain host names and advanced nginx config, so this stays opt-in.

func WithTimeout

func WithTimeout(d time.Duration) Option

WithTimeout sets the per-request timeout.

func WithUserAgent

func WithUserAgent(ua string) Option

WithUserAgent overrides the User-Agent header.

type Port

type Port struct {
	// contains filtered or unexported fields
}

Port models the polymorphic port fields of the stream resource. Upstream NPM types `incoming_port` and `forwarding_port` as integers, NPMplus types them as strings and additionally accepts a range ("8080-8090") for the incoming port and "$server_port" for the forwarded one.

The raw text is preserved so a foreign stream that uses one of the NPMplus forms still round-trips and keeps a stable identity.

func PortOf

func PortOf(n int) Port

PortOf returns a Port for a plain port number. The zero value means unset.

func PortText

func PortText(s string) Port

PortText returns a Port for one of the textual forms NPMplus allows.

func (Port) Int

func (p Port) Int() int

Int returns the numeric value, or 0 for an unset or non-numeric port such as a range or "$server_port".

func (Port) IsZero

func (p Port) IsZero() bool

IsZero reports whether no port is set.

func (Port) MarshalJSON

func (p Port) MarshalJSON() ([]byte, error)

MarshalJSON emits a number for numeric ports and a string otherwise, which matches what both flavours return. Request bodies do not use this: the per-flavour payload structs pick the type the server's schema demands.

func (Port) String

func (p Port) String() string

String returns the port in its textual form ("" when unset).

func (*Port) UnmarshalJSON

func (p *Port) UnmarshalJSON(data []byte) error

UnmarshalJSON accepts both the integer (NPM) and the string (NPMplus) form.

type Property

type Property struct {
	// Type is the JSON type, "" when the schema allows several.
	Type string
	// Enum lists the allowed values, nil when any value of the type is.
	// Strings are kept as they are, other values as their JSON text.
	Enum []string
}

Property is what a schema says about one request property.

type ProxyHost

type ProxyHost struct {
	ID          int    `json:"id,omitempty"`
	CreatedOn   string `json:"created_on,omitempty"`
	ModifiedOn  string `json:"modified_on,omitempty"`
	OwnerUserID int    `json:"owner_user_id,omitempty"`

	DomainNames   []string      `json:"domain_names"`
	ForwardHost   string        `json:"forward_host"`
	ForwardPort   int           `json:"forward_port"`
	ForwardScheme string        `json:"forward_scheme"`
	CertificateID CertificateID `json:"certificate_id"`
	// Every boolean the API hands back is read through Flag: NPM up to 2.11
	// returns the switches as the SQLite integers they are stored as
	// (`"ssl_forced": 1`), while 2.12 and NPMplus return real booleans.
	SSLForced             Flag       `json:"ssl_forced"`
	HSTSEnabled           Flag       `json:"hsts_enabled"`
	HSTSSubdomains        Flag       `json:"hsts_subdomains"`
	TrustForwardedProto   Flag       `json:"trust_forwarded_proto"`
	HTTP2Support          Flag       `json:"http2_support"`
	BlockExploits         Flag       `json:"block_exploits"`
	CachingEnabled        Flag       `json:"caching_enabled"`
	AllowWebsocketUpgrade Flag       `json:"allow_websocket_upgrade"`
	AdvancedConfig        string     `json:"advanced_config"`
	Enabled               Flag       `json:"enabled"`
	Locations             []Location `json:"locations"`
	Meta                  Meta       `json:"meta"`

	// Directory is NPMplus' `npmplus_directory` column. It is moved into the
	// meta object when the resource is decoded and never kept here.
	Directory string `json:"npmplus_directory,omitempty"`
	// OwnerMarker records the ownership marker in the advanced configuration;
	// see marker.go. Not an API field.
	OwnerMarker Flag `json:"ownership_marker,omitempty"`

	// HTTP3Support is an NPMplus extension (`npmplus_http3_support`).
	HTTP3Support Flag `json:"npmplus_http3_support"`

	// NPMplus extensions. The three Disable* switches are inverted in the
	// API (`npmplus_crowdsec_appsec: true` turns the AppSec component off),
	// which is why the model spells them out that way.
	NoIndex                  Flag   `json:"npmplus_noindex"`
	DisableCrowdsecAppsec    Flag   `json:"npmplus_crowdsec_appsec"`
	DisableRequestBuffering  Flag   `json:"npmplus_proxy_request_buffering"`
	DisableResponseBuffering Flag   `json:"npmplus_proxy_response_buffering"`
	UpstreamCompression      Flag   `json:"npmplus_upstream_compression"`
	FancyIndex               Flag   `json:"npmplus_fancyindex"`
	XFrameOptions            string `json:"npmplus_x_frame_options"`
	AuthRequest              string `json:"npmplus_auth_request"`
	AuthRequestUpstream      string `json:"npmplus_auth_request_upstream"`
	LocationConfig           string `json:"npmplus_location_config"`

	// Access lists: upstream NPM has a single `access_list_id`, NPMplus a
	// list plus an explicit type. Both spellings are decoded so a host can be
	// compared no matter which server returned it; accessLists() normalises.
	AccessListID   int    `json:"access_list_id"`
	AccessListIDs  []int  `json:"npmplus_access_list_ids"`
	AccessListType string `json:"npmplus_access_list_type"`
	// AccessListNames are names that still have to be resolved into ids.
	// They never reach the API.
	AccessListNames []string `json:"-"`
	// AccessLists is the expanded relation NPMplus returns instead of
	// npmplus_access_list_ids since it moved access lists into a table of
	// their own. Read into AccessListIDs on decoding, never kept.
	AccessLists []accessListRef `json:"access_lists,omitempty"`
}

ProxyHost mirrors `/api/nginx/proxy-hosts` as the API *returns* it. Request bodies are built by Payload; see payloads.go.

func (*ProxyHost) AdoptServerState

func (h *ProxyHost) AdoptServerState(current Resource)

AdoptServerState implements Resource.

func (*ProxyHost) Describe

func (h *ProxyHost) Describe() string

Describe implements Resource.

func (*ProxyHost) Domains

func (h *ProxyHost) Domains() []string

Domains returns the normalised domain names of the host.

func (*ProxyHost) Fingerprint

func (h *ProxyHost) Fingerprint() string

Fingerprint implements Resource.

func (*ProxyHost) IsEnabled

func (h *ProxyHost) IsEnabled() bool

IsEnabled implements Resource.

func (*ProxyHost) Kind

func (h *ProxyHost) Kind() Kind

Kind implements Resource.

func (*ProxyHost) OwnerMarked

func (h *ProxyHost) OwnerMarked() bool

OwnerMarked implements Resource.

func (*ProxyHost) Payload

func (h *ProxyHost) Payload(dialect Dialect) (any, error)

Payload implements Resource.

func (*ProxyHost) ResourceCertificate

func (h *ProxyHost) ResourceCertificate() CertificateID

ResourceCertificate implements Resource.

func (*ProxyHost) ResourceID

func (h *ProxyHost) ResourceID() int

ResourceID implements Resource.

func (*ProxyHost) ResourceKey

func (h *ProxyHost) ResourceKey() string

ResourceKey implements Resource.

func (*ProxyHost) ResourceMeta

func (h *ProxyHost) ResourceMeta() Meta

ResourceMeta implements Resource.

func (*ProxyHost) SetEnabled

func (h *ProxyHost) SetEnabled(enabled bool)

SetEnabled implements Resource.

func (*ProxyHost) SetResourceID

func (h *ProxyHost) SetResourceID(id int)

SetResourceID implements Resource.

func (*ProxyHost) UnmarshalJSON

func (h *ProxyHost) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes the resource and lifts the ownership marker and the group column into the meta object.

type ProxyProtocolLevel

type ProxyProtocolLevel int

ProxyProtocolLevel is NPMplus' `npmplus_proxy_protocol_forwarding`. The two generations of NPMplus disagree about its type: older ones declare an integer with the enum [0, 1, 2] (off, v1, v2), while 2.15.1 declares a boolean - it still coerces 0 and 1 on the way in, rejects 2 with "must be boolean", and reports the stored value back as true/false.

Decoding therefore has to accept both shapes; encoding stays numeric, because that is what both generations take.

func (ProxyProtocolLevel) MarshalJSON

func (p ProxyProtocolLevel) MarshalJSON() ([]byte, error)

MarshalJSON writes the numeric form.

func (*ProxyProtocolLevel) UnmarshalJSON

func (p *ProxyProtocolLevel) UnmarshalJSON(data []byte) error

UnmarshalJSON accepts 0/1/2, true/false and their string spellings.

type RedirectionHost

type RedirectionHost struct {
	ID          int    `json:"id,omitempty"`
	CreatedOn   string `json:"created_on,omitempty"`
	ModifiedOn  string `json:"modified_on,omitempty"`
	OwnerUserID int    `json:"owner_user_id,omitempty"`

	DomainNames       []string      `json:"domain_names"`
	ForwardScheme     string        `json:"forward_scheme"`
	ForwardDomainName string        `json:"forward_domain_name"`
	ForwardHTTPCode   int           `json:"forward_http_code"`
	PreservePath      Flag          `json:"preserve_path"`
	CertificateID     CertificateID `json:"certificate_id"`
	SSLForced         Flag          `json:"ssl_forced"`
	HSTSEnabled       Flag          `json:"hsts_enabled"`
	HSTSSubdomains    Flag          `json:"hsts_subdomains"`
	HTTP2Support      Flag          `json:"http2_support"`
	HTTP3Support      Flag          `json:"npmplus_http3_support"`
	BlockExploits     Flag          `json:"block_exploits"`
	AdvancedConfig    string        `json:"advanced_config"`
	Enabled           Flag          `json:"enabled"`
	Meta              Meta          `json:"meta"`

	// Directory is NPMplus' `npmplus_directory` column. It is moved into the
	// meta object when the resource is decoded and never kept here.
	Directory string `json:"npmplus_directory,omitempty"`
	// OwnerMarker records the ownership marker in the advanced configuration;
	// see marker.go. Not an API field.
	OwnerMarker Flag `json:"ownership_marker,omitempty"`
}

RedirectionHost mirrors `/api/nginx/redirection-hosts`.

func (*RedirectionHost) AdoptServerState

func (h *RedirectionHost) AdoptServerState(current Resource)

AdoptServerState implements Resource.

func (*RedirectionHost) Describe

func (h *RedirectionHost) Describe() string

Describe implements Resource.

func (*RedirectionHost) Domains

func (h *RedirectionHost) Domains() []string

Domains returns the normalised domain names of the host.

func (*RedirectionHost) Fingerprint

func (h *RedirectionHost) Fingerprint() string

Fingerprint implements Resource.

func (*RedirectionHost) IsEnabled

func (h *RedirectionHost) IsEnabled() bool

IsEnabled implements Resource.

func (*RedirectionHost) Kind

func (h *RedirectionHost) Kind() Kind

Kind implements Resource.

func (*RedirectionHost) OwnerMarked

func (h *RedirectionHost) OwnerMarked() bool

OwnerMarked implements Resource.

func (*RedirectionHost) Payload

func (h *RedirectionHost) Payload(dialect Dialect) (any, error)

Payload implements Resource.

func (*RedirectionHost) ResourceCertificate

func (h *RedirectionHost) ResourceCertificate() CertificateID

ResourceCertificate implements Resource.

func (*RedirectionHost) ResourceID

func (h *RedirectionHost) ResourceID() int

ResourceID implements Resource.

func (*RedirectionHost) ResourceKey

func (h *RedirectionHost) ResourceKey() string

ResourceKey implements Resource.

func (*RedirectionHost) ResourceMeta

func (h *RedirectionHost) ResourceMeta() Meta

ResourceMeta implements Resource.

func (*RedirectionHost) SetEnabled

func (h *RedirectionHost) SetEnabled(enabled bool)

SetEnabled implements Resource.

func (*RedirectionHost) SetResourceID

func (h *RedirectionHost) SetResourceID(id int)

SetResourceID implements Resource.

func (*RedirectionHost) UnmarshalJSON

func (h *RedirectionHost) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes the resource and lifts the ownership marker and the group column into the meta object.

type Resource

type Resource interface {
	// Kind reports which collection the resource belongs to.
	Kind() Kind
	// ResourceID is the NPM id (0 for a resource that does not exist yet).
	ResourceID() int
	// SetResourceID stores the id assigned by NPM.
	SetResourceID(id int)
	// ResourceKey is the stable identity within its kind: the first domain
	// name, or the incoming port for streams.
	ResourceKey() string
	// ResourceMeta exposes the ownership of the resource. For a resource read
	// from the API it combines the meta object with the marker in the
	// advanced configuration; see liftOwnership.
	ResourceMeta() Meta
	// OwnerMarked reports whether the resource carries a trusted ownership
	// marker - for a desired resource, whether it is going to.
	OwnerMarked() bool
	// ResourceCertificate reports the attached certificate, which the
	// automatic selection uses to stay on a certificate that still fits.
	ResourceCertificate() CertificateID
	// Fingerprint hashes the configuration-relevant fields. It deliberately
	// excludes the enabled flag, which is not part of the write payload and
	// is reconciled through the /enable and /disable endpoints instead.
	Fingerprint() string
	// Payload returns the create/update request body for the given API
	// flavour. It fails when the desired configuration uses a feature the
	// flavour does not have.
	Payload(dialect Dialect) (any, error)
	// IsEnabled reports the desired (or, for a resource read back from the
	// API, the live) enabled state.
	IsEnabled() bool
	// SetEnabled records the enabled state.
	SetEnabled(enabled bool)
	// AdoptServerState copies server-assigned values (such as an issued
	// certificate id) from the live resource into the desired payload, so a
	// converged resource does not produce an endless update loop.
	AdoptServerState(current Resource)
	// Describe returns a short, log friendly description.
	Describe() string
}

Resource is the behaviour every managed NPM resource shares. It lets the reconcile worker treat proxy hosts, redirections, streams and 404 hosts uniformly while each type keeps its own API payload.

type Stream

type Stream struct {
	ID          int    `json:"id,omitempty"`
	CreatedOn   string `json:"created_on,omitempty"`
	ModifiedOn  string `json:"modified_on,omitempty"`
	OwnerUserID int    `json:"owner_user_id,omitempty"`

	IncomingPort   Port          `json:"incoming_port"`
	ForwardingHost string        `json:"forwarding_host"`
	ForwardingPort Port          `json:"forwarding_port"`
	TCPForwarding  Flag          `json:"tcp_forwarding"`
	UDPForwarding  Flag          `json:"udp_forwarding"`
	CertificateID  CertificateID `json:"certificate_id"`
	Enabled        Flag          `json:"enabled"`
	Meta           Meta          `json:"meta"`

	// NPMplus extensions.
	ProxyProtocol  ProxyProtocolLevel `json:"npmplus_proxy_protocol_forwarding"`
	ProxyTLS       Flag               `json:"npmplus_proxy_tls"`
	AdvancedConfig string             `json:"npmplus_advanced_config"`
	Description    string             `json:"npmplus_description"`

	// Directory is NPMplus' `npmplus_directory` column. It is moved into the
	// meta object when the resource is decoded and never kept here.
	Directory string `json:"npmplus_directory,omitempty"`
	// OwnerMarker records the ownership marker in the advanced configuration;
	// see marker.go. Not an API field.
	OwnerMarker Flag `json:"ownership_marker,omitempty"`
}

Stream mirrors `/api/nginx/streams`. Streams have no domain names: their identity is the incoming port.

func (*Stream) AdoptServerState

func (s *Stream) AdoptServerState(current Resource)

AdoptServerState implements Resource.

func (*Stream) Describe

func (s *Stream) Describe() string

Describe implements Resource.

func (*Stream) Fingerprint

func (s *Stream) Fingerprint() string

Fingerprint implements Resource.

func (*Stream) IsEnabled

func (s *Stream) IsEnabled() bool

IsEnabled implements Resource.

func (*Stream) Kind

func (s *Stream) Kind() Kind

Kind implements Resource.

func (*Stream) OwnerMarked

func (s *Stream) OwnerMarked() bool

OwnerMarked implements Resource.

func (*Stream) Payload

func (s *Stream) Payload(dialect Dialect) (any, error)

Payload implements Resource.

func (*Stream) ResourceCertificate

func (s *Stream) ResourceCertificate() CertificateID

ResourceCertificate implements Resource.

func (*Stream) ResourceID

func (s *Stream) ResourceID() int

ResourceID implements Resource.

func (*Stream) ResourceKey

func (s *Stream) ResourceKey() string

ResourceKey implements Resource: NPM allows exactly one stream per incoming port, which makes the port the natural identity.

func (*Stream) ResourceMeta

func (s *Stream) ResourceMeta() Meta

ResourceMeta implements Resource.

func (*Stream) SetEnabled

func (s *Stream) SetEnabled(enabled bool)

SetEnabled implements Resource.

func (*Stream) SetResourceID

func (s *Stream) SetResourceID(id int)

SetResourceID implements Resource.

func (*Stream) UnmarshalJSON

func (s *Stream) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes the resource and lifts the ownership marker and the group column into the meta object.

type Version

type Version struct {
	Major    int `json:"major"`
	Minor    int `json:"minor"`
	Revision int `json:"revision"`
}

Version is what upstream NPM reports under `version` in GET /api/: {"major":2,"minor":11,"revision":3}. NPMplus reports a string there, which parses to the zero Version - "no version I can compare against".

func (Version) AtLeast

func (v Version) AtLeast(major, minor int) bool

AtLeast reports whether the version is at least major.minor.

An unknown version counts as new enough. A server that reports none is either NPMplus or something this tool has not seen, and assuming "old" there would silently drop settings the user asked for; assuming "new" at worst produces the server's own error message, which names the field.

func (Version) IsZero

func (v Version) IsZero() bool

IsZero reports whether the server gave no comparable version.

func (Version) String

func (v Version) String() string

String implements fmt.Stringer.

type WriteSchema

type WriteSchema map[Kind]map[string]Property

WriteSchema lists, per resource kind, the properties a create or update body may carry. It is read from GET /api/schema, which serves the very document both servers validate request bodies against, so it is the authoritative answer to "may this payload contain field X" - no version table needed.

Properties of a proxy host's custom locations are listed as "locations/<name>".

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL