bridge

package
v0.8.2 Latest Latest
Warning

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

Go to latest
Published: Oct 19, 2026 License: Apache-2.0 Imports: 54 Imported by: 1

Documentation

Overview

Package bridge orchestrates the Matter bridge runtime: endpoint topology assembly, IM dispatcher wiring, UDP listener, and mDNS advertisement. The bridge is the single composition unit a host starts (node.Node starts it for a host that uses the facade).

Layering: bridge depends on every other Matter sub-package (endpoint, transport/udp, mdns, im) but on no daemon-internal types — the assembled endpoint topology is supplied via the Snapshotter callback, so the host keeps ownership of its device model and this package imports none of it.

Lifecycle: New → [Start] → (optional) [Reassemble] → [Stop]. Start is idempotent (returns ErrAlreadyStarted on a second call). Stop is idempotent and safe to call after a failed Start.

Transports: Start binds the UDP socket and, when Config.TCP.Incoming is set, a TCP listener on the same port (matter.js ServerNetworkRuntime.ts:203-214). Every message enters through one transport-neutral receive path and every reply leaves on the connection the request arrived on (conn.go). Over TCP, MRP is off, session establishment is not served yet (an unsecured message is dropped), and a dropped connection closes every session bound to it. Config.TCP is configuration, not a port: the zero value is TCP off and the bridge behaves exactly as without it.

Bring-up sequence

New takes only a Snapshotter, an mdns.Advertiser and a Config; every other collaborator arrives through an Attach / Set call afterwards. That keeps the constructor free of the daemon's chicken-and-egg cycles (the operational session manager needs the bridge's listener, the bridge needs the manager), at the cost of a bridge that compiles, starts and serves while wired to nothing.

It does not fail loudly when a collaborator is missing. New installs a noop for each unattached port, so a half-wired bridge binds its UDP socket, publishes its mDNS record and answers datagrams — it simply answers them wrongly. The list below is therefore the actual requirement, ordered as a host should call it, each entry stating what a skip costs. Two failure modes recur and are not interchangeable: *fails closed* means requests are refused, which is at least visible as a denial on the controller; *silent* means the reply looks well-formed and the missing behaviour never shows up as an error anywhere.

Before [Start] — these are read once at Start, or feed a component Start constructs, so attaching them later has no effect until the next [Reassemble] (or, for the ACK tracker, not at all on a bridge that is already running):

  • AttachACLLister — FAILS CLOSED. The lister reaches the dispatcher only in reassembleLocked, which Start runs. Without one, TopologyDispatcher.CheckACL denies every fabric-scoped request with UnsupportedAccess (endpoint/dispatcher.go:790-802): a bridge that pairs normally and then answers nothing. A deployment that means to run unguarded passes endpoint.UnenforcedACL — the explicit spelling of the same effect.
  • AttachSessionLookup — silent. The default noopSessionLookup misses on every id, so each encrypted datagram is dropped as ErrSessionMissing. CASE can complete and still carry no traffic.
  • AttachPaseHandlerProvider (or the singleton AttachPaseHandler) — fails closed. The noop returns ErrPaseHandlerMissing for all three PASE opcodes, so commissioning cannot begin. The provider wins when both are wired; prefer it, since the singleton serialises commissioners onto one handler.
  • AttachCaseHandlerProvider (or the singleton AttachCaseHandler) — fails closed via ErrCaseHandlerMissing. Use the provider: a singleton CaseAdapter sticks in Finished after the first Sigma3 and rejects the parallel CASE sessions Apple Home opens from two addresses at once.
  • AttachAckTracker — silent, and wider than its name suggests. It also constructs the outbound reliability tracker, so without it sendReply never sets NeedsAck (reply.go:149,157): the bridge neither emits standalone ACKs nor retransmits its own reliable messages, and every reply degrades to best-effort UDP.
  • AttachAckHandler — silent and narrow: the per-exchange owed-ack tracker an inbound piggybacked ACK discharges (securechannel.go); unwired, the bridge still acknowledges through the AckTracker, it only cannot clear an owed standalone ACK early.
  • AttachSubscriptionManager — silent, and the most deceptive of the set. A Subscribe still returns the complete initial ReportData, but with HasSubscription=false and SubscriptionID=0, and no report ever follows; EmitEvent still appends to the event log and fans out to nobody. The controller sees a successful subscribe and a device that never changes.
  • AttachSessionRegistry — silent. Inbound CloseSession stops closing anything, [Stop] tears sessions down without the CloseSession farewell (leaving controllers on stale sessions after a restart), and the two reverse hooks this call self-wires — the graceful-close notifier and the mDNS reannounce trigger — are never installed. The registry's close hook is wired by the host and calls Bridge.SessionClosed besides the subscription manager's CloseSession; without it a TCP connection whose sessions were reaped or removed with their fabric stays open until the peer closes it.
  • AttachSubscriptionStore — silent. Start loads the previous run's subscriptions from it and clears it; each subscription of a CASE session is written when it becomes active and deleted when it is terminated (docs/adr/0008). Without one nothing survives a restart: every controller waits out its own subscription liveness timeout before it re-subscribes, and the device looks unresponsive for that long. *store.Store satisfies the port. SetSubscriptionPersistence (false) switches recording and re-establishment off altogether — matter.js's `persistenceEnabled`; on by default.

Cluster surface — order matters within this group, but not relative to [Start]: attaching clusters republishes them onto the live topology immediately as well as on the next [Reassemble]:

  • AttachRootClusters, then AttachAggregatorClusters. Skipping either leaves that endpoint with no servers. A concrete-endpoint read then returns UnsupportedCluster, but a wildcard read — what a commissioner actually sends — silently skips the endpoint (endpoint/dispatcher.go:155-169), so the bridge reads as present and empty rather than broken.
  • AttachRootPartsListProvider and AttachAggregatorPartsListProvider MUST follow their AttachRootClusters / AttachAggregatorClusters call: each walks the already-attached slice for the Descriptor cluster (0x001D) and installs the provider on it. Called first, or with no Descriptor mounted, they return false and wire nothing — check the bool, because the only other symptom is a PartsList frozen at whatever the Descriptor was constructed with.

Group communication — needed by any node with a Groups server, which a node with lights or plugs has:

  • AttachGroupMessaging — fails closed, silently. Pass the *groups.Manager GroupKeyManagement and the assembler (endpoint.Config.Groups) were given. Without it every group message is dropped, no multicast group is joined, a fabric removal leaves the group state behind and GroupTable changes reach no subscriber; since a group message is never answered, a controller sees a group command that did nothing. Attach before or after Start; the memberships are joined once both the port and the socket exist. The same port carries each group message's outcome to the Groupcast server's GroupcastTesting.
  • AttachAuxiliaryACL — fails closed, silently; needed once the root mounts the Groupcast server (core.NewGroupcast), which turns on AccessControl's Auxiliary feature. Pass the same *groups.Manager. Without it the dispatcher keeps the feature off: the auxiliary Operate grants a JoinGroup / ConfigureAuxiliaryAcl with UseAuxiliaryAcl creates are listed in AccessControl.AuxiliaryAcl but not enforced, so a group command relying on one is dropped like any other unauthorised group message — unanswered.

Genuinely optional — a skip costs exactly the named feature:

  • AttachCommissioningWindow stores the tracker for Bridge.CommissioningWindow to hand back; the bridge itself never reads it. Skipping it does not affect the mDNS commissionable record, which Bridge.AnnounceCommissioning publishes from its own argument.
  • AttachCaseInitiatorProvider and AttachOperationalResolver, then a call to Bridge.ReestablishFormerSubscriptions once the host's CASE identities are loaded (before it announces its operational records) — silent. Together they re-establish the former subscriptions under their old ids: the bridge resolves each controller's operational instance, opens CASE to it as the initiator, and re-sends the priming report. A skipped piece drops every former subscription; the controllers then recover through their liveness timeout, as without a store. This narrow initiator is the only one in the module (docs/adr/0008).
  • AttachBDXResponder — fails closed. The responder (a *bdx.Responder) serves the BDX exchanges a peer opens on a secure session — an OTA requestor's ReceiveInit for the image a provider offered — on the session the *Init arrived on. Without one such an exchange is acknowledged and dropped as a protocol without a handler (ErrUnknownProtocol), and a requestor's download fails.
  • AttachDiagnosticEvents makes Bridge.DiagnosticEvents non-empty.
  • SetOnReassembled, SetOnFabricAdded and SetOnFabricRemoved are nil-safe observer hooks.
  • SetDeviceTypeValidation — visible either way. Start and every Reassemble judge the topology against its device types; the default, matter.js's warn mode, logs each new violation once ("matter.devicetype.violation") and refuses only a misplaced singleton, strict refuses any new violation, off judges nothing but singleton placement. Bridge.DeviceTypeViolations returns what the installed topology violates (docs/adr/0016).

Index

Constants

View Source
const (
	OwnWindowTimeoutUncommissioned = 48 * time.Hour
	OwnWindowTimeoutCommissioned   = 15 * time.Minute
)

Timeouts of a window the node opens itself, matter.js DeviceCommissioner.ts #ownWindowTimeout: MAXIMUM_COMMISSIONING_TIMEOUT (48 h, Extended Announcement) while the node has no fabric, STANDARD_COMMISSIONING_TIMEOUT (15 min) once it has one (Matter §5.4.2.3).

View Source
const DefaultProbeInterval = 30 * time.Second

DefaultProbeInterval matches the MQTT and SQLite probe cadence so an operator scanning the Diagnostics surface sees every probe update on the same rhythm. Two missed probes downgrade the component to UNKNOWN via the tracker's StaleAfter rule.

View Source
const HealthComponentName = "matter"

HealthComponentName is the tracker component the Matter bridge reports under. Multi-CCU deployments share one bridge instance so the name is intentionally not scoped per central.

Variables

View Source
var (
	// ErrNotStarted surfaces when methods that require a running bridge
	// are called before [Start] (or after [Stop]).
	ErrNotStarted = errors.New("bridge: not started")
	// ErrAlreadyStarted surfaces from a second [Start] call on the
	// same bridge instance.
	ErrAlreadyStarted = errors.New("bridge: already started")
)

Errors.

View Source
var (
	// ErrCaseInitiatorMissing: no [CaseInitiatorProvider] is attached.
	ErrCaseInitiatorMissing = errors.New("bridge: no CASE initiator provider attached")
	// ErrOperationalResolverMissing: no [OperationalResolver] is attached.
	ErrOperationalResolverMissing = errors.New("bridge: no operational resolver attached")
	// ErrCaseRejected: the peer answered the handshake with a failure
	// StatusReport.
	ErrCaseRejected = errors.New("bridge: peer rejected the CASE handshake")
)

Errors of the initiator path.

View Source
var (
	// ErrCommissioningWindowAlreadyOpen surfaces when a second
	// OpenCommissioningWindow lands while a window is already live.
	// The REST handler maps it to 409 Conflict.
	ErrCommissioningWindowAlreadyOpen = errors.New("bridge: commissioning window already open")
	// ErrCommissioningWindowNotConfigured surfaces when the bridge
	// has no passcode configured. The REST handler maps it to 503.
	ErrCommissioningWindowNotConfigured = errors.New("bridge: commissioning not configured (no passcode)")
	// ErrCommissioningWindowDurationInvalid surfaces when the
	// requested duration is outside the §11.19.8.1 valid range.
	// For commissioned bridges the range is [180, 900] s; for
	// uncommissioned devices (FabricCount == 0) the range is
	// [180, 172800] s (48 h).
	// The typed backing value causes the IM dispatcher to return
	// InvalidCommand (0x85) per matter.js
	// AdministratorCommissioningServer.ts:#assertCommissioningWindowRequirements
	// (lines 199-209) which throws Status.InvalidCommand for out-of-range
	// timeout values.
	ErrCommissioningWindowDurationInvalid error = commWindowDurationInvalidErr{}
)

Errors surfaced by the commissioning-window APIs.

View Source
var (
	// ErrPBKDFParamsMissing is returned by [PaseAdapter.ProcessPBKDFParamRequest]
	// when the adapter has not been configured with the bridge's PBKDF
	// salt + iterations via [PaseAdapter.SetPBKDFParams]. Without that
	// the adapter cannot answer a commissioner that asks for the
	// in-band copy of the PBKDF parameters (HasPBKDFParameters=false).
	ErrPBKDFParamsMissing = errors.New("bridge: PBKDF params not configured")

	// ErrUnsupportedPasscodeID is returned by
	// [PaseAdapter.ProcessPBKDFParamRequest] when the commissioner's
	// PBKDFParamRequest carries a non-zero PasscodeId. The default (and
	// only) commissioning passcode lives at PasscodeId 0
	// (DEFAULT_PASSCODE_ID per Matter §4.14.1.2); a non-zero id addresses
	// a multi-passcode onboarding slot the bridge does not implement.
	// Mirrors matter.js packages/protocol/src/session/pase/PaseServer.ts:144-146
	// (throw UnexpectedDataError when passcodeId !== DEFAULT_PASSCODE_ID).
	ErrUnsupportedPasscodeID = errors.New("bridge: unsupported PASE passcode id")
)

Errors specific to the production handler adapters.

View Source
var (
	// ErrUnknownProtocol is logged when ProtocolHeader.ProtocolID is
	// neither [im.InteractionModelProtocolID] nor
	// [mrp.SecureChannelProtocolID]. Per Matter §4.10 the bridge
	// silently drops these.
	ErrUnknownProtocol = errors.New("receive: unknown protocol id")
	// ErrUnsupportedOpcode is logged when an IM datagram carries an
	// opcode the bridge does not service (responses, or future opcodes
	// outside the v1.1 surface).
	ErrUnsupportedOpcode = errors.New("receive: unsupported IM opcode")
	// ErrSessionMissing is logged when an encrypted datagram references
	// a SessionID that the operational session manager does not know.
	// Per Matter §4.6 the bridge silently drops the datagram.
	ErrSessionMissing = errors.New("receive: session not found")
)

Errors surfaced by the receive pipeline. They are not returned to callers (datagrams arrive on a UDP handler with no error channel) — they exist so the bridge can attach them to slog records via slog.String("err", err.Error()) for diagnostics.

View Source
var (
	// ErrReplyEncode wraps any TLV-encoding failure when assembling
	// the response payload.
	ErrReplyEncode = errors.New("reply: encode")
	// ErrReplyEncrypt wraps any AES-CCM seal failure when sealing the
	// response for an encrypted session.
	ErrReplyEncrypt = errors.New("reply: encrypt")
	// ErrReplySend wraps any UDP transmit failure.
	ErrReplySend = errors.New("reply: send")
)

Errors surfaced by the reply path. They are not returned to the UDP handler (which has no error channel); the bridge logs them and drops the reply, leaving the commissioner to retry per MRP.

View Source
var (
	// ErrPaseHandlerMissing is returned when a PASE opcode arrives
	// but no [PaseHandler] is wired. The bridge still drops the
	// datagram with a debug log so PASE traffic during pre-commissioner
	// boot does not flood the warn channel.
	ErrPaseHandlerMissing = errors.New("securechannel: PASE handler not wired")
	// ErrCaseHandlerMissing is returned when a CASE (Sigma) opcode
	// arrives but no [CaseHandler] is wired.
	ErrCaseHandlerMissing = errors.New("securechannel: CASE handler not wired")
)

Errors surfaced by the Secure-Channel routing layer. As with the IM routing errors, these are logged inside the bridge and dropped — the commissioner retries per MRP.

