api

package
v0.1.23 Latest Latest
Warning

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

Go to latest
Published: Nov 1, 2026 License: Apache-2.0 Imports: 117 Imported by: 0

Documentation

Overview

Package api serves the HTTP interface used by the web front end.

The important design constraint: channels are read from and written to files on disk. The API is an editor for those files, not a different storage system that happens to export YAML. A channel created in the browser and one written by hand are the same artifact, which is what makes "export to share" trivial and keeps git as the history of record.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrChannelNotFound means no channel of that name exists.
	ErrChannelNotFound = errors.New("channel not found")
	// ErrChannelExists means a channel of that name already exists.
	ErrChannelExists = errors.New("a channel with that name already exists")
	// ErrInvalidName means the name cannot be used as a filename.
	ErrInvalidName = errors.New("invalid channel name")
)

Repository errors.

View Source
var ErrBadLibraryPath = errors.New("a script library is written as lib/<name>.js or lib/<name>.lua, beside the channels")

ErrBadLibraryPath means a library file was named outside lib/ or without a .js or .lua extension.

Functions

func EnsureDemoAccount added in v0.1.12

func EnsureDemoAccount(ctx context.Context, st *store.Store, d *DemoAccount) error

EnsureDemoAccount makes the account exist as a viewer with the published password, and puts it back that way at every start: if anyone with more rights changed it, the page would otherwise go on showing a password that no longer works.

func FigureTiles added in v0.1.7

func FigureTiles() []string

FigureTiles are the tiles this endpoint draws.

Types

type CDSClient added in v0.1.5

type CDSClient struct {
	// Issuer is the iss the EHR signs with.
	Issuer string `yaml:"issuer" json:"issuer"`
	// JWKSURL is where its public keys are published.
	JWKSURL string `yaml:"jwks_url" json:"jwksUrl"`
	// DaVinciVersion is the CRD release the EHR speaks, "2.1" or "2.2", when its requests do not say (requestedVersion);
	// empty leaves it to the server's -davinci-version.
	DaVinciVersion string `yaml:"davinci_version,omitempty" json:"davinciVersion,omitempty"`
	// contains filtered or unexported fields
}

CDSClient is an EHR allowed to call the CRD service with a signed JWT.

func LoadCDSClients added in v0.1.5

func LoadCDSClients(path string) ([]*CDSClient, error)

LoadCDSClients reads the trusted EHR list.

type CMS0057Status added in v0.1.5

type CMS0057Status struct {
	FHIR       bool   `json:"fhir"`
	BaseURL    string `json:"baseUrl"`
	SMART      bool   `json:"smart"`
	BulkExport bool   `json:"bulkExport"`
	PayerAPIs  bool   `json:"payerApis"`
	Consent    bool   `json:"consentRequired"`
	// Subscriptions is topic-based FHIR subscriptions (encounter and appointment notifications).
	Subscriptions bool `json:"subscriptions"`
	ReadOnly      bool `json:"readOnly"`
}

CMS0057Status is what the serve command knows about the FHIR endpoint, which decides whether each API can answer.

type ChannelRepo

type ChannelRepo struct {
	// Dir is where channel files live.
	Dir string
	// contains filtered or unexported fields
}

ChannelRepo reads and writes channel definitions as files.

This is the whole point of the design: the browser edits files. A channel built by clicking and a channel written by hand are the same artifact, so git holds the history, a diff shows what changed, and exporting to share is just handing someone the file.

func NewChannelRepo

func NewChannelRepo(dir string) (*ChannelRepo, error)

NewChannelRepo prepares a repository, creating the directory if needed.

func (*ChannelRepo) Create

func (r *ChannelRepo) Create(raw []byte) (*config.Channel, error)

Create writes a new channel. It refuses to overwrite an existing one.

func (*ChannelRepo) Delete

func (r *ChannelRepo) Delete(name string) error

Delete removes a channel file.

func (*ChannelRepo) Get

func (r *ChannelRepo) Get(name string) (*config.Channel, error)

Get returns one channel definition as the generic map the front end edits, along with the raw YAML.

func (*ChannelRepo) List

func (r *ChannelRepo) List() (valid []ChannelSummary, broken map[string]string, err error)

List returns every channel, valid or not.

An invalid file is reported rather than hidden: a channel that will not load is exactly what an operator needs to see, and omitting it would make a broken file look like a missing one.

func (*ChannelRepo) PathFor

func (r *ChannelRepo) PathFor(name string) (string, error)

pathFor finds the file defining a channel, matching on the name inside the file rather than assuming the filename. PathFor returns the file backing a channel. Exported for the history handlers, which need the filename rather than the parsed channel.

func (*ChannelRepo) RawYAML

func (r *ChannelRepo) RawYAML(name string) ([]byte, error)

RawYAML returns the file contents, for the export button and the advanced editor.

func (*ChannelRepo) ReadTables

func (r *ChannelRepo) ReadTables(file string) (*codeset.Set, error)

ReadTables loads a shared mapping table file, returning an empty set when it does not exist yet.

Absent is not an error here: the first table written to a server creates the file, and treating "no file" as a failure would make creating the first one impossible through the same path that creates the second.

func (*ChannelRepo) Save

func (r *ChannelRepo) Save(cfg *config.Channel) error

Save writes a channel definition back, keeping its existing file.

Distinct from Update, which takes raw YAML from a person. This takes a definition the server has changed itself - attaching a contract that was just written, for instance - and re-serialises it.

func (*ChannelRepo) TableFiles

func (r *ChannelRepo) TableFiles() []string

TableFiles lists the shared mapping table files in the channels directory.

Named by convention rather than by being referenced, so a table nobody uses yet is still found. Without this the only way to discover a table was through a channel that loaded it, which made a newly created one invisible in the section that had just created it.

func (*ChannelRepo) Update

func (r *ChannelRepo) Update(name string, raw []byte) (*config.Channel, error)

Update replaces an existing channel.

Renaming is allowed: the file is written under the new name and the old one removed, so the filename keeps matching the channel name.

func (*ChannelRepo) Validate

func (r *ChannelRepo) Validate(raw []byte) (*config.Channel, error)

Validate parses and checks a definition without saving it, so the front end can tell someone what is wrong before they commit to it.

