# Kimlik Doğrulama ve Kapsamlar

> Kapsamlar, kurulum token’ı (tcfi_), 60 sn’lik oturum JWT’si, X-Tecof-Signature imzalı istekler ve istemci sırrı döndürme.
>
> Kaynak: https://tecof-app-dev.vercel.app/app-auth · Bölüm: Uygulama geliştirme · Son güncelleme: 2018-10-20

Partner uygulamaları üç kimlik kanalı kullanır: sunucunuzun Tecof API'sine eriştiği **kurulum token'ı**, panelin iframe arayüzünüze verdiği **oturum token'ı** ve Tecof'un size gönderdiği isteklerdeki **imza**. Üçünün de anahtarı istemci sırrınızdır (`tcfs_…`); bu yüzden sır yalnız sunucuda durur.

---

## Kimlik Bilgileri

| Kimlik | Biçim | Ömür | Not |
| :--- | :--- | :--- | :--- |
| İstemci kimliği | `tcfa_…` | Kalıcı | Herkese açık; tarayıcı koduna girebilir |
| İstemci sırrı | `tcfs_…` | Döndürülene kadar | Yalnız uygulama oluşturma ve sır döndürme yanıtında **bir kez** gösterilir; Tecof tarafında şifreli saklanır |
| Kurulum token'ı | `tcfi_…` | 1 saat | Bir kuruluma (mağaza + uygulama) bağlıdır; `/api/v1/*` için Bearer |
| Oturum token'ı | JWT, HS256 | 60 saniye | Panel → iframe kimlik kanıtı; anahtarı istemci sırrı |
| Geliştirici (CLI) token'ı | `tcfd_…` | Cihaz oturumu 30 gün (14 gün kullanılmazsa düşer); kapsamlı CI token'ı 1–365 gün | CLI, `/api/developer` uçları ve alanlar (Spaces); bkz. [Güvenlik ve CI](/dev-security) |

---

## Kapsamlar

Mağaza, kurulum onay ekranında uygulamanın istediği kapsamların tamamını görür ve onaylar. Uygulamaya açık kapsamlar Developer API kapsamlarının alt kümesidir:

| Kapsam | Ne verir |
| :--- | :--- |
| `orders:read` | Sipariş okuma (`/api/v1/orders`), `order.*` webhook'ları, sipariş ekranı aksiyonları, sipariş metafield'ları |
| `orders:write` | Sipariş yazma işlemleri (Tools API araçları) |
| `customers:read` | Müşteri okuma (`/api/v1/customers`), `customer.*` webhook'ları, müşteri ekranı aksiyonları, müşteri metafield'ları |
| `customers:write` | Müşteri yazma işlemleri |
| `products:read` | Ürün ve envanter okuma (`/api/v1/products`, `/api/v1/inventory`), `product.*`, `inventory.*`, `review.*` webhook'ları, ürün ekranı aksiyonları, ürün metafield'ları |
| `products:write` | Ürün yazma; **envanter dahil** (ör. `apply_stock_count` aracı). `status: "active"` ürün anında vitrine çıkar |
| `marketing:read` | Pazarlama verisi okuma, `subscriber.created` webhook'u |
| `marketing:write` | Pazarlama yazma işlemleri |
| `settings:read` / `settings:write` | Mağaza ayarlarını okuma / yazma |
| `cms:read` | CMS koleksiyon ve içerik okuma, `cms.item.published` ve `form.submitted` webhook'ları |
| `cms:write` | CMS içerik yazma (taslak) |
| `pages:read` / `pages:write` | Sayfa okuma / taslak yazma |
| `media:read` / `media:write` | Medya kütüphanesi okuma / dosya ekleme |
| `analytics:read` | Analitik ve mağaza özeti okuma |
| `domains:read` | Alan adı ve DNS kayıtlarını okuma |

### Uygulamaya kapalı kapsamlar

Aşağıdaki kapsamlar uygulamalara **verilemez**; `tecof.app.json` içinde istenirse yapılandırma reddedilir. Bunlar ya canlı siteyi doğrudan değiştirir ya da kredi harcar ya da mağaza sahibinin kişisel asistan verisidir.

| Kapsam | Developer API'deki etkisi |
| :--- | :--- |
| `code:read`, `code:write` | Tema deposu okuma/yazma; push üretim yayınını tetikler |
| `sandbox:run` | Bulut IDE'de kod çalıştırma; kredi düşer |
| `deploy:write` | Üretim yeniden dağıtımı, canlı temayı değiştirme |
| `memory:read`, `memory:write` | Mağaza sahibinin asistana yazdığı kalıcı talimatlar |
| `domains:write` | DNS ve nameserver değişikliği; canlı siteyi etkiler |
| `ai:generate` | AI görsel üretimi; kredi düşer |

