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
- Variables
- func ValidateRequestID(id string, now time.Time, maxAge, futureSkew time.Duration) (time.Time, error)
- type CanonicalRequest
- type ClaimDisposition
- type CompletedResult
- type Decision
- type Digest
- type Handle
- type Key
- type Ledger
- func (l *Ledger) Claim(ctx context.Context, key Key, digest Digest, now time.Time) (Decision, error)
- func (l *Ledger) Close() error
- func (l *Ledger) Complete(ctx context.Context, handle Handle, result CompletedResult, now time.Time) error
- func (l *Ledger) Lookup(ctx context.Context, key Key, now time.Time) (Decision, error)
- func (l *Ledger) MarkIndeterminate(ctx context.Context, handle Handle, reasonCode string, now time.Time) error
- func (l *Ledger) Prune(ctx context.Context, now time.Time, limit int) (int64, error)
- type Options
Constants ¶
const ( OutcomeSuccess = "success" OutcomeRejected = "rejected" ActionExecutedYes = "yes" ActionExecutedNo = "no" ActionNotApplicable = "not_applicable" ActionExecutedUnknown = "unknown" )
Variables ¶
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") )
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") )
var ( ErrSchemaTooNew = errors.New("claimstore: database schema is newer than this binary") ErrSchema = errors.New("claimstore: invalid database schema") )
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 ¶
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 ¶
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 ¶
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) 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 ¶
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.