func (*ChannelRepo) WriteLibrary added in v0.1.5

func (r *ChannelRepo) WriteLibrary(file string, source []byte) (string, error)

WriteLibrary writes a shared script file that channels include, such as a Mirth code template library carried across.

Only lib/<name>.js or .lua: the caller is an editor, and a path they choose must not reach a channel file, the database or anything outside this directory. Overwriting is allowed, because importing the same backup twice should converge rather than fail.

func (*ChannelRepo) WriteSidecar

func (r *ChannelRepo) WriteSidecar(channel, file string, body []byte) error

WriteSidecar writes a companion file beside a channel definition.

Contracts, code sets and the like live next to the channel that references them, because a channel and the things it depends on travel together: copying one file to another server and leaving its contract behind produces a channel that will not load at all, which is a confusing way to learn about a missing file.

The name is deliberately restricted rather than trusted. It arrives from a request, and a path with a separator or a parent reference in it would write outside the channels directory - which for a process that may be running as a service is a way to overwrite something that matters. Only a plain file name is allowed.

func (*ChannelRepo) WriteTables

func (r *ChannelRepo) WriteTables(file string, body []byte) error

WriteTables replaces a shared mapping table file.

type ChannelState

type ChannelState struct {
	Name      string     `json:"name"`
	Running   bool       `json:"running"`
	StartedAt *time.Time `json:"startedAt,omitempty"`
	Error     string     `json:"error,omitempty"`

	Received    int64 `json:"received"`
	Delivered   int64 `json:"delivered"`
	Filtered    int64 `json:"filtered"`
	Partial     int64 `json:"partial"`
	Failed      int64 `json:"failed"`
	Unparseable int64 `json:"unparseable"`

	Destinations []DestinationState `json:"destinations,omitempty"`
}

ChannelState is the live state of one channel.

type ChannelSummary

type ChannelSummary struct {
	Name        string `json:"name"`
	Description string `json:"description,omitempty"`

	// Group is what this channel is filed under. Empty means ungrouped, which is a legitimate answer
	// and not a missing value - a site with nine channels should not be made to invent a taxonomy.
	Group        string   `json:"group,omitempty"`
	Enabled      bool     `json:"enabled"`
	Listen       string   `json:"listen"`
	SourceType   string   `json:"sourceType"`
	AckWhen      string   `json:"ackWhen"`
	Filter       string   `json:"filter,omitempty"`
	Destinations []string `json:"destinations"`
	Reads        []string `json:"reads,omitempty"`
	File         string   `json:"file"`
}

ChannelSummary is the listing view.

type ClusterControl added in v0.1.17

type ClusterControl interface {
	Status(ctx context.Context) (cluster.Status, error)
	Runs(tenant, channel string) bool
	SetDesired(ctx context.Context, tenant, channel string, running bool, by string) error
	StepDown(ctx context.Context, d time.Duration, by string) error
	Move(ctx context.Context, tenant, channel, toNode, by string) error
	IsLeader() bool
	// RunningOn names the server running a channel, "" when none does.
	RunningOn(ctx context.Context, tenant, channel string) string
}

ClusterControl is what the console and API need from a server in a cluster: its view of the cluster, where a channel runs, and the controls (start and stop anywhere, step down, move).

type ComponentView

type ComponentView struct {
	Number int    `json:"number"`
	Path   string `json:"path"`
	Name   string `json:"name,omitempty"`
	Value  string `json:"value"`
}

ComponentView is one component of a composite field.

type DemoAccount added in v0.1.12

type DemoAccount struct {
	Username string
	Password string
}

DemoAccount is a shared, published sign-in for a public demonstration server.

Its password is shown on the sign-in page, so it is a viewer (it can look at everything and change nothing), and it can never change its own password or add a passkey: the first visitor to do either would lock out everyone after them.

func ParseDemoAccount added in v0.1.12

func ParseDemoAccount(v string) (*DemoAccount, error)

ParseDemoAccount reads "username:password".

type DestinationState

type DestinationState struct {
	Name      string `json:"name"`
	Delivered int64  `json:"delivered"`
	Failed    int64  `json:"failed"`
	Filtered  int64  `json:"filtered"`
}

DestinationState is per-destination counts for a running channel.

type DirectContact added in v0.1.16

type DirectContact struct {
	// Address is a Direct address, or a domain for a domain-bound certificate that covers every address at it.
	Address      string `json:"address"`
	Name         string `json:"name,omitempty"`
	Organisation string `json:"organisation,omitempty"`
	Notes        string `json:"notes,omitempty"`
	// Source says where the certificate came from: dns, upload, or none yet.
	Source  string            `json:"source"`
	Added   time.Time         `json:"added"`
	Updated time.Time         `json:"updated"`
	Certs   []directCertShown `json:"certificates"`
}

DirectContact is one correspondent.

type Drugs added in v0.1.16

type Drugs struct {
	Formulary *formulary.File
	// FormularyBase is the public Da Vinci US Drug Formulary endpoint's address.
	FormularyBase string
	RTPB          rtpb.Options
	// EPA answers SCRIPT ePA; its Submit is nil when PAS is off, which leaves initiation working and PARequests refused.
	EPA *epa.Service
	// ReplyChannel names the channel unsolicited ePA responses are routed to, or "".
	ReplyChannel string
}

Drugs is what the drug screens and endpoints need. Nil Formulary leaves them off.

type FieldView

type FieldView struct {
	Number      int             `json:"number"`
	Path        string          `json:"path"`
	Name        string          `json:"name"`
	Description string          `json:"description,omitempty"`
	Value       string          `json:"value"`
	Raw         string          `json:"raw"`
	Empty       bool            `json:"empty"`
	Repeats     int             `json:"repeats,omitempty"`
	Meaning     string          `json:"meaning,omitempty"`
	Components  []ComponentView `json:"components,omitempty"`
}

FieldView is one field, explained.

type Figure added in v0.1.7

type Figure struct {
	Rows []FigureRow `json:"rows"`
	// Note says what the figure covers (the window, the source); Unavailable, when set, is why it could not be computed.
	Note        string `json:"note,omitempty"`
	Unavailable string `json:"unavailable,omitempty"`
	Empty       string `json:"empty,omitempty"`
	// Links are downloads that go with the figure, such as each partner's connection sheet.
	Links []FigureLink `json:"links,omitempty"`
}

