# SDK və alətlər

> Node.js üçün @cosuite/connect paketi, uyğunluq testi CLI-ı, imza test vektorları və .NET, PHP, Python üçün yoxlanılmış imza funksiyaları.

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

[Connect v1 müqaviləsini](https://cosuite.io/az/developers/contract) reallaşdırmaq və test etmək üçün lazım olan hər şey. Düzgünlük proqramlaşdırma dilindən asılı deyil: eyni imza test vektorları və eyni uyğunluq testi hər tətbiqə şamil olunur.

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

TypeScript ilə yazılmış, Node.js 18.17+ üçün müqavilənin istinad reallaşdırması. MIT lisenziyası.

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

Paket `@cosuite/connect` adı ilə quraşdırılır, import sətirləri dəyişmir. Paket npm reyestrində də dərc olunacaq; o vaxta qədər bu ünvandan quraşdırın.

| Eksport | Məqsəd |
|---|---|
| `signRequest`, `verifyRequest`, `computeSignature` | HMAC sorğu imzası |
| `handleConnectRequest`, `matchAsistRoute`, `okResponse`, `errorResponse` | İmzalı endpoint qabığı: gövdə limiti, imza, request-id təkrarı, xəta zərfi |
| `ConnectError` | Müqavilə xətası qaytarmaq üçün handler-dən at (`new ConnectError("tenant_not_found", "…")`) |
| `CoSuiteClient` | Tətbiqin → CoSuite klienti (`getEntitlements`, `sendEvent`) |
| `createSsoVerifier`, `isSafeRedirect` | SSO tokeninin yoxlanması (ES256 + JWKS) və təhlükəsiz yönləndirmə yoxlaması |
| `schemas` | Hər sorğu və cavab üçün zod sxemləri və TypeScript tipləri |
| `verifyGatewayRequest`, `gatewayServer` | [Müştəri API şlüzünün](https://cosuite.io/az/developers/customer-api) tətbiqinizə ötürdüyü sorğuların yoxlanması |
| `checkModuleDefinitions`, `checkOpenApi` | Uyğunluq testinin işə saldığı [modul tərifi](https://cosuite.io/az/developers/module-definition#rules) yoxlamaları |
| `MemoryReplayStore` | Request-id və SSO `jti` təkrarı üçün tək prosesli anbar (bir neçə proses işlədəndə `ReplayStore`-u Redis və ya verilənlər bazan üzərində reallaşdır) |
| `createMockAsist`, `serveMockAsist` | Bütün müqaviləni reallaşdıran, yaddaşda işləyən istinad tətbiqi |
| `@cosuite/connect/conformance` | Kitabxana kimi uyğunluq testi (`runConformance`) |

`handleConnectRequest` veb-standart `Request` və `Response` ilə işləyir, ona görə də Next.js route handler-lərində, Hono, Bun və ya Deno-da olduğu kimi, Express və ya Fastify-da isə kiçik bir körpü ilə işləyir. Tam route üçün bax: [10 dəqiqəlik sürətli başlanğıc](https://cosuite.io/az/developers#quickstart).

### SSO-nun yoxlanması {#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 …
```

### Hadisələrin göndərilməsi {#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 },
});
```

## Uyğunluq testi {#conformance}

Uyğunluq testi tətbiqinin URL-inə qoşulur və müqaviləni başdan sona sınaqdan keçirir. Tətbiqinin hansı dildə yazıldığı onun üçün fərq etmir. CoSuite onu tətbiqin canlıya çıxmazdan əvvəl işə salır, sən də onu CI-da staging-ə qarşı işlətməlisən: **qırmızıdırsa, buraxma.**

```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çim | Mənası |
|---|---|
| `--url` | Tətbiqinin əsas URL-i (müqavilə `/api/cosuite/v1` altında yerləşir) |
| `--key` | İmzalamaq üçün `keyId` |
| `--secret-env` | Gizli açarı saxlayan mühit dəyişəninin adı (standart `COSUITE_CONNECT_SECRET`) |
| `--sso-key`, `--issuer` | SSO yoxlamaları üçün şəxsi JWK faylı və issuer. Onlar olmadan SSO yoxlamaları ötürülür. |
| `--json` | CI üçün maşın tərəfindən oxunan çıxış |

Bu yoxlamaları ardıcıllıqla işə salır:

1. manifest və health;
2. modul tərifi (müqavilə 1.7, yalnız doldurduğun sahələr), o cümlədən hər OpenAPI sənədinin yüklənib yoxlanması;
3. imza hücumları: imzasız, vaxtı keçmiş zaman möhürü, naməlum açar, dəyişdirilmiş gövdə, dəyişdirilmiş metod;
4. `test: true` ilə tenant açılması, request-id təkrarı (eyni gövdə ilk cavabı qaytarır, fərqli gövdə `409`), idempotent yenidən açılma, naməlum bağlama kodu `409 invalid_link_code`, naməlum tenant `404`, etibarsız gövdə `400`, `PATCH`;
5. entitlement versiyaları: 2, sonra 1 (tətbiq olunmamalıdır), yenə eyni 2 (tətbiq olunmamalıdır), sonra naməlum modul və sahə ilə 3 (nəzərə alınmamalıdır), sonra geri oxuma;
6. üzvlərin əlavə edilməsi, siyahılanması və çıxarılması;
7. müştərilər (`customers` elan edirsənsə): səhifə forması, etibarsız limit və cursor;
8. istifadə və xülasə, o cümlədən xülasə üçün 1 saniyəlik vaxt büdcəsi və ən çoxu 6 kart;
9. SSO (`--sso-key` ilə): etibarlı token istənilən səhifəyə çatır, təkrar istifadə olunmuş token, başqa tətbiq üçün token, pozulmuş imza;
10. tenant-ın silinməsi.

Hər şey keçəndə çıxış kodu `0`, əks halda `1` olur. İnsan üçün nəzərdə tutulmuş çıxış hazırda yoxlama adlarını türkcə yazır və `YEŞİL` (yaşıl) və ya `KIRMIZI` (qırmızı) ilə bitir; nəticəni parse edirsənsə, `--json` istifadə et.

### Lokal istinad tətbiqi {#mock}

Müqaviləni artıq reallaşdıran, yaddaşda işləyən tətbiqə qarşı inkişaf et və ya onun davranışını öz tətbiqininkilə müqayisə et:

```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 e-poçtun yalnız bir hesaba aid ola bildiyi tətbiq kimi davranmağa məcbur edir (`409 member_conflict`).

## İmza test vektorları {#test-vectors}

Hər reallaşdırma məhz bu imzaları yaratmalıdır. Bütün sətirlər UTF-8-dir; zaman möhürü yazıldığı kimi (onluq sətir kimi) istifadə olunur.

| Hal | Gizli açar | Metod | pathAndQuery | Zaman möhürü | Gövdə | İmza |
|---|---|---|---|---|---|---|
| GET, gövdəsiz | `s3cr3t-key-0123456789abcdef0123456789` | `GET` | `/api/cosuite/v1/manifest` | `1761469200` | *(boş)* | `v1=0742611504ab28bc645cd09b102fe78ba0ec192698d3905fcc0dbb7519c1bce9` |
| PUT, JSON gövdə (kiçik hərfli metod) | `s3cr3t-key-0123456789abcdef0123456789` | `put` | `/api/cosuite/v1/tenants/org_8f2c/entitlements` | `1761469200` | `{"version":42,"status":"active"}` | `v1=ae6344f124a267db473e8c4e3da23413e35926426959bebfe447ec287570fe6f` |
| Sorğu sətri və qeyri-ASCII mətn | `başka-gizli-ə` | `POST` | `/app/api/cosuite/v1/tenants?x=1&y=%C3%A7` | `1761469201` | `{"name":"Yılmaz Otel — Bakı"}` | `v1=c14f7a789416cde9b3b1efe524695b267ee8b12d0d2a91fdce5926dc73bb5601` |
| Yolda kodlaşdırılmış id | `k` | `DELETE` | `/api/cosuite/v1/tenants/org%3Aa%2Fb/members/usr_1` | `0` | *(boş)* | `v1=e83120960821a9035bdd4077ccf127dc077e4c8058bbf4b9633ae46071308744` |

Birinci halın kanonik sətri belədir:

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

Eyni vektorlar `@cosuite/connect` paketində `test-vectors/signature.json` kimi gəlir.

## İmza nümunələri {#signature-snippets}

Digər dillər üçün ayrıca paket yoxdur: imza tək bir funksiyadır. Aşağıdakı hər nümunə yuxarıdakı dörd test vektorunun hamısına qarşı işə salınıb və onları dəqiq yaradır. Ətrafındakı qaydaları unutma: **xam** gövdə baytlarını oxu, JSON-u parse etməzdən əvvəl yoxla, ən çoxu 256 KB qəbul et, yazma sorğularının id-lərini 10 dəqiqə saxla və [xəta zərfini](https://cosuite.io/az/developers/contract#errors) qaytar.

### .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 və xam gövdəni model binding-dən əvvəl oxu; `pathAndQuery` `Request.PathBase + Request.Path + Request.QueryString` olur. Müqavilə endpoint-lərindən ProblemDetails qaytarma; müqavilənin xəta zərfindən istifadə et. `Convert.ToHexStringLower` .NET 9 tələb edir; köhnə versiyalarda `Convert.ToHexString(...).ToLowerInvariant()` istifadə et.

`Microsoft.IdentityModel.JsonWebTokens` ilə 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ız daxili `hash` genişlənməsindən istifadə edir):

```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ız standart kitabxana):

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

Xam gövdə baytlarını (Django-da `request.body`, Flask-da `request.get_data()`) və klientin göndərdiyi xam sorğu hədəfini dəqiq olduğu kimi ötür (məsələn, WSGI serverindən `RAW_URI` və ya `REQUEST_URI`). Yolu yenidən quran framework köməkçiləri `%3A` kimi simvolları dekodlaşdıra və ya yenidən kodlaşdıra, imzanı poza bilər.

PHP və ya Python-da SSO üçün ES256 və JWKS dəstəkləyən, aktiv saxlanılan istənilən JWT kitabxanasından istifadə et (məsələn, `firebase/php-jwt` və ya `PyJWKClient` ilə `PyJWT`) və [SSO yoxlamalarını](https://cosuite.io/az/developers/contract#sso) tətbiq et.
