claimstore

package
v0.51.1 Latest Latest
Warning

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

Go to latest
Published: Jul 22, 2026 License: Apache-2.0 Imports: 18 Imported by: 0

Documentation

Overview

Package claimstore provides the durable at-most-once request ledger used by the local SpeechKit to Home Assistant bridge.

A claim is committed before an outbound Home Assistant request may start. Existing non-terminal claims are deliberately never re-dispatched: after a crash the store cannot know whether Home Assistant applied the command, so returning an indeterminate outcome is safer than duplicating a side effect.

The ledger stores request fingerprints and a small, allow-listed replay result. It never stores the command text, pairing credentials, Home Assistant credentials, raw Home Assistant payloads, or synthesized audio.

Index

Constants

View Source
const (
	OutcomeSuccess        = "success"
	OutcomeRejected       = "rejected"
	ActionExecutedYes     = "yes"
	ActionExecutedNo      = "no"
	ActionNotApplicable   = "not_applicable"
	ActionExecutedUnknown = "unknown"
)

Variables

View Source
var (
	// ErrDigestKeyTooShort prevents low-entropy pairing material from being
	// used directly as the HMAC key.
	ErrDigestKeyTooShort = errors.New("claimstore: digest key must contain at least 32 bytes")
	// ErrInvalidCanonicalRequest reports a missing or over-sized field in the
	// exact request representation that will be sent to Home Assistant.
	ErrInvalidCanonicalRequest = errors.New("claimstore: invalid canonical request")
)
View Source
var (
	ErrInvalidRequestID = errors.New("claimstore: request id must be a canonical UUIDv7")
	ErrStaleRequestID   = errors.New("claimstore: request id is outside the accepted age window")
	ErrFutureRequestID  = errors.New("claimstore: request id is too far in the future")
)
View Source
var (
	ErrSchemaTooNew = errors.New("claimstore: database schema is newer than this binary")
	ErrSchema       = errors.New("claimstore: invalid database schema")
)
View Source
var (
	ErrInvalidOptions    = errors.New("claimstore: invalid options")
	ErrUnsafeRetention   = errors.New("claimstore: retention must exceed the request admission window")
	ErrInvalidKey        = errors.New("claimstore: invalid claim key")
	ErrInvalidDigest     = errors.New("claimstore: invalid request digest")
	ErrNotFound          = errors.New("claimstore: claim not found")
	ErrCapacity          = errors.New("claimstore: claim capacity reached")
	ErrIndeterminate     = errors.New("claimstore: Home Assistant outcome is indeterminate")
	ErrDigestConflict    = errors.New("claimstore: request id was already used with different content")
	ErrTerminalConflict  = errors.New("claimstore: claim already has a different terminal outcome")
	ErrInvalidResult     = errors.New("claimstore: invalid completed result")
	ErrInvalidTransition = errors.New("claimstore: invalid claim state transition")
)

Functions

func ValidateRequestID

func ValidateRequestID(id string, now time.Time, maxAge, futureSkew time.Duration) (time.Time, error)

ValidateRequestID verifies the UUIDv7 format and its embedded Unix millisecond timestamp. This freshness gate is what makes bounded claim retention safe: once a claim can be pruned, its request id is too old to be admitted again.

Types

type CanonicalRequest

type CanonicalRequest struct {
	PairedDeviceID string
	RequestID      string
	RuleID         string
	Locale         string
	Text           string
	EntityID       string
	ExpectedState  string
	// InputSHA256 optionally binds a higher-level, already authenticated
	// transport payload to the same at-most-once claim. The legacy
	// speechkit.device_agent.v1 JSON routes leave this empty, preserving their
	// exact v1 digest. The separate Box media ingress supplies the verified
	// L16 payload digest so a request_id cannot be replayed with different
	// audio even when both clips happen to map to the same static command.
	InputSHA256 string
}

CanonicalRequest contains every client-controlled value that can affect the Home Assistant command. Callers must dispatch these normalized values, not the pre-normalization HTTP payload, so fingerprinting and execution cannot diverge.