Figure is one tile's content.

type FigureLink struct {
	Label string `json:"label"`
	Href  string `json:"href"`
}

FigureLink is a download beside a figure.

type FigureRow added in v0.1.7

type FigureRow struct {
	Label string `json:"label"`
	Value string `json:"value"`
	Tone  string `json:"tone,omitempty"`
}

FigureRow is one line of a figure: a label, a value, and how to colour it (ok, warn, bad, or empty for neutral).

type FrictionFunnel

type FrictionFunnel struct {
	FirstSignIn  *time.Time `json:"firstSignIn,omitempty"`
	FirstChannel *time.Time `json:"firstChannel,omitempty"`
	FirstMessage *time.Time `json:"firstMessage,omitempty"`

	ChannelCount int   `json:"channelCount"`
	MessageCount int64 `json:"messageCount"`

	// Delivered is how many messages reached a destination. The number that separates a channel somebody configured from one that
	// does the job: a channel can receive for weeks and deliver nothing, and from inside the interface that looks like traffic.
	Delivered int64 `json:"delivered"`

	// StuckAt names the first step that has not happened, rather than leaving a reader to work it out from four timestamps. The whole
	// value of the funnel is in saying where somebody stopped, and a report that needs interpreting gets interpreted by whoever wrote
	// the software.
	StuckAt string `json:"stuckAt"`

	// MinutesToFirstMessage is from being able to sign in to a message actually arriving. The single most useful number here, and the
	// one a person watching over somebody's shoulder would have been timing.
	MinutesToFirstMessage *float64 `json:"minutesToFirstMessage,omitempty"`
}

FrictionFunnel is the sequence a new installation has to get through before Perfuse does anything useful.

Each step is a fact looked up elsewhere: the audit trail for signing in and creating a channel, the message store for traffic. If a step has not happened, that is the answer - an installation stuck at "a channel exists but no message has ever arrived" is the most interesting state this report can show, and it is invisible from inside.

type FrictionReport

type FrictionReport struct {
	// Refusals are the messages hit most often, worst first. The point of the whole report.
	Refusals []store.FrictionGroup `json:"refusals"`

	// Total is every refusal recorded, so the list above can be read as a sample of something rather than as everything.
	Total int `json:"total"`

	// Funnel is how far the installation has got. Derived from records that already existed rather than newly written, because a
	// milestone this code records about itself is worth less than one it has to go and find.
	Funnel FrictionFunnel `json:"funnel"`
}

FrictionReport is what the report endpoint returns.

type LDAPConfig

type LDAPConfig struct {
	// Directory talks to the directory.
	Directory *ldap.Directory

	// Settings are the loaded configuration, for role mapping and the button label.
	Settings *ldap.Config
}

LDAPConfig holds the directory the server authenticates against.

type MCPSettings added in v0.1.16

type MCPSettings struct {
	// Enabled is the switch; without it /mcp answers 404.
	Enabled func() bool
	// CallsPerMinute bounds tool calls per token; 0 is 60.
	CallsPerMinute func() int
	// Origins are browser origins allowed to call /mcp, beside this server's own. Requests with no Origin (agents, CLIs)
	// are allowed; a browser page on another origin is refused, which is what MCP asks of a server for DNS rebinding.
	Origins []string
}

MCPSettings switch MCP on and bound it.

type MeasureService added in v0.1.16

type MeasureService interface {
	ListMeasures(ctx context.Context) ([]fhirserver.MeasureSummary, error)
	MeasureJSON(ctx context.Context, id string) ([]byte, error)
	PutMeasure(ctx context.Context, raw []byte) (string, error)
	DeleteMeasure(ctx context.Context, id string) error
	EvaluateMeasure(ctx context.Context, req fhirserver.EvaluateMeasureRequest, caller *fhirserver.Caller) (map[string]any, error)
}

MeasureService is the FHIR server's measure side (fhirserver.Server); nil without -fhir.

type MessageView

type MessageView struct {
	MessageType  string        `json:"messageType"`
	TriggerEvent string        `json:"triggerEvent"`
	EventMeaning string        `json:"eventMeaning,omitempty"`
	ControlID    string        `json:"controlId"`
	Version      string        `json:"version"`
	Sender       string        `json:"sender"`
	Segments     []SegmentView `json:"segments"`
	Separators   string        `json:"separators"`
}

MessageView is a whole message, explained.

type OIDCConfig

type OIDCConfig struct {
	// Issuer is the identity provider's issuer URL.
	Issuer string

	// ClientID and ClientSecret identify Perfuse to the provider.
	ClientID     string
	ClientSecret string

	// RedirectURL is where the provider sends the browser back. Must match what is registered with the provider exactly.
	RedirectURL string

	// Scopes are requested at sign-in. Empty asks for openid, profile and email.
	Scopes []string

	// Roles maps the provider's groups to Perfuse roles.
	Roles *oidc.RoleMapping

	// CreateUsers allows a first-time sign-in to create an account. Defaults to false.
	//
	// False is the safe default and the useful one to explain: with it off, somebody has to exist in Perfuse before they
	// can sign in, so the directory decides who they are and Perfuse decides who is allowed in. With it on, anybody the
	// directory vouches for and whose groups map to a role gets an account - which is what most sites want, and is a
	// decision they should make deliberately.
	CreateUsers bool

	// Label is what the sign-in button says. Defaults to naming the issuer's host.
	Label string

	// Provider is the discovered provider, filled in at startup.
	Provider *oidc.Provider

	// Keys are the provider's signing keys.
	Keys *oidc.KeySet
}

OIDCConfig is how federated sign-in is configured.

func (*OIDCConfig) ButtonLabel

func (c *OIDCConfig) ButtonLabel() string

ButtonLabel is what the sign-in button should say.

The issuer's host when nothing is configured, because "sign in with 127.0.0.1" is unhelpful but "sign in with SSO" is worse - a site with two providers would show the same word twice.

func (*OIDCConfig) Enabled

func (c *OIDCConfig) Enabled() bool

Enabled reports whether federated sign-in is configured.

type Runtime