### Kapsam istemeyen veriler

Uygulamanın **kendi** verisi kapsam istemez; kurulumun kendisi yetkidir:

- Ayarlar (`/api/v1/app/settings`)
- Sırlar / Vault (`/api/v1/app/secrets`)
- `merchant` sahipli metafield'lar (diğer sahip tipleri, kaynağın okuma kapsamını ister)
- Uygulama olayları (`/api/v1/app/events`)

### Kapsam değişikliği

- **Artış:** Yeni yapılandırma ek kapsam isterse mevcut kurulumlara `pendingScopes` yazılır. Panel "Ek izin istiyor" uyarısı gösterir; mağaza onaylayana kadar kurulum **eski kapsamla** çalışır. Onayda `grantedScopes` güncellenir, kurulumun token'ları iptal edilir ve `app.scopes_updated` webhook'u gönderilir. Yayındaki uygulamada kapsam artışı yeniden inceleme ister.
- **Azalma:** Anında uygulanır.

Yetersiz kapsamla yapılan çağrı `403 insufficient-scope` döner; `data` alanı `{ required: [...], missing: [...] }` içerir.

---

## Kurulum Token'ı (Sunucu → Tecof API)

Sunucunuz, istemci kimliği ve sırrıyla bir kurulum için kısa ömürlü erişim token'ı alır (OAuth 2.0 `client_credentials` benzeri). Kullanıcıdan bağımsızdır: webhook işleyicilerinde ve arka plan işlerinde de çalışır.

### İstek

```bash
curl -X POST https://api.tecof.com/api/apps/oauth/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "client_credentials",
    "client_id": "tcfa_xxxxxxxxxxxxxxxx",
    "client_secret": "tcfs_xxxxxxxxxxxxxxxx",
    "installation_id": "6720aa00bb11cc22dd33ee55"
  }'
```

`application/x-www-form-urlencoded` gövde de kabul edilir. `installation_id` yerine `merchant_id` verilebilir; bu durumda o mağazadaki bu uygulamanın kurulumu kullanılır.

### Yanıt

```json
{
  "access_token": "tcfi_xxxxxxxxxxxxxxxx",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "orders:read customers:read",
  "installation_id": "6720aa00bb11cc22dd33ee55",
  "merchant_id": "66f0c0ffee0000000000abcd"
}
```

### Hatalar

Hatalar OAuth biçimindedir:

| HTTP | Gövde | Neden |
| :--- | :--- | :--- |
| 400 | `{ "error": "invalid_request" }` | Eksik ya da hatalı alan |
| 401 | `{ "error": "invalid_client" }` | İstemci kimliği/sırrı yanlış |
| 400 | `{ "error": "invalid_grant", "error_description": "installation-not-found" }` | Kurulum yok ya da bu uygulamaya ait değil |
| 400 | `{ "error": "invalid_grant", "error_description": "installation-inactive" }` | Kurulum aktif değil (kaldırılmış, askıda…) |

### Kurallar

- Token 1 saat geçerlidir. Kapsam değiştiğinde ya da uygulama askıya alındığında **anında** iptal edilir. Kaldırmada token'lar `app.uninstalled` teslimatı **bittikten sonra** iptal edilir; bu sayede kaldırma işleyiciniz API'yi hâlâ çağırabilir.
- Token Tecof'ta sha256 özeti olarak saklanır; token isteği ve yanıtı istek günlüklerine yazılmaz.
- `installation_id` değerini şuralardan alırsınız: `app.installed` webhook'u, her webhook'taki `X-Tecof-Installation-Id` başlığı, doğrulanmış oturum token'ındaki `iid` iddiası.
- Uygulama token'ında paket özelliği kapısı (`apiAccess`) **uygulanmaz**: uygulama erişimi mağazanın paketine bağlı değildir.
- Hız sınırı kurulum başınadır: okuma **240/dk**, yazma **60/dk**.
- `/api/v1` uçlarında geçersiz token `401` döner; `messageCode`: `token-invalid`, `token-revoked`, `token-expired` ya da `app-suspended` (uygulama askıda). Ayrıntılar: [API ile Veri Erişimi](/app-api).

### SDK ile

