Tərtibatçılar · Connect v1

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

Connect v1 müqaviləsini 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)

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

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 tətbiqinizə ötürdüyü sorğuların yoxlanması
checkModuleDefinitions, checkOpenApi Uyğunluq testinin işə saldığı modul tərifi 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.

SSO-nun yoxlanması

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

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

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.

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

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:

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ı

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:

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

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

İmza nümunələri

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

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

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ız daxili hash genişlənməsindən istifadə edir):

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

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ı tətbiq et.