View Source
var (
	// ErrUnsolicitedListenerMissing is returned when the bridge has no
	// UDP listener bound (Stop racing with a tick).
	ErrUnsolicitedListenerMissing = errors.New("bridge: unsolicited send: listener missing")
	// ErrUnsolicitedSessionMissing is returned when the operational
	// session backing a subscription is gone (peer disconnected; the
	// next manager tick reaps the subscription).
	ErrUnsolicitedSessionMissing = errors.New("bridge: unsolicited send: session missing")
	// ErrUnsolicitedEncrypt wraps an AES-CCM seal failure.
	ErrUnsolicitedEncrypt = errors.New("bridge: unsolicited send: encrypt")
)

Errors surfaced by the unsolicited-IM send path. They are logged and dropped — best-effort delivery is the v1.1 mode.

View Source
var ErrAckWaitTimeout = errors.New("bridge: outbound ack wait: timeout")

ErrAckWaitTimeout is returned by [outboundReliableTracker.WaitForAck] when the deadline expires before the peer ACKs the tracked counter. Subscribe-initial chunking uses this to enforce Matter §10.6.6's per-chunk handshake: the sender must wait for a `StatusResponse(SUCCESS)` (which piggybacks an MRP ACK on the chunk's counter) before shipping the next chunk. matter.js mirrors the same flow with `await this.waitForSuccess("DataReport")` in `InteractionMessenger.ts:721`.

View Source
var ErrAdministratorWindowOpen = errors.New("bridge: an administrator's commissioning window is open")

ErrAdministratorWindowOpen is returned by CommissioningWindow.OpenOwnWindow while an administrator's window is open: the node's own window never replaces one (matter.js DeviceCommissioner #open throws MatterFlowError).

View Source
var ErrCommissioningBlocked = errors.New("bridge: commissioning is blocked by the host")

ErrCommissioningBlocked is wrapped by every window open refused while the host has blocked commissioning (CommissioningWindow.SetBlocked). An AdministratorCommissioning command refused this way answers FAILURE: none of the cluster's own statuses means "refused by policy" (matter.js AdministratorCommissioningServer.ts: Busy only for a window already open, :196; PakeParameterError and InvalidCommand for the request, :74-80, :200-209).

View Source
var ErrExchangeClosed = errors.New("bridge: exchange closed")

ErrExchangeClosed is a send or receive on an exchange already closed.

View Source
var ErrGroupOverReliable = errors.New("bridge: group message over a reliable connection")

ErrGroupOverReliable reports a group message that arrived over TCP. Group messages are multicast UDP only.

Functions

func DefaultSessionParameters added in v0.2.0

func DefaultSessionParameters() sigma.SessionParameters

DefaultSessionParameters is what a node advertises in Sigma2 when the host chose nothing: matter.js SessionParameters.defaults (packages/protocol/src/session/SessionParameters.ts) — the MRP intervals of SessionIntervals.defaults (idle 500 ms, active 300 ms, active threshold 4000 ms), this module's DataModelRevision, Interaction Model revision and SpecificationVersion, and MaxPathsPerInvoke equal to what BasicInformation advertises and the invoke path enforces (im.DefaultMaxPathsPerInvoke).

func EncodeInvokeResponse

func EncodeInvokeResponse(ir im.InvokeResponse) ([]byte, error)

EncodeInvokeResponse serialises ir into the TLV bytes that ride as the IM InvokeResponse payload. Uses [defaultCommandFieldsWriter] for response payloads — currently emits an empty struct for non-status responses (covers the v1.1 cluster set, which surfaces status-only commands).

func EncodeReportData

func EncodeReportData(rd im.ReportData) ([]byte, error)

EncodeReportData serialises rd into the TLV bytes that ride as the IM ReportData payload. Uses [defaultAttributeValueWriter] for every attribute value — clusters that need richer value shapes (structs, lists) need a richer writer; the v1.1 measurement + custom-DP cluster set sticks to scalars.

func EncodeStatusResponse

func EncodeStatusResponse(sr im.StatusResponse) ([]byte, error)

EncodeStatusResponse serialises sr into the TLV bytes that ride as the IM StatusResponse payload. The bridge emits this in response to a TimedRequest (always Success in v1.1) and could in principle reply with one for any IM-action error condition (today errors are embedded into the dedicated response shapes).

func EncodeSubscribeResponse

func EncodeSubscribeResponse(sr im.SubscribeResponse) ([]byte, error)

EncodeSubscribeResponse serialises sr into the TLV bytes for an IM SubscribeResponse payload.

func EncodeWriteResponse

func EncodeWriteResponse(wr im.WriteResponse) ([]byte, error)

EncodeWriteResponse serialises wr into the TLV bytes that ride as the IM WriteResponse payload.

func EncodesAttributeValue added in v0.3.0

func EncodesAttributeValue(v any) bool

EncodesAttributeValue reports whether the bridge's attribute writer has an encoding for a Go value a cluster server returns from MatterRead. A value of a type the writer does not know goes on the wire as TLV null (the writer's default arm), which a guard over a fleet's servers must catch before a controller does; a nil value encodes as null by design and reports false too.

func RandomDiscriminator

func RandomDiscriminator() (uint16, error)

RandomDiscriminator returns a uniformly-random 12-bit discriminator suitable for a Matter setup payload. The Matter spec (§5.1.1.1.2) reserves no specific values, so any 0..0xFFF is legal.

func RandomPasscode

func RandomPasscode() (uint32, error)

RandomPasscode returns a uniformly-random 8-decimal-digit passcode from the §5.1.1.1.1 valid-passcode set. Per spec the following are invalid and excluded by re-rolling: the all-same-digit values 00000000..99999999 plus 12345678 and 87654321.

func RandomSalt

func RandomSalt() ([]byte, error)

RandomSalt returns 16 random bytes — the Matter §3.10.1.5 minimum salt length for the Spake2+ Verifier. The spec accepts up to 32 bytes; 16 is the conventional choice.

func SessionParametersFor added in v0.3.0

func SessionParametersFor(transports uint16, maxTCPMessageSize uint32) sigma.SessionParameters

SessionParametersFor is DefaultSessionParameters with the node's transports stated: the SupportedTransports bitmap (sessionparams.TransportTCPClient, sessionparams.TransportTCPServer) and, when it names TCP, MaxTCPMessageSize — matter.js's fallback of 64000 bytes unless the host passes its own bound (SessionParameters.ts:58-63, :152). A node without TCP states neither field.

func StartHealthProbe

func StartHealthProbe(
	ctx context.Context,
	status Status,
	tracker HealthRecorder,
	interval time.Duration,
) func()

StartHealthProbe spawns a goroutine that polls the Matter bridge on a fixed cadence and reports the listener verdict to tracker. The returned stop function cancels the goroutine and waits for it to exit.

Silent when status or tracker is nil — same shape as the MQTT and SQLite probes so callers can wire it unconditionally during boot even when the Matter feature flag is off.

func StartHealthProbeWithClock added in v0.7.2

func StartHealthProbeWithClock(
	ctx context.Context,
	c clock.Clock,
	status Status,
	tracker HealthRecorder,
	interval time.Duration,
) func()

StartHealthProbeWithClock is StartHealthProbe with the probe interval on c instead of the system clock; nil is clock.System.

Types

type AckHandler

type AckHandler interface {
	// Discharge marks the obligation for the (session, exchange, role)
	// triple as fulfilled (the peer ACKed our outbound message).
	// Exchange IDs are only unique per session AND per side, so both
	// extra dimensions are needed to keep concurrent controllers — and a
	// bridge-opened exchange colliding with a peer-opened one — from
	// discharging each other's obligations. initiator is the LOCAL
	// side's role. Returns whether an obligation existed
	// (informational; the router does not branch on the result).
	Discharge(sessionID, exchangeID uint16, initiator bool) bool
}

AckHandler is the inbound MRP-ack port — the mrp.AckTracker typically backs it. The router calls Discharge for any datagram that carries HasAck=true regardless of opcode, so the IM and SecureChannel paths share this hook.

type BDXResponder added in v0.4.0

type BDXResponder interface {
	HandleExchange(ctx context.Context, peer bdx.Peer, first bdx.Message, ex bdx.Exchange)
}

BDXResponder serves the BDX exchanges a peer opens on a secure session — matter.js's BdxProtocol, the protocol handler ExchangeManager hands the first message of a new BDX exchange (BdxProtocol.ts:110-173, onNewExchange; ExchangeManager.ts:407-440). *bdx.Responder implements it.

HandleExchange runs on a goroutine of its own, one per exchange, with the first message already taken off the exchange; the exchange's later messages arrive through ex. It closes ex when it is done.

type Bridge

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

Bridge owns the live Matter runtime: the assembled topology, the dispatcher routing IM Read/Write/Invoke into cluster servers, the UDP listener accepting Matter datagrams, and the mDNS advertiser announcing the bridge to commissioners.

Concurrency: Bridge is safe for concurrent use after Start. The topology / dispatcher pair is swapped atomically on [Reassemble] so in-flight IM dispatches see a consistent view.

func New

func New(snap Snapshotter, advertiser mdns.Advertiser, cfg Config, logger *slog.Logger) (*Bridge, error)

New constructs a Bridge. The snapshotter is required; when advertiser is nil the bridge falls back to mdns.NewNoop, useful for tests and headless boot phases. When logger is nil the bridge uses slog.Default.

It takes no endpoint.Store: the topology arrives fully assembled from snap, so the bridge has nothing to look up. A host still needs a store — to build that topology — but it passes it to its assembler, not here.

New does NOT touch the network — call [Start] for that.

func (*Bridge) AnnounceCommissioning

func (b *Bridge) AnnounceCommissioning(ctx context.Context, params CommissioningAdvertisement) error

AnnounceCommissioning publishes the `_matterc._udp` record (with `_L<long>`, `_S<short>`, `_V<vendor>` subtypes per Matter §4.3.1.4) so commissioners discover the bridge by service-type filter while the window is open. Idempotent — a second call replaces the prior publish.

The instance name is remembered on the bridge so Bridge.WithdrawCommissioning can withdraw the right record on window close.

func (*Bridge) AnnounceFabric

func (b *Bridge) AnnounceFabric(ctx context.Context, compressedFabricID [8]byte, nodeID uint64)

