Browse API 1 documentation
Upload providers
Contribute a native upload destination from a headless Root.
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:
- an exclusive
headlesssurface; - the
uploads.providecapability; - a matching entry under
contributions.uploadProviders.
{
"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:
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:
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:
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.
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:
- creates an upload with
POST,Tus-Resumable: 1.0.0,Upload-Length, and encoded metadata; - resolves and validates the returned
Locationagainst the configured HTTPS origin; - sends chunks with
PATCH,Upload-Offset, andapplication/offset+octet-stream; - uses
HEADto recover the authoritative offset after an ambiguous failure; - 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 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 can conduct the user-visible Authorization Code and PKCE step; network.request 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.