# Network

> card.net.fetch — HTTP through NeuroSquad's proxy to the hosts a card declared, JSON, binary and streamed responses, secrets in headers, redirects and limits.

Source: https://docs.neurosquad.ai/en/card-sdk/api/network

A card's page cannot reach the network at all — its sandbox blocks every request, image and font
from outside the package. `card.net.fetch` goes through a proxy in the app instead, which lets
through only what the user granted:

- [`network`](https://docs.neurosquad.ai/en/card-sdk/permissions#network) with `hosts`: `https` requests to those hosts;
- [`network.local`](https://docs.neurosquad.ai/en/card-sdk/permissions#network-local): `http` or `https` to `localhost`,
`127.0.0.1` and `::1` — except NeuroSquad's own local servers (its MCP server, remote access,
developer link, the browser cards' Chrome debugging ports), which are unreachable.

## Fetch

```ts
const res = await card.net.fetch('https://api.github.com/repos/acme/app/issues?state=open', {
  headers: { accept: 'application/vnd.github+json' },
  responseType: 'json'
})
if (!res.ok) throw new Error(`GitHub said ${res.status} ${res.statusText}`)
const issues = await res.json<{ number: number; title: string }[]>()
render(issues.map((i) => `#${i.number} ${i.title}`))
```

It reads like `fetch`, with a few differences:

| Option | Meaning |
| --- | --- |
| `method` | `GET` (default), `HEAD`, `POST`, `PUT`, `PATCH`, `DELETE`. A body without a method means `POST`. |
| `headers` | An object or `[name, value]` pairs. May contain [`{{secret:<key>}}`](#secrets). |
| `body` | A string (sent as is), bytes (`Uint8Array`, `ArrayBuffer`), or any other JSON value (serialized, with `content-type: application/json` unless you set one). ≤ 5 MB. |
| `responseType` | `text` (default), `json` (parsed by the app), `base64` (binary), or `stream`. |
| `timeoutMs` | 1 000–120 000, default 30 000. |
| `signal` | An `AbortSignal`; aborting cancels the request in the app. |

The result is a `CardResponse`: `ok`, `status`, `statusText`, `url` (after redirects), `headers`
(`get`, `has`, iterable; names lowercase), `truncated`, and the body methods `text()`, `json()`,
`bytes()`, `arrayBuffer()`, `chunks()`, `lines()` — read the body once, like a `Response`.

```ts
// POST JSON, read binary.
const upload = await card.net.fetch('https://api.example.com/v1/render', {
  method: 'POST',
  body: { chart: 'coverage', width: 640 },
  responseType: 'base64'
})
const png = await upload.bytes()
render(png.byteLength)
```

**Conditional requests and empty bodies.** Conditional headers such as `If-None-Match` and
`If-Modified-Since` are forwarded, and every response header except `set-cookie` comes back (so
`etag` and rate-limit headers are there). An empty body — a `304 Not Modified` or `204 No Content` —
with `responseType: 'json'` gives `null` from `res.json()`.

## Secrets in headers

Never put an API key in your card's code or storage. Declare a `secret` [setting](https://docs.neurosquad.ai/en/card-sdk/manifest#settings),
let the user type it into the app's settings form, and reference it in a **header value**:

```ts
if (!card.settings.hasSecret('apiKey')) {
  await card.settings.open()
} else {
  const res = await card.net.fetch('https://api.example.com/v1/me', {
    headers: { authorization: 'Bearer {{secret:apiKey}}' },
    responseType: 'json'
  })
  render(await res.json())
}
```

The app replaces `{{secret:apiKey}}` with the stored secret (this card's, then the package-scope
one) on its way out — only for requests to granted **internet** hosts, never to this computer
(`network.local`). Your card never sees the value. Placeholders work only in header values — not
in the URL or the body, which end up in server logs — and the secret headers are **dropped** if a
redirect leads to another host. A placeholder for a secret the user has not set fails with
`INVALID_PARAMS`.

## Streaming

For server-sent events, NDJSON or long downloads, ask for a stream. The promise resolves as soon as
the headers arrive; the body comes in chunks (≤ 64 KB each):

```ts
const controller = new AbortController()
const stream = await card.net.fetch('https://api.example.com/v1/events', {
  headers: { accept: 'text/event-stream' },
  responseType: 'stream',
  signal: controller.signal
})
for await (const line of stream.lines()) {
  if (line.startsWith('data: ')) render(JSON.parse(line.slice(6)))
}
// controller.abort() ends it early.
```

`chunks()` gives strings for text chunks and `Uint8Array` for binary ones; `lines()` splits on
line breaks; `for await (const chunk of response)` works too. A failed stream throws
`NETWORK_ERROR`. At most 4 streams open at once; a stream is closed after 5 minutes without data or
after 256 MB. WebSockets are not supported in version 1.

## What the proxy enforces

1. **Scheme.** `https` only, except `http` to this computer with `network.local`. No user name or
password in the URL; the `#fragment` is not sent.
2. **Host.** Exactly your declared patterns (`api.example.com`, `*.example.com`,
`host.example.com:8443`). Anything else fails with `HOST_NOT_ALLOWED`.
3. **Address.** The app resolves the name itself; for a public host grant, every address must be
public — private networks, loopback, link-local and cloud metadata addresses are refused, and the
request goes to the address that was checked (no DNS rebinding).
4. **Headers.** You cannot set `host`, `cookie`, `origin`, `referer`, `user-agent`,
`content-length`, `connection`, `proxy-*`, `sec-*`, `x-forwarded-*` and similar. The app sends
`User-Agent: NeuroSquad-Card/<app version> (<your card name>)`. No cookies are ever stored or
sent, and `set-cookie` is never returned to you.
5. **Redirects.** Followed by the app, at most 5, and every hop is checked again by rules 1–4.
6. **Limits.** Request body ≤ 5 MB; response ≤ 10 MB (a longer text or base64 body comes back cut,
with `truncated: true`; a longer JSON body fails with `TOO_LARGE`); 6 requests at once;
120 a minute; timeout up to 120 s — and the timeout covers reading the whole body, not just the
headers.

Errors: `HOST_NOT_ALLOWED` (host or address not granted), `NETWORK_ERROR` (DNS, connection, TLS),
`TIMEOUT`, `TOO_LARGE`, `RATE_LIMITED`, `PERMISSION_DENIED` (no network permission at all). An HTTP
error status is **not** an exception — check `res.ok`.

> The install dialog tells users that anything your card can see may be sent to the hosts you list.
> Keep the list short and specific: a wildcard or a generic host (a paste bin, a URL shortener)
> makes a careful user decline.