AnnounceFabric re-publishes the operational mDNS record with the supplied CompressedFabricID + NodeID so commissioners can resolve the bridge to its post-commissioning Matter identity (chip-tool's `FindOperationalForStayActive` queries `<compressed>-<node>._matter._tcp.local`).

Idempotent — calling with the same values overwrites the prior publish. Errors fall back to a debug log; the advertiser itself reports its failure modes.

func (*Bridge) AttachACLLister

func (b *Bridge) AttachACLLister(l endpoint.ACLLister)

AttachACLLister wires the Matter ACL source the IM dispatcher enforces against (Matter §9.10). Production daemons MUST pass a `matter/store.Store`-backed lister so operational (CASE) reads / writes / invokes are gated by the AccessControl cluster's stored entries; without one the dispatcher's CheckACL denies every operational request, so a bridge left unwired answers nothing rather than answering everything. The lister is applied to the dispatcher on the next [Reassemble] and on every subsequent rebuild.

A setup that means to run without stored entries passes endpoint.UnenforcedACL.

func (*Bridge) AttachAckHandler

func (b *Bridge) AttachAckHandler(h AckHandler)

AttachAckHandler wires the MRP-ack port. Pass nil to revert to noop.

func (*Bridge) AttachAckTracker

func (b *Bridge) AttachAckTracker(t *mrp.AckTracker)

AttachAckTracker wires the mrp.AckTracker in two roles at once:

  1. The Discharge half (peer ACKs of our outbound messages) goes through AckHandler just like before — the adapter is constructed and registered transparently.
  2. The Owe + Pump half (synthesising StandaloneAck datagrams for un-piggybacked obligations) gets direct access to the tracker. A pump goroutine spawns from Bridge.Start when this method has been called; without it the bridge falls back to noop ACK handling (useful for tests that do not need ACK emission).

Calling AttachAckTracker AFTER Bridge.Start is allowed, but the pump goroutine starts only at the next Start (or never if the bridge is already started — the daemon is expected to wire the tracker before Start). For tests that wire post-Start, schedule pump exercises via direct Bridge.RunAckPumpOnce calls instead.

func (*Bridge) AttachAggregatorClusters

func (b *Bridge) AttachAggregatorClusters(servers []contract.ClusterServer)

AttachAggregatorClusters wires the cluster servers the dispatcher answers for endpoint 1 (Aggregator). Bug C topology fix: matter.js's bridge pattern always lands the Aggregator on its own endpoint with Descriptor (mandatory) + optionally Identify. Apple Home walks RootNode.PartsList → Aggregator → Aggregator.PartsList → bridged devices; without a dedicated Aggregator endpoint the HAP service mapper renders the bridge as empty.

Pass nil to clear. Calling this after Bridge.Start swaps in the new servers on the next Bridge.Reassemble and on the live topology immediately.

func (*Bridge) AttachAggregatorPartsListProvider

func (b *Bridge) AttachAggregatorPartsListProvider(provider func() []uint16) bool

AttachAggregatorPartsListProvider walks the attached aggregator clusters and installs the live PartsList provider on the Aggregator's Descriptor cluster (cluster ID 0x001D on endpoint 1). Returns true when the provider was wired, false when no Descriptor is mounted on the Aggregator.

func (*Bridge) AttachAuxiliaryACL added in v0.2.0

func (b *Bridge) AttachAuxiliaryACL(src endpoint.AuxiliaryACLLister)

AttachAuxiliaryACL wires the auxiliary access control entries of the AccessControl cluster's Auxiliary feature into the dispatcher's access check: a group message is then evaluated against the stored entries plus the auxiliary ones, and a Group entry without targets no longer reaches endpoint 0. Pass the *groups.Manager the Groupcast server was built with — the server turns the feature on in AccessControl, this port makes the dispatcher enforce what that cluster reports. Applied on the next [Reassemble]. nil (the default) keeps the feature off in the dispatcher: a Groupcast auxiliary grant is then not honoured, so group commands relying on it are dropped — fails closed, silently, since a group message is never answered. Mirrors matter.js AccessControlServer #applyFabricAcl installing `[...realAcl, ...auxiliary]` and FabricAccessControl.auxiliaryFeatureEnabled.

func (*Bridge) AttachBDXResponder added in v0.4.0

func (b *Bridge) AttachBDXResponder(r BDXResponder)

AttachBDXResponder installs the responder for the BDX exchanges peers open — an OTA requestor's ReceiveInit, say, for the provider's image. Without one the bridge refuses them as it refuses any protocol it has no handler for: the message is acknowledged when it asks for it and dropped with ErrUnknownProtocol, matter.js ExchangeManager.ts:441-455 for a protocol without a handler (the acknowledgement for a message that requires one, MessageFlowError "Unsupported protocol" otherwise). nil detaches it. Attach before or after Start.

func (*Bridge) AttachCaseHandler

func (b *Bridge) AttachCaseHandler(h CaseHandler)

AttachCaseHandler wires the CASE port. Pass nil to revert to noop.

func (*Bridge) AttachCaseHandlerProvider

func (b *Bridge) AttachCaseHandlerProvider(p CaseHandlerProvider)

AttachCaseHandlerProvider wires a per-exchange CASE provider. When set, the provider takes precedence over Bridge.AttachCaseHandler for every inbound CASE opcode. Pass nil to clear and fall back to the singleton handler.

func (*Bridge) AttachCaseInitiatorProvider added in v0.2.0

func (b *Bridge) AttachCaseInitiatorProvider(p CaseInitiatorProvider)

AttachCaseInitiatorProvider wires the CASE initiator the re-establishment of former subscriptions uses. Unattached (the default), every former subscription is dropped at Bridge.ReestablishFormerSubscriptions — silently: each controller recovers through its own liveness timeout.

func (*Bridge) AttachCommissioningWindow

func (b *Bridge) AttachCommissioningWindow(w *CommissioningWindow)

AttachCommissioningWindow parks the bridge-side CommissioningWindow tracker where Bridge.CommissioningWindow can hand it back. That is the whole effect: no bridge path reads the field, and [Reassemble] does not re-emit from it. Callers must therefore also attach the AdministratorCommissioning cluster server (with the window as its WindowController) to the root endpoint via [AttachRootClusters], and drive the mDNS side through Bridge.AnnounceCommissioning / Bridge.WithdrawCommissioning, which take their parameters as arguments rather than from this tracker.

Pass nil to detach.

func (*Bridge) AttachDiagnosticEvents

func (b *Bridge) AttachDiagnosticEvents(ring *diagevent.Ring)

AttachDiagnosticEvents wires the ring the bridge records pairing and session moments into. Passing nil detaches it.

Safe to call while the bridge serves: the receive path loads the ring atomically, and a nil ring drops the record rather than panicking, so a datagram in flight during an attach or a detach either records into the old ring or into the new one.

func (*Bridge) AttachGroupMessaging added in v0.2.0

func (b *Bridge) AttachGroupMessaging(g GroupMessaging)

AttachGroupMessaging wires group communication: the bridge then authenticates group messages through g and routes a group Invoke or Write to the member endpoints, joins the multicast address of every group with a member endpoint (and leaves it when the group goes), reports GroupKeyManagement.GroupTable changes to subscribers, and hands a removed fabric to g. Pass the *groups.Manager GroupKeyManagement was configured with. nil reverts to the noop, which drops every group message — fails closed, and silently, since a group message is never answered. Mirrors matter.js ServerNetworkRuntime installing ServerGroupNetworking and the session manager's group-session path.

func (*Bridge) AttachOperationalResolver added in v0.2.0

func (b *Bridge) AttachOperationalResolver(r OperationalResolver)

AttachOperationalResolver wires the operational address resolution the re-establishment of former subscriptions uses. Unattached (the default), every former subscription is dropped, as with no initiator.

func (*Bridge) AttachPaseHandler

func (b *Bridge) AttachPaseHandler(h PaseHandler)

AttachPaseHandler wires the PASE port. Pass nil to revert to noop. Calling this twice replaces the previous handler.

The handler is shared across every PASE exchange — appropriate for the v1.1 single-Commissioner-window flow. For concurrent PASE dispatch (multiple commissioners pairing in parallel) use Bridge.AttachPaseHandlerProvider instead; when a provider is wired the singleton handler is ignored.

func (*Bridge) AttachPaseHandlerProvider

func (b *Bridge) AttachPaseHandlerProvider(p PaseHandlerProvider)

AttachPaseHandlerProvider wires a per-exchange PASE provider. When set, the provider takes precedence over Bridge.AttachPaseHandler for every inbound PASE opcode. Pass nil to clear and fall back to the singleton handler.

func (*Bridge) AttachRootClusters

func (b *Bridge) AttachRootClusters(servers []contract.ClusterServer)

AttachRootClusters wires the cluster servers the dispatcher answers for endpoint 0. Pass nil to clear (root endpoint reverts to no cluster servers — every read returns UnsupportedCluster). The supplied slice is defensively copied; subsequent mutations by the caller do not affect the bridge.

Calling this after Bridge.Start swaps in the new servers on the next Bridge.Reassemble *and* on the live topology immediately — chip-tool that has already established a session and started reading must see the cluster set without waiting for a topology re-roll.

func (*Bridge) AttachRootPartsListProvider

func (b *Bridge) AttachRootPartsListProvider(provider func() []uint16) bool

AttachRootPartsListProvider walks the attached root clusters and installs the live PartsList provider on the Descriptor cluster (cluster ID 0x001D). Returns true when the provider was wired, false when no Descriptor is mounted.

func (*Bridge) AttachSessionLookup

func (b *Bridge) AttachSessionLookup(lookup SessionLookup)

AttachSessionLookup wires a session lookup into the bridge after construction. Calling this twice replaces the previous lookup. Pass nil to revert to the no-op.

Wiring is post-construction (rather than a New parameter) because the operational manager itself needs the bridge's UDP listener to drive its session-establishment flows — a chicken-and-egg resolution the daemon orchestrates by passing the bridge to the manager and the manager to the bridge after both exist.

func (*Bridge) AttachSessionRegistry

func (b *Bridge) AttachSessionRegistry(reg SessionRegistry)

AttachSessionRegistry wires the operational session manager into the bridge's Secure-Channel path. Beyond storing the registry for the inbound-CloseSession and shutdown paths, it self-wires the reverse hooks when the registry supports them: the graceful-close notifier (outbound CloseSession StatusReport before key zeroise) and the mDNS reannounce trigger. Pass nil to detach.

The registry's own close hook is the host's to wire (it also feeds the subscription manager): the host calls Bridge.SessionClosed from it, or a TCP connection whose sessions all closed by another path than an inbound or outbound CloseSession stays open until the peer drops it.

func (*Bridge) AttachSubscriptionManager

func (b *Bridge) AttachSubscriptionManager(m *subscription.Manager)

AttachSubscriptionManager wires the subscription manager that powers IM Subscribe. When set, inbound SubscribeRequest datagrams produce:

  1. Initial ReportData (synchronous, with HasSubscription=true and the freshly-allocated SubscriptionID).
  2. SubscribeResponse carrying SubscriptionID + MaxInterval.

The manager's engine ticks ongoing reports through the Bridge.SubscriptionReporter callback the daemon plumbs into subscription.NewManager; that callback re-reads the cached values and ships a fresh ReportData over the original commissioner session, looked up via [exchangeRouting.subTargets] (populated here on successful Subscribe).

Pass nil to revert to noop. The initial ReportData is still built and streamed in full — buildInitialReport runs off the dispatcher and never consults the manager — but it carries HasSubscription=false and SubscriptionID=0, and no report ever follows. The controller reads a successful subscribe and a device that never changes, so treat an unattached manager as a wiring defect rather than a degraded mode.

func (*Bridge) AttachSubscriptionStore added in v0.2.0

func (b *Bridge) AttachSubscriptionStore(s SubscriptionStore)

AttachSubscriptionStore wires where the subscriptions of CASE sessions are persisted. *store.Store satisfies SubscriptionStore. Attach before Bridge.Start: Start loads the previous run's subscriptions from it. Pass nil to revert to the noop store, which persists nothing — silently: a restart then drops every subscription and each controller waits out its own liveness timeout before re-subscribing.

func (*Bridge) Clock added in v0.7.2

func (b *Bridge) Clock() clock.Clock

Clock is the bridge's time source (Config.Clock, or clock.System).

func (*Bridge) CloseFabricSessionsAfterUpdate added in v0.6.0

func (b *Bridge) CloseFabricSessionsAfterUpdate(ctx context.Context, fabricIndex uint8)

CloseFabricSessionsAfterUpdate is the session teardown of UpdateNOC: every other session of fabricIndex is told with a CloseSession and closed at once (Bridge.CloseFabricSessionsExcept), and the session that carried UpdateNOC — named by ctx — is closed after its response, without a CloseSession. Mirrors matter.js Fabric.replaced (Fabric.ts:408-415): every session of the fabric runs initiateForceClose, the invoking one included, which closes once its exchange ends and, marked peer-lost, sends nothing (NodeSession.ts:349-352). The controller reconnects with CASE under the new NOC.

func (*Bridge) CloseFabricSessionsExcept added in v0.3.1

func (b *Bridge) CloseFabricSessionsExcept(fabricIndex uint8, exceptSessionID uint16)

CloseFabricSessionsExcept ends every secure session of fabricIndex but exceptSessionID (0: none is kept), telling each peer with a CloseSession before its keys go. The host calls it after UpdateNOC rotated the fabric's operational identity, with the session that carried the command as exceptSessionID so the NOCResponse still reaches the wire. Needs a SessionRegistry that can close a fabric but one session (operational.Manager.CloseFabricExcept); without one it does nothing.

Mirrors matter.js packages/protocol/src/fabric/Fabric.ts:408-415 `replaced(currentExchange)` (OperationalCredentialsServer.ts:373): each session of the fabric runs `initiateClose`, whose close emits gracefulClose and so a CloseSession (NodeSession.ts:384-393, ExchangeManager.ts:693, :716-726), before `initiateForceClose`. The invoking session is kept open here, where matter.js closes it after its exchange without a CloseSession (the force close marks it peer-lost, NodeSession.ts:349-352); keeping it is the existing rule of operational.Manager.CloseFabricExcept, which this does not change.

func (*Bridge) CommissioningWindow

func (b *Bridge) CommissioningWindow() *CommissioningWindow

CommissioningWindow returns the attached window tracker or nil when none was wired. Useful for tests + the daemon's AdministratorCommissioning cluster construction.

func (*Bridge) ConnectOtaProvider added in v0.3.0

func (b *Bridge) ConnectOtaProvider(ctx context.Context, fabricIndex uint8, providerNodeID uint64) (core.OtaProviderSession, error)

ConnectOtaProvider implements core.OtaProviderConnector: it returns a secure session with the provider, reusing the newest live one or establishing one by CASE as this node's initiator (matter.js OtaSoftwareUpdateRequestorServer.ts:553-565 #connectOtaProviderFor → ServerNode.peers.forAddress → Peer.connect). Needs the collaborators ADR 0008 attaches: Bridge.AttachCaseInitiatorProvider and Bridge.AttachOperationalResolver.

func (*Bridge) ControllerRTT

func (b *Bridge) ControllerRTT() MRPRTTStats

ControllerRTT summarises how long paired Matter controllers are taking to acknowledge this bridge's reliable messages. Diagnostic only — see [mrpRTTWindow] for why this stays off the cluster surface.

Safe on a nil bridge (Matter disabled), which reports an empty summary.

func (*Bridge) DeviceLoad added in v0.2.0

func (b *Bridge) DeviceLoad(fabricIndex uint8) mattercore.DeviceLoadStruct

DeviceLoad reports the node's Interaction Model load for the accessing fabric, as DeviceLoadStatus carries it. Each count saturates at its field's maximum, as matter.js clamps them.

func (*Bridge) DeviceTypeViolations added in v0.2.0

func (b *Bridge) DeviceTypeViolations() []endpoint.DeviceTypeViolation

DeviceTypeViolations returns what the validation of the installed topology found — the current violations, endpoint by endpoint, whether or not they were logged before (matter.js violationsOf). Empty in mode off and before Start.

func (*Bridge) DiagnosticEvents

func (b *Bridge) DiagnosticEvents() []diagevent.Event

DiagnosticEvents returns the recorded trace, newest first. Empty when no ring is attached.

func (*Bridge) Dispatcher

func (b *Bridge) Dispatcher() im.Dispatcher

Dispatcher returns the currently active IM dispatcher. Returns nil when the bridge has not been started.

func (*Bridge) EmitEvent

func (b *Bridge) EmitEvent(endpoint uint16, cluster, event uint32, data any, priority contract.EventPriority)

EmitEvent is the bridge-side contract.EventEmitter implementation. Cluster servers (or model-side cluster servers that implement contract.EventReceiver) call this when the underlying DP fires an event that should surface to subscribers. Non-blocking — fan-out into per-subscription queues happens in the manager; the engine tick later drains them.

In addition to the live subscription fan-out, EmitEvent appends the event to the bridge's persistent im.EventLog so controllers that send a ReadRequest with EventRequests (e.g. chip-tool's `read-event-by-id`, Apple MTRDevice liveness checks) can retrieve historical events — Matter §10.6.6 requires Critical events to be buffered and answerable out-of-band from live subscriptions.

`endpoint` is the Matter endpoint ID hosting the cluster (assigned by the topology assembler). `cluster` is the cluster ID (e.g. 0x003B Switch). `event` is the event ID per the cluster spec (e.g. 0x00 InitialPress).

func (*Bridge) EmitFabricAdded

func (b *Bridge) EmitFabricAdded(fabricIndex uint8)

EmitFabricAdded is the bridge-side dispatch helper the OperationalCredentials cluster's `OnFabricInstalled` closure calls on a successful AddNOC. Forwards to whatever closure the daemon wired via [SetOnFabricAdded]; nil-safe.

func (*Bridge) EmitFabricRemoved

func (b *Bridge) EmitFabricRemoved(fabricIndex uint8)

EmitFabricRemoved is the bridge-side dispatch helper the daemon invokes inside the core.OperationalCredentials.SetOnFabricRemoved closure. Forwards to whatever closure the daemon wired via [SetOnFabricRemoved]; nil-safe.

func (*Bridge) EmitFabricRemovedContext added in v0.2.0

func (b *Bridge) EmitFabricRemovedContext(ctx context.Context, fabricIndex uint8)

