tefca

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: 17 Imported by: 0

Documentation

Overview

Package tefca implements the parts of TEFCA participation that do not require a QHIN connection.

What is real

Purpose-of-use validation, configuration validation, and the audit trail - including its persistence, its query interface and its obligation to record failed attempts as well as successful ones. That last part is not a technicality: a run of failed deliveries to one recipient is how a misconfigured partner shows up, and a run of failed queries for different patients is how probing shows up.

What is not

The network. There is no QHIN transport in this build, so no exchange happens, and every exchange function refuses with ErrExchangeNotImplemented rather than returning success.

That distinction was not always drawn, and the way it went wrong is worth keeping. All four exchange functions used to validate their inputs, make no network call, and return success; Deliver returned an Accepted response with a tracking identifier built from the clock. The audit layer then recorded each as a completed exchange - in the log that exists to prove what was exchanged when somebody asks months later. Four tests asserted that behaviour and passed.

What completing it requires

Onboarding with a Qualified Health Information Network, mutually authenticated TLS using certificates issued through that process, and either the IHE profiles the Common Agreement started with or Facilitated FHIR as defined by the Sequoia Project's standard operating procedure, effective 8 March 2026. None of it can be written speculatively: it is tested against a real QHIN or it is not tested, and an untested exchange implementation is the same defect as no implementation wearing a better disguise.

Index

Constants

View Source
const (
	PurposeTreatment        = "treatment"
	PurposePayment          = "payment"
	PurposeOperations       = "operations"
	PurposePublicHealth     = "public_health"
	PurposeIndividualAccess = "individual_access"
)

Purpose of Use constants per TEFCA specification.

Variables

View Source
var ErrExchangeNotImplemented = errors.New(
	"tefca: this build has no QHIN transport, so nothing was exchanged. " +
		"Purpose-of-use checking, configuration validation and the audit trail are real and work; the network call is not " +
		"implemented and is refused rather than reported as a success. Onboarding with a QHIN, mutually authenticated TLS and " +
		"either the IHE profiles or Facilitated FHIR are what remain")

ErrExchangeNotImplemented means no transport exists to carry a TEFCA exchange.

Why this error exists rather than a stub that returns success

Every exchange function in this package validated its inputs, made no network call, and returned success. Deliver went further and returned Accepted with a fabricated tracking identifier. The audit layer above then recorded the exchange as having happened - in a persistent log whose entire purpose is to prove what was exchanged when somebody asks months later, prompted by a complaint or an investigation.

The audit code's own comment says that a trail disagreeing with what happened is "worse than no trail, because the trail is what gets believed". That was exactly right, and the layer underneath it made the comment false.

So the functions refuse. A refusal is auditable as a failure, which TEFCA requires anyway, and it cannot be mistaken for an exchange. This project has a standing invariant that a feature which cannot work yet is refused at load rather than silently skipped at run time; this is that rule applied one level down, because the audit trail is real and worth keeping available even while exchange is not.

What is missing

A QHIN transport: mutually authenticated TLS to a Qualified Health Information Network, using certificates issued through QHIN onboarding, speaking either the IHE profiles the Common Agreement started with or Facilitated FHIR as defined by the Sequoia Project's standard operating procedure. None of that can be written speculatively - it is tested against a real QHIN or it is not tested at all.

Functions

func Notify

func Notify(ctx context.Context, cfg TEFCAConfig, req NotificationRequest) error

Notify broadcasts an event notification across the TEFCA network.

func NotifyWithAudit

func NotifyWithAudit(ctx context.Context, cfg TEFCAConfig, req NotificationRequest, auditLog *AuditLog) error

NotifyWithAudit broadcasts a notification and records the exchange whether or not it succeeded.

A notification is the pattern most easily mistaken for not needing an audit entry, because it carries no patient record and asks for nothing back. But it discloses that a named patient had an event at a named organisation, and to everybody subscribed. That is a disclosure, and TEFCA does not exempt it.

func ValidPurpose

func ValidPurpose(purpose string) bool

ValidPurpose reports whether purpose is a recognized TEFCA Purpose of Use.

Types

type AuditLog

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

AuditLog records and queries TEFCA audit entries. It is safe for concurrent use.

func (*AuditLog) Query

func (a *AuditLog) Query(from, to time.Time) []TEFCAAudit

Query returns all audit entries whose timestamp falls within [from, to].

func (*AuditLog) Record