type Runtime struct {
	// Channels supplies channel definitions from disk.
	Repo *ChannelRepo
	// Messages persists what each channel handled. Optional.
	Messages *msgstore.Store
	// FHIR is the embedded FHIR store, if one is configured.
	FHIR *fhirserver.Store
	// Metrics collects what the engine is doing. Optional; when nil the metrics
	// endpoints say so rather than returning empty series that look like an idle
	// system.
	Metrics *metrics.Collector

	// Queues is the durable delivery queue, nil when queueing is unavailable.
	Queues *engine.Queues

	// Admit bounds deliveries in flight, shared with every channel this runtime starts.
	//
	// The same controller the engine uses, on purpose. A channel started from the interface must draw on the
	// same descriptor budget as one started at boot, or the limit is per channel and therefore not a limit.
	Admit *admit.Controller

	// Terminology counts the values mapping tables did not have; nil when there is no database.
	Terminology *terminology.Store

	// QueryState records what a DICOM query source has already seen, nil when there is no database.
	//
	// A channel that polls an archive refuses to start without it rather than running stateless: with nothing to
	// remember it re-emits every matching study on every poll, which against a real archive is a message storm that
	// looks like the archive malfunctioning.
	QueryState engine.DICOMQueryState

	// PHIWatch watches the traffic of every channel this runtime starts for the patterns a breach looks like. Nil
	// when it is off.
	PHIWatch *compliance.Monitor

	// Tracer exports message traces. Nil when tracing is not configured.
	Tracer *trace.Tracer

	// Contracts checks each channel's contract against recent traffic, on a timer.
	//
	// Nil when nothing has a contract. Created lazily by EnableContracts so a server with no contracts does
	// no work and holds no state for them.
	Contracts *contractChecker

	// Tenant is which tenant this runtime serves. Empty in single-tenant operation,
	// which reports as the default tenant in metrics rather than as an empty label.
	Tenant tenant.ID

	// Admission is asked before each received message on every channel of this runtime; nil admits everything. It is
	// the tenant's limits (tenant.Gate), so one customer cannot crowd out the rest of a shared server.
	Admission func() error

	// Gate, on a server in a cluster, refuses to start a channel another server runs: the error says where.
	Gate func(name string) error
	// Expected, on a server in a cluster, says whether a channel not running here counts as down: one another server
	// runs does not.
	Expected func(name string) bool
	// contains filtered or unexported fields
}

Runtime owns the running engine, so the interface can show what is happening and start or stop it.

Without this the web interface is a text editor with a login: you can change a channel but not see whether it is running, how many messages it has handled, or look at one that failed. That gap is the difference between a configuration tool and an operations console.

func NewRuntime

func NewRuntime(repo *ChannelRepo, messages *msgstore.Store, fhirStore *fhirserver.Store) *Runtime

NewRuntime prepares a runtime.

func (*Runtime) Channel

func (rt *Runtime) Channel(name string) (*engine.Channel, bool)

Channel returns one running channel by name.

func (*Runtime) ChannelStates

func (rt *Runtime) ChannelStates() (configured []string, running map[string]bool)

ChannelStates reports which enabled channels exist and which are running.

Used by alerting to notice a channel that is configured, enabled, and not listening — which looks identical to a healthy channel on every other measure, because a channel that never started also never fails.

func (*Runtime) CheckContractsDue

func (rt *Runtime) CheckContractsDue(ctx context.Context, channels []*config.Channel)

CheckContractsDue re-checks any channel whose interval has elapsed.

Driven from the alert loop rather than its own goroutine: one timer is easier to reason about than two, and it means a contract result is never newer or older than the reading it is attached to.

func (*Runtime) ContractDrift

func (rt *Runtime) ContractDrift() map[string]alerts.ContractDrift

ContractDrift reports each channel's contract result for the alert path.

Returns the cached result rather than checking, because the alert loop runs every thirty seconds and profiling five hundred messages on that schedule would be load for no benefit.

func (*Runtime) EnableContracts

func (rt *Runtime) EnableContracts(log *slog.Logger)

EnableContracts turns on periodic contract checking.

Called by the server only when at least one channel has a contract, so an installation that uses none pays nothing - no goroutine, no state, no reads of the message store.

func (*Runtime) IsRunning

func (rt *Runtime) IsRunning(name string) bool

IsRunning reports whether a channel is running.

func (*Runtime) RestartUsingTables added in v0.1.17

func (rt *Runtime) RestartUsingTables(ctx context.Context, file string, log *slog.Logger) (restarted []string, failed map[string]string)

RestartUsingTables restarts every running channel that loads the named tables file, so a table change reaches them: a channel reads its tables when it starts. Each is stopped (waiting for the message in hand) and started again. It returns the channels restarted and, for any that would not start again, why.

func (*Runtime) Route

func (rt *Runtime) Route(ctx context.Context, name string, raw []byte) error

Route hands a message to another running channel, satisfying engine.ChannelRouter.

The message goes in at the top of the target channel, so its filter, transformations and destinations all run while its source does not. A routed message is already inside the server; the source connector exists to receive from outside.

func (*Runtime) RunningChannels

func (rt *Runtime) RunningChannels() []*engine.Channel

RunningChannels returns the channels currently started.

Copied under the lock rather than handing out the map, so a caller iterating it cannot race a channel being started or stopped underneath them.

func (*Runtime) Start

func (rt *Runtime) Start(name string, log *slog.Logger) error

Start starts one channel by name.

func (*Runtime) StartEnabled

func (rt *Runtime) StartEnabled(log *slog.Logger) []error

StartEnabled starts every channel whose file says it is enabled.

func (*Runtime) States

func (rt *Runtime) States() []ChannelState

States returns the live state of every channel on disk, running or not.

Never returns nil. The result is serialised straight into the status response, where the contract says a list, and a nil slice would marshal to null and crash a caller that trusted it. This returned nil when the channels directory could not be read - a mount dropping out, or permissions changing - which is exactly the moment the dashboard needs to stay up in order to say so.

func (*Runtime) Stop

func (rt *Runtime) Stop(ctx context.Context, name string) error

Stop stops one channel, waiting for messages in flight.

func (*Runtime) StopAll

func (rt *Runtime) StopAll(ctx context.Context)

