# Upload providers

A headless Root can contribute a destination to Mango's native upload workflow. Mango keeps ownership of file selection, drag and drop, paste confirmation, progress UI, provider preference, and draft insertion. The Root receives only metadata and bounded reads for the file the user explicitly selected.

The provider returns a shareable HTTP or HTTPS URL. Mango inserts that URL into the composer for review; IRC sees nothing until the user sends the ordinary message. The IRC server, including Ergo, does not participate in the upload.

## Declare the provider

Upload providers require all three of these manifest elements:

1. an exclusive `headless` surface;
2. the `uploads.provide` capability;
3. a matching entry under `contributions.uploadProviders`.

```json
{
  "grove": 1,
  "id": "com.example.grove.files",
  "name": "Private files",
  "version": "1.0.0",
  "entry": "index.html",
  "settings": "settings.html",
  "presentation": { "surfaces": ["headless"], "scope": "application" },
  "contributions": {
    "uploadProviders": [
      {
        "id": "primary",
        "name": "Private files",
        "kind": "userConfigured",
        "acceptsImagesOnly": false,
        "maxFileSize": 1073741824
      }
    ]
  },
  "capabilities": ["uploads.provide", "storage", "storage.secure"],
  "hostPermissions": ["https://uploads.example.com"]
}
```

`userConfigured` means a destination the user configured or signed into. Once its Root is running and its handler is registered, Mango may choose it for the privacy-preserving Automatic preference. `thirdParty` describes a shared external service and must be selected explicitly.

`acceptsImagesOnly` filters the native picker. `maxFileSize` rejects larger files before the handler runs. Mango namespaces the local provider identifier as `grove:<extension-id>:<provider-id>` in global preferences.

Use `scope: "application"` when one configured destination should be available on every network. Mango presents one logical Root in management and mirrors a passive runtime into each top-level IRC session. One manager action serially starts or stops every mirror. Each upload is routed through the mirror belonging to its destination session, so duplicate provider identifiers in other mirrors cannot steal the request. These runtimes have an empty `grove.info.networkID`, receive no IRC traffic, register the same provider idempotently, and share configuration through extension Web storage and `grove.secrets.application`.

Use `scope: "server"` only when each IRC network intentionally needs a separate uploader configuration or account. That Root is started, stopped, and restored independently for each session, and its default Keychain namespace follows that network.

## Register a handler

Register only after configuration is sufficient to upload:

```js
grove.uploads.register('primary', async (file, context) => {
  const responseURL = await upload(file, fraction => context.progress(fraction))
  return { url: responseURL }
})
```

The identifier must match a manifest declaration. Re-registering replaces the handler. Make a temporarily unavailable destination ineligible without stopping the Root:

```js
grove.uploads.unregister('primary')
```

If the Root is not running for the destination session, has not registered its handler, or loses its grant, Mango reports the provider as unavailable. Application-scoped code must tolerate more than one mirrored runtime when several networks are connected; registration is idempotent within each runtime.

## Read the selected file

The handler receives immutable metadata and no local path:

```ts
interface GroveUploadFile {
  readonly name: string
  readonly type: string
  readonly size: number
  read(offset: number, length: number): Promise<Uint8Array>
}
```

Each `read` is bounded to 1 MiB. Offsets and lengths must remain inside the file. Mango permits at most four concurrent reads and bounds total read amplification, so stream forward rather than repeatedly scanning the file.

```js
const CHUNK_SIZE = 1024 * 1024

for (let offset = 0; offset < file.size;) {
  const length = Math.min(CHUNK_SIZE, file.size - offset)
  const chunk = await file.read(offset, length)
  await sendChunk(chunk, offset)
  offset += chunk.byteLength
  context.progress(file.size === 0 ? 1 : offset / file.size)
}
```

Mango clamps progress to `0...1` and completes it when a valid result returns.

The handler may return a URL string or `{ url: string }`. Only `http` and `https` results are accepted. Throw an `Error` with a short user-safe message when the service fails. Do not include a token, local filename path, or response body containing private data.

## TUS example

TUS is a good fit because its offset protocol supports streaming bounded chunks without exposing a file URL. A typical handler:

1. creates an upload with `POST`, `Tus-Resumable: 1.0.0`, `Upload-Length`, and encoded metadata;
2. resolves and validates the returned `Location` against the configured HTTPS origin;
3. sends chunks with `PATCH`, `Upload-Offset`, and `application/offset+octet-stream`;
4. uses `HEAD` to recover the authoritative offset after an ambiguous failure;
5. returns the service's shareable download URL.

The TUS upload resource is not universally a public download URL. If the service uses a separate sharing API, call it after the upload and return that URL instead.

For a user-configured endpoint, validate HTTPS, reject embedded credentials and fragments, disable redirects when sending authorization, and verify every returned upload location remains on the configured origin.

[tus-uploader v0.2.2](https://github.com/Mediaware-EU/tus-uploader/tree/v0.2.2) is a complete implementation with a settings page, Keychain bearer token, resumable offset recovery, unit tests, and an optional live test against `tusd`.

## Other providers

The same bridge can support Dropbox, Google Drive, S3-compatible storage, or another file service. Grove's native [`oauth.authorize`](bridge.md#oauth-authorization) bridge can conduct the user-visible Authorization Code and PKCE step; [`network.request`](bridge.md#native-https-requests) can call a provider that does not support browser CORS. Provider-specific token exchange, refresh, revocation, and sharing behavior remain extension responsibilities. Store service credentials in `grove.secrets`, request exact service origins whenever possible, and return only a URL that the intended recipient can actually open.