func (a *AuditLog) Record(entry TEFCAAudit)

Record appends an audit entry to the log.

type DeliveryRequest

type DeliveryRequest struct {
	Recipient string
	Purpose   string
	Bundle    []byte // FHIR Bundle JSON
}

DeliveryRequest describes a FHIR bundle push to another TEFCA participant.

type DeliveryResponse

type DeliveryResponse struct {
	Accepted   bool
	TrackingID string
	Reason     string
}

DeliveryResponse describes the outcome of a message delivery.

func Deliver

Deliver pushes a FHIR bundle to the specified recipient via the QHIN.

func DeliverWithAudit

func DeliverWithAudit(ctx context.Context, cfg TEFCAConfig, req DeliveryRequest,
	auditLog *AuditLog) (*DeliveryResponse, error)

DeliverWithAudit pushes a bundle and records the exchange whether or not it succeeded.

Failures are audited too, and that is not a technicality. A run of failed deliveries to one recipient is how a misconfigured partner shows up, and a run of failed queries for different patients is how probing shows up. Auditing only successes hides exactly the patterns an audit trail exists to reveal.

type Exchange

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

Exchange is a configured Facilitated FHIR client.

Holds the registered client_id and the current access token, because re-registering on every query would be both rude and slow, and re-fetching a token that has fifty minutes left is a request nobody needs.

func NewExchange

func NewExchange(participant TEFCAConfig, cfg ExchangeConfig, client *http.Client) (*Exchange, error)

NewExchange builds an exchange from configuration, reading the key, certificate and trust anchors from disk.

Refuses at construction rather than at first use. A participant who has misconfigured their certificate should find out when the server starts, not when the first clinician is waiting for a record - which is the standing rule in this repository that a feature which cannot work is refused at load.

func (*Exchange) Discover

Discover fetches and verifies the partner's UDAP metadata.

Separated from the query so the endpoints can be checked without exchanging anything, which is what the GUI's test button does and what somebody debugging an onboarding needs first.

func (*Exchange) Partner added in v0.1.23

func (e *Exchange) Partner() string

Partner is the responding participant's FHIR base.

func (*Exchange) QueryPatient

func (e *Exchange) QueryPatient(ctx context.Context, req QueryRequest) (*FHIRBundle, error)

QueryPatient performs a Facilitated FHIR patient search against the partner and returns the bundle.

The search parameters are the ones the SOP's patient matching section rests on. Matching on a name alone is refused by the validation in Query below, and for a reason worth restating: a wrong match does not produce an error, it produces somebody else's medical record.

type ExchangeConfig

type ExchangeConfig struct {
	// PartnerFHIRBase is the FHIR base URL of the responding participant. UDAP metadata is discovered relative to it.
	PartnerFHIRBase string `yaml:"partner_fhir_base"`

	// ClientURI identifies this client to the trust community and must appear as a URI in the certificate's subject alternative names.
	ClientURI string `yaml:"client_uri"`

	// TrustAnchorPath holds the community's certificate authorities in PEM form.
	//
	// Required. Verifying a server's signed metadata against the certificate that arrived inside it proves only that one party made both,
	// which was a real SAML defect in this repository: the verifier preferred the embedded certificate and authenticated an attacker as
	// an administrator while every test passed.
	TrustAnchorPath string `yaml:"trust_anchor_path"`

	// ClientName and Contacts go into the software statement at registration. The specification requires a mailto address so that the
	// other side has somebody to tell when an exchange starts failing.
	ClientName string   `yaml:"client_name"`
	Contacts   []string `yaml:"contacts"`

	// Scope is the space-delimited list requested at registration. System scopes, since this is machine to machine.
	Scope string `yaml:"scope"`

	// ClientID, once a server has assigned one. Empty means register on first use.
	ClientID string `yaml:"client_id,omitempty"`

	// AllowUnanchoredMetadata relaxes chain verification of the partner's signed metadata.
	//
	// For sandboxes whose community authority nobody would install. Never for production: with it on, any party can sign a metadata
	// document and be believed about where to send client assertions.
	AllowUnanchoredMetadata bool `yaml:"allow_unanchored_metadata,omitempty"`

	// PurposeCodes chooses the code system the purpose of use is sent in: empty or "tefca" for the TEFCA Exchange
	// Purpose codes the Facilitated FHIR SOP requires, "hl7" for the HL7 v3 PurposeOfUse codes a UDAP community outside
	// TEFCA (HL7 FAST SSRAA) expects. A partner refuses a token request in the system it does not use.
	PurposeCodes string `yaml:"purpose_codes,omitempty"`
}

