Skip to content

halvorai-ingest 0.5.0: device helpers for Flutter apps - #8

Open
hedge-mustafa wants to merge 1 commit into
mainfrom
feat/device-sources
Open

hedge-mustafa wants to merge 1 commit into
mainfrom
feat/device-sources

Conversation

@hedge-mustafa

Copy link
Copy Markdown

Why

Mobile apps built with halvorai_ingest_flutter sign events on the phone, with a key each install makes for itself. The app never holds an API key. The customer's backend has two jobs:

  • register each install's public key
  • forward its signed batches to HalvorAI unchanged

This PR adds that backend half: items SDK-1 to SDK-3 in the Flutter backend requirements. The API-side items are separate. The same change is going into the other server SDK (halvorAI/ingest-js) so both behave the same.

App ──public key──▶ customer backend ──POST /v1/sources──▶ HalvorAI      (once per install)
App ──signed batch──▶ customer backend ──byte for byte──▶ HalvorAI /v1/events

What's added: halvorai_ingest.device (0.5.0)

  • register_device_source / register_device_source_async:
    • Accepts only the public half of an Ed25519 key, and refuses a key with d.
    • Sends only kty, crv and x to POST /v1/sources, and returns the src_ id.
    • If HalvorAI doesn't register the key, the answer is 502, so the app retries.
  • forward_envelopes / forward_envelopes_async:
    • Checks the batch shape, at most 100 envelopes and at most 1 MB.
    • Checks that each event's owner.id is the signed-in user, and that its environment matches the API key.
    • Posts the original body to POST /v1/events byte for byte. It's parsed only for the checks, because re-serializing could change bytes the device signed.
    • Returns HalvorAI's status and body unchanged; the app reads its per-event results from them.
  • Async versions for FastAPI and other async frameworks. The README shows both routes in FastAPI.

Refusals use HalvorAI's error format ({"error":{"code","message"}}), so the Flutter SDK reports the code as serverCode. The status tells the app what to do:

Status Code The app
400 invalid_request drops the batch
413 payload_too_large splits the batch
422 owner_mismatch, environment_mismatch drops the batch
502 upstream_error (HalvorAI unreachable) retries
401 / 403 (the customer's own session check) keeps the batch on disk, retries later

init now registers its source through the same code (sources.py). A refused registration still raises RuntimeError (now SourceRegistrationError, a subclass).

Tested

  • uv run mypy (strict), uv run pytest (164 tests, 19 new) on the default Python and on 3.10.

  • End to end with the Dart SDK. A local backend built from these helpers sat in front of a fake HalvorAI, which verifies every forwarded signature against the key registered for its source. The client was halvorai_ingest (Dart), doing exactly what the Flutter package does: registering per the provisioning contract and delivering through ForwardingTransport. Results:

    • Registration returns a src_ id; a private key is refused with 400.
    • Three signed events are forwarded and all three signatures verify upstream (the fake rejects a tampered event).
    • Events naming another owner are refused, 422 owner_mismatch.
    • Without a session the app gets 401 and keeps the events on disk.

    The same run passes against the other SDK's helpers.

Not in this PR

  • API side:
    • registration idempotent per public key (the app may retry with the same key after a lost response)
    • kind: "device" and paging for many sources
    • DELETE /v1/sources/{id}
    • a restricted key scope
  • revokeDeviceSource (SDK-5): waits for that DELETE endpoint.
  • Optional signature check before forwarding: HalvorAI checks signatures anyway.
  • The fix-list items: the card number next to other digits, the CVC, and the environment and unconfirmed rules. They aren't needed for Flutter to work, and are a separate change.

Please don't merge until it has been reviewed.

🤖 Generated with Claude Code

Apps built with halvorai_ingest_flutter sign events on the phone and
never hold an API key; the customer's backend registers each install's
key and forwards its batches. halvorai_ingest.device provides that
half, with the same behaviour as @halvorai/ingest/device 0.4.0:

- register_device_source(_async): registers an install's public Ed25519
  key with POST /v1/sources; a key with d is refused.
- forward_envelopes(_async): checks each event's owner against the
  signed-in user and its environment against the key, then passes the
  body to POST /v1/events byte for byte and returns HalvorAI's answer
  unchanged. Refusals use HalvorAI's error format.
- README: a FastAPI example of both routes.

init now registers its source through the same code (sources.py); a
refused registration still raises RuntimeError.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@hedge-urk hedge-urk left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gtg

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants