# Сеть

> card.net.fetch — HTTP через прокси NeuroSquad к хостам, которые объявила карточка; ответы JSON, двоичные и потоковые, секреты в заголовках, редиректы и лимиты.

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

Страница карточки вообще не может выйти в сеть — её песочница блокирует любой запрос, картинку или
шрифт не из пакета. Вместо этого `card.net.fetch` идёт через прокси в приложении, который
пропускает только то, что разрешил пользователь:

- [`network`](https://docs.neurosquad.ai/ru/card-sdk/permissions#network) с `hosts`: запросы `https` к этим хостам;
- [`network.local`](https://docs.neurosquad.ai/ru/card-sdk/permissions#network-local): `http` или `https` к `localhost`,
`127.0.0.1` и `::1` — кроме собственных локальных серверов NeuroSquad (его MCP-сервера, удалённого
доступа, подключения папок для разработчиков, портов отладки Chrome браузерных карточек): до них не
достучаться.

## Запрос

```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}`))
```

Выглядит как `fetch`, но с несколькими отличиями:

| Параметр | Значение |
| --- | --- |
| `method` | `GET` (по умолчанию), `HEAD`, `POST`, `PUT`, `PATCH`, `DELETE`. Тело без метода означает `POST`. |
| `headers` | Объект или пары `[name, value]`. Может содержать [`{{secret:<key>}}`](#secrets). |
| `body` | Строка (отправляется как есть), байты (`Uint8Array`, `ArrayBuffer`) или любое другое значение JSON (сериализуется, с `content-type: application/json`, если вы не задали свой). До 5 МБ. |
| `responseType` | `text` (по умолчанию), `json` (разбирает приложение), `base64` (двоичные данные) или `stream`. |
| `timeoutMs` | 1 000–120 000, по умолчанию 30 000. |
| `signal` | `AbortSignal`; отмена прерывает запрос в приложении. |

Результат — `CardResponse`: `ok`, `status`, `statusText`, `url` (после редиректов), `headers`
(`get`, `has`, перебираемые; имена в нижнем регистре), `truncated` и методы тела `text()`, `json()`,
`bytes()`, `arrayBuffer()`, `chunks()`, `lines()` — тело читается один раз, как у `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)
```

**Условные запросы и пустые ответы.** Условные заголовки вроде `If-None-Match` и
`If-Modified-Since` передаются, и возвращаются все заголовки ответа, кроме `set-cookie` (так что
`etag` и заголовки лимитов на месте). Пустое тело — `304 Not Modified` или `204 No Content` — при
`responseType: 'json'` даёт `null` из `res.json()`.

## Секреты в заголовках

Никогда не кладите API-ключ в код или хранилище карточки. Объявите [настройку](https://docs.neurosquad.ai/ru/card-sdk/manifest#settings)
типа `secret`, пусть пользователь введёт ключ в форме настроек приложения, и сошлитесь на него в
**значении заголовка**:

```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())
}
```

Приложение подставляет вместо `{{secret:apiKey}}` сохранённый секрет (сначала этой карточки, потом
общий для пакета) на выходе — только в запросах к выданным хостам **в интернете**, никогда к этому
компьютеру (`network.local`). Ваша карточка значения не видит никогда. Подстановки работают только в
значениях заголовков — не в URL и не в теле, которые оседают в логах серверов, — а заголовки с
секретами **отбрасываются**, если редирект ведёт на другой хост. Подстановка секрета, который
пользователь не задал, падает с `INVALID_PARAMS`.

## Потоковые ответы

Для server-sent events, NDJSON или долгих загрузок запросите поток. Промис разрешается, как только
пришли заголовки; тело приходит кусками (до 64 КБ каждый):

```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()` отдаёт строки для текстовых кусков и `Uint8Array` для двоичных; `lines()` делит по
переводам строк; `for await (const chunk of response)` тоже работает. Оборвавшийся поток бросает
`NETWORK_ERROR`. Одновременно открыто не больше 4 потоков; поток закрывается после 5 минут без данных
или после 256 МБ. WebSocket в версии 1 не поддерживается.

## Что проверяет прокси

1. **Схема.** Только `https`, кроме `http` к этому компьютеру с `network.local`. Никаких имени
пользователя и пароля в URL; `#fragment` не отправляется.
2. **Хост.** Ровно ваши объявленные шаблоны (`api.example.com`, `*.example.com`,
`host.example.com:8443`). Всё остальное падает с `HOST_NOT_ALLOWED`.
3. **Адрес.** Приложение само резолвит имя; при выдаче на публичный хост каждый адрес должен быть
публичным — частные сети, loopback, link-local и адреса облачных метаданных отклоняются, а запрос
идёт на тот адрес, который проверили (никакого DNS rebinding).
4. **Заголовки.** Нельзя задавать `host`, `cookie`, `origin`, `referer`, `user-agent`,
`content-length`, `connection`, `proxy-*`, `sec-*`, `x-forwarded-*` и подобные. Приложение
отправляет `User-Agent: NeuroSquad-Card/<app version> (<your card name>)`. Cookies никогда не
сохраняются и не отправляются, а `set-cookie` вам никогда не возвращается.
5. **Редиректы.** Их выполняет приложение, не больше 5, и каждый шаг заново проверяется правилами
1–4.
6. **Лимиты.** Тело запроса до 5 МБ; ответ до 10 МБ (более длинное тело text или base64 приходит
обрезанным, с `truncated: true`; более длинное тело JSON падает с `TOO_LARGE`); 6 запросов
одновременно; 120 в минуту; тайм-аут до 120 с — причём он покрывает чтение всего тела, а не
только заголовков.

Ошибки: `HOST_NOT_ALLOWED` (хост или адрес не разрешён), `NETWORK_ERROR` (DNS, соединение, TLS),
`TIMEOUT`, `TOO_LARGE`, `RATE_LIMITED`, `PERMISSION_DENIED` (сетевых разрешений нет вовсе).
HTTP-статус с ошибкой исключением **не** считается — проверяйте `res.ok`.

> Диалог установки говорит пользователям, что всё, что видит ваша карточка, может уйти на
> перечисленные вами хосты. Держите список коротким и конкретным: маска или хост общего назначения
> (сервис для вставки текста, сокращатель ссылок) заставит осторожного пользователя отказаться.