StopAll stops every running channel.

func (*Runtime) TenantName added in v0.1.23

func (rt *Runtime) TenantName() string

watchName is the name the access monitor knows a channel by: prefixed by the tenant on a shared server, where two customers may each have an "adt". TenantName is the tenant this runtime serves, "" on a single-tenant server, for what its senders record.

type SAMLConfig

type SAMLConfig struct {
	// EntityID identifies Perfuse to the identity provider, and is the audience the assertion must be addressed to.
	EntityID string

	// ACSURL is where the identity provider posts its response. It has to match what is registered there exactly.
	ACSURL string

	// IdPSSOURL is where the browser is sent to sign in.
	IdPSSOURL string

	// IdPCertPEM is the certificate that signs responses, and the only one trusted. A certificate carried inside a response is
	// never used: see internal/saml/embeddedcert_test.go for what that would cost.
	IdPCertPEM string

	// GroupsAttribute names the assertion attribute carrying group membership.
	//
	// There is no standard for this and no useful default. Entra sends "groups", Okta usually sends whatever the administrator
	// named in the app configuration, ADFS sends a claim URI. Guessing would produce a sign-in that works and grants nobody
	// anything, which reads as a broken product rather than as a missing setting.
	GroupsAttribute string

	// Roles maps the provider's groups to Perfuse roles.
	Roles *oidc.RoleMapping

	// CreateUsers allows a first sign-in to create an account. False is the safe default, as with OIDC.
	CreateUsers bool

	// AllowUnsolicited accepts a response that answers no request this server sent, which is how a portal tile works. Off by
	// default: it is also what lets a captured response be posted into somebody else's browser.
	AllowUnsolicited bool

	// Label is what the sign-in button says.
	Label string

	// SP is built at startup so a bad certificate is reported then rather than at somebody's first sign-in.
	SP *saml.ServiceProvider
}

SAMLConfig is how SAML sign-in is configured.

Deliberately shaped like OIDCConfig, including reusing oidc.RoleMapping. The two protocols disagree about almost everything on the wire and agree completely about what an administrator has to decide: which directory, who is allowed in, which groups grant which role, and whether a first sign-in may create an account. Two different shapes for that would mean two screens to learn and two places for the same misconfiguration to hide.

func SAMLFromFile

func SAMLFromFile(path string) (*SAMLConfig, error)

SAMLFromFile builds a runtime configuration from a file, for the server to use at startup.

Here rather than in cmd so that the assembly is tested: this is where a field can be read from the file and silently not passed on, which produces a setting that exists everywhere except where it matters.

func (*SAMLConfig) ButtonLabel

func (c *SAMLConfig) ButtonLabel() string

ButtonLabel is what the sign-in button says.

func (*SAMLConfig) Enabled

func (c *SAMLConfig) Enabled() bool

Enabled reports whether SAML sign-in is configured.

type SegmentView

type SegmentView struct {
	Index       int         `json:"index"`
	Name        string      `json:"name"`
	Description string      `json:"description"`
	Known       bool        `json:"known"`
	Raw         string      `json:"raw"`
	Fields      []FieldView `json:"fields"`
}

SegmentView is one segment, explained.

type Server

