SDK & tools
The @cosuite/connect package for Node.js, the conformance test CLI, signature test vectors and verified signature functions for .NET, PHP and Python.
Everything you need to implement and test the Connect v1 contract. Correctness does not depend on your language: the same signature test vectors and the same conformance test apply to every app.
@cosuite/connect (Node.js)
The reference implementation of the contract for Node.js 18.17+, written in TypeScript. MIT license.
npm install https://cosuite.io/developers/sdk/cosuite-connect-1.8.0.tgz
The package installs as @cosuite/connect, so imports stay the same. It will also be published to the npm registry; until then install it from this address.
| Export | Purpose |
|---|---|
signRequest, verifyRequest, computeSignature |
HMAC request signature |
handleConnectRequest, matchAsistRoute, okResponse, errorResponse |
Signed endpoint shell: body limit, signature, request-id replay, error envelope |
ConnectError |
Throw it from a handler to return a contract error (new ConnectError("tenant_not_found", "…")) |
CoSuiteClient |
Your app → CoSuite client (getEntitlements, sendEvent) |
createSsoVerifier, isSafeRedirect |
SSO token verification (ES256 + JWKS) and safe redirect check |
schemas |
zod schemas and TypeScript types for every request and response |
verifyGatewayRequest, gatewayServer |
Verify requests the customer API gateway forwards to your app |
checkModuleDefinitions, checkOpenApi |
The module definition checks the conformance test runs |
MemoryReplayStore |
Single-process store for request-id and SSO jti replay (implement ReplayStore on Redis or your database when you run several processes) |
createMockAsist, serveMockAsist |
In-memory reference app that implements the whole contract |
@cosuite/connect/conformance |
The conformance test as a library (runConformance) |
handleConnectRequest works with the web-standard Request and Response, so it runs as is in Next.js route handlers, Hono, Bun or Deno, and with a small bridge in Express or Fastify. See the 10-minute quick start for a full route.
Verifying SSO
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 …
Sending events
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 },
});
Conformance test
The conformance test connects to your app's URL and exercises the contract end to end. It does not care which language your app is written in. CoSuite runs it before your app goes live and you should run it in your CI against staging: if it is red, do not release.
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
| Option | Meaning |
|---|---|
--url |
Your app's base URL (the contract lives under /api/cosuite/v1) |
--key |
The keyId to sign with |
--secret-env |
Name of the environment variable that holds the secret (default COSUITE_CONNECT_SECRET) |
--sso-key, --issuer |
A private JWK file and issuer for the SSO checks. Without them the SSO checks are skipped. |
--json |
Machine-readable output for CI |
It runs these checks in order:
- manifest and health;
- module definition (contract 1.7, only the fields you fill in), including downloading and checking each OpenAPI document;
- signature attacks: unsigned, expired timestamp, unknown key, changed body, changed method;
- opening a tenant with
test: true, request-id replay (same body returns the first response, different body is409), idempotent re-open, unknown link code is409 invalid_link_code, unknown tenant404, invalid body400,PATCH; - entitlement versions: 2, then 1 (must not apply), the same 2 again (must not apply), then 3 with an unknown module and field (must be ignored), then read back;
- adding, listing and removing members;
- customers (if you declare
customers): page shape, invalid limit and cursor; - usage and summary, including the 1-second summary budget and the 6-card maximum;
- SSO (with
--sso-key): valid token reaches the requested page, reused token, token for another app, broken signature; - deleting the tenant.
The exit code is 0 when everything passes and 1 otherwise. The human-readable output currently prints its check names in Turkish and ends with YEŞİL (green) or KIRMIZI (red); use --json if you parse the result.
Local reference app
Develop against an in-memory app that already implements the contract, or compare its behaviour with yours:
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 makes it behave like an app where an email can belong to only one account (409 member_conflict).
Signature test vectors
Every implementation must produce exactly these signatures. All strings are UTF-8; the timestamp is used as written (as a decimal string).
| Case | Secret | Method | pathAndQuery | Timestamp | Body | Signature |
|---|---|---|---|---|---|---|
| GET, no body | s3cr3t-key-0123456789abcdef0123456789 |
GET |
/api/cosuite/v1/manifest |
1761469200 |
(empty) | v1=0742611504ab28bc645cd09b102fe78ba0ec192698d3905fcc0dbb7519c1bce9 |
| PUT, JSON body (lower-case method) | s3cr3t-key-0123456789abcdef0123456789 |
put |
/api/cosuite/v1/tenants/org_8f2c/entitlements |
1761469200 |
{"version":42,"status":"active"} |
v1=ae6344f124a267db473e8c4e3da23413e35926426959bebfe447ec287570fe6f |
| Query string and non-ASCII text | başka-gizli-ə |
POST |
/app/api/cosuite/v1/tenants?x=1&y=%C3%A7 |
1761469201 |
{"name":"Yılmaz Otel — Bakı"} |
v1=c14f7a789416cde9b3b1efe524695b267ee8b12d0d2a91fdce5926dc73bb5601 |
| Encoded id in path | k |
DELETE |
/api/cosuite/v1/tenants/org%3Aa%2Fb/members/usr_1 |
0 |
(empty) | v1=e83120960821a9035bdd4077ccf127dc077e4c8058bbf4b9633ae46071308744 |
The canonical string of the first case is:
GET|/api/cosuite/v1/manifest|1761469200|e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
The same vectors ship as test-vectors/signature.json in the @cosuite/connect package.
Signature snippets
There is no separate package for other languages: the signature is a single function. Each snippet below was run against all four test vectors above and produces them exactly. Remember the rules around it: read the raw body bytes, verify before parsing JSON, accept at most 256 KB, keep write-request ids for 10 minutes and return the error envelope.
.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;
}
}
In ASP.NET Core, call Request.EnableBuffering() and read the raw body before model binding; pathAndQuery is Request.PathBase + Request.Path + Request.QueryString. Do not return ProblemDetails from contract endpoints; use the contract's error envelope. Convert.ToHexStringLower needs .NET 9; on older versions use Convert.ToHexString(...).ToLowerInvariant().
SSO with Microsoft.IdentityModel.JsonWebTokens:
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+ (uses only the built-in hash extension):
<?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+ (standard library only):
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
Pass the raw body bytes (request.body in Django, request.get_data() in Flask) and the raw request target exactly as the client sent it (for example RAW_URI or REQUEST_URI from your WSGI server). Framework helpers that rebuild the path may decode or re-encode characters such as %3A and break the signature.
For SSO in PHP or Python, use any maintained JWT library that supports ES256 and JWKS (for example firebase/php-jwt or PyJWT with PyJWKClient) and apply the SSO checks.