EmitFabricRemovedContext is Bridge.EmitFabricRemoved for a removal the host learns about inside the RemoveFabric command (ctx is the command's context): it also ends every secure session of the removed fabric. Every session but the one carrying the command is told with a CloseSession and closed at once; the invoking session, which must still carry the NOCResponse, is closed once the command's response is out, without a CloseSession. Outside a command every session of the fabric is told and closed at once. Without the close a controller whose fabric is gone kept being answered on it (TC-CADMIN-1.15 step 12). Needs a SessionRegistry that can close by fabric (the operational manager does).

Mirrors matter.js packages/protocol/src/fabric/Fabric.ts:423-432 `leave(currentExchange)` (OperationalCredentialsServer.ts:425 `fabric.leave(this.context.exchange)`): each session of the fabric runs `initiateClose` — a session without active exchanges closes at once and its close emits gracefulClose (NodeSession.ts:384-393), which the ExchangeManager answers with a CloseSession on a fresh exchange (ExchangeManager.ts:693, :716-726 #sendCloseSession); the session of the current exchange defers its close until that exchange ends (NodeSession.ts:337-347, Session.ts:292-308). `delete` → `#closeSessions` (Fabric.ts:440-465) then force-closes what is left with the current exchange: `NodeSession.initiateForceClose` marks the session peer-lost (NodeSession.ts:349-352), so the deferred close of the invoking session emits no gracefulClose — it closes after its exchange without a CloseSession.

func (*Bridge) EventLog

func (b *Bridge) EventLog() *im.EventLog

EventLog returns the bridge's persistent event buffer. Callers (e.g. the IM ReadRequest handler) query it to answer historical event read requests (Matter §10.6.6, chip-tool `read-event-by-id`). Never nil.

func (*Bridge) ForgetSigma1Replied

func (b *Bridge) ForgetSigma1Replied(exchangeID uint16)

ForgetSigma1Replied is the exported entry point the daemon wires into PerExchangeCaseProvider.SetOnEvict. The Sigma3 success path already calls [forgetSigma1Replied]; an ABORTED handshake (Sigma1 received, Sigma3 never) leaves its dedupe entry orphaned forever because the successful-path cleanup never runs. Routing the TTL reaper's eviction through here lets a per-exchange provider clean up aborted handshakes so the sigma1Replied map cannot grow without bound on a daemon that sees many half-completed CASE attempts.

The eviction also ends the handshake's hold on a TCP connection it ran on ([Bridge.holdHandshake]): the handshake can no longer complete.

func (*Bridge) FormerSubscriptionCount added in v0.2.0

func (b *Bridge) FormerSubscriptionCount() int

FormerSubscriptionCount reports how many subscriptions of the previous run are waiting for Bridge.ReestablishFormerSubscriptions.

func (*Bridge) IncreaseConfigurationVersion added in v0.2.0

func (b *Bridge) IncreaseConfigurationVersion(scope, deviceAddress string) ConfigurationVersions

IncreaseConfigurationVersion raises the BridgedDeviceBasicInformation ConfigurationVersion of every bridged endpoint backed by the given physical device (scope + deviceAddress, as for Bridge.NotifyDeviceReachable) and, once, the node's BasicInformation ConfigurationVersion — a bridged node's configuration change is the bridge's too — and reports the changes to subscribers. Call it when the device's functionality changes (a thermostat rewired to support a new mode). Mirrors matter.js BridgedDeviceBasicInformationServer.increaseConfigurationVersion, which raises the root's BasicInformationServer version when called standalone (TC-BRBINFO-3.2).

func (*Bridge) IncreaseNodeConfigurationVersion added in v0.2.0

func (b *Bridge) IncreaseNodeConfigurationVersion() uint32

IncreaseNodeConfigurationVersion raises the root BasicInformation ConfigurationVersion — call it when the node's functionality changes in a way controllers should detect — reports it to subscribers and returns the new value for the host to persist. Zero when no BasicInformation server is attached to the root endpoint. Mirrors matter.js BasicInformationServer.increaseConfigurationVersion.

func (*Bridge) LocalAddr

func (b *Bridge) LocalAddr() string

LocalAddr returns the effective UDP bind address — useful when Listen=":0" lets the OS pick the port. Returns nil when not started.

func (*Bridge) MatterEmitEvent

func (b *Bridge) MatterEmitEvent(endpoint uint16, cluster, event uint32, data any, priority contract.EventPriority)

MatterEmitEvent is an alias of Bridge.EmitEvent satisfying contract.EventEmitter — the cluster-side hook signature the bridge passes to model-side receivers via SetMatterEventEmitter.

func (*Bridge) NotifyDeviceReachable

func (b *Bridge) NotifyDeviceReachable(scope, deviceAddress string, reachable bool)

NotifyDeviceReachable fires the §9.13.6 BridgedDeviceBasicInformation ReachableChanged event for every bridged endpoint backed by the given physical device (scope + deviceAddress) when its availability flips. Both coordinates are the owner's own — see endpoint.Snapshot.Scope and endpoint.Spec.DeviceAddress — so one device's several endpoints are addressed together without this package knowing what a device is.

The attribute itself self-heals — cluster servers are reconstructed per dispatch and read dev.Available() live (see endpoint/materialize.go) — but a Read alone does not push the change to an active subscription. This method supplies the event half: the daemon subscribes to the central's device-availability lifecycle signal and calls in here so subscribed commissioners (Apple Home, Google Home) learn about a CCU device dropping offline without polling.

Mirrors matter.js BridgedDeviceBasicInformationServer, where mutating `reachable` triggers `events.reachableChanged.emit({ reachableNewValue })`. We route the same event through the bridge's MatterEmitEvent pipeline, addressed to each matching bridged endpoint.

func (*Bridge) OpenSessionExchange added in v0.4.0

func (b *Bridge) OpenSessionExchange(ctx context.Context, sessionID uint16) (bdx.Exchange, error)

OpenSessionExchange implements core.BDXExchangeOpener: a new exchange this node initiates on the secure session sessionID — the session a command arrived on, for the BDX transfer chip's DiagnosticLogs server opens there (connectedhomeip BDXDiagnosticLogsProvider.cpp:67-70 at the harness pin, exchangeMgr->NewContext(sessionHandle)). Called from that command's handler, it hands the session's turn on (session_order.go): the handler goes on to wait for the peer's SendAccept on this session.

func (*Bridge) Reassemble

func (b *Bridge) Reassemble(ctx context.Context) error

Reassemble re-runs the topology assembly against a fresh snapshot. Call after the model registry sees changes (device added / removed, channel configuration changed, …). The new topology and a fresh dispatcher swap in atomically so in-flight IM dispatches see a consistent view at every point.

Returns ErrNotStarted when the bridge has not been [Start]ed.

func (*Bridge) ReestablishFormerSubscriptions added in v0.2.0

func (b *Bridge) ReestablishFormerSubscriptions(ctx context.Context) ReestablishResult

ReestablishFormerSubscriptions re-establishes the subscriptions of the previous run, loaded at Bridge.Start, towards the controllers that held them. For each peer it obtains a CASE session — opening one as the initiator, within subscription.ReestablishTimeout — then re-creates each subscription under its old id and sends its priming report on a fresh exchange, without a SubscribeResponse: the controller still believes the subscription exists.

Mirrors matter.js SubscriptionsServer.reestablishFormerSubscriptions (packages/node/src/behavior/system/subscriptions/SubscriptionsServer.ts): peers are handled in parallel, a peer that begins a normal subscribe meanwhile is skipped, a peer that cannot be reached is skipped entirely, and a peer's loop stops at a transport failure or an InvalidSubscription answer. A subscription that does not come back is not recorded again — it is dropped. The former set is consumed: a second call does nothing.

A host calls it once its CASE identities are loaded and before it announces its operational records; matter.js runs it in CommissioningServer before enterOperationalMode. Nothing here fails the bridge; every failure costs only that peer's re-establishment.

func (*Bridge) RunAckPumpOnce

func (b *Bridge) RunAckPumpOnce(now time.Time) int

RunAckPumpOnce is the test-side entry point — drains the tracker's Due(now) once and emits a StandaloneAck for each, returning the number emitted. Production code uses [Bridge.runAckPump] instead.

func (*Bridge) SessionClosed added in v0.3.0

func (b *Bridge) SessionClosed(sessionID uint16)

SessionClosed tells the bridge that secure session sessionID is gone. The host calls it from the session registry's close hook — the hook that also cascades into the subscription manager (operational.Manager.SetOnSessionClose) — so every close path reaches it: CloseSession, fabric removal, NOC rotation, the idle reaper, a peer-loss eviction. The bridge forgets the session's route and closes a TCP connection whose last session this was (unbindSessionRoute; matter.js ExchangeManager.ts:674-681). Calling it for a session the bridge never routed, or twice, is a no-op.

func (*Bridge) SetDeviceTypeValidation added in v0.2.0

func (b *Bridge) SetDeviceTypeValidation(mode endpoint.DeviceTypeValidationMode)

SetDeviceTypeValidation selects what an assembly does with a topology that departs from its device types — matter.js's `endpoint.validation`:

A refused topology is not installed: Bridge.Start or Bridge.Reassemble returns the *endpoint.DeviceTypeConformanceError, naming every endpoint, device type and requirement, and the bridge keeps serving the topology it had. Call before Bridge.Start; a later call takes effect at the next Reassemble and forgets what was reported.

func (*Bridge) SetOnFabricAdded

func (b *Bridge) SetOnFabricAdded(fn func(fabricIndex uint8))

SetOnFabricAdded wires the post-AddNOC hook. The bridge does not install fabrics itself — the OperationalCredentials cluster does — but the OpCreds wiring inside the daemon delegates here so a downstream listener (typically the WS event publisher) can fan the event out without taking a direct dependency on the cluster. Pass nil to detach.

func (*Bridge) SetOnFabricRemoved

func (b *Bridge) SetOnFabricRemoved(fn func(fabricIndex uint8))

SetOnFabricRemoved wires the post-RemoveFabric hook. Mirrors [SetOnFabricAdded]'s nil-safe semantics. Used by the daemon's WS-event publisher; the operational + subscription manager cleanup runs separately on core.OperationalCredentials.SetOnFabricRemoved.

func (*Bridge) SetOnReassembled

func (b *Bridge) SetOnReassembled(fn func(endpointCount int))

SetOnReassembled wires the post-Reassemble hook. The bridge calls the supplied closure with the freshly-assembled bridged-endpoint count (excluding the root and the aggregator) after every successful reassembly — initial Start, manual Reassemble, and any allowlist-driven re-build. Pass nil to detach.

func (*Bridge) SetSubscriptionPersistence added in v0.2.0

func (b *Bridge) SetSubscriptionPersistence(enabled bool)

SetSubscriptionPersistence switches subscription persistence and the re-establishment of former subscriptions on or off. On by default, as matter.js SubscriptionsServer `persistenceEnabled`. Off, nothing is recorded, nothing is loaded at Start and Bridge.ReestablishFormerSubscriptions does nothing.

func (*Bridge) Start

func (b *Bridge) Start(ctx context.Context) error

Start brings the bridge online: assembles the initial topology, constructs the dispatcher, opens the UDP listener, and publishes the operational mDNS record (when the topology has at least one fabric — for v1.1 GA the operational record is emitted unconditionally; commissioning-window logic lands in v1.1.x).

Start blocks only on the synchronous setup steps; the receive loop runs in a background goroutine. Returns immediately when the listener is bound and the goroutine is running. Cancel ctx (or call [Stop]) to tear everything down.

Calling Start a second time on the same instance returns ErrAlreadyStarted.

func (*Bridge) Stop

func (b *Bridge) Stop(ctx context.Context) error

Stop tears down the bridge: gracefully closes every secure session (best-effort CloseSession StatusReport per peer, capped by [stopSessionCloseBudget]), closes the UDP listener, withdraws every published mDNS record, and waits up to ctx's deadline for the serve goroutine to exit. Idempotent — a second call is a no-op. Safe to call after a failed [Start].

func (*Bridge) SubscriptionEventReporter

func (b *Bridge) SubscriptionEventReporter() subscription.EventReporter

SubscriptionEventReporter is the symmetrical hook for ongoing EVENT reports. The daemon wires this via subscription.Manager.SetEventReporter so a cluster-server-fired event lands on every subscription whose EventPaths cover the (endpoint, cluster, event) triple.

The closure encodes the events under the same per-subscription [subTarget] used for attribute reports, so a single peer session receives both report types interleaved.

func (*Bridge) SubscriptionPersistenceEnabled added in v0.2.0

func (b *Bridge) SubscriptionPersistenceEnabled() bool

SubscriptionPersistenceEnabled reports the switch Bridge.SetSubscriptionPersistence sets.

func (*Bridge) SubscriptionReporter

func (b *Bridge) SubscriptionReporter() subscription.Reporter

SubscriptionReporter returns the subscription.Reporter closure the daemon plugs into subscription.NewManager. The closure looks up the per-subscription [subTarget], reads the dirty paths through the dispatcher, and ships a fresh ReportData over the same session.

Reliability: when the bridge has an outbound MRP tracker wired (the standard daemon path), each ReportData is shipped with NeedsAck=true and registered with the tracker — the ACK pump retransmits until the peer ACKs or the retransmit cap fires. On cap exhaustion the [Bridge.releaseReportCounter] callback reaps the dead subscription so the manager stops emitting against a vanished peer. Replay after a peer reconnect relies on the manager's dirty-flag re-emission rather than a side replay buffer — every attribute change re-marks the path dirty, so the next healthy tick ships a fresh ReportData.

func (*Bridge) Topology

func (b *Bridge) Topology() *endpoint.Topology

Topology returns the currently assembled topology. Returns nil when the bridge has not been started; safe to call concurrently with [Reassemble].

func (*Bridge) WithdrawCommissioning

func (b *Bridge) WithdrawCommissioning(ctx context.Context)

WithdrawCommissioning withdraws the most recent Bridge.AnnounceCommissioning publish so commissioners no longer discover the bridge once the window has closed. No-op when no publish is on record.

func (*Bridge) WithdrawFabric

func (b *Bridge) WithdrawFabric(ctx context.Context, compressedFabricID [8]byte, nodeID uint64)

WithdrawFabric retracts the operational `_matter._tcp` instance for the given (compressedFabricID, nodeID) identity — the counterpart to Bridge.AnnounceFabric for RemoveFabric and for an UpdateNOC that changed the NodeID. A republish alone cannot retire the record: the advertiser re-announces what it still holds, so the stale <compressedID>-<nodeID> instance keeps answering until its TTL and commissioners resolve an identity that no longer exists. Mirrors matter.js DeviceAdvertiser.ts:76-86 (close the fabric's advertisements on update/delete before re-advertising).

type CaseAdapter

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

CaseAdapter wraps a sigma.Responder in the CaseHandler port. The responder owns its own state machine — Sigma1 → Sigma2 → Sigma3 advance through the responder's internal phase tracking.

Sigma2_Resume (Matter §4.13.2.4) is supported when the Responder has been wired with a ResumptionStore via sigma.Responder.SetResumptionStore: a Sigma1 carrying valid resumptionId + initiatorResumeMic triggers a one-round-trip resume instead of Full Sigma.

func NewCaseAdapter

func NewCaseAdapter(r *sigma.Responder) *CaseAdapter

NewCaseAdapter wraps r.

A responder without session parameters of its own is given DefaultSessionParameters: matter.js always answers Sigma1 with its SessionParameters (CaseServer.ts:258-264 `responderSessionParams: this.#sessions.sessionParameters`), and a controller that finds none assumes MaxPathsPerInvoke 1 and the oldest revisions — the CHIP Python harness then batches two commands into a device it believes accepts one and fails TC-IDM-1.4 when the device, rightly by its own BasicInformation, accepts them.

func (*CaseAdapter) ProcessSigma1

func (a *CaseAdapter) ProcessSigma1(payload []byte) (opcode uint8, respPayload []byte, err error)

ProcessSigma1 hands the wire payload to sigma.Responder.ProcessSigma1WithResume. When the Sigma1 carries a valid resumptionId + initiatorResumeMic pair AND the responder has a ResumptionStore wired, the fast Sigma2_Resume path is taken: the reply uses opcode SCOpcodeSigma2Resume and the session is established immediately — no Sigma3 is expected. Otherwise Full Sigma runs as before.

Mirrors matter.js packages/protocol/src/session/case/CaseServer.ts::#handleSigma1.

func (*CaseAdapter) ProcessSigma2Resume

func (a *CaseAdapter) ProcessSigma2Resume(_ []byte) (opcode uint8, respPayload []byte, err error)

ProcessSigma2Resume handles an inbound Sigma2_Resume (opcode 0x33) which would arrive only if the bridge were acting as CASE initiator resuming a prior session. The bridge is always a CASE responder, so in practice this path is never reached — the bridge SENDS opcode 0x33 (via ProcessSigma1 on the resume fast path) and does NOT receive it. Implemented as a stub that rejects with an error; the commissioner (if it ever sends 0x33 in error) falls back to a fresh Sigma1.

func (*CaseAdapter) ProcessSigma3

func (a *CaseAdapter) ProcessSigma3(payload []byte) (opcode uint8, respPayload []byte, err error)

ProcessSigma3 hands the wire payload to sigma.Responder.ProcessSigma3. On success the optional CaseAdapter.SetOnSessionEstablished callback fires with the derived sigma.SessionKeys and the adapter returns a Secure-Channel StatusReport(Success/Success) for the caller to ship — Apple Home (and matter.js by extension) only starts operational reads on the freshly-established CASE session AFTER receiving this success report. Without it Apple retransmits Sigma1 every ~30s and the pairing UI hangs at "verbinden …".

func (*CaseAdapter) SetOnSessionEstablished

func (a *CaseAdapter) SetOnSessionEstablished(cb CaseSessionEstablished)

SetOnSessionEstablished installs a callback that fires after a successful Sigma3 verification. Pass nil to clear.

func (*CaseAdapter) SetResponder

func (a *CaseAdapter) SetResponder(r *sigma.Responder)

SetResponder swaps the underlying sigma responder. Called after AddNOC installs a real fabric so subsequent CASE handshakes use the persisted NOC + ICAC instead of the empty ephemeral identity the daemon boots with.

func (*CaseAdapter) SnapshotResponder

func (a *CaseAdapter) SnapshotResponder() *sigma.Responder

SnapshotResponder is the public read-only accessor for the wrapped responder. Callers use it from inside the OnSessionEstablished callback to lift state (peer node id, peer session id, …) the callback signature does not expose directly.

type CaseHandler

type CaseHandler interface {
	// ProcessSigma1 handles the commissioner's Sigma1 and produces
	// Sigma2 (opcode 0x31).
	ProcessSigma1(payload []byte) (uint8, []byte, error)
	// ProcessSigma3 handles Sigma3 and produces no reply on success
	// (the operational session is registered out-of-band). May
	// return a status ack.
	ProcessSigma3(payload []byte) (uint8, []byte, error)
	// ProcessSigma2Resume handles the resumption variant. Returns
	// either a Sigma2_Resume reply or an error rejecting the
	// resumption (commissioner falls back to full Sigma1).
	ProcessSigma2Resume(payload []byte) (uint8, []byte, error)
}

CaseHandler is the inbound CASE (Sigma) port — typically backed by `secure/sigma.Responder`.

type CaseHandlerProvider

type CaseHandlerProvider func(exchangeID uint16) CaseHandler

CaseHandlerProvider returns a CaseHandler scoped to a single CASE exchange. The bridge's SecureChannel router calls the provider once per inbound CASE opcode (Sigma1, Sigma3, Sigma2Resume) and dispatches to the returned handler. Returning a fresh handler per exchange-id is required because Apple Home opens parallel CASE sessions from different IPs (iPhone over IPv6, HomePod over IPv4) — a singleton CaseAdapter gets stuck in `Finished` after the first successful Sigma3 and rejects every subsequent Sigma1 with `ProcessSigma1 already called`.

Returning nil drops the datagram with a debug log — the commissioner retries.

type CaseInitiation added in v0.2.0

type CaseInitiation struct {
	// Initiator is a fresh [sigma.NewPeerInitiator] for the peer, carrying
	// this node's identity on the peer's fabric, the local session id it
	// reserved, and — when the host holds one — the resumption record of
	// the peer (matter.js CaseClient offers resumption whenever
	// SessionManager has a record for the peer address).
	Initiator *sigma.Initiator
	// CompressedFabricID names the peer's operational DNS-SD instance.
	CompressedFabricID [8]byte
	// OnEstablished registers the session under Initiator.SessionID()
	// (secure/operational.Manager.OpenFromSigmaAsInitiatorWithID) and may
	// persist the new resumption record. ctx is the handshake's. Returning
	// an error abandons the session.
	OnEstablished func(ctx context.Context, result sigma.InitiatorResult) error
	// OnAbandoned, optional, releases what the provider reserved — the
	// local session id — when the handshake does not complete.
	OnAbandoned func()
}

CaseInitiation is one CASE handshake this node opens as the initiator, prepared by the host's CaseInitiatorProvider.

type CaseInitiatorProvider added in v0.2.0

type CaseInitiatorProvider func(ctx context.Context, fabricIndex uint8, peerNodeID uint64) (*CaseInitiation, error)

CaseInitiatorProvider prepares a CASE handshake towards peerNodeID on the fabric at fabricIndex. It is consulted only to re-establish former subscriptions; ctx bounds the attempt.

type CaseSessionEstablished

type CaseSessionEstablished func(keys sigma.SessionKeys, peerSessionID uint16) error

CaseSessionEstablished fires after a successful Sigma3 verification. keys are the derived I2R/R2I/AttestationChallenge bundle from sigma.Responder.SessionKeys; peerSessionID is the InitiatorSessionID the commissioner sent in Sigma1 — the bridge stamps it into outbound Header.SessionID so the peer resolves the session in its own table. Implementations typically hand both to [operational.Manager.OpenFromSigmaWithID].

type CommissioningAdvertisement

type CommissioningAdvertisement struct {
	Discriminator      uint16
	VendorID           uint16
	ProductID          uint16
	NodeLabel          string
	CommissioningMode  uint8
	DeviceTypeID       uint32
	PairingHint        uint16
	PairingInstruction string
	InstanceID         [8]byte

	// RotatingID is the optional Matter §5.4.2.4 "Rotating Device
	// Identifier" — 36 uppercase-hex characters when present, empty
	// to suppress the `RI` TXT key. Callers compute it via
	// [mdns.GenerateRotatingID] from a persisted UniqueID +
	// LifetimeCounter pair; the bridge merely forwards the precomputed
	// value into the mDNS service config.
	RotatingID string
}

CommissioningAdvertisement describes the parameters the bridge publishes on `_matterc._udp` while a commissioning window is open. All fields are stamped from the configured / ephemeral commissioning state held by the CommissioningWindow and the daemon's setup payload — the bridge itself does not know about passcodes.

type CommissioningWindow

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

CommissioningWindow tracks an open enhanced-commissioning window. The bridge owns at most one live window at a time per ADR 0012's single-PASE-acceptor design — concurrent windows would require multiple verifiers, which is a separate v1.2 extension.

Window state transitions:

closed → OpenWindow → enhanced
enhanced → RevokeWindow → closed (early)
enhanced → closeTimer fires → closed (expired)

Each transition fires `onTransition` (for the WS event publisher + the AdministratorCommissioning cluster's WindowStatus attribute) and, when set, the `restore` closure the [CommissioningWindowOpener.EphemeralProvider] returned at open — guaranteeing the bridge's long-lived PASE acceptor is reinstated regardless of whether the window closed early or by timeout.

func NewCommissioningWindow

func NewCommissioningWindow() *CommissioningWindow

NewCommissioningWindow returns a fresh, closed window.

func (*CommissioningWindow) CurrentWindow

func (w *CommissioningWindow) CurrentWindow() wire.WindowStatusSnapshot

CurrentWindow implements wire.WindowController. Returns the snapshot the AdministratorCommissioning cluster reads attributes from.

func (*CommissioningWindow) Discriminator

func (w *CommissioningWindow) Discriminator() uint16

Discriminator returns the 12-bit discriminator of the currently-open window (0 when closed). Used by the daemon's transition hook to build the CM=2 commissionable advertisement for an Enhanced Commissioning Window.

func (*CommissioningWindow) EndCommissioning added in v0.2.0

func (w *CommissioningWindow) EndCommissioning()

EndCommissioning closes the window after a successful CommissioningComplete: the commissioner it was opened for is done, so the window must not admit another one until an administrator opens a new one. Unlike CommissioningWindow.RevokeWindow it neither evicts PASE sessions nor touches the fail-safe — CommissioningComplete has already disarmed it. No-op when no window is open. Mirrors matter.js DeviceCommissioner.endCommissioning, which failsafeContext.commissioned triggers (DeviceCommissioner.ts:160). Without it a second OpenCommissioningWindow right after a commissioning answered BUSY (TC-CADMIN-1.3 step 9, TC-ACL-2.8).

func (*CommissioningWindow) FabricRemoved added in v0.2.0

func (w *CommissioningWindow) FabricRemoved(fabricIndex uint8)

FabricRemoved clears AdminFabricIndex when the fabric that opened the window is removed while it is open, and reports the change. Mirrors matter.js AdministratorCommissioningServer #fabricRemovedCallback, wired to the admin fabric's deleting event when the window opens; the window itself and AdminVendorId stay (TC-CADMIN-1.25 step 29).

func (*CommissioningWindow) HasSuppliedVerifier

func (w *CommissioningWindow) HasSuppliedVerifier() bool

HasSuppliedVerifier reports whether the currently-open window was opened with a commissioner-supplied PAKE verifier (a true Matter Enhanced Commissioning Window), as opposed to the REST/operator open (also Enhanced status but the bridge's own passcode). The daemon advertises CM=2 only for the former; the REST path advertises CM=1 via the opener.

func (*CommissioningWindow) IsOpen added in v0.2.0

func (w *CommissioningWindow) IsOpen() bool

IsOpen reports whether a commissioning window is open, whoever opened it: an administrator, the host's opener or the node itself (CommissioningWindow.OpenOwnWindow). Mirrors matter.js DeviceCommissioner.windowStatus, which GeneralCommissioningServer's ArmFailSafe guard and AdministratorCommissioningServer.revokeCommissioning consult.

func (*CommissioningWindow) IsOwnWindow added in v0.3.0

func (w *CommissioningWindow) IsOwnWindow() bool

IsOwnWindow reports whether the open window is one the node opened itself (CommissioningWindow.OpenOwnWindow). A host's transition hook reads it to tell the node's own window — commissionable with the configured passcode, advertised CM=1 — from an administrator's.

func (*CommissioningWindow) OpenOwnWindow added in v0.3.0

func (w *CommissioningWindow) OpenOwnWindow(ctx context.Context, timeout time.Duration) error

OpenOwnWindow opens the node's own commissioning window — the window an uncommissioned node opens at start, or one the application opens — for timeout, or, when timeout is 0, for 48 h while the node has no fabric and 15 min once it has one. Invoked again while the node's own window is open, it restarts it. It is closed by its timer, by CommissioningComplete (CommissioningWindow.EndCommissioning) and by RevokeCommissioning; an administrator's OpenCommissioningWindow replaces it instead of answering Busy. The AdministratorCommissioning attributes keep reading WindowNotOpen, which matter.js keeps for administrator windows only.

The window owns no PASE acceptor and no advertisement: the transition hook (CommissioningWindow.SetTransitionHook) fires on open and close, and the host attaches its configured-passcode acceptor and announces CM=1 while CommissioningWindow.IsOwnWindow holds, as matter.js DeviceCommissioner.allowBasicCommissioning sets the PASE commissioner and enters commissioning mode.

Mirrors matter.js packages/protocol/src/protocol/DeviceCommissioner.ts (allowBasicCommissioning without byAdministrator, #ownWindowTimeout) and packages/node/src/behavior/system/commissioning/CommissioningServer.ts enterCommissionableMode.

func (*CommissioningWindow) OpenWindow

func (w *CommissioningWindow) OpenWindow(ctx context.Context, params wire.OpenWindowParams) error

OpenWindow implements wire.WindowController. The cluster command path supplies a verifier-based wire.OpenWindowParams; v1.1 stores the resulting state but does not yet swap a per-window verifier into the bridge's PASE adapter (the bridge already accepts PASE against the configured passcode for `duration_seconds`).

A second OpenWindow while one is already live returns BUSY per Matter §11.19.7.1.

After a successful state transition, the FailSafeArmer (if wired) is called to arm the fail-safe for the window duration per Matter §11.19.6. A FailSafe arm failure is logged but does not abort the window: the window is already open and the controller expects Success.

func (*CommissioningWindow) RequestedDurationSeconds

func (w *CommissioningWindow) RequestedDurationSeconds() uint16

RequestedDurationSeconds returns the duration the currently-open window was opened with (0 when closed). It is not a Matter wire attribute — the AdministratorCommissioning cluster's WindowStatus carries no duration — so it is a sibling accessor rather than part of CommissioningWindow.CurrentWindow's snapshot. It exists so a client that rehydrates state after a page reload (GET /api/v1/matter/status's commissioning_window_duration_seconds) can restore its countdown instead of losing it whenever it re-syncs against an already-open window.

func (*CommissioningWindow) RevokeWindow

func (w *CommissioningWindow) RevokeWindow(ctx context.Context) error

RevokeWindow implements wire.WindowController. Per Matter §11.19.7.3 step 1, any open PASE session is evicted first (via PaseSessionCloser when wired), regardless of whether a commissioning window is open. Then the window is closed (if open) and the transition hook fires.

func (*CommissioningWindow) SetBlocked added in v0.5.4

func (w *CommissioningWindow) SetBlocked(ctx context.Context, err error)

SetBlocked refuses every commissioning window while err is non-nil: CommissioningWindow.OpenOwnWindow and CommissioningWindow.OpenWindow return an error wrapping ErrCommissioningBlocked and err, so the node's own window, a basic window, the host's opener and both AdministratorCommissioning commands stay closed and no commissionable record is advertised; a window open when the block is set is revoked. nil lifts the block; nothing reopens by itself here (the node facade reopens an uncommissioned node's own window). Paired controllers are unaffected: the block concerns new commissioning only.

matter.js has no counterpart; it is a host extension for a node that cannot serve the attestation it promised (issue #133, BD-Matter-CommissioningBlocked).

func (*CommissioningWindow) SetClock added in v0.7.2

func (w *CommissioningWindow) SetClock(c clock.Clock)

SetClock sets the clock the window's timeout runs on; nil is clock.System. Bridge.AttachCommissioningWindow sets the bridge's. A window already open keeps the timer it was opened with.

func (*CommissioningWindow) SetFabricCounter added in v0.3.0

func (w *CommissioningWindow) SetFabricCounter(fn func(context.Context) (int, error))

SetFabricCounter wires (or clears, when nil) the fabric count CommissioningWindow.OpenOwnWindow derives its default timeout from. Without it the node counts as uncommissioned.

func (*CommissioningWindow) SetFailSafeArmer

func (w *CommissioningWindow) SetFailSafeArmer(a FailSafeArmer)

SetFailSafeArmer wires (or clears, when nil) the hook called after a successful window open to arm the FailSafe timer per Matter §11.19.6. Safe to call concurrently with [OpenWindow].

func (*CommissioningWindow) SetFailSafeChecker

func (w *CommissioningWindow) SetFailSafeChecker(c FailSafeChecker)

SetFailSafeChecker wires (or clears, when nil) the guard consulted at the start of every [OpenWindow] call. When the checker reports an armed FailSafe, OpenWindow returns wire.ErrAdmCommBusy rather than opening a second window that would overwrite the active commissioning session. Safe to call concurrently with [OpenWindow].

func (*CommissioningWindow) SetPaseSessionCloser

func (w *CommissioningWindow) SetPaseSessionCloser(c PaseSessionCloser)

SetPaseSessionCloser wires (or clears, when nil) the hook called at the start of [RevokeWindow] to evict open PASE sessions per Matter §11.19.7.3. Safe to call concurrently with [RevokeWindow].

func (*CommissioningWindow) SetPaseVerifierInstaller

func (w *CommissioningWindow) SetPaseVerifierInstaller(i PaseVerifierInstaller)

SetPaseVerifierInstaller wires (or clears, when nil) the hook that installs a window-lifetime PASE acceptor from a commissioner-supplied PAKE verifier on the Enhanced Commissioning Window path (see PaseVerifierInstaller). Safe to call concurrently with [OpenWindow].

func (*CommissioningWindow) SetTransitionHook

func (w *CommissioningWindow) SetTransitionHook(fn func())

SetTransitionHook wires the optional callback fired on Open / Close. Pass nil to detach. Safe to call concurrently.

type CommissioningWindowOpener

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

CommissioningWindowOpener is the [handlers.MatterCommissioningOpener]-compatible REST adapter the daemon wires into [rest.Deps.MatterCommissioningOpener].

Two operating modes:

  • Configured mode (default): the bridge's PASE acceptor is open against the configured passcode/discriminator. "Opening a window" advertises that pair, returns the QR + manual code, and starts the duration timer. Used by ops who want a fixed pairing pad.
  • Ephemeral mode (when [SetEphemeralProvider] is wired): each OpenCommissioningWindow generates a fresh discriminator + passcode + Spake2+ verifier, swaps them into the bridge's PASE adapter for the window duration, and reverts on close. Recommended for production: the configured passcode never leaves the bridge process and pairing codes auto-rotate.

func NewCommissioningWindowOpener

func NewCommissioningWindowOpener(w *CommissioningWindow, discriminator uint16, passcode uint32, vendorID, productID uint16) *CommissioningWindowOpener

NewCommissioningWindowOpener wires the opener against an existing window controller and the bridge's setup-payload parameters.

func (*CommissioningWindowOpener) OpenCommissioningWindow

func (o *CommissioningWindowOpener) OpenCommissioningWindow(ctx context.Context, durationSeconds uint16) (OpenCommissioningWindowResult, error)

OpenCommissioningWindow generates the setup-payload artifacts and kicks the underlying CommissioningWindow into the open state. Returns a ErrCommissioningWindowAlreadyOpen-style error when a window is already live; the REST handler maps it to 409 Conflict.

In ephemeral mode the call generates fresh credentials before touching the window. Failure paths after a successful generation invoke the Restore closure so the bridge's long-lived PASE adapter is re-armed even if the QR/manual code build or the window OpenWindow rejects later.

func (*CommissioningWindowOpener) SetEphemeralProvider

func (o *CommissioningWindowOpener) SetEphemeralProvider(p EphemeralProvider)

SetEphemeralProvider wires (or clears, when nil) the per-window credential generator. When set, the next call to [OpenCommissioningWindow] generates fresh credentials and installs a window-scoped PASE adapter on the bridge instead of reusing the configured one.

Safe to call concurrently with [OpenCommissioningWindow]; the switch is observed atomically per call.

type Config

type Config struct {
	// Listen is the UDP bind address. Empty defaults to ":5540" via
	// the udp package's MatterPort default.
	Listen string

	// PreferIPv4 forces an IPv4-only socket. Defaults to dual-stack
	// IPv6 (which also accepts IPv4 traffic).
	PreferIPv4 bool

	// TCP selects the Matter TCP transport: a listener on the UDP
	// socket's port for Incoming, and the T key of the operational
	// record for both roles. The zero value is TCP off. See [TCPConfig].
	TCP TCPConfig

	// VendorID is the bridge's IANA-assigned vendor identifier
	// (BasicInformation.VendorID, Matter §11.1.5.2). Mandatory.
	VendorID uint16

	// ProductID is the bridge's vendor-assigned product identifier
	// (BasicInformation.ProductID, Matter §11.1.5.4). Mandatory.
	ProductID uint16

	// NodeLabel is the bridge's user-visible label
	// (BasicInformation.NodeLabel, Matter §11.1.5.6). Mandatory.
	NodeLabel string

	// Discriminator is the 12-bit Matter commissioning discriminator
	// (Matter §5.1.3.1). Surfaces in the commissionable mDNS record;
	// commissioner UIs use it to pre-filter candidates. Bits above
	// the 12-bit limit are masked off at advertise time.
	Discriminator uint16

	// AdvertiseTimeout caps the per-call timeout for mDNS publish /
	// withdraw operations. Defaults to 5 seconds when zero. Each
	// individual call is given its own context derived from the
	// caller's; no global ticker is involved.
	AdvertiseTimeout time.Duration
	// Clock is the bridge's time source: the acknowledgement pump, MRP
	// stamps, the TimedRequest window, PASE lockout, the subscription
	// reports' timers and every other interval of the device side. nil is
	// [clock.System]. A [github.com/SukramJ/go-fabric/node] hands its
	// own clock in here.
	Clock clock.Clock
}

Config bundles the bridge's identity and listener parameters. All fields are validated at New time.

It carries nothing that governs topology assembly — which sources are exposed, whether measurements become endpoints, how labels are resolved. Those belong to whoever builds the topology behind the Snapshotter, and holding a second copy of them here would be a setting that looks live and changes nothing.

type ConfigurationVersions added in v0.2.0

type ConfigurationVersions struct {
	Node    uint32
	Bridged map[uint16]uint32
}

ConfigurationVersions is what an increase produced: the node's BasicInformation ConfigurationVersion and the BridgedDeviceBasicInformation one of each bridged endpoint that was raised, for the host to persist (BasicInformationConfig.ConfigurationVersion, endpoint.Spec.ConfigurationVersion). A version may never decrease, so a host that raises one keeps it.

type EphemeralCredentials

type EphemeralCredentials struct {
	Discriminator uint16
	Passcode      uint32
	Restore       func()
}

EphemeralCredentials are the freshly-generated credentials for one commissioning window. The daemon-side EphemeralProvider builds them against a random salt + the configured iteration count and installs the corresponding PASE adapter into the bridge for the window's lifetime.

Restore is invoked exactly once per successful generation, after the window closes (early revoke or timer expiry), so the bridge's long-lived configured PASE acceptor is re-armed. The opener wires it via [CommissioningWindow.setRestore] so both close paths fire it deterministically.

type EphemeralProvider

type EphemeralProvider interface {
	GenerateAndInstall(ctx context.Context) (EphemeralCredentials, error)
}

EphemeralProvider is the daemon-side hook the CommissioningWindowOpener consumes when ephemeral mode is wired. `GenerateAndInstall` MUST: (1) compute a fresh Spake2+ verifier against a random passcode + salt, (2) install the resulting PASE adapter on the bridge — taking precedence over the configured long-lived adapter — and (3) return a Restore closure that re-arms the previous adapter on window close.

Returning an error aborts the open: no swap happened, no Restore is owed. The opener never invokes Restore on the error path.

type FailSafeArmer

type FailSafeArmer interface {
	ArmFailSafeFor(ctx context.Context, seconds uint32, fabricIndex uint8) error
}

FailSafeArmer is the optional bridge-side hook CommissioningWindow calls after successfully opening a window. Matter §11.19.6 mandates that OpenCommissioningWindow MUST arm the fail-safe for the window duration — the cluster itself does not own the FailSafe state, so it delegates via this interface.

Pass fabricIndex=0 for pre-commissioning windows (the window opener is not yet associated with any fabric). The implementation is wired from daemon.go to GeneralCommissioning.ArmFailSafe.

Mirrors matter.js packages/node/src/behaviors/administrator-commissioning/ AdministratorCommissioningServer.ts:openCommissioningWindow call to GeneralCommissioningBehavior.armFailSafeLogic.

type FailSafeChecker

type FailSafeChecker interface {
	FailSafeArmed() bool
}

FailSafeChecker is the optional pre-open guard CommissioningWindow consults before opening a new commissioning window. Matter §11.19.7.1 requires that OpenCommissioningWindow returns BUSY when a FailSafe is already armed by a different commissioner. The bridge wires this to GeneralCommissioning so OpenWindow can reject immediately rather than overwriting an active commissioning session.

Returning true from FailSafeArmed causes OpenWindow to return wire.ErrAdmCommBusy. Implementations must be goroutine-safe.

type GroupMessaging added in v0.2.0

type GroupMessaging interface {
	// Decode authenticates a group message ([groups.Manager.Decode]).
	Decode(ctx context.Context, datagram []byte) (*groups.Message, error)
	// Memberships lists the groups with member endpoints and their
	// multicast addresses.
	Memberships(ctx context.Context) ([]groups.Membership, error)
	// OnMembershipChanged registers a callback for a change of the
	// membership set.
	OnMembershipChanged(fn func())
	// OnGroupTableChanged registers a callback for a change of a
	// fabric's group table.
	OnGroupTableChanged(fn func(fabricIndex uint8))
	// ForgetFabric drops a removed fabric's group state.
	ForgetFabric(fabricIndex uint8)
	// ReportGroupMessage hands the outcome of a received group message
	// to whoever observes it — the Groupcast server while a fabric is
	// under GroupcastTesting ([groups.Manager.ReportGroupMessage]).
	ReportGroupMessage(ev groups.GroupMessageEvent)
}

GroupMessaging is the port group communication runs through: it authenticates a group message, names the multicast addresses the node must listen on, and reports the changes to both. *groups.Manager — the same instance GroupKeyManagement and the Groups servers use — satisfies it; group membership is stack state, so there is nothing for a host to implement here, only an instance to hand over.

type HealthRecorder

type HealthRecorder interface {
	Record(name string, sample HealthSample)
	// RecordUnhealthy states an unhealthy condition rather than sampling one,
	// so it takes effect without the flap-damping Record applies.
	RecordUnhealthy(name string, sample HealthSample)
}

HealthRecorder is the slim tracker contract the probe touches. The daemon's health tracker satisfies it through a host-side adapter that translates HealthSample into the tracker's own sample type.

type HealthSample

type HealthSample struct {
	Healthy bool
	// Note is the stable, English machine string. The tracker's scoring
	// treats it as a sentinel, so it must stay stable and un-localized.
	Note string
}

HealthSample is everything this probe has to say about the bridge: a verdict and the stable, un-localized machine string that explains it. It is deliberately narrower than the daemon's tracker sample — the probe never sets a catalogue key, a timestamp or the staleness exemption — so the Matter subtree needs no dependency on the tracker package. The host converts on the way in.

type MRPAckAdapter

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

MRPAckAdapter wraps an mrp.AckTracker in the AckHandler port. Trivial passthrough — the MRP layer owns the obligation bookkeeping; the adapter exists so the bridge can stay decoupled from the concrete tracker type (tests substitute a fake).

func NewMRPAckAdapter

func NewMRPAckAdapter(t *mrp.AckTracker) *MRPAckAdapter

NewMRPAckAdapter wraps t.

func (*MRPAckAdapter) Discharge

func (a *MRPAckAdapter) Discharge(sessionID, exchangeID uint16, initiator bool) bool

Discharge forwards to mrp.AckTracker.Discharge. initiator is the LOCAL side's role on the exchange.

type MRPRTTStats

type MRPRTTStats struct {
	// Total counts every measurement since start. Zero is the steady state of
	// a bridge nobody has paired; a total that stops advancing says the
	// median describes the last exchange rather than the present.
	Total uint64
	// MedianMs is the middle of the window.
	MedianMs float64
	// MaxMs is the worst in the window. A controller that is usually prompt
	// but occasionally takes seconds is the retransmit-storm signature, and
	// the median alone hides it.
	MaxMs float64
}

MRPRTTStats summarises recent first-try round-trips to Matter controllers.

Every field is published as a gauge; see [mqtt.LatencyStats] for why window occupancy and the single most recent sample are not among them.

type OpenCommissioningWindowResult

type OpenCommissioningWindowResult struct {
	Discriminator   uint16
	Passcode        uint32
	DurationSeconds uint16
	QRCode          string
	ManualCode      string
}

OpenCommissioningWindowResult is the value the REST handler returns. Mirrors `handlers.MatterCommissioningWindowResult` without the import cycle.

type OperationalResolver added in v0.2.0

type OperationalResolver interface {
	ResolveOperational(ctx context.Context, compressedFabricID [8]byte, nodeID uint64) ([]*net.UDPAddr, error)
}

OperationalResolver finds the addresses of a peer's operational instance (`<CompressedFabricID>-<NodeID>._matter._tcp.local`), most desirable first. *mdns.OperationalResolver implements it.

type OperationalSessionLookup

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

OperationalSessionLookup wraps a session-lookup function from the `secure/operational` layer in the SessionLookup port the receive pipeline depends on. Decouples bridge from the concrete `*operational.Manager` type so tests can substitute a fake.

The function shape mirrors [operational.Manager.Get] — passing `manager.Get` directly would fail because Get returns `(*Entry, error)`. The adapter unwraps to `(*channel.Session, bool)`.

Implements the optional SessionFabricResolver when wired with a non-nil `fabricFor` closure — Subscribe, ACL checks and any other fabric-scoped logic can then resolve `(sessionID → fabricIndex)`.

func NewOperationalSessionLookup

func NewOperationalSessionLookup(get func(sessionID uint16) (*channel.Session, bool)) *OperationalSessionLookup

NewOperationalSessionLookup builds the adapter. The `get` closure typically wraps an [operational.Manager.Get] call:

NewOperationalSessionLookup(func(id uint16) (*channel.Session, bool) {
    e, err := mgr.Get(id)
    if err != nil {
        return nil, false
    }
    return e.Session, true
})

func (*OperationalSessionLookup) FabricFor

func (l *OperationalSessionLookup) FabricFor(sessionID uint16) (uint8, bool)

FabricFor implements SessionFabricResolver. Returns (0, false) when the adapter was built without a fabric resolver closure.

func (*OperationalSessionLookup) FabricResolutionWired

func (l *OperationalSessionLookup) FabricResolutionWired() bool

FabricResolutionWired reports whether a fabric-resolver closure was actually supplied.

Implementing SessionFabricResolver is not the same as being able to resolve: this adapter carries the method unconditionally, so a type assertion says nothing about whether [WithFabricResolver] was ever called. Without it every FabricFor answers (0, false), CheckACL reads that 0 as "PASE, still commissioning" and waves the request through -- so the shape that passes a type check is exactly the shape that bypasses access control. Bridge.Start asks this question instead of asking the type system.

func (*OperationalSessionLookup) IsPASE

func (l *OperationalSessionLookup) IsPASE(sessionID uint16) (pase, ok bool)

IsPASE implements SessionPASEResolver. Returns (false, false) when the adapter was built without a PASE resolver closure or the session is unknown.

func (*OperationalSessionLookup) Lookup

func (l *OperationalSessionLookup) Lookup(sessionID uint16) (*channel.Session, bool)

Lookup implements SessionLookup.

func (*OperationalSessionLookup) MarkActiveRx

func (l *OperationalSessionLookup) MarkActiveRx(sessionID uint16)

MarkActiveRx implements SessionActivityMarker. No-op when the adapter was built without activity-marker closures.

func (*OperationalSessionLookup) MarkActiveTx

func (l *OperationalSessionLookup) MarkActiveTx(sessionID uint16)

MarkActiveTx implements SessionActivityMarker. No-op when the adapter was built without activity-marker closures.

func (*OperationalSessionLookup) RetransmitBaseInterval

func (l *OperationalSessionLookup) RetransmitBaseInterval(sessionID uint16, now time.Time) (time.Duration, bool)

RetransmitBaseInterval implements SessionRetransmitIntervalResolver. Returns (0, false) when the adapter was built without an interval resolver closure.

func (*OperationalSessionLookup) SubjectFor

func (l *OperationalSessionLookup) SubjectFor(sessionID uint16) (nodeID uint64, cats []uint32, ok bool)

SubjectFor implements SessionSubjectResolver. Returns (0, nil, false) when the adapter was built without a subject resolver closure or the session is unknown.

func (*OperationalSessionLookup) TransportParametersFor added in v0.3.0

func (l *OperationalSessionLookup) TransportParametersFor(sessionID uint16) (transports uint16, maxTCPMessageSize uint32, ok bool)

TransportParametersFor implements SessionTransportResolver. Returns (0, 0, false) when no resolver closure was supplied or the session is unknown.

func (*OperationalSessionLookup) WithActivityMarkers

func (l *OperationalSessionLookup) WithActivityMarkers(markRx, markTx func(sessionID uint16)) *OperationalSessionLookup

WithActivityMarkers wires the optional SessionActivityMarker side of the adapter. Pass closures that resolve the operational entry and call its MarkActiveRx / MarkActiveTx (unknown sessions are a no-op). Returns the receiver so callers can chain.

func (*OperationalSessionLookup) WithFabricResolver

func (l *OperationalSessionLookup) WithFabricResolver(fabricFor func(sessionID uint16) (uint8, bool)) *OperationalSessionLookup

WithFabricResolver wires the optional FabricFor side of the adapter. Pass a closure returning `(entry.FabricIndex, true)` for known sessions and `(0, false)` for unknown ones. Returns the receiver so callers can chain.

func (*OperationalSessionLookup) WithPASEResolver

func (l *OperationalSessionLookup) WithPASEResolver(paseFor func(sessionID uint16) (bool, bool)) *OperationalSessionLookup

WithPASEResolver wires the optional IsPASE side of the adapter. Pass a closure returning `(entry.IsPASE(), true)` for known sessions and `(false, false)` for unknown ones — typically `operational.Entry.IsPASE`. Returns the receiver so callers can chain. Without it every IsPASE answers (false, false) and a PASE session adopted onto a fabric by AddNOC loses its implicit Administer grant on the next request.

func (*OperationalSessionLookup) WithRetransmitIntervalResolver

func (l *OperationalSessionLookup) WithRetransmitIntervalResolver(intervalFor func(sessionID uint16, now time.Time) (time.Duration, bool)) *OperationalSessionLookup

WithRetransmitIntervalResolver wires the optional RetransmitBaseInterval side of the adapter. Pass a closure that resolves the peer-appropriate MRP base interval (typically `operational.Entry.RetransmitBaseInterval`) for known sessions and returns `(0, false)` for unknown ones. Returns the receiver so callers can chain.

func (*OperationalSessionLookup) WithSubjectResolver

func (l *OperationalSessionLookup) WithSubjectResolver(subjectFor func(sessionID uint16) (uint64, []uint32, bool)) *OperationalSessionLookup

WithSubjectResolver wires the optional SubjectFor side of the adapter. Pass a closure returning `(peerNodeID, peerCATs, true)` for known CASE sessions and `(0, nil, false)` for unknown ones. Returns the receiver so callers can chain.

func (*OperationalSessionLookup) WithTransportResolver added in v0.3.0

func (l *OperationalSessionLookup) WithTransportResolver(transportsFor func(sessionID uint16) (uint16, uint32, bool)) *OperationalSessionLookup

WithTransportResolver installs the closure OperationalSessionLookup.TransportParametersFor answers from: the peer's SupportedTransports bitmap and MaxTcpMessageSize a host keeps on its operational entry ([operational.Entry.PeerTransportParameters]).

type PartsListProviderSetter

type PartsListProviderSetter interface {
	SetPartsListProvider(func() []uint16)
}

PartsListProviderSetter is the duck-typed surface a root-endpoint Descriptor cluster (`0:0x001D`) implements so the daemon can wire a closure that returns the live bridged-endpoint set on every read. Defined here (instead of importing the cluster/core package directly) to avoid a bridge → core import cycle.

type PaseAdapter

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

PaseAdapter wraps a spake2.Verifier in the PaseHandler port the SecureChannel router consumes. The bridge constructs one adapter per pending PASE exchange (Verifier state is not safe for concurrent use across distinct exchanges, but the adapter itself has no mutable state — concurrency safety is the verifier's responsibility).

PASE flow:

  1. Commissioner sends PBKDFParamRequest (opcode 0x20). Bridge responds with PBKDFParamResponse (0x21) carrying the salt + iterations the bridge advertises. The response is assembled here: the request is decoded, the PasscodeID validated against the brute-force cap, and the answer built from the configured parameters. Refusals are ErrPBKDFParamsMissing when the daemon configured none and ErrUnsupportedPasscodeID otherwise.
  2. Commissioner sends Pake1 (0x22) carrying pA. Bridge runs spake2.Verifier.ProcessPake1 and replies Pake2 (0x23) with Y + cB.
  3. Commissioner sends Pake3 (0x24) carrying cA. Bridge runs spake2.Verifier.ProcessPake3; on success the optional [PaseAdapter.OnSessionEstablished] callback fires with the freshly-derived shared secret so the operational session manager can register the new session in one atomic step.

func NewPaseAdapter

func NewPaseAdapter(v *spake2.Verifier) *PaseAdapter

NewPaseAdapter wraps v as a single-shot adapter — the verifier is reused for every Pake1 the adapter sees. Suitable for tests and single-PASE-attempt scenarios. The factory ignores the per-Pake1 context override and always returns the same verifier; this is OK for tests that don't exercise the PBKDFParamRequest/Response round, but production should use NewPaseAdapterWithFactory so the verifier's context can be derived from the actual exchanged PBKDFParam wire bytes (Matter §4.13.4).

A nil verifier produces an adapter that returns errors from every Pake* call — useful for tests of the missing-verifier defensive path.

func NewPaseAdapterWithFactory

func NewPaseAdapterWithFactory(factory func(context []byte) *spake2.Verifier) *PaseAdapter

NewPaseAdapterWithFactory wraps a verifier factory. The adapter invokes the factory at every Pake1 with the **per-exchange SPAKE2+ context** — SHA-256("CHIP PAKE V1 Commissioning" || PBKDFParamRequest_bytes || PBKDFParamResponse_bytes) per Matter §4.13.4 — and the returned verifier MUST initialise its transcript with that 32-byte digest, not the literal context string.

The factory MUST return a fresh verifier each call. Returning a shared verifier defeats the purpose; tests that need observable state across calls should use NewPaseAdapter directly.

Concurrent PASE attempts (multiple commissioners pairing in parallel) are NOT supported — the adapter still tracks one "current" verifier between Pake1 and Pake3. Per-exchange concurrent dispatch requires a per-exchange-id PaseHandler-Provider on the bridge router; in practice operators open the commissioning window for one commissioner at a time so serial-with-retry covers the realistic case.

func (*PaseAdapter) PeerMRPParams deprecated

func (a *PaseAdapter) PeerMRPParams() *spake2.MRPParameters

PeerMRPParams returns the MRP triplet of the commissioner's SessionParameters, or nil when it stated none of the three.

Deprecated: use PaseAdapter.PeerSessionParams. Marked in v0.3.0; removal is permissible from v0.5.0, the second minor release after the marking one (README §API stability).

func (*PaseAdapter) PeerSessionParams added in v0.3.0

func (a *PaseAdapter) PeerSessionParams() *sessionparams.Parameters

PeerSessionParams returns the commissioner's SessionParameters from the most recent PBKDFParamRequest, or nil when it sent none. The PASE session opener stamps them onto the operational entry (matter.js PaseServer.ts:155-157).

func (*PaseAdapter) ProcessPBKDFParamRequest

func (a *PaseAdapter) ProcessPBKDFParamRequest(payload []byte) (opcode uint8, respPayload []byte, err error)

ProcessPBKDFParamRequest decodes the commissioner's request, picks a fresh ResponderRandom, and assembles the PBKDFParamResponse. The response carries the bridge's PBKDF salt + iterations only when the request flagged HasPBKDFParameters=false; otherwise the commissioner already knows them (e.g. from the QR / manual code).

The wire-bytes of both the inbound request and the outbound response are captured on the adapter so the subsequent ProcessPake1 can build the SPAKE2+ context per Matter §4.13.4 (SHA-256("CHIP PAKE V1 Commissioning" || req || resp)).

Returns ErrPBKDFParamsMissing when [SetPBKDFParams] hasn't been called yet — the SecureChannel router degrades to a debug log so the bridge's pre-commissioner boot phase doesn't flood warnings.

func (*PaseAdapter) ProcessPake1

func (a *PaseAdapter) ProcessPake1(payload []byte) (opcode uint8, respPayload []byte, err error)

ProcessPake1 decodes the wire payload, allocates a fresh verifier from the factory (so a retry after a failed previous Pake3 starts clean), runs it, and encodes the Pake2 reply.

The verifier-context passed to the factory is the SPAKE2+ context hash per Matter §4.13.4: SHA-256("CHIP PAKE V1 Commissioning" || PBKDFParamRequest_bytes || PBKDFParamResponse_bytes). Both sides derive this hash independently from the wire-bytes that crossed the channel during the PBKDF round. Without it the prover and verifier compute different transcript hashes and the cB tag verification on the prover side fails.

func (*PaseAdapter) ProcessPake3

func (a *PaseAdapter) ProcessPake3(payload []byte) (opcode uint8, respPayload []byte, err error)

ProcessPake3 decodes the wire payload and runs the verifier allocated during the matching ProcessPake1. On success the optional PaseAdapter.SetOnSessionEstablished callback fires with the freshly-derived Spake2+ shared secret and the adapter returns a Secure-Channel StatusReport with SESSION_ESTABLISHMENT_SUCCESS so the commissioner can complete the handshake (Matter §4.13.4 step 11). The verifier is cleared after Pake3 so a stray follow-up Pake3 (e.g. retransmit) cannot accidentally succeed against stale state.

On a Pake3 failure — a malformed payload OR a key-confirmation mismatch (the on-wire symptom of a wrong passcode) — the adapter clears the verifier and RETURNS the underlying error (a decode error, or spake2.ErrConfirmationFailed) instead of emitting its own StatusReport. The SecureChannel router's handlePase path then sends exactly one StatusReport(FAILURE) AND counts the attempt toward the PASE brute-force cap. matter.js counts every PASE pairing failure — an incorrect key confirmation included — toward PASE_COMMISSIONING_MAX_ERRORS; swallowing the error here let an attacker guess passcodes without ever tripping the cap. Mirrors matter.js packages/protocol/src/session/pase/PaseServer.ts:178-181 (throw UnexpectedDataError on incorrect key confirmation) → :94-95 (catch → #pairingErrors++).

func (*PaseAdapter) SetOnSessionEstablished

func (a *PaseAdapter) SetOnSessionEstablished(cb PaseSessionEstablished)

SetOnSessionEstablished installs a callback that fires after a successful Pake3 verification. Pass nil to clear.

func (*PaseAdapter) SetPBKDFParams

func (a *PaseAdapter) SetPBKDFParams(iterations uint32, salt []byte, responderSessionID uint16)

SetPBKDFParams configures the PBKDF salt + iteration count the adapter advertises in the PBKDFParamResponse, plus the responder session ID that goes into the response and into the bridge's inbound encrypted Header.SessionID once the PASE round completes. The values must match what was passed to spake2.NewVerifierContext — otherwise the commissioner's (w0,w1) derivation diverges from the bridge's and Pake1 verification fails.

func (*PaseAdapter) SetResponderMRPParams deprecated

func (a *PaseAdapter) SetResponderMRPParams(p *spake2.MRPParameters)

SetResponderMRPParams configures the MRP triplet of the advertised SessionParameters, keeping the other fields of DefaultSessionParameters. Nil restores the defaults.

Deprecated: use PaseAdapter.SetResponderSessionParams. Marked in v0.3.0; removal is permissible from v0.5.0, the second minor release after the marking one (README §API stability).

func (*PaseAdapter) SetResponderSessionParams added in v0.3.0

func (a *PaseAdapter) SetResponderSessionParams(p *sessionparams.Parameters)

SetResponderSessionParams configures the SessionParameters the adapter advertises as PBKDFParamResponse tag 5. Nil restores DefaultSessionParameters; an empty structure omits the field. Mirrors matter.js PaseServer.ts:151 responderSessionParams.

type PaseHandler

type PaseHandler interface {
	// ProcessPBKDFParamRequest handles the commissioner's PBKDF param
	// request and produces a PBKDFParamResponse (opcode 0x21).
	ProcessPBKDFParamRequest(payload []byte) (uint8, []byte, error)
	// ProcessPake1 handles Pake1 and produces Pake2 (opcode 0x23).
	ProcessPake1(payload []byte) (uint8, []byte, error)
	// ProcessPake3 handles Pake3 and produces no reply (the bridge's
	// operational manager picks up the freshly-derived session keys
	// out-of-band). A non-nil response is allowed — the router ships
	// it if returned, useful for status acks.
	ProcessPake3(payload []byte) (uint8, []byte, error)
}

PaseHandler is the inbound PASE port — the Spake2+ verifier the `secure/spake2.Verifier` typically backs. Each method takes the inbound TLV payload (already stripped of message + protocol headers) and returns either a response opcode + payload pair (the router ships it back via [Bridge.sendReply]) or an error. A nil response with nil error indicates "no reply needed" (e.g. Pake3 completes the exchange and the bridge's session manager picks up the new session in a separate hook).

Method naming mirrors the spec opcode names. The bridge's router fans out by opcode → method, so adding a new PASE opcode is a one-line addition both here and in the router's switch.

type PaseHandlerProvider

type PaseHandlerProvider func(exchangeID uint16) PaseHandler

PaseHandlerProvider returns a PaseHandler scoped to a single PASE exchange. The bridge's SecureChannel router calls the provider once per inbound PASE opcode (PBKDFParamRequest, Pake1, Pake3) and dispatches to the returned handler. Returning the same handler instance for the same exchangeID across the three opcode arrivals lets the underlying PaseAdapter track verifier state across Pake1 → Pake3.

Returning nil drops the datagram with a debug log — the commissioner retries.

type PaseSessionCloser

type PaseSessionCloser interface {
	ClosePaseSessions(ctx context.Context) error
}

PaseSessionCloser is the optional bridge-side hook CommissioningWindow calls at the start of RevokeWindow. Matter §11.19.7.3 step 1 mandates closing any open PASE session regardless of window state. The bridge's secure-channel layer owns PASE session cleanup; this interface lets CommissioningWindow drive it without importing the secure package.

Mirrors matter.js packages/node/src/behaviors/administrator-commissioning/ AdministratorCommissioningServer.ts:revokeCommissioning call to paseCommissioner.close().

type PaseSessionEstablished

type PaseSessionEstablished func(sharedSecret []byte, peerSessionID uint16) error

PaseSessionEstablished fires after a successful Pake3 verification. sharedSecret is the raw 16-byte Spake2+ Ke; peerSessionID is the commissioner's local session ID (InitiatorSessionID from PBKDFParamRequest) — the bridge MUST stamp this into outbound Header.SessionID so the commissioner can resolve the session in its own table. Implementations typically hand sharedSecret + peerSessionID to [operational.Manager.OpenFromPase] which derives the I2R/R2I session keys and registers the new channel.Session. A non-nil error from the callback is wrapped and returned from ProcessPake3 (the SecureChannel router logs it and the commissioner falls back).

type PaseVerifierInstaller

type PaseVerifierInstaller interface {
	// InstallVerifier installs a window-lifetime PASE acceptor from the
	// 97-byte Matter PAKE passcode verifier (w0 || L) plus the PBKDF
	// iterations + salt the commissioner supplied. Returns a restore
	// closure (re-installs the prior acceptor) or an error if the verifier
	// is malformed.
	InstallVerifier(verifier []byte, iterations uint32, salt []byte) (restore func(), err error)
}

PaseVerifierInstaller is the optional daemon-side hook CommissioningWindow calls when an Enhanced Commissioning Window is opened with a commissioner-supplied PAKE passcode verifier (Matter §11.19 / §3.10.5). It installs a PASE acceptor built from that verifier for the window lifetime — so the bridge accepts PASE against the passcode the commissioner chose, not the bridge's configured passcode — and returns a restore closure that re-installs the configured acceptor when the window closes. Mirrors matter.js AdministratorCommissioningServer openCommissioningWindow → PaseServer.fromVerificationValue.

When no installer is wired (Basic Commissioning Window, or no verifier supplied), OpenWindow leaves the configured acceptor in place.

type PerExchangeCaseProvider

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

PerExchangeCaseProvider is a CaseHandlerProvider that lazily allocates a fresh CaseAdapter per inbound CASE exchange ID. It supports concurrent operational sessions by isolating responder state per exchange — Apple Home opens parallel CASE sessions from the iPhone (IPv6) and HomePod (IPv4); a singleton CaseAdapter gets stuck in `Finished` after the first successful Sigma3 and rejects every subsequent Sigma1 with `ProcessSigma1 already called`.

The factory MUST return a fully-configured CaseAdapter (sigma responder seeded from the current operational identity, fabric verifier, OnSessionEstablished callback wired). Operators rotate the underlying identity by replacing the factory closure.

Memory model: every Resolve stamps the entry's `lastTouched` so stale entries (no Resolve hit within `ttl`) get reaped by the optional PerExchangeCaseProvider.StartReaper background goroutine. Daemons handling many concurrent commissioners over time should always wire the reaper; without it the adapters map grows unboundedly.

The optional onEvict callback (set via PerExchangeCaseProvider.SetOnEvict) is called on TTL-eviction of each stale entry — the bridge wires [Bridge.forgetSigma1Replied] here so aborted CASE exchanges (Sigma1 arrived, Sigma3 never came) do not leak entries in the sigma1Replied dedupe map — see [Bridge.forgetSigma1Replied].

func NewPerExchangeCaseProvider

func NewPerExchangeCaseProvider(factory func() *CaseAdapter) *PerExchangeCaseProvider

NewPerExchangeCaseProvider constructs a provider backed by factory. A nil factory produces a provider that always returns nil from Resolve — equivalent to "no CASE handler", which the SC router degrades to a debug-logged drop.

func (*PerExchangeCaseProvider) Active

func (p *PerExchangeCaseProvider) Active() int

Active reports the count of in-flight per-exchange adapters. Test helper.

func (*PerExchangeCaseProvider) Forget

func (p *PerExchangeCaseProvider) Forget(exchangeID uint16)

Forget removes the adapter for exchangeID so the next [Resolve] allocates a fresh one. Operators wire this from a post-Sigma3 hook for immediate cleanup; production daemons typically rely on the TTL reaper instead.

func (*PerExchangeCaseProvider) Reset

func (p *PerExchangeCaseProvider) Reset()

Reset drops every per-exchange adapter. Operators call this when rotating the underlying identity so an in-flight commissioner cannot complete against the stale identity. The onEvict hook is called for every dropped entry so the Bridge's sigma1Replied map is also cleared.

func (*PerExchangeCaseProvider) Resolve

func (p *PerExchangeCaseProvider) Resolve(exchangeID uint16) CaseHandler

Resolve implements CaseHandlerProvider. Returns the existing adapter for exchangeID, allocating a fresh one via the factory on first sighting. Returns nil when the factory is nil or returned nil; callers (the SC router) drop the datagram on nil. Every successful Resolve refreshes the entry's `lastTouched`.

func (*PerExchangeCaseProvider) SetClock added in v0.7.2

func (p *PerExchangeCaseProvider) SetClock(c clock.Clock)

SetClock sets the clock the entries are stamped and reaped on; nil is clock.System. Call it before PerExchangeCaseProvider.StartReaper.

func (*PerExchangeCaseProvider) SetOnEvict

func (p *PerExchangeCaseProvider) SetOnEvict(fn func(exchangeID uint16))

SetOnEvict wires the TTL-eviction callback. The bridge sets this to [Bridge.forgetSigma1Replied] so aborted CASE exchanges (Sigma1 arrived, Sigma3 never completed) do not leak entries in the sigma1Replied dedupe map. Mirrors matter.js packages/protocol/src/session/case/CaseServer.ts per-exchange handler GC: TypeScript's GC reclaims the per-exchange handler automatically on exchange.close(); Go needs an explicit hook because the dedupe map is Bridge-owned, not handler-owned. Pass nil to detach.

func (*PerExchangeCaseProvider) StartReaper

func (p *PerExchangeCaseProvider) StartReaper(parent context.Context, interval, ttl time.Duration)

StartReaper spawns a background goroutine that drops adapters untouched for `ttl`, scanning every `interval`. Cancel via PerExchangeCaseProvider.StopReaper. Calling twice without an intervening Stop is a no-op.

func (*PerExchangeCaseProvider) StopReaper

func (p *PerExchangeCaseProvider) StopReaper()

StopReaper stops the reaper goroutine and waits for it to exit.

type PerExchangePaseProvider

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

PerExchangePaseProvider is a PaseHandlerProvider that lazily allocates a fresh PaseAdapter per inbound PASE exchange ID. It enforces the Matter §11.19.7 single-active-PASE-session invariant and the 60 s per-exchange hard timeout (Matter §4.13.1.7).

The factory MUST return a fully-configured PaseAdapter (PBKDF params + random source + onSessionEstablished callback already wired) — the provider only handles the per-exchange isolation.

Memory model: every Resolve stamps the entry's `lastTouched` so stale entries (no Resolve hit within `ttl`) get reaped by the optional PerExchangePaseProvider.StartReaper background goroutine. Operators pairing thousands of distinct commissioners over a daemon's lifetime should always wire the reaper; without it the adapters map grows unboundedly.

func NewPerExchangePaseProvider

func NewPerExchangePaseProvider(factory func() *PaseAdapter) *PerExchangePaseProvider

NewPerExchangePaseProvider constructs a provider backed by factory. A nil factory produces a provider that always returns nil from Resolve — equivalent to "no PASE handler", which the SC router degrades to a debug-logged drop.

func (*PerExchangePaseProvider) Active

func (p *PerExchangePaseProvider) Active() int

Active reports the count of in-flight per-exchange adapters. Test helper.

func (*PerExchangePaseProvider) Forget

func (p *PerExchangePaseProvider) Forget(exchangeID uint16)

Forget removes the adapter for exchangeID so the next [Resolve] call allocates a fresh one. Operators wire this from a post-Pake3 hook when they want immediate cleanup; production daemons typically rely on the TTL reaper instead. Also cancels the entry's 60 s hard-timeout timer.

func (*PerExchangePaseProvider) Reset

func (p *PerExchangePaseProvider) Reset()

Reset drops every per-exchange adapter. Operators call this when rotating the underlying PBKDF params so an in-flight commissioner cannot complete against the stale transcript. Also cancels all pending 60 s hard-timeout timers.

func (*PerExchangePaseProvider) Resolve

func (p *PerExchangePaseProvider) Resolve(exchangeID uint16) PaseHandler

Resolve implements PaseHandlerProvider. Returns the existing adapter for exchangeID, allocating a fresh one via the factory on first sighting. Returns nil when the factory is nil or returned nil; callers (the SC router) drop the datagram on nil. Every successful Resolve refreshes the entry's `lastTouched`.

Concurrency: the Matter §11.19.7 single-active-PASE invariant is enforced one layer up by CommissioningWindow.open — only one window is open at a time, and PASE traffic outside that window is dropped at the SC router. The per-exchange routing here only multiplexes datagrams of the SAME active window across retransmits.

60 s hard timeout per Matter §4.13.1.7: every new exchange starts a 60 s timer that calls Forget(exchangeID) on expiry. Mirrors matter.js PaseServer.ts:37 `PASE_PAIRING_TIMEOUT = Seconds(60)`.

func (*PerExchangePaseProvider) SetClock added in v0.7.2

func (p *PerExchangePaseProvider) SetClock(c clock.Clock)

SetClock sets the clock the 60 s exchange timeout, the entries' stamps and the reaper run on; nil is clock.System. Call it before the first Resolve.

func (*PerExchangePaseProvider) StartReaper

func (p *PerExchangePaseProvider) StartReaper(parent context.Context, interval, ttl time.Duration)

StartReaper launches a background goroutine that ticks every `interval` and evicts any adapter whose `lastTouched` is older than `ttl`. Idempotent — a second StartReaper call cancels the previous goroutine before launching a fresh one.

Recommended values: interval = ttl/4, ttl = 60s. PASE exchanges that complete cleanly take < 5s; a 60s window comfortably covers chip-tool retransmits while keeping the map size bounded.

Pass `ctx.Done()`-cancellation OR call [Stop] to terminate the reaper.

func (*PerExchangePaseProvider) Stop

func (p *PerExchangePaseProvider) Stop()

Stop cancels the background reaper started by [StartReaper] and blocks until the goroutine exits. Safe to call when no reaper is running. Idempotent.

type ReestablishResult added in v0.2.0

type ReestablishResult struct {
	// Former is the number of former subscriptions considered.
	Former int
	// Reestablished lists the ids that run again.
	Reestablished []uint32
}

ReestablishResult reports what Bridge.ReestablishFormerSubscriptions did.

type SessionActivityMarker

type SessionActivityMarker interface {
	MarkActiveRx(sessionID uint16)
	MarkActiveTx(sessionID uint16)
}

SessionActivityMarker is an optional capability a SessionLookup can implement so the wire paths can refresh a session's activity timestamps: MarkActiveRx after every successfully decrypted and authenticated inbound message, MarkActiveTx after every outbound secure send. The operational manager's idle reaper reads the timestamp to evict dead sessions; the Rx half additionally feeds the MRP peer-active determination. Mirrors matter.js packages/protocol/src/session/Session.ts:127-133 notifyActivity(messageReceived) — `timestamp` refreshes in both directions, `activeTimestamp` (peer activity) only on receive — invoked from MessageExchange.ts:429 (receive, duplicates included) and :562/:814 (sends).

type SessionFabricResolver

type SessionFabricResolver interface {
	FabricFor(sessionID uint16) (uint8, bool)
}

SessionFabricResolver is an optional capability a SessionLookup can implement so the bridge can resolve the operational FabricIndex of an active session. Used by Subscribe (for fabric-scoped quotas), the ACL access check, and the AdministratorCommissioning admin fabric attribute. PASE / pre-fabric sessions yield (0, true).

Returning (_, false) signals "session not known" — callers that need a fabric usually fall back to 0 (pre-fabric semantics).

type SessionLookup

type SessionLookup interface {
	// Lookup returns the session for sessionID. The second return is
	// false when no session is registered.
	Lookup(sessionID uint16) (*channel.Session, bool)
}

SessionLookup is the narrow operational-session port the receive pipeline depends on. Backed by `secure/operational.Manager` in the production daemon; tests substitute an in-memory fake.

SessionID==0 always returns (nil, false) by convention — the unsecured "session 0" is reserved by Matter for PASE / commissioning traffic, which the receive pipeline routes to the unsecured path before consulting SessionLookup.

type SessionPASEResolver

type SessionPASEResolver interface {
	IsPASE(sessionID uint16) (pase, ok bool)
}

SessionPASEResolver is an optional capability a SessionLookup can implement so the bridge can tell whether a session was established over PASE. It cannot be inferred from the FabricIndex: AddNOC adopts the commissioner's PASE session onto the freshly built fabric (matter.js packages/node/src/behaviors/operational-credentials/ OperationalCredentialsServer.ts:279-283 `session.fabric = fabric`), after which the session reports a non-zero fabric while still being PASE and still carrying the implicit Administer grant (FabricAccessControl.ts:189-191). Returning (_, false) signals "session not known" — the request is then evaluated by the ordinary ACL path, never handed Administer.

type SessionRegistry

type SessionRegistry interface {
	// Close removes the session and runs the manager's close hooks
	// (subscription cascade). Returns an error when the id is unknown.
	Close(sessionID uint16) error
	// CloseAllGraceful notifies every peer (best-effort CloseSession
	// StatusReport via the registry's graceful-close notifier) and
	// closes all sessions. Notifications stop once deadline passes;
	// local teardown always completes. Returns the sessions closed.
	CloseAllGraceful(deadline time.Time) int
}

SessionRegistry is the bridge-side seam to the operational session manager for session lifecycle actions the Secure-Channel wire path initiates: closing a single session on an inbound CloseSession StatusReport, and draining every session gracefully at shutdown. Backed by `secure/operational.Manager` in the production daemon via Bridge.AttachSessionRegistry; tests substitute a fake.

type SessionRetransmitIntervalResolver

type SessionRetransmitIntervalResolver interface {
	RetransmitBaseInterval(sessionID uint16, now time.Time) (time.Duration, bool)
}

SessionRetransmitIntervalResolver is an optional capability a SessionLookup can implement so the outbound-reliable tracker can size its retransmission backoff to the peer's advertised MRP session parameters (active/idle interval selected by peer activity — matter.js MRP.ts:129 retransmissionIntervalOf). Sessions the resolver does not know (session 0 / PASE) return (_, false) and the tracker falls back to the spec idle default.

type SessionSubjectResolver

type SessionSubjectResolver interface {
	SubjectFor(sessionID uint16) (nodeID uint64, cats []uint32, ok bool)
}

SessionSubjectResolver is an optional capability a SessionLookup can implement so the bridge can resolve the requesting peer subject (operational NodeID + CASE Authenticated Tags) of an active CASE session. Used by the IM dispatcher's ACL gate to evaluate per-subject ACEs (Matter §9.10.5.6). Returning (_, _, false) signals "session not known" — the ACL gate then matches only ACEs whose Subjects list is empty (the fabric-wide wildcard).

type SessionTransportResolver added in v0.3.0

type SessionTransportResolver interface {
	TransportParametersFor(sessionID uint16) (transports uint16, maxTCPMessageSize uint32, ok bool)
}

SessionTransportResolver resolves a session id to the transport parameters its peer stated in the handshake (SupportedTransports and MaxTcpMessageSize after the Matter 1.5 gate, sessionparams); ok is false when the session is unknown or stated none.

type Snapshotter

type Snapshotter func(ctx context.Context) (*endpoint.Topology, error)

Snapshotter supplies the endpoint topology the bridge serves.

The bridge consumes a topology; it does not build one. Assembly — walking a device model, deciding which sources are exposed, mapping them onto clusters — is the host's job, and keeping it there is what lets this package stay free of the daemon's model packages. A host closure typically composes its own walk with the topology assembler it owns:

func(ctx context.Context) (*endpoint.Topology, error) {
    return assembler.AssembleDevices(ctx, walkTheRegistry(ctx))
}

The bridge calls Snapshotter once at [Start] and on every [Reassemble]; it never caches the result — every call gets a freshly assembled topology. Returning a nil topology without an error is a programming error and is rejected as one.

type Status

type Status interface {
	// LocalAddr returns the bound UDP `host:port` of the operational
	// listener, or the empty string when the bridge has not started
	// (or has been torn down). Empty string is the unhealthy signal.
	LocalAddr() string
}

Status is the minimal contract the probe needs from the running bridge. The concrete *Bridge satisfies it via LocalAddr; tests can drop in a fake.

type SubscriptionStore added in v0.2.0

type SubscriptionStore interface {
	// SaveServerSubscription inserts or replaces the row of subscriptionID.
	SaveServerSubscription(ctx context.Context, subscriptionID uint32, fabricIndex uint8, peerNodeID uint64, payload []byte) error
	// DeleteServerSubscription removes one row; a missing row is no error.
	DeleteServerSubscription(ctx context.Context, subscriptionID uint32) error
	// DeleteServerSubscriptionsByFabric removes every row of a fabric.
	DeleteServerSubscriptionsByFabric(ctx context.Context, fabricIndex uint8) error
	// LoadServerSubscriptions returns every row's payload.
	LoadServerSubscriptions(ctx context.Context) ([][]byte, error)
	// ClearServerSubscriptions removes every row.
	ClearServerSubscriptions(ctx context.Context) error
}

SubscriptionStore is the durable home of the subscriptions of the bridge's CASE sessions, so they can be re-established under their old SubscriptionId after a restart (docs/adr/0008). It mirrors the persisted `subscriptions` state of matter.js SubscriptionsServer (packages/node/src/behavior/system/subscriptions/SubscriptionsServer.ts).

The payload is the bridge's encoding of one subscription (subscription.MarshalPeerSubscription) and is opaque to the store; the key columns are there for deletion. *store.Store implements it. Implementations must be safe for concurrent use.

type TCPConfig added in v0.3.0

type TCPConfig struct {
	// Incoming accepts TCP connections (the "tcpServer" role).
	Incoming bool
	// Outgoing may open TCP connections (the "tcpClient" role).
	Outgoing bool
}

TCPConfig selects the Matter TCP transport (ADR 0019). Incoming opens a TCP listener on the UDP socket's port; Outgoing declares that this node may open TCP connections itself. Both are advertised in the operational record's T key (bit 1 client, bit 2 server), which is absent when neither is set. The zero value is TCP off — matter.js NetworkServer.resolveTcpConfig with nothing configured (NetworkServer.ts:261-272) — and leaves the bridge exactly as it was before TCP existed.

Directories

Path Synopsis
Package bridgetest provides the affordances a test outside package bridge needs to put a bridge.Bridge into a chosen state without sending anything over the wire — and, in Commissioner, the controller a test needs to reach that state over the wire instead: a commissioner that commissions a device onto a fabric of its own and then reads, writes, invokes and subscribes over CASE.
Package bridgetest provides the affordances a test outside package bridge needs to put a bridge.Bridge into a chosen state without sending anything over the wire — and, in Commissioner, the controller a test needs to reach that state over the wire instead: a commissioner that commissions a device onto a fabric of its own and then reads, writes, invokes and subscribes over CASE.

Jump to

Keyboard shortcuts

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