type Server struct {
	// Channels manages the channel files on disk.
	Channels *ChannelRepo
	// Alerts is the alert evaluator, nil when alerting is off.
	Alerts *alerts.Evaluator
	// PHIWatch is the patient-data access monitor, nil when it is off (serve -watch-access=false).
	PHIWatch *compliance.Monitor
	// Terminology holds imported code systems and the values mapping tables did not have; nil without a database.
	Terminology *terminology.Store

	// PatientIndex is the master patient index; nil when the server keeps none (serve -mpi).
	PatientIndex *mpi.Store

	// Privacy answers data protection requests (GDPR, UK GDPR); nil without a database.
	Privacy *privacy.Service

	// Cluster is this server's place in a cluster of servers sharing one store; nil for a server on its own.
	Cluster ClusterControl
	// SNMP is the SNMP agent and trap sender, nil when SNMP is off.
	SNMP *snmp.Service

	// Store holds users, sessions and the audit log.
	Store *store.Store
	// Log receives request and error events.
	Log *slog.Logger
	// OIDC configures federated sign-in. Nil when it is not configured.
	OIDC *OIDCConfig

	// SAML is SAML 2.0 sign-in, nil when not configured.
	SAML *SAMLConfig

	// PasskeyRPID is the relying party identifier passkeys are registered for: a bare domain.
	//
	// This is what binds a credential to this site and makes it unphishable. Empty disables passkeys, because there is no
	// safe default: guessing it from a request's Host header would let whoever controls DNS decide what a credential is
	// bound to, which is the one thing the value exists to prevent.
	PasskeyRPID string

	// Demo is a shared sign-in shown on the sign-in page, for a public demonstration server. Nil: none.
	Demo *DemoAccount

	// PasskeyRPName is what the authenticator shows a person when they register.
	PasskeyRPName string

	// PasskeyOrigins are the exact origins accepted, scheme and port included.
	PasskeyOrigins []string

	// SCIMDefaultRole is the role an account gets when the identity provider does not say.
	//
	// Viewer unless an operator changes it, and the default is downward on purpose: an account whose role could not be
	// determined should be able to read and nothing else. Defaulting upward would hand out administrator to anybody the
	// provider failed to describe, and provisioning failures are exactly the case where a description goes missing.
	SCIMDefaultRole store.Role

	// SCIMEnabled turns the provisioning endpoints on.
	//
	// Off by default. These endpoints create and delete accounts, so an installation that is not being provisioned by an
	// identity provider should not expose them at all - an endpoint nobody uses is one nobody is watching.
	SCIMEnabled bool

	// PasskeyAttestation is what an operator requires of an authenticator's own attestation.
	//
	// Zero verifies nothing and claims nothing, which is the right default: attestation answers "which device", not
	// "is this legitimate", and the origin binding does the security work either way.
	PasskeyAttestation webauthn.AttestationPolicy

	// Restart asks the server to stop cleanly and start again, reading its settings afresh. Nil when the process cannot
	// restart itself, and the console then says so rather than offering a button that does nothing.
	Restart func()

	// MetricsAccessFn supplies the configured metrics access mode, superseding MetricsOpen when set.
	//
	// A function so the setting applies without a restart, read at request time. Nil falls back to the flags.
	MetricsAccessFn func() string

	// MetricsToken is a dedicated credential for the Prometheus scrape endpoint.
	//
	// Separate from an API token because a scraper cannot sign in, cannot hold a cookie and should not hold a
	// credential that also permits writes. Empty means no scrape token is configured, and then only a signed-in
	// operator can read metrics.
	MetricsToken string

	// MetricsOpen serves metrics to anybody who can reach the port.
	//
	// Was the only behaviour, and remains available because plenty of installations really do have the port bound to a
	// private interface. Named rather than implied so it shows up in the command line of any server running this way:
	// "we thought it was firewalled" is how this turns into a finding.
	MetricsOpen bool

	// SettingsPaths are the configuration files the interface may edit.
	//
	// Paths rather than parsed values, because the interface edits the same files the flags point at. Keeping a second
	// copy of these settings in the database would raise the question of which one wins, and the answer would be
	// discovered during an incident.
	SettingsPaths SettingsFiles

	// Startup describes what the process was started with, for the read-only half of the settings page.
	//
	// Shown rather than hidden. Somebody diagnosing a problem needs to know how the process was started, and "not
	// shown anywhere" is how a wrong flag survives for months.
	Startup StartupSettings

	// Started is when this process began, for the settings page.
	Started time.Time

	// LDAP configures directory sign-in. Nil when it is not configured.
	//
	// Separate from OIDC rather than one "external identity" field, because both can be configured at once and they
	// answer different problems: OIDC needs a browser that can reach the provider, and a directory does not.
	LDAP *LDAPConfig

	// SecureCookies marks the session cookie Secure. It should be on whenever
	// the server is reachable over anything but plain localhost.
	SecureCookies bool
	// StaticHandler serves the built front end, if one is embedded.
	StaticHandler http.Handler

	// Repos hands out a channel repository per tenant. Nil in single-tenant
	// operation, where Channels is used directly.
	Repos *TenantRepos

	// Runtimes hands out one engine per tenant, nil in single-tenant operation.
	//
	// When set it takes precedence over Runtime, and handlers must reach engines
	// through runtimeFor rather than reading Runtime - a test enforces that,
	// because it is the mistake review cannot catch across sixty handlers.
	Runtimes *TenantRuntimes

	// Runtime owns the running engine. When nil, the interface can edit channels
	// but not run them, and the endpoints that need it say so rather than
	// pretending.
	Runtime *Runtime

	// Version is reported by the probe endpoints. During a rollback, which build
	// is answering is the only question anybody has.
	Version string

	// Settings is every option an operator may change, and the file they live in.
	//
	// Nil on a server started without one, which is legitimate: the engine runs on declared defaults and the settings
	// area says it cannot save rather than presenting a form whose button fails.
	Settings *settings.Store

	// Branding holds a customer's logo, so an installation can present itself as their product.
	//
	// Optional: nil means the built-in mark, and an upload is refused rather than kept in memory.
	Branding *branding.Store

	// FleetLabel names this instance in a fleet view. Empty means the hostname.
	//
	// Configured rather than taken from anything in a message, following the rule
	// that user-visible labels never come from message content.
	FleetLabel string

	// PeersFile is where fleet peers are stored, and empty when this server has nowhere to put them.
	//
	// Needed because peers can now be added from the interface. Without a path the Fleet section says so rather
	// than offering a form whose save would fail.
	PeersFile string

	// SelfURL is this server's own address, used only to refuse a peer that points back here.
	//
	// A self-referencing peer polls this instance through its own HTTP stack and then appears twice in the fleet
	// view - once as this server and once as a peer - which reads as two machines and is one.
	SelfURL string

	// PublicURL is where patients' phones and other organisations reach this server, for the SMART Health Links it hosts. Defaults to
	// SelfURL, which is right only when the two are the same machine name.
	PublicURL string

	// SHLAllowHTTP lets a received SMART Health Link point at plain HTTP. Off: only tests and a laboratory want it.
	SHLAllowHTTP bool

	// Dashboards says which dashboard each role and directory group opens on (-dashboards). Nil: everyone opens on operations.
	Dashboards *dashboards.Assignments

	// ECRTriggers are the trigger codes the case report preview uses: the RCTC when the server was given one (-rctc), else nil
	// for the built-in sample.
	ECRTriggers *publichealth.TriggerSet

	// CRD is the Da Vinci Coverage Requirements Discovery rules, when the payer's CDS service is on.
	CRD *crd.Rules
	// PAS is the FHIR server answering Da Vinci PAS, for the reviewer queue; nil or without PAS, the queue says it is off.
	PAS *fhirserver.Server

	// SMART is the built-in SMART authorization server, and the files its apps and people are kept in, which Users →
	// SMART apps edits. Nil without -smart-clients.
	SMART            *smartauth.Server
	SMARTClientsFile string
	SMARTUsersFile   string

	// VPN reads the partner tunnels listed in -vpn; nil without it.
	VPN *vpn.Monitor

	// DTRPackage runs this server's own DTR $questionnaire-package on a Parameters body, for the console. Nil when no FHIR endpoint
	// is served.
	DTRPackage func(ctx context.Context, params []byte) (status int, body []byte)

	// DICOMweb is the imaging archive served at /dicomweb, nil without -dicomweb-archive; DICOMwebCORS are the browser
	// origins (a viewer such as OHIF served elsewhere) allowed to call it.
	DICOMweb     *dicomweb.Archive
	DICOMwebCORS []string

	// Drugs is the formulary, Real-Time Prescription Benefit and NCPDP SCRIPT ePA, nil without -formulary.
	Drugs *Drugs

	// XDS is the IHE XDS.b registry and repository and XCA responding gateway, nil without -xds. FHIRStore is this server's
	// FHIR store, which MHD resolves patients from and PIXm and PDQm answer from; nil without -fhir.
	XDS *xds.Registry

	// DirectDir is the Direct address book: one PEM file per correspondent, which a direct destination's recipient_certs_dir
	// reads (and which it reads by default).
	DirectDir string

	// ViewsDir keeps SQL on FHIR ViewDefinitions; RunView runs one over the FHIR store (nil without -fhir).
	ViewsDir string
	RunView  ViewRunner
	// Measures evaluates digital quality measures over the FHIR store (nil without -fhir).
	Measures MeasureService

	FHIRStore *fhirserver.Store

	// MCP is the Model Context Protocol server at /mcp, off unless its Enabled says so. FHIRHandler is the FHIR endpoint
	// (with /fhir stripped), which MCP's fhir_search calls with the agent's own token; nil without -fhir.
	MCP         *MCPSettings
	FHIRHandler http.Handler

	// PatientSummary builds this server's own International Patient Summary for a patient, for Health links. Nil when
	// no FHIR endpoint is served.
	PatientSummary func(ctx context.Context, patientID string) (status int, body []byte)

	// CDSClients are the EHRs trusted to call it with signed JWTs.
	CDSClients []*CDSClient

	// TLSCertFile and TLSKeyFile are the server's own certificate and key, when it was started with them.
	//
	// Held as paths rather than as a loaded key pair so they can be read at the moment they are used. Signing is
	// rare, the files are small, and a cached key means a replaced certificate keeps producing signatures against
	// the old one until somebody restarts - with nothing on screen to explain why.
	TLSCertFile string
	TLSKeyFile  string

	// TSAURL is the RFC 3161 time-stamping authority for DSDR signatures on claims attachments. Without one a signature stops
	// short of the XAdES-X-L the guide asks for, and says so.
	TSAURL string

	// CMS0057 describes the FHIR endpoint, so the CMS-0057 page can say which payer APIs this instance can answer.
	CMS0057 CMS0057Status

	// TEFCA is this instance's participation in national exchange, nil when it does not participate.
	//
	// Held as a Participant rather than as a configuration, because a Participant cannot exchange without auditing -
	// there is no unaudited path through it, so nothing here has to remember to record anything.
	TEFCA *tefca.Participant

	// TEFCAAudit is the trail, kept separately so the audit view can read the file rather than only memory.
	TEFCAAudit *tefca.PersistentAuditLog

	// Fleet polls other instances, nil when no peers are configured.
	//
	// Any instance can be the aggregator, because the console ships inside every
	// binary. Mirth needed a separate product for this - a plug-in registering
	// with a Command Center server - because its administrator is a desktop
	// application and there was nowhere in that design to put a fleet view.
	Fleet *peers.Fleet
	// contains filtered or unexported fields
}

