# SDK ve araçlar

> Node.js için @cosuite/connect paketi, uyumluluk testi CLI'ı, imza test vektörleri ve .NET, PHP ve Python için doğrulanmış imza fonksiyonları.

Source: https://cosuite.io/developers/sdk

[Connect v1 sözleşmesini](https://cosuite.io/developers/contract) uygulamak ve test etmek için ihtiyacın olan her şey. Doğruluk kullandığın dile bağlı değildir: aynı imza test vektörleri ve aynı uyumluluk testi her uygulama için geçerlidir.

## @cosuite/connect (Node.js) {#node}

Sözleşmenin Node.js 18.17+ için TypeScript ile yazılmış referans uygulaması. MIT lisansı.

```bash
npm install https://cosuite.io/developers/sdk/cosuite-connect-1.8.0.tgz
```

Paket `@cosuite/connect` adıyla kurulur, import satırları değişmez. Paket npm kaydına da yayınlanacak; o zamana kadar bu adresten kurun.

| Dışa aktarım | Amaç |
|---|---|
| `signRequest`, `verifyRequest`, `computeSignature` | HMAC istek imzası |
| `handleConnectRequest`, `matchAsistRoute`, `okResponse`, `errorResponse` | İmzalı uç kabuğu: gövde sınırı, imza, istek kimliği tekrarı, hata zarfı |
| `ConnectError` | Bir sözleşme hatası dönmek için handler içinden fırlat (`new ConnectError("tenant_not_found", "…")`) |
| `CoSuiteClient` | Uygulamandan CoSuite'e istemci (`getEntitlements`, `sendEvent`) |
| `createSsoVerifier`, `isSafeRedirect` | SSO jetonu doğrulama (ES256 + JWKS) ve güvenli yönlendirme denetimi |
| `schemas` | Her istek ve yanıt için zod şemaları ve TypeScript tipleri |
| `verifyGatewayRequest`, `gatewayServer` | [Müşteri API ağ geçidinin](https://cosuite.io/developers/customer-api) uygulamanıza ilettiği istekleri doğrulama |
| `checkModuleDefinitions`, `checkOpenApi` | Uyumluluk testinin çalıştırdığı [modül tanımı](https://cosuite.io/developers/module-definition#rules) denetimleri |
| `MemoryReplayStore` | İstek kimliği ve SSO `jti` tekrarı için tek süreçli depo (birden fazla süreç çalıştırıyorsan `ReplayStore` arayüzünü Redis ya da veritabanın üzerinde uygula) |
| `createMockAsist`, `serveMockAsist` | Sözleşmenin tamamını uygulayan, bellekte çalışan referans uygulama |
| `@cosuite/connect/conformance` | Kütüphane olarak uyumluluk testi (`runConformance`) |

`handleConnectRequest` web standardı `Request` ve `Response` ile çalışır; bu yüzden Next.js route handler'larında, Hono, Bun ya da Deno'da olduğu gibi, Express ya da Fastify'da ise küçük bir köprüyle çalışır. Eksiksiz bir route için [10 dakikalık hızlı başlangıca](https://cosuite.io/developers#quickstart) bak.

### SSO doğrulama {#node-sso}

```ts
import { createSsoVerifier, MemoryReplayStore } from "@cosuite/connect";

const verifySso = createSsoVerifier({
  issuer: "https://cosuite.io",
  audience: "dent", // your app key
  jwksUrl: "https://api.cosuite.io/connect/v1/jwks.json",
  replay: new MemoryReplayStore(),
});

// POST /api/cosuite/v1/sso, form field "token"
const claims = await verifySso(token); // throws ConnectError when invalid, expired or reused
// claims.sub, claims.tid, claims.email, claims.role, claims.redirect …
```

### Olay gönderme {#node-events}

```ts
import { CoSuiteClient } from "@cosuite/connect";

const cosuite = new CoSuiteClient({
  baseUrl: "https://api.cosuite.io/connect/v1",
  keyId: process.env.COSUITE_KEY_ID!,
  secret: process.env.COSUITE_CONNECT_SECRET!,
});

await cosuite.sendEvent({
  id: "evt_01J9ZK3Q", // stable id from your outbox; also used as the request id
  type: "usage.threshold_reached",
  tenantId: "org_8f2c",
  occurredAt: new Date().toISOString(),
  data: { metric: "smsPerMonth", percent: 80 },
});
```

## Uyumluluk testi {#conformance}

Uyumluluk testi uygulamanın URL'ine bağlanır ve sözleşmeyi uçtan uca sınar. Uygulamanın hangi dilde yazıldığıyla ilgilenmez. CoSuite onu uygulaman yayına çıkmadan önce çalıştırır; sen de CI'ında hazırlık ortamına karşı çalıştırmalısın: **kırmızıysa yayınlama.**

```bash
export COSUITE_CONNECT_SECRET=…   # the secret of the key you test with; never put it on the command line
npx --yes --package=https://cosuite.io/developers/sdk/cosuite-connect-1.8.0.tgz cosuite-connect conformance --url https://staging.dentcloud.example --key csk_dent_test
```

| Seçenek | Anlamı |
|---|---|
| `--url` | Uygulamanın taban URL'i (sözleşme `/api/cosuite/v1` altında durur) |
| `--key` | İmzalamada kullanılacak `keyId` |
| `--secret-env` | Secret'ı tutan ortam değişkeninin adı (varsayılan `COSUITE_CONNECT_SECRET`) |
| `--sso-key`, `--issuer` | SSO denetimleri için özel bir JWK dosyası ve yayıncı (issuer). Bunlar olmadan SSO denetimleri atlanır. |
| `--json` | CI için makinenin okuyabileceği çıktı |

Şu denetimleri sırayla çalıştırır:

1. manifest ve health;
2. modül tanımı (sözleşme 1.7, yalnızca doldurduğun alanlar), her OpenAPI belgesinin indirilip denetlenmesi dahil;
3. imza saldırıları: imzasız, süresi geçmiş zaman damgası, bilinmeyen anahtar, değiştirilmiş gövde, değiştirilmiş yöntem;
4. `test: true` ile kiracı açma, istek kimliği tekrarı (aynı gövde ilk yanıtı döner, farklı gövde `409`), idempotent yeniden açma, bilinmeyen bağlama kodu `409 invalid_link_code`, bilinmeyen kiracı `404`, geçersiz gövde `400`, `PATCH`;
5. yetki belgesi sürümleri: 2, sonra 1 (uygulanmamalı), aynı 2 tekrar (uygulanmamalı), sonra bilinmeyen bir modül ve alanla 3 (yok sayılmalı), ardından geri okuma;
6. üye ekleme, listeleme ve çıkarma;
7. müşteriler (`customers` bildirdiysen): sayfa yapısı, geçersiz limit ve imleç;
8. kullanım ve özet, 1 saniyelik özet bütçesi ve en fazla 6 kart sınırı dahil;
9. SSO (`--sso-key` ile): geçerli jeton istenen sayfaya ulaşır, tekrar kullanılan jeton, başka bir uygulamaya ait jeton, bozuk imza;
10. kiracıyı silme.

Her şey geçtiğinde çıkış kodu `0`, aksi hâlde `1` olur. İnsanın okuyacağı çıktı şu anda denetim adlarını Türkçe yazar ve `YEŞİL` ya da `KIRMIZI` ile biter; sonucu ayrıştırıyorsan `--json` kullan.

### Yerel referans uygulama {#mock}

Sözleşmeyi zaten uygulayan, bellekte çalışan bir uygulamaya karşı geliştir ya da onun davranışını kendininkiyle karşılaştır:

```bash
COSUITE_CONNECT_SECRET=… npx --yes --package=https://cosuite.io/developers/sdk/cosuite-connect-1.8.0.tgz cosuite-connect mock --asist dent --port 4010 --key csk_dent_dev [--single-tenant-users]
```

`--single-tenant-users`, onu bir e-postanın yalnızca tek bir hesaba ait olabildiği bir uygulama gibi davrandırır (`409 member_conflict`).

## İmza test vektörleri {#test-vectors}

Her uygulama tam olarak bu imzaları üretmelidir. Tüm metinler UTF-8'dir; zaman damgası yazıldığı gibi (ondalık bir metin olarak) kullanılır.

| Durum | Secret | Yöntem | pathAndQuery | Zaman damgası | Gövde | İmza |
|---|---|---|---|---|---|---|
| GET, gövdesiz | `s3cr3t-key-0123456789abcdef0123456789` | `GET` | `/api/cosuite/v1/manifest` | `1761469200` | *(boş)* | `v1=0742611504ab28bc645cd09b102fe78ba0ec192698d3905fcc0dbb7519c1bce9` |
| PUT, JSON gövde (küçük harfli yöntem) | `s3cr3t-key-0123456789abcdef0123456789` | `put` | `/api/cosuite/v1/tenants/org_8f2c/entitlements` | `1761469200` | `{"version":42,"status":"active"}` | `v1=ae6344f124a267db473e8c4e3da23413e35926426959bebfe447ec287570fe6f` |
| Sorgu dizesi ve ASCII dışı metin | `başka-gizli-ə` | `POST` | `/app/api/cosuite/v1/tenants?x=1&y=%C3%A7` | `1761469201` | `{"name":"Yılmaz Otel — Bakı"}` | `v1=c14f7a789416cde9b3b1efe524695b267ee8b12d0d2a91fdce5926dc73bb5601` |
| Yolda kodlanmış kimlik | `k` | `DELETE` | `/api/cosuite/v1/tenants/org%3Aa%2Fb/members/usr_1` | `0` | *(boş)* | `v1=e83120960821a9035bdd4077ccf127dc077e4c8058bbf4b9633ae46071308744` |

İlk durumun kanonik metni şudur:

```text
GET|/api/cosuite/v1/manifest|1761469200|e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
```

Aynı vektörler `@cosuite/connect` paketinde `test-vectors/signature.json` olarak gelir.

## İmza örnekleri {#signature-snippets}

Diğer diller için ayrı bir paket yok: imza tek bir fonksiyondan ibaret. Aşağıdaki her örnek yukarıdaki dört test vektörünün tamamına karşı çalıştırıldı ve onları birebir üretiyor. Çevresindeki kuralları unutma: **ham** gövde baytlarını oku, JSON'u ayrıştırmadan önce doğrula, en fazla 256 KB kabul et, yazma isteklerinin kimliklerini 10 dakika tut ve [hata zarfını](https://cosuite.io/developers/contract#errors) dön.

### .NET (C#) {#dotnet}

```csharp
using System.Security.Cryptography;
using System.Text;

public static class CoSuiteSignature
{
    public const int MaxSkewSeconds = 300;

    public static string Compute(string secret, string method, string pathAndQuery, string timestamp, byte[] body)
    {
        var bodyHash = Convert.ToHexStringLower(SHA256.HashData(body));
        var canonical = $"{method.ToUpperInvariant()}|{pathAndQuery}|{timestamp}|{bodyHash}";
        var mac = HMACSHA256.HashData(Encoding.UTF8.GetBytes(secret), Encoding.UTF8.GetBytes(canonical));
        return "v1=" + Convert.ToHexStringLower(mac);
    }

    /// <summary>secrets: the valid secrets for this keyId (two during key rotation).</summary>
    public static bool Verify(IReadOnlyList<string> secrets, string method, string pathAndQuery, string timestamp, byte[] body, string signature, long nowUnix)
    {
        if (!long.TryParse(timestamp, out var ts) || Math.Abs(nowUnix - ts) > MaxSkewSeconds) return false;
        if (!signature.StartsWith("v1=") || signature.Length != 67) return false;
        var given = Encoding.ASCII.GetBytes(signature);
        var match = false;
        foreach (var s in secrets)
            match |= CryptographicOperations.FixedTimeEquals(Encoding.ASCII.GetBytes(Compute(s, method, pathAndQuery, timestamp, body)), given);
        return match;
    }
}
```

ASP.NET Core'da `Request.EnableBuffering()` çağır ve model bağlamadan (model binding) önce ham gövdeyi oku; `pathAndQuery` değeri `Request.PathBase + Request.Path + Request.QueryString` olur. Sözleşme uçlarından ProblemDetails dönme; sözleşmenin hata zarfını kullan. `Convert.ToHexStringLower` .NET 9 gerektirir; eski sürümlerde `Convert.ToHexString(...).ToLowerInvariant()` kullan.

`Microsoft.IdentityModel.JsonWebTokens` ile SSO:

```csharp
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;

var jwks = new JsonWebKeySet(await http.GetStringAsync("https://api.cosuite.io/connect/v1/jwks.json")); // cache for 1 hour
var result = await new JsonWebTokenHandler().ValidateTokenAsync(token, new TokenValidationParameters
{
    ValidIssuer = "https://cosuite.io",
    ValidAudience = "dent",                 // your app key
    IssuerSigningKeys = jwks.GetSigningKeys(),
    ValidAlgorithms = new[] { SecurityAlgorithms.EcdsaSha256 },
    ClockSkew = TimeSpan.FromSeconds(30),
    RequireExpirationTime = true,
});
// Then: reject if exp - iat > 60, keep jti for 2 minutes (single use), allow only "/path" redirects.
```

### PHP {#php}

PHP 8.0+ (yalnızca yerleşik `hash` eklentisini kullanır):

```php
<?php
const COSUITE_MAX_SKEW_SECONDS = 300;

function cosuite_signature(string $secret, string $method, string $pathAndQuery, string $timestamp, string $body): string
{
    $canonical = strtoupper($method) . '|' . $pathAndQuery . '|' . $timestamp . '|' . hash('sha256', $body);
    return 'v1=' . hash_hmac('sha256', $canonical, $secret);
}

/** @param string[] $secrets the valid secrets for this keyId (two during key rotation) */
function cosuite_verify(array $secrets, string $method, string $pathAndQuery, string $timestamp, string $body, string $signature, ?int $now = null): bool
{
    $now ??= time();
    if (!ctype_digit($timestamp) || abs($now - (int) $timestamp) > COSUITE_MAX_SKEW_SECONDS) {
        return false; // 401 signature_expired
    }
    $match = false;
    foreach ($secrets as $secret) {
        $match = hash_equals(cosuite_signature($secret, $method, $pathAndQuery, $timestamp, $body), $signature) || $match;
    }
    return $match; // false -> 401 invalid_signature
}

// Usage: $body = file_get_contents('php://input'); $path = $_SERVER['REQUEST_URI'];
// cosuite_verify($secrets, $_SERVER['REQUEST_METHOD'], $path, $_SERVER['HTTP_X_COSUITE_TIMESTAMP'], $body, $_SERVER['HTTP_X_COSUITE_SIGNATURE']);
```

### Python {#python}

Python 3.10+ (yalnızca standart kütüphane):

```python
import hashlib
import hmac
import time

MAX_SKEW_SECONDS = 300


def cosuite_signature(secret: str, method: str, path_and_query: str, timestamp: str, body: bytes) -> str:
    body_hash = hashlib.sha256(body).hexdigest()
    canonical = f"{method.upper()}|{path_and_query}|{timestamp}|{body_hash}"
    mac = hmac.new(secret.encode("utf-8"), canonical.encode("utf-8"), hashlib.sha256).hexdigest()
    return "v1=" + mac


def cosuite_verify(secrets: list[str], method: str, path_and_query: str, timestamp: str,
                   body: bytes, signature: str, now: int | None = None) -> bool:
    now = int(time.time()) if now is None else now
    if not timestamp.isdigit() or abs(now - int(timestamp)) > MAX_SKEW_SECONDS:
        return False  # 401 signature_expired
    match = False
    for secret in secrets:  # two secrets are valid during key rotation
        expected = cosuite_signature(secret, method, path_and_query, timestamp, body)
        match |= hmac.compare_digest(expected, signature)
    return match  # False -> 401 invalid_signature
```

Ham gövde baytlarını (Django'da `request.body`, Flask'ta `request.get_data()`) ve ham istek hedefini istemcinin gönderdiği hâliyle (örneğin WSGI sunucundan `RAW_URI` ya da `REQUEST_URI`) ilet. Yolu yeniden oluşturan framework yardımcıları `%3A` gibi karakterlerin kodunu çözebilir ya da onları yeniden kodlayabilir ve imzayı bozabilir.

PHP ya da Python'da SSO için ES256 ve JWKS destekleyen, bakımı süren herhangi bir JWT kütüphanesini kullan (örneğin `firebase/php-jwt` ya da `PyJWKClient` ile `PyJWT`) ve [SSO denetimlerini](https://cosuite.io/developers/contract#sso) uygula.