SDK token'ı alır, önbelleğe alır ve süresi dolmadan yeniler:

```ts
import { tecof } from "@/lib/tecof";

const api = tecof.client(installationId);
const orders = await api.orders.list({ paymentStatus: "paid", limit: 50 });
```

---

## Oturum Token'ı (Panel → iframe)

Panel, gömülü uygulamanızı açarken kurulum için 60 saniyelik bir JWT basar. iframe arayüzünüz bu token'ı kendi sunucunuza `Authorization: Bearer` olarak gönderir; sunucunuz istemci sırrınızla ek bir çağrı yapmadan doğrular.

### İddialar (claims)

```json
{
  "iss": "https://api.tecof.com",
  "aud": "tcfa_xxxxxxxxxxxxxxxx",
  "sub": "66f0aa00bb11cc22dd33ee66",
  "mid": "66f0c0ffee0000000000abcd",
  "iid": "6720aa00bb11cc22dd33ee55",
  "locale": "tr",
  "jti": "a1b2c3d4",
  "iat": 1760000000,
  "nbf": 1759999995,
  "exp": 1760000060
}
```

| İddia | Anlamı |
| :--- | :--- |
| `iss` | `https://api.tecof.com` |
| `aud` | Uygulamanızın istemci kimliği |
| `sub` | Paneli kullanan kullanıcının kimliği |
| `mid` | Mağaza kimliği |
| `iid` | Kurulum kimliği |
| `locale` | Panel dili |
| `jti` | Token kimliği |
| `iat` / `nbf` / `exp` | Basım zamanı / `iat - 5` / `iat + 60` |

Başlık: `{ "alg": "HS256", "typ": "JWT" }`.

### Açılış (launch) adresi

Panel uygulamayı ilk kez yüklerken iframe adresini şöyle kurar: `appUrl + path` + sorgu dizesi. Test mağazasında `devUrl` (CLI tüneli) `appUrl`'in yerine geçer.

```text
https://isbasi.apps.tecof.site/invoices?installationId=6720aa00bb11cc22dd33ee55&merchantId=66f0c0ffee0000000000abcd&locale=tr&embedded=1&host=aHR0cHM6Ly9hcHAudGVjb2YuY29t&session=eyJhbGciOiJIUzI1NiIs...
```

| Parametre | Anlamı |
| :--- | :--- |
| `installationId` | Kurulum kimliği |
| `merchantId` | Mağaza kimliği |
| `locale` | Panel dili |
| `embedded` | `1` — panel içinde açıldı |
| `host` | Panel origin'inin base64url kodlaması; App Bridge yalnız bu origin'den gelen mesajı kabul eder |
| `session` | İlk oturum token'ı |
| `actionRunId`, `actionKey`, `placement`, `ids` | Yalnız iframe aksiyonlarında. Bkz. [Aksiyonlar](/app-actions) |

> **Önemli:** Sorgu dizesindeki `installationId` ve `merchantId` imzalı değildir; yönlendirme ve görüntüleme için kullanın. Yetki kararını her zaman **doğrulanmış oturum token'ındaki** `mid` / `iid` değerlerine göre verin.

İlk token 60 saniyede dolar. Sonraki istekler için App Bridge `getSessionToken()` ile yeni token alın; SDK bitişe 10 saniye kala kendiliğinden yeniler. Bkz. [App Bridge](/app-bridge).

### Oturum token'ı doğrulama kuralları

1. İmza HS256, anahtar istemci sırrı. Panel token'ı her zaman **güncel** sırla imzalar; sır döndürmeden sonraki 24 saat boyunca doğrulamada eski sır da denenir (döndürme anından hemen önce basılmış token'lar için).
2. `aud` = istemci kimliğiniz.
3. `exp` ve `nbf` ±10 saniye toleransla.
4. `iss` = `https://api.tecof.com`.

### SDK ile doğrulama

```ts
// app/api/orders/route.ts
import { TecofAuthError } from "@tecof/app-sdk";
import { tecof } from "@/lib/tecof";

export async function GET(req: Request) {
  let session;
  try {
    // Authorization: Bearer <oturum token'ı>
    session = tecof.sessionFromRequest(req);
  } catch (err) {
    if (err instanceof TecofAuthError) {
      return Response.json({ error: "unauthorized" }, { status: 401 });
    }
    throw err;
  }

  // session = { merchantId, installationId, userId, locale, exp }
  const api = tecof.client(session.installationId);
  const orders = await api.orders.list({ limit: 20 });
  return Response.json({ orders });
}
```

