# VniDrop Core Send/Receive Flow

This crate owns the transfer backend. Platform/UI code should pass file handles
or paths into Rust and react to `CoreEvent` updates; it should not move file
bytes through Kotlin memory.

## Send

1. `initialize(app_data_dir, event_sink)` starts the Iroh endpoint, blob
   provider, handshake protocol, SQLite repository, and event hub.
2. `share_files(sources, metadata)` validates platform sources, streams each
   file into `iroh-blobs`, stores a collection, and returns a VniDrop ticket.
3. New VniDrop shares are `ApprovalRequired` by default. A copied ticket is not
   enough to read bytes until the sender approves the receiver endpoint.
4. The blob provider is **default-deny**: only hashes registered for an active
   share (collection root **and** each member blob) may be served, and only when
   access policy allows that remote endpoint. Unknown hashes are refused.
5. The sender observes receiver requests through `CoreEvent` entries with
   `phase="approval"` and can query them with
   `list_receiver_requests(transfer_id)`.
6. `respond_receiver_request(request_id, accepted, reason)` accepts or refuses a
   pending request. Accepted requests create a time-limited access session for
   the receiver endpoint.
7. Ticket strings are capabilities. Share events emit hash/size metadata only —
   never the full ticket payload.

## Receive

1. `receive(ticket, output_dir, receiver_name)` parses and validates the ticket.
2. VniDrop tickets first connect to the handshake ALPN
   `/vnidrop/handshake/2` and send `RequestTransfer` metadata to the sender.
3. If approved, the receiver connects to the blobs ALPN, downloads the
   collection, and streams files to `output_dir`.
4. If refused, expired, unknown, or cancelled, the receive transfer is marked
   `failed` or `cancelled` and emits an error/lifecycle event.
5. Only `vnd1:` VniDrop tickets are accepted. Raw iroh `BlobTicket` strings are
   rejected at parse time so receive always runs the approval handshake.

## Saved Devices And Targeted Transfers

1. A completed authenticated invitation transfer creates a short-lived pairing
   eligibility. Both devices must explicitly consent before either relationship
   becomes `Saved`; decline, forget, block, expiry, or replay cannot save it.
2. A saved device's remote display name comes from authenticated transfer
   metadata and may refresh after later authenticated transfers. A local label
   is private to this installation, takes display precedence in the UI, and is
   preserved independently across restart and schema migration.
3. `create_targeted_transfer` imports an immutable manifest and sends only a
   receiver-bound offer. Approval stores protected authorization in core custody;
   targeted work creates no invitation history, receiver approval request,
   invitation delivery receipt, received-artifact row, or pairing eligibility.
4. Targeted blobs are default-deny and scoped to the intended saved endpoint.
   Knowing a transfer id, manifest hash, member hash, or blob address does not
   authorize another device to discover, approve, or download the payload.
5. Approved receives use the same streaming, no-overwrite paths, and output-sink
   terminal-callback guarantees as invitation receives. Verified progress is
   monotonic and bounded; interrupted work and protected authorization resume
   after restart without another approval.
6. Receiver completion is durable locally and acknowledged to the sender with
   idempotent retry. Targeted cancel and delete synchronously signal local active
   work before durable cleanup; forget and block revoke affected relationships
   and active targeted work within their core operation. All four durably deny
   reuse and perform idempotent payload/secret cleanup.
7. A missing or corrupted endpoint credential remains fail-closed during normal
   startup. The explicit pre-start identity-reset constructor accepts only that
   unrecoverable state, atomically invalidates identity-bound relationships,
   eligibility, targeted authorization, and retry state, then creates a new
   protected identity. Transfer history and received files remain; old active
   Invitation transfers are stopped and Saved devices must be paired again.
8. Saved devices, relationships, pairing eligibility, and targeted transfers are
   shipped Rust core domains. Graduating the KMP and Apple Saved-device UI and
   their existing experimental preference gates is outside this release gate.

## Core States And Events

- Transfer statuses: `sharing`, `receiving`, `done`, `failed`, `cancelled`,
  `stopped`.
- Main event phases: `endpoint`, `import`, `ticket`, `handshake`, `approval`,
  `access`, `transfer`, `download`, `export`, `delivery`, `lifecycle`, `error`,
  `pairing`, and `targeted_transfer` (see catalog below).
