diff --git a/docs/auth/browser-keys.mdx b/docs/auth/browser-keys.mdx new file mode 100644 index 0000000..3373d29 --- /dev/null +++ b/docs/auth/browser-keys.mdx @@ -0,0 +1,197 @@ +--- +title: Browser Keys +description: Use a FastNear API key directly from a browser app by restricting it to your website's domains, with a per-key rate cap and one-click rotation. +slug: /auth/browser-keys +sidebar_position: 1 +displayed_sidebar: rpcSidebar +page_actions: + - markdown +keywords: + - browser api key + - frontend api key + - restrict api key to domain + - restrict api key to website + - origin + - referer + - CORS + - website restriction + - ui api key + - public api key +--- + +import Link from '@docusaurus/Link'; +import IconExternalLink from '@theme/Icon/ExternalLink'; + +# Browser Keys + +A FastNear API key normally lives on a backend. But if your app talks to FastNear directly from the +browser, you can publish a key in your frontend and **restrict it to your website's domains** so a +copied key is hard to reuse elsewhere. This is the *browser key* (or *UI key*) posture. + +Two postures, one credential model: + +| Posture | Where the key lives | How it is protected | +| --- | --- | --- | +| **Server key** (default) | backend, worker, proxy, secret manager | kept secret; never shipped to clients. See [Auth & Access](/auth). | +| **Browser key** | shipped in your frontend bundle | restricted to your site's `Origin`/`Referer`, plus a per-key rate cap. This page. | + +A browser key is **not a secret** — anyone can read it from your page source. The restriction makes it +inconvenient to reuse and the rate cap limits the damage if it is. For anything that must stay secret, +use a server key. + +:::info Availability +Website restrictions are a **paid-plan** feature. On the free tier the control is disabled — upgrade +your project in FastNear Dashboard +to enable browser keys. +::: + +## If you only need the rule + +- Create a key in the dashboard, then turn on **Restrict to websites** and list your domains. +- A restricted key only works from a browser on a matching `Origin` (or `Referer`); everything else gets + `403 origin_not_allowed` — including your own backend. +- Calling from the browser works with either `Authorization: Bearer ...` or `?apiKey=...`; FastNear's RPC + answers the CORS preflight, so both forms work. +- Set a **Rate cap** on the key so a lifted key cannot burn your whole plan's quota. +- A restriction is a **deterrent, not a secret**. Keep a separate, unrestricted server key for backend work. + +## Restrict a key to your website + +1. Open your project in FastNear Dashboard + and create (or pick) an API key. +2. Turn on **Restrict to websites**. +3. List the domains your app is served from, comma-separated: + + ```text + example.com, *.example.com + ``` + + - Matching is **exact** on the hostname. `example.com` does **not** cover `www.example.com` or + `app.example.com` — add each host, or use a wildcard. + - `*.example.com` matches any direct subdomain (`app.example.com`, `www.example.com`). + - You can list up to **20 domains** per key. + - Do not include the scheme, port, or path — just the hostname (`app.example.com`, not + `https://app.example.com/`). + +Turning the restriction **on** hard-blocks every caller without a matching `Origin`/`Referer`, *including +your own server-side traffic with that key*. Use a separate unrestricted key for backend calls. + +## How the restriction is enforced + +The check runs at the edge, before the request reaches the RPC, against the browser-set `Origin` header +(with `Referer` as a fallback): + +| Request | Result | +| --- | --- | +| `Origin` matches an allowed domain | ✅ passes | +| No `Origin`, but `Referer` matches | ✅ passes | +| `Origin` present but not allowed | ⛔ `403 origin_not_allowed` | +| Neither `Origin` nor `Referer` (e.g. curl, a backend) | ⛔ `403 origin_not_allowed` | +| A subdomain of an allowed domain, with no wildcard | ⛔ `403 origin_not_allowed` | + +`Origin` takes precedence over `Referer` — a request with a disallowed `Origin` is rejected even if the +`Referer` would have matched. The `403` body is `{"error":"origin_not_allowed"}`. + +A key with **no** website restriction is treated as a server key: the origin check is skipped and it works +from anywhere (so keep it secret). + +## Call FastNear from the browser + +FastNear's RPC endpoint returns permissive CORS headers and answers the preflight `OPTIONS` request, so a +browser `fetch()` works directly — no proxy required. + +```js +// Served from https://app.example.com (an allowed domain on this key) +const FASTNEAR_API_KEY = 'YOUR_BROWSER_KEY'; // restricted to app.example.com + +const res = await fetch('https://rpc.mainnet.fastnear.com', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${FASTNEAR_API_KEY}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + jsonrpc: '2.0', + id: 1, + method: 'block', + params: { finality: 'final' }, + }), +}); +const data = await res.json(); +``` + +The browser sends your site's `Origin` automatically, so the edge can enforce the restriction. The +`?apiKey=` query form works too and is handy when setting headers is awkward: + +```js +const res = await fetch(`https://rpc.mainnet.fastnear.com?apiKey=${FASTNEAR_API_KEY}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'block', params: { finality: 'final' } }), +}); +``` + +:::note +If a browser call exceeds your rate limit, the `429` response may not yet include CORS headers, so the +browser can surface it as a generic network error rather than the JSON limit message. Treat a sudden +`fetch` failure under load as a possible rate-limit hit. +::: + +## Limit the blast radius + +A published key will eventually be copied. Two controls keep that cheap: + +- **Rate cap** — set a per-key cap (requests per minute) below your plan default. A lifted key can then + only consume up to that cap, not your whole quota. Leave it blank to use the plan default. +- **Rotate** — if a key is abused, use **Rotate** to issue a new value while keeping the same + restrictions and cap. Update your site with the new value; the old value stops working at the edge + shortly after, once the change syncs. + +## This is a deterrent, not a secret + +:::caution +`Origin` and `Referer` are set by the browser, but a non-browser client (curl, a script) can forge them. +So a website restriction stops **casual reuse of a key lifted from your page**, but it is **not** a hard +security boundary. +::: + +What this means in practice: + +- The risk of a leaked browser key is **quota and cost abuse** — someone running up usage against your + plan. FastNear API keys are rate-limit/billing credentials, not NEAR account keys, so a leaked browser + key cannot move funds or sign transactions on your accounts. +- Your real backstops are the **rate cap** (bounds the damage) and **rotation** (ends it). +- Never put a credential that must stay secret in the browser. Use a [server key](/auth) behind your + backend for anything sensitive or unrestricted. + +## Common failure modes + +### `403 origin_not_allowed` from curl or my backend + +Expected — a restricted key only works from a browser on an allowed domain. Backends and curl send no +`Origin`/`Referer`. Use a separate unrestricted server key for backend traffic. + +### It works on `example.com` but not `www.example.com` + +Hostname matching is exact. Add `www.example.com` to the key, or use `*.example.com` to cover subdomains. + +### My browser call fails with no useful error + +If you are over your rate limit, the `429` may arrive without CORS headers and show up as a generic +network error. Check your usage, and consider whether your key's rate cap is too low. + +### The docs UI worked, but my app does not + +The docs site can forward a saved key for convenience; that is not the production pattern. Ship your own +restricted browser key, served from a domain you listed on the key. + +### I need both browser and backend access + +Use two keys: a restricted browser key for the frontend, and an unrestricted server key for the backend. +Do not try to share one key across both. + +## Related guides + +- [Auth & Access](/auth) — the server-key default and transport choices +- [Auth for Agents](/agents/auth) — runtime posture for agents and automations +- [RPC Reference](/rpc) diff --git a/docs/auth/index.mdx b/docs/auth/index.mdx index fe5a6f7..3772c24 100644 --- a/docs/auth/index.mdx +++ b/docs/auth/index.mdx @@ -38,6 +38,7 @@ Live example pages also support `Copy example URL` for sharing prefilled request - Prefer `Authorization: Bearer ${FASTNEAR_API_KEY}` for production backends. - Use `?apiKey=${FASTNEAR_API_KEY}` when headers are awkward or you are doing quick curl/debug work. - Agents and automations should keep the key in env vars or a secret manager, not in browser storage. +- Building a browser or frontend app? Ship a [browser key](/auth/browser-keys) restricted to your domains, not a plain server key. ## Choose the auth form @@ -107,3 +108,7 @@ Do not rely on browser storage behavior from the docs UI. Production backends sh ### I am building an agent or automation Use [Auth for Agents](/agents/auth) for the runtime posture. Browser `localStorage` is a docs convenience, not an agent secret store. + +### I am building a browser or frontend app + +Do not ship a server key in your frontend. Use a [browser key](/auth/browser-keys) restricted to your site's domains, with a per-key rate cap, so a copied key is hard to reuse and cannot drain your quota. diff --git a/i18n/ru/docusaurus-plugin-content-docs/current/auth/browser-keys.mdx b/i18n/ru/docusaurus-plugin-content-docs/current/auth/browser-keys.mdx new file mode 100644 index 0000000..ff26a2b --- /dev/null +++ b/i18n/ru/docusaurus-plugin-content-docs/current/auth/browser-keys.mdx @@ -0,0 +1,170 @@ +--- +title: "Браузерные ключи" +description: "Используйте API-ключ FastNear прямо из браузерного приложения, ограничив его доменами своего сайта, с лимитом частоты на ключ и ротацией в один клик." +slug: /auth/browser-keys +sidebar_position: 1 +displayed_sidebar: rpcSidebar +page_actions: + - markdown +keywords: + - browser api key + - frontend api key + - restrict api key to domain + - restrict api key to website + - origin + - referer + - CORS + - website restriction + - ui api key + - public api key + - браузерный ключ + - ограничение по сайтам + - привязка к домену + - подписка +--- + +import Link from '@docusaurus/Link'; +import IconExternalLink from '@theme/Icon/ExternalLink'; + +# Браузерные ключи + +Обычно API-ключ FastNear живёт на бэкенде. Но если приложение обращается к FastNear напрямую из браузера, можно опубликовать ключ во фронтенде и **ограничить его доменами своего сайта** — тогда скопированный ключ трудно использовать где-то ещё. Это режим *браузерного ключа* (или *UI-ключа*). + +Два режима, одна модель учётных данных: + +| Режим | Где живёт ключ | Как защищён | +| --- | --- | --- | +| **Серверный ключ** (по умолчанию) | бэкенд, воркер, прокси, менеджер секретов | хранится в секрете; никогда не отправляется клиентам. См. [Аутентификацию и доступ](/auth). | +| **Браузерный ключ** | поставляется в сборке фронтенда | ограничен по `Origin`/`Referer` сайта плюс лимит частоты на ключ. Эта страница. | + +Браузерный ключ — это **не секрет**: его может прочитать любой из исходного кода страницы. Ограничение делает повторное использование неудобным, а лимит частоты снижает ущерб, если ключ всё же скопировали. Для всего, что должно оставаться секретом, используйте серверный ключ. + +:::info Доступность +Ограничение по сайтам — функция платных тарифов. На бесплатном тарифе этот элемент управления отключён — повысьте тариф проекта в FastNear Dashboard, чтобы включить браузерные ключи. +::: + +## Если нужно только правило + +- Создайте ключ в панели управления, включите **Restrict to websites** и перечислите свои домены. +- Ключ с ограничением работает только из браузера на подходящем `Origin` (или `Referer`); всё остальное получает `403 origin_not_allowed` — включая собственный бэкенд. +- Вызов из браузера работает и через `Authorization: Bearer ...`, и через `?apiKey=...`; RPC FastNear отвечает на предварительный запрос `CORS`, поэтому работают оба варианта. +- Задайте лимит частоты (**Rate cap**) для ключа, чтобы украденный ключ не сжёг всю квоту тарифа. +- Ограничение — это сдерживающая мера, а **не** секрет. Держите отдельный серверный ключ без ограничений для работы с бэкендом. + +## Привязка ключа к сайту + +1. Откройте проект в FastNear Dashboard и создайте (или выберите) API-ключ. +2. Включите **Restrict to websites**. +3. Перечислите через запятую домены, с которых обслуживается приложение: + + ```text + example.com, *.example.com + ``` + + - Совпадение по имени хоста — точное. `example.com` не покрывает `www.example.com` или `app.example.com` — добавьте каждый хост или используйте подстановочный шаблон. + - `*.example.com` совпадает с любым прямым поддоменом (`app.example.com`, `www.example.com`). + - На один ключ можно указать до **20 доменов**. + - Не указывайте схему, порт или путь — только имя хоста (`app.example.com`, а не `https://app.example.com/`). + +Включение ограничения жёстко блокирует любой вызов без подходящего `Origin`/`Referer`, *включая собственный серверный трафик с этим ключом*. Для вызовов с бэкенда используйте отдельный ключ без ограничений. + +## Как применяется ограничение + +Проверка выполняется на периметре, до того как запрос дойдёт до RPC, по выставленному браузером заголовку `Origin` (с запасным вариантом `Referer`): + +| Запрос | Результат | +| --- | --- | +| `Origin` совпадает с разрешённым доменом | ✅ проходит | +| нет `Origin`, но совпадает `Referer` | ✅ проходит | +| `Origin` есть, но не в списке | ⛔ `403 origin_not_allowed` | +| нет ни `Origin`, ни `Referer` (например, curl или бэкенд) | ⛔ `403 origin_not_allowed` | +| поддомен разрешённого домена без подстановочного шаблона | ⛔ `403 origin_not_allowed` | + +`Origin` имеет приоритет над `Referer` — запрос с недопустимым `Origin` отклоняется, даже если `Referer` подошёл бы. Тело ответа `403` — `{"error":"origin_not_allowed"}`. + +Ключ **без** ограничения по сайтам считается серверным: проверка источника пропускается, и он работает откуда угодно (поэтому держите его в секрете). + +## Вызов FastNear из браузера + +RPC-эндпоинт FastNear возвращает разрешающие заголовки `CORS` и отвечает на предварительный запрос `OPTIONS`, поэтому браузерный `fetch()` работает напрямую — прокси не нужен. + +```js +// Запущено на https://app.example.com (разрешённый домен этого ключа) +const FASTNEAR_API_KEY = 'YOUR_BROWSER_KEY'; // ограничен доменом app.example.com + +const res = await fetch('https://rpc.mainnet.fastnear.com', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${FASTNEAR_API_KEY}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + jsonrpc: '2.0', + id: 1, + method: 'block', + params: { finality: 'final' }, + }), +}); +const data = await res.json(); +``` + +Браузер автоматически отправляет `Origin` сайта, поэтому периметр может применить ограничение. Форма с параметром `?apiKey=` тоже работает и удобна, когда выставлять заголовки неудобно: + +```js +const res = await fetch(`https://rpc.mainnet.fastnear.com?apiKey=${FASTNEAR_API_KEY}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'block', params: { finality: 'final' } }), +}); +``` + +:::note +Если браузерный вызов превышает лимит частоты, ответ `429` пока может не содержать заголовков `CORS`, поэтому браузер покажет его как обычную сетевую ошибку, а не как JSON-сообщение о лимите. Внезапный сбой `fetch` под нагрузкой стоит трактовать как возможное срабатывание лимита. +::: + +## Ограничение масштаба ущерба + +Опубликованный ключ рано или поздно скопируют. Два механизма удерживают цену этого низкой: + +- **Лимит частоты (Rate cap)** — задайте лимит на ключ (запросов в минуту) ниже тарифного по умолчанию. Тогда украденный ключ сможет потратить не больше этого лимита, а не всю квоту. Оставьте поле пустым, чтобы использовать значение тарифа по умолчанию. +- **Ротация (Rotate)** — при злоупотреблении ключом выпустите новое значение, сохранив те же ограничения и лимит. Обновите сайт новым значением; старое значение перестаёт работать на периметре вскоре после синхронизации изменения. + +## Это сдерживающая мера, а не секрет + +:::caution +Заголовки `Origin` и `Referer` выставляет браузер, но небраузерный клиент (curl, скрипт) может их подделать. Поэтому ограничение по сайтам останавливает **случайное** повторное использование ключа, вытащенного со страницы, но это **не** строгая граница безопасности. +::: + +Что это значит на практике: + +- Риск утёкшего браузерного ключа — злоупотребление **квотой и расходами**: кто-то накручивает потребление по вашему тарифу. API-ключи FastNear — это учётные данные для лимитов и биллинга, а не ключи аккаунта NEAR, поэтому утёкший браузерный ключ не может переводить средства или подписывать транзакции по вашим аккаунтам. +- Настоящая подстраховка — это **лимит частоты** (ограничивает ущерб) и **ротация** (прекращает его). +- Никогда не помещайте в браузер учётные данные, которые должны оставаться секретом. Для всего чувствительного или неограниченного используйте [серверный ключ](/auth) за своим бэкендом. + +## Частые проблемы + +### `403 origin_not_allowed` от curl или с бэкенда + +Так и должно быть — ключ с ограничением работает только из браузера на разрешённом домене. Бэкенды и curl не отправляют `Origin`/`Referer`. Для бэкенд-трафика используйте отдельный серверный ключ без ограничений. + +### Работает на `example.com`, но не на `www.example.com` + +Совпадение по имени хоста — точное. Добавьте `www.example.com` к ключу или используйте `*.example.com`, чтобы покрыть поддомены. + +### Браузерный вызов падает без внятной ошибки + +При превышении лимита частоты ответ `429` может прийти без заголовков `CORS` и выглядеть как обычная сетевая ошибка. Проверьте потребление и подумайте, не слишком ли низкий лимит частоты у ключа. + +### В UI документации сработало, а в приложении — нет + +Сайт документации может подставлять сохранённый ключ для удобства — это не продовый шаблон. Поставляйте собственный браузерный ключ с ограничением, обслуживаемый с домена, указанного в ключе. + +### Нужен доступ и из браузера, и с бэкенда + +Используйте два ключа: браузерный ключ с ограничением для фронтенда и серверный ключ без ограничений для бэкенда. Не пытайтесь использовать один ключ для обоих случаев. + +## Связанные руководства + +- [Аутентификация и доступ](/auth) — серверный ключ по умолчанию и выбор способа передачи +- [Аутентификация для агентов](/agents/auth) — режим выполнения для агентов и автоматизаций +- [Справочник RPC](/rpc) diff --git a/sidebars.js b/sidebars.js index 648be21..34f4e62 100644 --- a/sidebars.js +++ b/sidebars.js @@ -89,6 +89,7 @@ const rpcSidebar = withExamplesFooter( ...nearcoreReleaseSidebarItems, 'rpc/index', 'auth/index', + 'auth/browser-keys', { type: 'category', label: 'Account',