Geliştiriciler · Connect v1

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ı.

Connect v1 sözleşmesini 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)

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

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 uygulamanıza ilettiği istekleri doğrulama
checkModuleDefinitions, checkOpenApi Uyumluluk testinin çalıştırdığı modül tanımı 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 bak.

SSO doğrulama

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

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

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.

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

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

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

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:

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

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

İmza örnekleri

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ı dön.

.NET (C#)

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:

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 8.0+ (yalnızca yerleşik hash eklentisini kullanır):

<?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 3.10+ (yalnızca standart kütüphane):

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 uygula.