Build extensions / Concepts
Network requests
Extensions have no network of their own. They reach the web through fetch from @orbtorrent/api, and only the hosts they list.
List the hosts
Section titled “List the hosts”Add each host the extension talks to in package.json:
{ "permissions": { "network": ["www.virustotal.com"] }}- Exact host names, in lowercase, without
https://or a path.virustotal.comdoesn’t coverwww.virustotal.com: list each one you use. There are no wildcards. - Users see the list on the extension’s page and before they install it, as “Connect to www.virustotal.com”. Ask only for what you need.
Make a request
Section titled “Make a request”import { Download, Result, fetch, getPreferenceValues } from "@orbtorrent/api";
export default async function notify(download: Download): Promise<Result> { const { webhook } = getPreferenceValues(); const response = await fetch(webhook, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ content: `Finished: ${download.name}` }), }); if (!response.ok) { return { status: "skipped", summary: `Discord didn't take the message (status ${response.status})` }; } return { status: "passed", summary: "Sent to your Discord channel" };}This needs "network": ["discord.com"], and a webhook setting for the channel’s webhook address.
The request
Section titled “The request”fetch(url, init?) takes an address, as a string or a URL, and:
method |
GET, HEAD, POST, PUT, PATCH or DELETE. GET unless you say. |
headers |
An object, such as { "x-apikey": key }. OrbTorrent sets host, content-length, connection and transfer-encoding itself, and drops cookie. |
body |
Text, using JSON.stringify for JSON, or a form from files.formData. |
The response
Section titled “The response”status |
The HTTP status, such as 200. |
ok |
Whether the status is 200 to 299. |
url |
The address it came from, after any redirects. |
headers |
get(name), has(name) and entries(). Names are case-insensitive. |
text() |
The body as text. |
json() |
The body parsed as JSON. |
bytes() |
The body as a Uint8Array, for images, zip files and anything that isn’t text. |
arrayBuffer() |
The body as an ArrayBuffer. |
Text and JSON arrive as they are, and anything else arrives intact as bytes, so an extension can fetch an image and save it next to a download:
const poster = await fetch(posterAddress);await files.save(video, "-poster.jpg", await poster.bytes());The rules
Section titled “The rules”OrbTorrent checks every request before it’s made:
- HTTPS only, on the standard port, 443.
- Only listed hosts. A request to any other is refused with an error that says which host to add.
- Never your Mac or your network. A host that points to an address on the Mac or the local network is refused, wherever its name says it is. OrbTorrent looks the host up, checks every address, and connects to the very address it checked, so a host can’t pass the check and then switch to a local address.
- Redirects only to listed hosts. A redirect to any other host isn’t followed: the hook gets the redirect response itself.
- No credentials in the address. Put them in a header instead.
- Responses up to 10 MB, and 30 seconds for each request.
- No cookies and no shared cache, so one extension can’t see another’s sessions.
A refused request, or one that fails, throws an error. Errors and fixes lists the messages.
Handle failure
Section titled “Handle failure”The network can always fail, so decide what that means for your check:
let response;try { response = await fetch(`https://api.example.com/check/${download.id}`);} catch (error) { // Before a download starts, a network error means skipped, never failed. return { status: "skipped", summary: `Couldn't reach example.com: ${(error as Error).message}` };}Before a download starts, a network error or a timeout means skipped, never failed. A beforeStart hook has 15 seconds in all, so keep its requests few and quick.
Respect rate limits
Section titled “Respect rate limits”Many free APIs allow only so many requests a minute. To stay within them:
- Wait between requests with
sleep, which doesn’t use CPU time. AnafterFinishhook has 6 hours, so it can pace itself. - Show progress on long runs with
progress, so the user sees it working. - Keep answers in
storage, so running the hook again doesn’t ask again. - Back off on 429. Wait, try once more, and if it’s refused again, return
skippedwith what happened.
Each extension’s afterFinish and afterChecks runs go one at a time, so a hook never competes with itself for a rate limit.
Test it
Section titled “Test it”Tests answer requests from mocks, so they run without the network. Testing explains.
{ "preferences": { "webhook": "https://discord.com/api/webhooks/123/abc" }, "download": { "name": "Ubuntu 26.04", "files": [{ "path": "ubuntu-26.04.iso", "size": 6000000000 }] }, "mocks": { "fetch": [{ "method": "POST", "url": "https://discord.com/api/webhooks/*", "status": 204 }] }, "expect": { "status": "passed", "fetched": ["https://discord.com/api/webhooks/123/abc"] }}