ExchangeConfig is what a Facilitated FHIR exchange needs beyond the participant details in TEFCAConfig.

func (ExchangeConfig) Validate

func (e ExchangeConfig) Validate() error

Validate checks the exchange configuration.

type FHIRBundle

type FHIRBundle struct {
	Raw []byte // JSON-encoded FHIR Bundle
}

FHIRBundle is a placeholder for a FHIR Bundle resource returned from a query.

type NotificationRequest

type NotificationRequest struct {
	EventType string
	PatientID string
	Bundle    []byte

	// Purpose is why the disclosure is being made.
	//
	// Absent from the first version of this type, which made notification the one pattern that could disclose
	// without stating why. A notification is easy to think of as exempt: it carries no record and asks for nothing
	// back. But it tells everybody subscribed that a named patient had an event at a named organisation, and that
	// is a disclosure. TEFCA does not exempt it, and an audit entry with an empty purpose column is an audit entry
	// that answers no question anybody asks.
	Purpose string
}

NotificationRequest describes an ADT-style event notification broadcast.

type Participant

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

Participant is a configured TEFCA participant that audits every exchange.

Constructed once and reused. The audit log travels with it, so no call site has to remember to pass one - which is the failure mode the wrappers leave open, because a wrapper that takes an audit log as a parameter can be called with a fresh empty one and nobody notices.

func NewParticipant

func NewParticipant(cfg TEFCAConfig, audit *AuditLog) (*Participant, error)

NewParticipant returns a participant, refusing a configuration that cannot work.

Validated here rather than on each call. A participant that exists is a participant that can exchange, and discovering at the moment of a real query that the OID was never set is discovering it at the worst time.

func (*Participant) AttachExchange added in v0.1.23

func (p *Participant) AttachExchange(e *Exchange)

AttachExchange gives the participant its Facilitated FHIR exchange.

func (*Participant) Config

func (p *Participant) Config() TEFCAConfig

Config returns the participant's configuration.

func (*Participant) Deliver

Deliver pushes a bundle to another participant, auditing the attempt.

func (*Participant) Exchange added in v0.1.23

func (p *Participant) Exchange() *Exchange

Exchange is the Facilitated FHIR exchange, nil when none is configured.

func (*Participant) Notify

func (p *Participant) Notify(ctx context.Context, req NotificationRequest) error

Notify broadcasts an event notification, auditing the attempt.

func (*Participant) Query

func (p *Participant) Query(ctx context.Context, req QueryRequest) (*QueryResponse, error)

Query performs an Individual Access Services query, auditing the attempt.

func (*Participant) QueryFHIR added in v0.1.23

func (p *Participant) QueryFHIR(ctx context.Context, req QueryRequest) (*FHIRBundle, error)

QueryFHIR searches the Facilitated FHIR partner for a patient, auditing the attempt whatever its outcome.

The purpose must be one this participant declares: a query under a purpose the organisation never agreed to is refused here rather than by the partner, so the refusal is ours to explain and is in our own trail.

func (*Participant) Retrieve

Retrieve fetches a document, auditing the attempt.

type PersistentAuditLog

type PersistentAuditLog struct {
	AuditLog
	// contains filtered or unexported fields
}

PersistentAuditLog records exchanges to a file as well as in memory.

The in-memory copy is kept because the interface reads recent entries constantly and re-reading a file that grows without bound for every page refresh would make the audit view slower the longer a site has been compliant.

func OpenAuditLog

func OpenAuditLog(path string, retain time.Duration) (*PersistentAuditLog, error)

OpenAuditLog opens or creates an audit file.

Existing entries are read back, so a restart does not present an empty trail - which would read as "no exchanges have happened" rather than "this process started recently".

func (*PersistentAuditLog) Close

func (a *PersistentAuditLog) Close() error

Close flushes and closes the file.

func (*PersistentAuditLog) LastError

func (a *PersistentAuditLog) LastError() error

LastError returns the most recent failure to write the audit file, if any.

Surfaced deliberately. A site whose audit file has become unwritable is out of compliance and needs to know today, not when somebody asks for records.

func (*PersistentAuditLog) Path

func (a *PersistentAuditLog) Path() string

Path returns where the trail is being written, so the interface can say.