Server is the HTTP API.

func (*Server) BeginDraining

func (s *Server) BeginDraining()

BeginDraining marks the instance as no longer accepting new traffic.

Called at the top of shutdown, before anything is actually stopped, so there is a window in which the load balancer can notice and stop routing while the process is still perfectly able to finish what it already accepted.

func (*Server) Draining

func (s *Server) Draining() bool

Draining reports whether shutdown has begun.

func (*Server) Handler

func (s *Server) Handler() http.Handler

Handler returns the routed HTTP handler.

func (*Server) MarkReady

func (s *Server) MarkReady()

MarkReady records that startup finished and channels have been started.

Until this is called, readiness answers no. Reporting ready before channels are up would let a load balancer send messages to an instance with nothing listening, which presents to the sender as a connection refused rather than as a deploy in progress.

func (*Server) OutboundClient added in v0.1.6

func (s *Server) OutboundClient() *http.Client

shlClient fetches a stranger's link. The URL comes from a QR code, so the dial is held to the egress policy at connect time - after DNS, so a name that resolves to a metadata address is caught - and plain HTTP is refused unless the server was told otherwise. OutboundClient is the client this server uses to reach other systems on a user's or EHR's behalf: egress rules apply, and redirects are limited.

func (*Server) Routes

func (s *Server) Routes() []routeRecord

Routes returns every route this server registers, with the role each requires.

Exported so a test can assert over the whole surface rather than over a list of what somebody remembered to write down. Building the handler is cheap and its only side effect is the recording.

func (*Server) ViewLibrary added in v0.1.16

func (s *Server) ViewLibrary() func(ctx context.Context, reference, canonical string) ([]byte, bool)

ViewLibrary is the views directory as the FHIR endpoint's resolver.

type SettingsFiles

type SettingsFiles struct {
	// Alerts is the alert rules file, from -alerts.
	Alerts string

	// OIDC is the single sign-on configuration, from -oidc.
	OIDC string

	// LDAP is the directory configuration, from -ldap.
	LDAP string

	// SAML is the SAML 2.0 sign-in configuration, from -saml.
	SAML string

	// Peers is the fleet peer list, from -peers.
	Peers string
}

SettingsFiles are the configuration files the interface may edit.

Paths come from the flags the process was started with. The interface edits the same files rather than keeping a second copy in the database, which keeps configuration diffable on disk and avoids the question of which source wins.

A file that was not configured has an empty path, and the interface offers to create one rather than silently having nowhere to write.

type StartupSettings

type StartupSettings struct {
	Addr           string   `json:"addr"`
	ChannelsDir    string   `json:"channelsDir"`
	DatabasePath   string   `json:"databasePath"`
	TLS            bool     `json:"tls"`
	MultiTenant    bool     `json:"multiTenant"`
	EngineRunning  bool     `json:"engineRunning"`
	FHIRServing    bool     `json:"fhirServing"`
	StoreMessages  bool     `json:"storeMessages"`
	StorePayloads  bool     `json:"storePayloads"`
	RetentionDays  int      `json:"retentionDays"`
	AllowMetadata  bool     `json:"allowMetadataEgress"`
	JSONLogs       bool     `json:"jsonLogs"`
	Version        string   `json:"version"`
	GoVersion      string   `json:"goVersion"`
	Platform       string   `json:"platform"`
	StartedAt      string   `json:"startedAt"`
	CommandLine    []string `json:"commandLine"`
	FleetLabel     string   `json:"fleetLabel,omitempty"`
	TraceEndpoint  string   `json:"traceEndpoint,omitempty"`
	AlertWebhook   bool     `json:"alertWebhook"`
	AlertSeverity  string   `json:"alertSeverity,omitempty"`
	AuthMethods    []string `json:"authMethods"`
	MetadataReason string   `json:"metadataReason,omitempty"`
}