- Events are sent to `CoreEventSink` immediately and persisted through the event
  hub. `list_events` flushes queued persistence before reading SQLite.
- `shutdown()` is idempotent and flushes events before stopping the router.

### Pairing and targeted-transfer event catalog

Treat every event as a wake-up: refresh durable state via list/get APIs.
Targeted progress updates persist monotonic `verified_bytes`; event payloads are
advisory and the durable targeted-transfer snapshot is authoritative.

**`pairing`:** `eligibility-available`, `eligibility-removed`,
`relationship-changed`, `relationship-grant-rotated`, `saved-device-forgotten`,
`device-blocked`.

**`targeted_transfer`:** `offer-received`, `approved`, `offer-declined`,
`created`, `offering`, `awaiting-approval`, `connecting`, `transferring`,
`progress`, `interrupted`, `completed`, `cancelled`, `failed`, `deleted`.
Lifecycle payloads identify the durable row with `targeted_transfer_id`.

## Platform File Rules

- Desktop uses normal filesystem paths.
- Android opens SAF/content URIs in Kotlin and passes a borrowed file
  descriptor; Rust duplicates the descriptor before streaming.
- iOS starts the security-scoped URL lease in Swift and keeps it alive while
  Rust streams from the accessible file URL/path.

## Durability And Filesystem Policy

- SQLite records have a local UUID in addition to the protocol transfer ID.
  Schema-v2 records are migrated in place and keep their tickets and history.
- Imports and receives are recorded before work begins. A process restart marks
  interrupted work failed and expires approval requests that no longer have an
  in-memory responder.
- Persisted shares are restored only when their root collection is complete and
  readable. Missing or corrupt roots fail closed and emit a recovery event.
- Receive destinations use a no-overwrite policy. Rust writes a uniquely named
  temporary file in the destination directory, syncs it, and publishes it with
  a no-clobber hard link when the filesystem supports it. On platforms that
  reject hard links (notably Android emulated external storage), publication
  falls back to an exclusive rename (`renameat2(RENAME_NOREPLACE)` /
  `renamex_np(RENAME_EXCL)`). Failure or cancellation removes the temporary
  file. Stale VniDrop temporary files are cleaned on later writes.
- Android defaults to the shared system Downloads collection via MediaStore
  (`ReceiveFolderKind.AndroidPublicDownloads` on API 29+). Files show up in the
  user's Downloads UI like a browser download. Custom folders still use a SAF
  tree URI from the folder picker. Both Android sinks stream through
  `ReceiveOutputSink` instead of raw filesystem paths. Pre-Android 10 falls
  back to the public Downloads path with legacy storage permission.
- Foreign output sinks receive exactly one terminal callback after a successful
  `start_file`: `finish_file` or `abort_file`.

## Blob Retention Policy

Stopping a share immediately removes its provider mapping and approval state,
so outstanding VniDrop tickets can no longer download content. Physical blob
chunks are not force-deleted at stop time because content-addressed chunks may be
shared by another active collection. Every active outgoing share has a persistent
`vnidrop/share/<local-id>` tag; stopping or deleting it removes that tag, and the
configured garbage collector later reclaims content with no remaining persistent
or temporary tag. Receive downloads keep a temporary tag through export and become
reclaimable after publication. Restart reconciliation repairs active-share tags,
removes orphan share tags, and never restores a stopped share.

The explicit transfer-cache action is available only when no transfer or share is
active. It shuts the core down cleanly, removes the app-owned blob store, and then
restarts the core with the same identity and network configuration. Deleting all
transfer records invokes the same cleanup after live shares have been stopped.

## Resource Limits

`CoreLimits` controls source count, collection files and bytes, path and ticket
sizes, metadata, retained events, pending approvals, concurrent transfers, and
the event persistence queue. `initialize` uses conservative defaults (including
bounded ticket size, pending approvals, and total collection bytes);
`initialize_with_limits` supports stricter deployments and tests. Cheap limits
are checked before durable or network work, while remote collection limits are
checked before downloading file content.

Manual `approve_endpoint_for_transfer` only applies to active shares, requires a
non-empty endpoint id, and creates a time-limited session (same TTL as handshake
approval), never a permanent grant.