func (*PersistentAuditLog) ReadAll

func (a *PersistentAuditLog) ReadAll(ctx context.Context, from, to time.Time) ([]TEFCAAudit, error)

ReadAll returns every entry in the file within a window, including ones outside the memory window.

Reads the file rather than memory, because the questions auditors ask are about last year and memory holds weeks. The context is honoured so a request for a very large range can be abandoned.

func (*PersistentAuditLog) Record

func (a *PersistentAuditLog) Record(entry TEFCAAudit)

Record writes an entry to the file and keeps it in memory.

The file is written first. If the write fails the entry is still kept in memory and the error is returned, because losing the record entirely is the worse outcome and a caller that can report the failure is better than silence - but the caller has to be told, or a full disk becomes a silent gap in a compliance record.

func (*PersistentAuditLog) RecordErr

func (a *PersistentAuditLog) RecordErr(entry TEFCAAudit) error

RecordErr is the same as Record but returns any failure to persist.

Offered because a caller that can tell somebody the audit trail is not being written should be given the chance.

func (*PersistentAuditLog) Skipped

func (a *PersistentAuditLog) Skipped() int

Skipped returns how many lines could not be read when the log was loaded.

Reported rather than hidden. A non-zero count means part of the trail is unreadable, and that is something a site has to know about its own compliance record.

type QueryRequest

type QueryRequest struct {
	PatientID   string
	PatientName string
	DOB         string
	Gender      string
	Purpose     string
}

QueryRequest describes a patient-matching query sent to the TEFCA network.

type QueryResponse

type QueryResponse struct {
	Records   []FHIRBundle
	SourceOrg string
	Timestamp time.Time
}

QueryResponse contains the results of a TEFCA query.

func Query

func Query(ctx context.Context, cfg TEFCAConfig, req QueryRequest) (*QueryResponse, error)

Query performs a TEFCA Individual Access Services query against the configured QHIN endpoint. The context controls cancellation and deadlines.

func QueryWithAudit

func QueryWithAudit(ctx context.Context, cfg TEFCAConfig, req QueryRequest, auditLog *AuditLog) (*QueryResponse, error)

QueryWithAudit performs a TEFCA query and records an audit entry for the exchange regardless of success or failure. Every exchange MUST be audited per TEFCA requirements — including failed attempts, which could indicate probing attacks.

type RetrievalRequest

type RetrievalRequest struct {
	DocumentRef string
	PatientID   string
	Purpose     string
}

RetrievalRequest describes a request to fetch a specific document by reference.

type RetrievalResponse

type RetrievalResponse struct {
	Document    []byte
	ContentType string
	SourceOrg   string
}

RetrievalResponse contains the retrieved document.

func Retrieve

Retrieve fetches a specific document from the TEFCA network by reference.

func RetrieveWithAudit

func RetrieveWithAudit(ctx context.Context, cfg TEFCAConfig, req RetrievalRequest,
	auditLog *AuditLog) (*RetrievalResponse, error)

RetrieveWithAudit fetches a document and records the exchange whether or not it succeeded.

type TEFCAAudit

type TEFCAAudit struct {
	Timestamp     time.Time
	Direction     string // "inbound" or "outbound"
	Purpose       string
	PatientID     string
	RequestingOrg string
	RespondingOrg string
	ExchangeType  string // "query", "delivery", "retrieval", "notification"
	Success       bool
	ErrorDetail   string
}

TEFCAAudit represents a single TEFCA-mandated audit entry.

type TEFCAConfig

type TEFCAConfig struct {
	// OrganizationName is the human-readable name of the participating organization.
	OrganizationName string

	// OrganizationOID is the OID assigned to the organization.
	OrganizationOID string

	// QHINEndpoint is the base URL of the Qualified Health Information Network.
	QHINEndpoint string

	// ParticipantType is one of: provider, payer, public_health.
	ParticipantType string

	// CertificatePath is the filesystem path to the mTLS certificate.
	CertificatePath string

	// KeyPath is the filesystem path to the mTLS private key.
	KeyPath string

	// SupportedPurposes lists the purposes of use this participant supports.
	SupportedPurposes []string
}

TEFCAConfig holds the configuration for a TEFCA network participant.

func (TEFCAConfig) Validate

func (c TEFCAConfig) Validate() error

Validate checks that all required TEFCAConfig fields are present and valid.

Jump to

Keyboard shortcuts

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