Token'ı başka bir kaynaktan aldıysanız `tecof.verifySessionToken(token)` aynı iddiaları döner ya da `TecofAuthError` fırlatır.

### Oturum token'ını elle doğrulama (Node.js)

SDK kullanmayan bir sunucu için eşdeğer doğrulama:

```ts
import { createHmac, timingSafeEqual } from "node:crypto";

type SessionClaims = {
  iss: string; aud: string; sub: string; mid: string; iid: string;
  locale: string; jti: string; iat: number; nbf: number; exp: number;
};

function b64urlToBuffer(input: string): Buffer {
  return Buffer.from(input.replace(/-/g, "+").replace(/_/g, "/"), "base64");
}

export function verifyTecofSessionToken(
  token: string,
  opts: { clientId: string; secrets: string[]; leewaySec?: number },
): SessionClaims {
  const leeway = opts.leewaySec ?? 10;
  const parts = token.split(".");
  if (parts.length !== 3) throw new Error("malformed-token");
  const [headerB64, payloadB64, signatureB64] = parts;

  const header = JSON.parse(b64urlToBuffer(headerB64).toString("utf8"));
  if (header.alg !== "HS256") throw new Error("unsupported-alg");

  const signature = b64urlToBuffer(signatureB64);
  const signed = `${headerB64}.${payloadB64}`;
  // secrets: [güncel sır, (döndürmeden sonraki 24 saat içinde) eski sır]
  const valid = opts.secrets.some((secret) => {
    const expected = createHmac("sha256", secret).update(signed).digest();
    return expected.length === signature.length && timingSafeEqual(expected, signature);
  });
  if (!valid) throw new Error("invalid-signature");

  const claims = JSON.parse(b64urlToBuffer(payloadB64).toString("utf8")) as SessionClaims;
  const now = Math.floor(Date.now() / 1000);
  if (claims.aud !== opts.clientId) throw new Error("invalid-audience");
  if (claims.iss !== "https://api.tecof.com") throw new Error("invalid-issuer");
  if (now > claims.exp + leeway) throw new Error("token-expired");
  if (now < claims.nbf - leeway) throw new Error("token-not-yet-valid");
  return claims;
}
```

---

## İmzalı İstekler (Tecof → Uygulama)

Webhook teslimatları ve API aksiyonu çağrıları aynı imzayı taşır:

```http
X-Tecof-Timestamp: 1760000000
X-Tecof-Signature: t=1760000000,v1=5f2c…e91a
```

```text
v1 = HEX( HMAC_SHA256( clientSecret, `${t}.${rawBody}` ) )
```

Sır döndürmeden sonraki 24 saat boyunca başlık iki imza taşır: `t=…,v1=<yeni sırla>,v1=<eski sırla>`.

### İmza doğrulama kuralları

1. `t` değeri ile şimdiki zaman arasındaki fark **en fazla 300 saniye** olmalı (tekrar saldırısı penceresi).
2. Başlıktaki `v1` değerlerinden **herhangi biri** beklenen imzayla eşleşirse istek geçerlidir.
3. Karşılaştırma sabit zamanlı yapılmalıdır (`timingSafeEqual`).
4. İmza **ham gövde** üzerinden hesaplanır. Next.js'te `await req.text()` kullanın; `req.json()` ile okuyup yeniden serileştirmek imzayı bozar.

### SDK ile (tek satır)

```ts
const event = tecof.verifyWebhook(rawBody, req.headers);           // webhook
const action = tecof.verifyActionRequest(rawBody, req.headers);    // API aksiyonu
```

İkisi de imza geçersizse ya da zaman damgası pencere dışındaysa hata fırlatır.

### İmzayı elle doğrulama (Node.js)

```ts
import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyTecofSignature(
  rawBody: string,
  signatureHeader: string | null,
  secrets: string[],          // [güncel sır, (24 saat içinde) eski sır]
  toleranceSec = 300,
): boolean {
  if (!signatureHeader) return false;

  const parts = signatureHeader.split(",").map((p) => p.trim());
  const t = parts.find((p) => p.startsWith("t="))?.slice(2);
  const candidates = parts.filter((p) => p.startsWith("v1=")).map((p) => p.slice(3));
  if (!t || candidates.length === 0) return false;

  const ts = Number(t);
  const now = Math.floor(Date.now() / 1000);
  if (!Number.isFinite(ts) || Math.abs(now - ts) > toleranceSec) return false;

  for (const secret of secrets) {
    const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
    for (const candidate of candidates) {
      const given = Buffer.from(candidate, "hex");
      if (given.length === expected.length && timingSafeEqual(given, expected)) return true;
    }
  }
  return false;
}

// Kullanım (Next.js route handler):
// const raw = await req.text();
// const ok = verifyTecofSignature(raw, req.headers.get("x-tecof-signature"), [process.env.TECOF_CLIENT_SECRET!]);
```