func NormalizeCanonicalRequest

func NormalizeCanonicalRequest(req CanonicalRequest) (CanonicalRequest, error)

NormalizeCanonicalRequest trims the bounded textual fields used by both the request digest and the outbound Home Assistant request.

type ClaimDisposition

type ClaimDisposition uint8

ClaimDisposition describes the only safe action after Claim returns.

const (
	DispatchNew ClaimDisposition = iota + 1
	ReplayCompleted
	OutcomeIndeterminate
	DigestConflict
)

type CompletedResult

type CompletedResult struct {
	Outcome        string
	ConversationID string
	SpeechText     string
	Language       string
	ResponseType   string
	ErrorCode      string
	ReasonCode     string
	Retryable      bool
	ActionExecuted string
}

CompletedResult is the allow-listed, client-visible subset persisted for a replay. It intentionally has no field for raw Home Assistant JSON, entity context, headers, credentials, SSML, or audio.

type Decision

type Decision struct {
	Disposition ClaimDisposition
	Handle      Handle
	Result      *CompletedResult
}

Decision is returned only after the corresponding transaction committed.

type Digest

type Digest [sha256.Size]byte

Digest is the HMAC-SHA-256 fingerprint of a canonical request.

func HMACDigest

func HMACDigest(key []byte, req CanonicalRequest) (Digest, error)

HMACDigest computes the persisted request fingerprint. Length-prefixing avoids delimiter ambiguity, while the domain prefix prevents cross-protocol reuse. The HMAC key is not retained by the store.

type Handle

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

Handle proves that this process won a new durable claim. Its fields are intentionally private so only Claim can mint it.

type Key

type Key struct {
	PairedDeviceID string
	RequestID      string
}

Key is scoped to the server-authenticated pairing identity. Callers must derive PairedDeviceID from authentication state, never from an untrusted request-body device id.

type Ledger

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

Ledger owns a single-purpose SQLite database. It must not be shared with the configurable transcription/content store.

func Open

func Open(ctx context.Context, options Options) (*Ledger, error)

Open opens or creates the durable claim ledger. In-memory databases and non-regular paths are rejected because they cannot provide crash recovery.

func (*Ledger) Claim

func (l *Ledger) Claim(ctx context.Context, key Key, digest Digest, now time.Time) (Decision, error)

Claim atomically resolves an existing request or creates a durable claim. DispatchNew is returned only after the insert commit succeeds.

func (*Ledger) Close

func (l *Ledger) Close() error

Close releases the database. It does not remove the durable ledger.

func (*Ledger) Complete

func (l *Ledger) Complete(ctx context.Context, handle Handle, result CompletedResult, now time.Time) error

Complete records the allow-listed Home Assistant result. Callers must wait for this commit before returning success or starting TTS.

func (*Ledger) Lookup

func (l *Ledger) Lookup(ctx context.Context, key Key, now time.Time) (Decision, error)

Lookup returns the authenticated pairing's existing claim without accepting a caller-supplied digest. It is used only to authorize downstream work, such as TTS of the exact persisted HA response, and can never create or redispatch a command claim.

func (*Ledger) MarkIndeterminate

func (l *Ledger) MarkIndeterminate(ctx context.Context, handle Handle, reasonCode string, now time.Time) error

MarkIndeterminate terminalizes a claim whose outbound HA outcome cannot be proven. Such a request remains permanently non-dispatchable until it safely ages out of the request admission window and retention period.

func (*Ledger) Prune

func (l *Ledger) Prune(ctx context.Context, now time.Time, limit int) (int64, error)

Prune removes at most limit claims whose retention window elapsed. Safety additionally depends on Claim rejecting stale UUIDv7 request ids.

type Options

type Options struct {
	Path          string
	MaxEntries    int
	Retention     time.Duration
	MaxRequestAge time.Duration
	FutureSkew    time.Duration
	CleanupBatch  int
}

Options configures the dedicated local ledger.

Jump to

Keyboard shortcuts

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