SDK reference
vos.net — Network
Sandboxed apps cannot reach the internet directly. vos.net.fetch routes requests through the VibeOS host, which re-validates every URL against the origins your app declared — and sidesteps CORS entirely.
Why a proxy
VibeOS injects a strict Content-Security-Policy built from your declared origins, so a plain fetch(), XMLHttpRequest or WebSocket to anywhere else is blocked by the browser before it leaves the window. Even for allowed origins, most public APIs would then fail CORS.
- No CORS. The request is made server-side, so APIs without CORS headers work normally.
- Re-validated host-side. The allow-list is checked against the installed grant on every call — an app cannot widen its own reach at runtime.
- Hardened. Private networks are unreachable and unsafe headers are stripped in both directions.
Declaring origins
List each https:// origin — optionally with a path prefix — in permissions.network. An entry matches any URL on the same origin whose path begins with the given prefix.
{
"permissions": {
"network": [
"https://api.openlibrary.org/",
"https://covers.openlibrary.org/"
]
}
}| Entry | Matches |
|---|---|
https://api.example.com | Every path on that origin. |
https://api.example.com/v1/ | Only URLs beginning /v1/. |
https://api.example.com/v1/* | Same as above — a trailing /* is stripped. |
- Wildcard hostnames and credentials in the URL are rejected.
"network": []declares the capability with no targets — every request is blocked. Omit the key entirely if your app is offline.- Users approve the list at install time and can revoke individual origins later from Web Apps → app details → Permissions, so handle a permission failure gracefully at any time.
net.fetch
net.fetch
requires networkfetch(
url: string,
init?: { method?: string; headers?: Record<string, string>; body?: string },
): Promise<{
status: number;
statusText: string;
headers: Record<string, string>;
body?: string; // text responses
bodyBase64?: string; // binary responses
}>returns a plain response object — never a Response instance
Performs the request through the host. It resolves for any HTTP status, including 404 and 500 — check status yourself. It rejects only when the request never happened: missing permission, a disallowed URL, no session, or a transport failure.
{
status: 200,
statusText: "OK",
headers: { "content-type": "application/json; charset=utf-8" },
body: "{\"docs\":[...]}" // text responses
// bodyBase64: "iVBORw0K..." // binary responses instead of body
}init.bodymust be a string. Serialise JSON yourself; there is noFormDataor stream support.- Response header names are lower-cased.
set-cookieis stripped, so cookie-based sessions are not available. - These request headers are removed for safety:
host,connection,content-length,transfer-encoding,cookie,authorization,proxy-authorization,origin,referer.
You cannot ship a secret API key
Your app is a plain HTML document the user can read, and the authorization header is stripped by the proxy. Only use APIs that are safe to call without a secret, or ask the user to paste their own key and store it in your app sandbox.
Examples
Fetch JSON
const res = await window.vos.net.fetch(
"https://api.openlibrary.org/search.json?q=tolkien",
{ method: "GET", headers: { Accept: "application/json" } },
);
if (res.status >= 200 && res.status < 300) {
const data = JSON.parse(res.body);
render(data.docs.slice(0, 10));
} else {
setStatus("Request failed with status " + res.status);
}Send JSON
const res = await window.vos.net.fetch("https://api.example.com/v1/items", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title: "New item" }),
});
// The proxy never throws on a non-2xx response — check the status yourself.
if (res.status === 201) setStatus("Created");A fetch-like helper
Wrap the call once so the rest of your app reads like ordinary code.
// A small wrapper that behaves like the fetch you already know.
async function apiGet(url) {
const res = await window.vos.net.fetch(url, {
headers: { Accept: "application/json" },
});
if (res.status < 200 || res.status >= 300) {
throw new Error("HTTP " + res.status + " " + res.statusText);
}
return JSON.parse(res.body);
}Binary responses
The host decides text versus binary from the response content type. Binary bodies arrive as bodyBase64 instead of body — turn them into a data URI to display them, since the CSP blocks loading remote images directly.
const res = await window.vos.net.fetch("https://covers.openlibrary.org/b/id/240727-L.jpg");
if (res.bodyBase64) {
const type = res.headers["content-type"] || "image/jpeg";
img.src = "data:" + type + ";base64," + res.bodyBase64;
}Limits & blocked hosts
| Limit | Value |
|---|---|
| Upstream request timeout | 15 seconds |
| SDK call timeout (whole round trip) | 30 seconds |
| Maximum response size | 5 MB — larger responses fail with a “response too large” error |
| Allowed protocols | http and https only |
| Allow-list entries | Up to 32 per app |
Requests to private and internal networks are refused outright, whatever your manifest says: localhost, *.local, *.internal, loopback and link-local addresses, IPv6 link-local and unique-local ranges, and the private IPv4 blocks 10.x, 127.x, 169.254.x, 172.16–31.x, 192.168.x and multicast.
Failure modes
| Rejection message contains | Cause |
|---|---|
permission | No network permission, or the URL is not covered by the allow-list the user approved. |
invalid url | Malformed URL, or a protocol other than http/https. |
auth | No signed-in VibeOS session to proxy the request under. |
blocked host | The target resolves to a private or internal address. |
response too large | The response exceeded the 5 MB cap. |
timeout | The upstream server did not answer within 15 seconds. |
Degrade gracefully
Because a user can revoke an origin after install, treat network failure as a normal state: show cached data from your app sandbox when you have it, and a clear retry when you do not. For the full error model see errors & limits.