### Terminalde imza üretmek

Yerel testte aynı imzayı `openssl` ile üretebilirsiniz:

```bash
T=$(date +%s)
BODY='{"id":"evt_test","event":"order.paid","test":true}'
SIG=$(printf '%s.%s' "$T" "$BODY" | openssl dgst -sha256 -hmac "$TECOF_CLIENT_SECRET" | awk '{print $NF}')
echo "X-Tecof-Signature: t=$T,v1=$SIG"
```

---

## Sır Döndürme

```bash
tecof app secret rotate
```

ya da `POST /api/developer/apps/:id/secret/rotate` → `{ clientSecret, previousValidUntil }`.

Döndürmeden sonraki **24 saat** boyunca:

| Kanal | Eski sırrın durumu |
| :--- | :--- |
| Token ucu (`/api/apps/oauth/token`) | Eski sır da kabul edilir |
| Webhook / API aksiyonu imzası | Başlık iki `v1` taşır (yeni + eski) |
| Oturum token'ı | Panel geçiş boyunca **eski** sırla imzalar (yeni sırra henüz geçmemiş sunucu da doğrular); 24 saat sonra yeni sırla. SDK her iki sırrı da dener (`previousClientSecret`) |

- **Tecof barındırması:** Vercel ortam değişkenleri güncellenir (`TECOF_CLIENT_SECRET` = yeni sır, `TECOF_PREVIOUS_CLIENT_SECRET` = `eski_sır|bitiş_zamanı`) ve yeniden dağıtım kendiliğinden tetiklenir. SDK eski sırrı bitiş zamanından sonra kendiliğinden yok sayar.
- **Kendi sunucunuz:** Yeni sırrı `TECOF_CLIENT_SECRET`'a, eski sırrı geçiş süresince `TECOF_PREVIOUS_CLIENT_SECRET`'a yazın (isterseniz `tcfs_…|2026-10-11T20:00:00Z` biçiminde bitişle — SDK bitişten sonra yok sayar); 24 saat içinde dağıtın. Geçiş boyunca oturum token'ları eski sırla imzalandığı için yeni sırra geçene kadar arayüz kesintisiz çalışır.

```ts
export const tecof = new TecofApp({
  clientId: process.env.TECOF_CLIENT_ID!,
  clientSecret: process.env.TECOF_CLIENT_SECRET!,                    // yeni (güncel) sır
  previousClientSecret: process.env.TECOF_PREVIOUS_CLIENT_SECRET,    // isteğe bağlı: 24 saatlik geçişte eski sır
});
```

---

## Güvenlik Kontrol Listesi

- [ ] İstemci sırrı yalnız sunucu ortam değişkeninde; `NEXT_PUBLIC_` önekli değişkende, repoda, istemci paketinde yok.
- [ ] Her webhook ve API aksiyonu isteği ham gövde üzerinden imza + zaman damgası ile doğrulanıyor.
- [ ] Yetki kararları sorgu dizesine göre değil, doğrulanmış oturum token'ının `mid` / `iid` değerlerine göre veriliyor.
- [ ] Oturum token'ı ve kurulum token'ı günlüğe, URL'e, hata mesajına yazılmıyor.
- [ ] Yalnız gerçekten gereken kapsamlar isteniyor.
- [ ] Mağazadan panel parolası, panel JWT'si ya da mağaza `secretKey`'i istenmiyor; platform bunları uygulamalara vermez.
- [ ] Üçüncü taraf API anahtarları (ör. muhasebe yazılımı parolası) Vault'ta saklanıyor. Bkz. [Vault](/app-data).
- [ ] `app.uninstalled` geldiğinde o kuruluma ait kendi kopyalarınız siliniyor; teslimattan sonra Tecof token'ları iptal eder, ayarları ve sırları siler.
- [ ] App Bridge mesajları yalnız `host` parametresindeki origin'le alınıp veriliyor (SDK bunu yapar).
- [ ] Sır döndürme prosedürü (24 saatlik geçiş) belgelenmiş.