StartupSettings are what the process was started with.

Read-only, and shown rather than hidden. Somebody diagnosing a problem needs to know how the process was started, and "not shown anywhere" is how a wrong flag survives for months.

type TenantRepos

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

TenantRepos hands out one ChannelRepo per tenant.

Channels stay in files. That is the invariant everything else in this program rests on, and multi-tenancy does not get to break it - it just means a directory per tenant instead of one directory.

The layout, and why it is asymmetric

<base>/*.yaml            the default tenant's channels
<base>/<tenant>/*.yaml   any other tenant's channels

The default tenant uses the base directory itself rather than <base>/main. That asymmetry is deliberate and it is the whole point: every installation running today points -channels at a directory of loose yaml files. Requiring them to become <base>/main/*.yaml would make every existing channel vanish on upgrade, and an operator discovering that has already lost their feeds.

It works because ChannelRepo.files() skips directories and does not recurse, so the default tenant cannot see another tenant's subdirectory. That is load-bearing rather than incidental, and there is a test asserting it.

func NewTenantRepos

func NewTenantRepos(base string) (*TenantRepos, error)

NewTenantRepos roots a set of per-tenant repositories at base.

func (*TenantRepos) Base

func (tr *TenantRepos) Base() string

Base reports the root directory.

func (*TenantRepos) DirFor

func (tr *TenantRepos) DirFor(id tenant.ID) (string, error)

DirFor returns the directory a tenant's channels live in.

Exported because the error messages elsewhere are much more useful when they can name the path somebody needs to look at.

func (*TenantRepos) Forget

func (tr *TenantRepos) Forget(id tenant.ID)

Forget drops a cached repository.

Called when a tenant is deleted. The directory is deliberately left on disk: a channel file is a definition somebody wrote, frequently the only record of how an interface was configured, and deleting a customer should not silently destroy it. Removing the directory is a separate, explicit act.

func (*TenantRepos) Known

func (tr *TenantRepos) Known() ([]tenant.ID, error)

Known returns the tenants that currently have a directory on disk.

Reads the filesystem rather than the cache, because the answer wanted is "what is actually there", which is what somebody investigating a missing channel needs.

func (*TenantRepos) Repo

func (tr *TenantRepos) Repo(id tenant.ID) (*ChannelRepo, error)

Repo returns the repository for a tenant, creating the directory on first use.

type TenantRuntimes

type TenantRuntimes struct {

	// LimitsFor reads a tenant's limits. Nil: no limits are enforced.
	LimitsFor func(context.Context, tenant.ID) (tenant.Limits, error)

	// PHIWatch is the server's patient-data access monitor, given to every tenant's runtime; nil watches nothing.
	// Its channel names carry the tenant (watchName), so one tenant's traffic is never another's baseline.
	PHIWatch *compliance.Monitor
	// contains filtered or unexported fields
}

TenantRuntimes hands out one engine per tenant.

This is the half of tenancy that channel isolation did not cover. Isolating the channel *files* stopped one tenant reading another's definitions; it did nothing about the engine, so start and stop, metrics, the queue and the message store were all shared. One tenant stopping a channel could stop another's, and the metrics endpoint reported the sum of everybody.

Runtimes are created on demand rather than up front, because a platform with fifty tenants where three are active should not hold fifty engines, and a tenant that never signs in should cost nothing.

func NewTenantRuntimes

func NewTenantRuntimes(
	repos *TenantRepos,
	messages *msgstore.Store,
	fhirStore *fhirserver.Store,
	collector *metrics.Collector,
	log *slog.Logger,
) *TenantRuntimes

NewTenantRuntimes prepares the registry.

func (*TenantRuntimes) For

func (tr *TenantRuntimes) For(id tenant.ID) (*Runtime, error)

For returns the runtime for a tenant, creating it if this is the first request.

func (*TenantRuntimes) StopAll

func (tr *TenantRuntimes) StopAll(ctx context.Context)

StopAll stops every tenant's channels.

Called on shutdown. Sequential rather than concurrent: shutdown already has a deadline, and a hundred engines draining queues in parallel makes the slowest one slower rather than the whole thing faster.

func (*TenantRuntimes) StoredBytes added in v0.1.11

func (tr *TenantRuntimes) StoredBytes(ctx context.Context, id tenant.ID) (int64, error)

StoredBytes is how much message content a tenant holds, for the platform's list of tenants and their limits.

type Trend

type Trend struct {
	// Verdicts is the window, oldest first, for a sparkline.
	Verdicts []contractVerdict `json:"verdicts"`

	// Checks is how many are in the window, and Failing how many of those did not hold. A rate is unreadable
	// without both.
	Checks  int `json:"checks"`
	Failing int `json:"failing"`

	// ChangedAt is when the current outcome first appeared, or nil when the whole window agrees. This is the answer
	// to "when did this start", which is the first question after "what changed".
	ChangedAt *time.Time `json:"changedAt,omitempty"`

	// ObservedSince is when this instance started watching. The interface needs it to avoid claiming a duration it
	// cannot support: if ChangedAt equals the first entry, the change may well have happened before we were
	// looking.
	ObservedSince time.Time `json:"observedSince"`

	// FromStartOfWindow is true when the current outcome runs all the way back to the oldest entry, meaning the
	// real change happened earlier than anything recorded - so a duration derived from ChangedAt is a floor, not a
	// measurement.
	FromStartOfWindow bool `json:"fromStartOfWindow"`

	// Flapping reports the outcome changing repeatedly rather than once. It matters because a feed alternating
	// between holding and failing is a different problem from one that broke and stayed broken, and the same
	// number of failures describes both.
	Flapping bool `json:"flapping"`
}

Trend describes a channel's recent contract behaviour.

type ValidationFailure

type ValidationFailure struct {
	Problems []string
}

ValidationFailure carries every problem found in a definition, so the front end can show them all at once.

func (*ValidationFailure) Error

func (e *ValidationFailure) Error() string

type ViewRunner added in v0.1.16

type ViewRunner func(ctx context.Context, v *sof.View, patients []string, limit int) ([]sof.Row, bool, error)

ViewRunner runs a view over this server's FHIR store (fhirserver.Server.RunView).

Jump to

Keyboard shortcuts

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