# Xaere Receiver

The package is pure Dart and therefore can be used by Flutter Android, iOS,
desktop and platform adapters without coupling the XR protocol to an audio
library.

1. Implement XaereTransport for the selected receiver channel.
2. Use XaereHttpResolver with the Core URL.
3. Subscribe to receiver.resolutions and let the host application decide how
   to execute the returned generic intent.
4. Call setConsent with the host application's separate, purpose-specific
   privacy choices.

Before executing an intent, apply a host-side policy. The SDK includes
`XaereHttpsUrlAllowList` for the common `url/open` case; it accepts only HTTPS
URLs whose hostname is explicitly listed, and it never opens or follows a URL
itself. Other intent types should remain denied until the host defines an
equally explicit policy.

An audio/ultrasound decoder emits a string frame into XaereFrameTransport:
XRC1:brd_xxx. The frame is only a transport envelope. It does not carry an
intent or any customer data; Core resolves the opaque broadcast token.

`XaereHttpResolver` does not trust the JSON field `signature_status`. It sends
`X-Xaere-Resolution-Proof: ed25519-v1`, and Core places the complete intent,
policy and optional Audience proof in a short-lived
Ed25519 envelope. The SDK downloads `/v1/keys`, requires `application/json` on
both successful Core responses, requires the envelope key ID to match the
advertised Ed25519 key, verifies the exact signed bytes locally and then
confirms that the signed values equal the returned resolution. An altered,
expired, unsigned or mismatched-key response never reaches the host intent
policy. Keys are cached only by their explicit Core key identifier, so a normal
Core signing-key rotation triggers fresh discovery.

When Core returns a non-successful resolution, `XaereHttpResolver` throws a
`XaereResolutionException`. Its `failure` is one of the closed
`XaereResolutionFailure` values and its `retryable` flag is true only for rate
limiting or temporary service failure. Core intentionally maps both unknown
and revoked Codes to `codeUnavailable`; the SDK never exposes an untrusted
server message. Host applications should map the enum to their own bounded UI
and must not infer revocation state from a 404 response.

For a Flutter platform channel backed by ggwave, use `XaereGgWaveTransport`
and pass its decoded bytes to `ingestGgWavePayload`. It removes a possible
trailing NUL and accepts only an opaque `brd_` token (with or without the
`XRC1:` prefix). It deliberately rejects the older arbitrary ggwave messages
used by hardware experiments.

Measurement is not a prerequisite for resolving a code. The SDK reports only
when all of these conditions hold: the host gave consent, Core policy enables
Audience, an integration identifier is present, and a reporter was supplied.
The reporter should forward data to a trusted publisher backend: never embed an
Audience integration HMAC secret in a Flutter app.

`XaerePrivacyConsent` keeps Audience measurement, advertising and
personalization separate. All three are false unless the host explicitly sets
them. This SDK acts only on `audienceMeasurement`; advertising and
personalization have no capture, resolution, reporting or profiling data flow.
Setting either to true cannot enable Audience measurement.

For consented measurements that fail because the publisher backend is offline,
an integrator can opt in to `XaereEncryptedMeasurementQueue`. It encrypts a
bounded retry queue with AES-256-GCM before it reaches
`XaereFileQueueStorage`; the application supplies the 256-bit key from its
platform keystore/keychain and calls `await receiver.setConsent(...)` whenever
consent changes. Withdrawing Audience consent clears the queue. Nothing is
persisted unless an application explicitly supplies this queue, and the queue
contains no device or account identifier. `flushPendingMeasurements()` is run
at receiver start and can also be invoked after a network-recovery signal.
Each measurement receives one random `evt_` idempotency key when it is created.
That key is stored only inside the encrypted optional queue and is reused for
every retry, so a lost HTTP response cannot turn the same measurement into a
new event. Version 1 preview queues are upgraded on their first retry.

The built-in backend reporter implements
`XaereCancelableMeasurementReporter`. Consent withdrawal invalidates any
authorization still in flight and aborts active HTTP delivery before
`setConsent` returns. Receiver disposal follows the same cancellation path.
Custom reporters should implement this optional capability whenever their
transport can cancel pending measurement delivery.

When reporting is enabled, each receiver instance uses one random measurement
token in memory for 30 minutes by default, then rotates it. Withdrawal of
consent rotates it immediately before any later re-consent. It is not written
to disk, not derived from hardware or account data, and disappears when the
receiver is disposed. This permits only a short-lived, session-level reach and
frequency estimate. Set `measurementTokenLifetime` between one minute and
24 hours when the host needs a shorter campaign policy.

`XaereBackendMeasurementReporter` can POST the minimised event to an
integrator-owned endpoint. Its optional authorization callback authenticates
only with that backend; the backend adds the Audience HMAC. Do not point a
Flutter app directly at the Audience ingestion endpoint. The Core resolver and
reporter require HTTPS endpoints and never follow redirects; `http://localhost`
or a loopback address is allowed only for local development. Both operations
have a 15-second timeout by default; `requestTimeout` can be set from one
second to two minutes. The reporter validates the event and its Core proof
before opening a connection and sends the same per-event idempotency key on
every attempt. It removes an event from an optional retry queue only after the
publisher backend returns exactly `202 {"accepted":true}` as JSON; a generic
2xx, negative acknowledgement or extended response remains a failed delivery.

`stop()` invalidates resolutions still in flight, so a result cannot arrive
after microphone capture has ended. Transport-level failures such as native
microphone loss are emitted as errors on `resolutions`. `dispose()` is
idempotent and permanently closes the receiver.

`examples/flutter-audience-backend/` contains a Node.js 22 reference proxy for
that endpoint. It verifies a short-lived publisher user token, accepts only
one configured integration and HMAC-signs the exact minimised JSON to
Audience. The token must be minted only after publisher-side consent and bind
that consent to the configured integration for at most five minutes. Its
secret variables stay exclusively in the publisher backend.

To avoid repeated openings and measurements while an ultrasonic transmitter is
in range, `XaereReceiver` ignores the same opaque token for eight seconds by
default. This is in-memory only; it is not a device identifier and resets when
the receiver is disposed. Set `deduplicationWindow` when the host needs a
different short interaction window.

The existing ggwave RP2040 experiments are a candidate transport adapter, not
part of the protocol or identity layer.

For Flutter apps that receive native decoder events over an EventChannel, use
the companion local package `xaere_receiver_flutter`. It accepts only raw
ggwave UTF-8 payloads and forwards validated opaque broadcast tokens into this
package; it does not bundle a transmitter, a secret or a device identifier.

Run `dart test` to exercise frame normalisation, local Ed25519 verification,
alteration rejection, Audience opt-in, in-memory anti-duplication and the URL
allow-list contract.

## Developer preview licence

This preview is not published on pub.dev. Xaere-specific code is available
only to recipients already authorised under separate preview terms; see
`LICENSE`. A public production licence must be selected before the stable SDK
release.
