getsupertab/connect-sdk-php

Supertab Connect PHP SDK

Maintainers

Package info

github.com/getsupertab/connect-sdk-php

pkg:composer/getsupertab/connect-sdk-php

Transparency log

Statistics

Installs: 403

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 4

v1.4.0-beta.10 2026-07-14 13:11 UTC

README

Check our documentation for more information on Supertab Connect.

Packagist Version License CI PHP Version Code Style

Installation

composer require getsupertab/connect-sdk-php

Requirements: PHP 8.1+, extensions: ext-curl, ext-json, ext-openssl, ext-simplexml

Quick Start

Publisher — verify incoming requests:

use Supertab\Connect\SupertabConnect;
use Supertab\Connect\Enum\EnforcementMode;
use Supertab\Connect\Enum\HandlerAction;

$connect = new SupertabConnect(
    apiKey: 'stc_live_your_api_key',
    enforcement: EnforcementMode::ENFORCE,
);

$result = $connect->handleRequest();

// Send returned headers (RSL headers such as Link / WWW-Authenticate, or
// Cache-Control for the self-report status endpoint).
foreach ($result->headers as $name => $value) {
    header("{$name}: {$value}");
}

// Any non-ALLOW result carries a status + body to emit: BLOCK (invalid/missing
// token) and RESPOND (the self-report status endpoint) both end the request.
if ($result->action !== HandlerAction::ALLOW) {
    http_response_code($result->status);
    echo $result->body;
    exit;
}

// Token is valid — serve content

Bot — obtain a license token:

use Supertab\Connect\SupertabConnect;

$token = SupertabConnect::obtainLicenseToken(
    clientId: 'your_client_id',
    clientSecret: 'your_client_secret',
    resourceUrl: 'https://example.com/article/my-slug',
);

$ch = curl_init('https://example.com/article/my-slug');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ["Authorization: License {$token}"],
]);
$response = curl_exec($ch);

Enforcement Modes

The EnforcementMode enum controls how handleRequest() responds to detected bots when a token is absent or invalid. Non-bot requests without a token are always allowed regardless of mode. Requests with an invalid token are always blocked (except in DISABLED mode).

Mode Behavior
ENFORCE Bots without a valid token are blocked (401/403 with WWW-Authenticate header). Invalid tokens from any source are rejected.
OBSERVE All requests allowed. Bots without a token receive X-RSL-Status: token_required and Link headers to signal that licensing is available. Invalid tokens are still rejected.
DISABLED All requests allowed unconditionally — no bot detection, no token verification, even if a token is present.

Default is OBSERVE.

Analytics

When enabled, the SDK emits one analytics event per request to the Supertab Connect relay at {analyticsBaseUrl}/ingest/events — defaulting to the dedicated ingest service (https://ingest-connect.supertab.co) — carrying bot-classification signals (user agent, client IP, request metadata, and the verification/enforcement decision). It is off by default — enable it with analyticsEnabled: true:

$connect = new SupertabConnect(
    apiKey: 'stc_live_your_api_key',
    analyticsEnabled: true,
);
  • Fail-open. Emission never throws or alters request handling; errors are swallowed and the relay POST uses a short timeout.
  • Isolated from billing. Analytics goes only to /ingest/events; the billing /events path is untouched.
  • Independent of setBaseUrl. The analytics host defaults to the ingest service and is separate from the API base URL (token/JWKS/verify). Point it at another environment (or localhost) with setAnalyticsBaseUrl(), or per-instance via the analyticsBaseUrl constructor option.

Events are emitted with schema_version: 2 (Capture v2), adding spoof-detection signals read from the request: Sec-Fetch-* and client hints (Sec-CH-UA*), accept, host, cookie presence, and the stripped/sorted header_names set, plus query-shape signals (query_length, query_param_count, query_suspicious). The raw query string is never stored. CDN-only transport signals (TLS version/cipher, JA4, verified-bot category, AS organization, …) are emitted as null at a PHP origin unless injected explicitly via RequestContext's cdnSignals (see below).

Delivery

By default the relay POST is wrapped in DeferredAnalyticsTransport: on FastCGI-style SAPIs (PHP-FPM, LiteSpeed, FrankenPHP) it runs after the response has been flushed to the visitor via fastcgi_finish_request(), so analytics never adds to user-perceived latency. Where that function is unavailable (Apache mod_php, the built-in CLI server), it falls back to a bounded synchronous POST. Either way the PHP worker stays occupied until the POST finishes — this hides latency from the visitor, it does not reduce server-side work.

To route delivery yourself — e.g. onto a job queue so it leaves the request worker entirely — inject an AnalyticsTransportInterface via analyticsTransport. An injected transport is used exactly as given (the SDK does not wrap it). CallbackAnalyticsTransport adapts a closure, and AnalyticsEvent::toArray() / fromArray() serialize and rehydrate an event across a queue boundary. A WordPress plugin using Action Scheduler:

use Supertab\Connect\Analytics\AnalyticsEvent;
use Supertab\Connect\Analytics\CallbackAnalyticsTransport;
use Supertab\Connect\Analytics\HttpAnalyticsTransport;
use Supertab\Connect\Http\HttpClient;

$connect = new SupertabConnect(
    apiKey: $apiKey,
    analyticsEnabled: true,
    // emit() just enqueues — returns immediately, off the visitor request
    analyticsTransport: new CallbackAnalyticsTransport(
        fn (AnalyticsEvent $e) => as_enqueue_async_action('supertab_connect_emit_analytics', [$e->toArray()]),
    ),
);

// The scheduled job runs in a cron/loopback worker; the POST is plain synchronous there.
add_action('supertab_connect_emit_analytics', function (array $payload) use ($apiKey) {
    (new HttpAnalyticsTransport($apiKey, SupertabConnect::getAnalyticsBaseUrl(), new HttpClient))
        ->emit(AnalyticsEvent::fromArray($payload));
});

API Reference

new SupertabConnect()

Creates a singleton instance. Returns the existing instance if one already exists with the same apiKey. Throws if an instance with a different apiKey already exists.

Parameter Type Required Default Description
apiKey string Yes Your Supertab Connect API key (stc_live_... or stc_sandbox_...)
enforcement EnforcementMode No OBSERVE How to handle missing or invalid tokens
debug bool No false Emit debug logs via error_log()
baseUrl ?string No null Set the global default base URL (same as setBaseUrl())
httpClient ?HttpClientInterface No null Inject a custom HTTP client (defaults to built-in cURL client)
botDetector ?BotDetectorInterface No null Inject a custom bot detector (defaults to DefaultBotDetector)
analyticsEnabled bool No false Emit one relay analytics event per request to {analyticsBaseUrl}/ingest/events (see Analytics)
analyticsTransport ?AnalyticsTransportInterface No null Route analytics through a custom delivery path (e.g. a job queue). Used as-is when provided — bypasses the default deferred HTTP transport (see Delivery)
analyticsBaseUrl ?string No null Base URL of the analytics ingest service (resolves to https://ingest-connect.supertab.co by default). Independent of baseUrl/setBaseUrl() (token/JWKS/verify). Also settable globally via setAnalyticsBaseUrl(); the per-instance option wins

handleRequest(?RequestContext $context): HandlerResult

Handles an incoming request end-to-end: extracts the license token from the Authorization header, verifies it, runs bot detection, records a billing event, emits one relay analytics event (when analytics is enabled), and applies the enforcement mode. When a token is present, it is verified (unless DISABLED mode). When no token is present, bot detection determines whether enforcement kicks in — non-bot requests are always allowed. Returns a result object — the caller is responsible for sending HTTP headers and status codes.

Parameter Type Required Default Description
context ?RequestContext No null Request info. Defaults to RequestContext::fromGlobals() which reads from $_SERVER.

Returns: HandlerResultAllowResult (action: ALLOW), BlockResult (action: BLOCK, with status, body, and headers), or RespondResult (action: RESPOND, same shape as BlockResult) for the self-report status endpoint. Emit status/body/headers for any non-ALLOW result.

When integrating with a framework, pass a RequestContext instead of relying on $_SERVER:

use Supertab\Connect\Analytics\CdnRequestSignals;
use Supertab\Connect\Http\RequestContext;

// `headers` must be a flat array<string, string>. Join multi-value headers
// (e.g. `implode(', ', $values)`) before passing them in; non-string values
// are silently dropped.
$headers = [];
foreach ($request->headers->all() as $name => $values) {
    $headers[$name] = is_array($values) ? implode(', ', $values) : (string) $values;
}

$ctx = new RequestContext(
    url: $request->getUri(),
    authorizationHeader: $request->header('Authorization'),
    userAgent: $request->header('User-Agent'),
    accept: $request->header('Accept'),           // used by bot detection
    acceptLanguage: $request->header('Accept-Language'), // used by bot detection
    secChUa: $request->header('Sec-CH-UA'),        // used by bot detection
    headers: $headers,                             // forwarded into event properties (h_* prefix)
    method: $request->method(),                    // analytics
    clientIp: $request->ip(),                      // analytics — your framework's trusted client IP
    // Optionally inject edge signals if your stack provides them (never auto-derived):
    // requestCountry: $request->header('CF-IPCountry'),
    // requestAsn: 13335,
    // tlsFingerprint: $request->header('CF-JA3'),
    // cdnSignals: new CdnRequestSignals(  // Capture-v2 CDN plumbing, if a CDN fronts the origin
    //     tlsVersion: $request->header('CF-Visitor-TLS'),
    //     cdnVerifiedBotCategory: $request->header('CF-Verified-Bot-Category'),
    // ),
);

$result = $connect->handleRequest($ctx);

All entries in headers are forwarded to the analytics event under an h_<lowercased-name> key. Credential and PII headers (authorization, cookie, set-cookie, proxy-authorization, x-api-key, x-amz-security-token, user-agent, x-license-auth, forwarded, x-forwarded-for, x-real-ip, cf-connecting-ip, true-client-ip) are filtered out. RequestContext::fromGlobals() populates headers automatically from $_SERVER.

SupertabConnect::verify() (static)

Pure token verification without creating an instance. Does not apply enforcement mode or set response headers.

Parameter Type Required Default Description
token string Yes Raw JWT token (without the License prefix)
resourceUrl string Yes The URL being accessed
baseUrl ?string No null Per-call override (does not change the global default)
debug bool No false Emit debug logs
httpClient ?HttpClientInterface No null Inject a custom HTTP client

Returns: VerificationResult with valid: bool and error: ?string.

$result = SupertabConnect::verify(
    token: $token,
    resourceUrl: 'https://example.com/article/my-slug',
);

if (! $result->valid) {
    http_response_code(401);
    echo $result->error;
    exit;
}

$connect->verifyAndRecord()

Verifies a license token and records an analytics event. Requires an instance (uses the instance's apiKey for event recording).

Parameter Type Required Default Description
token string Yes Raw JWT token (without the License prefix)
resourceUrl string Yes The URL being accessed
userAgent ?string No null User-Agent string for analytics
requestHeaders ?array<string, string> No null Incoming request headers to forward into event properties under an h_ prefix (credential/PII headers filtered out)

Returns: VerificationResult with valid: bool and error: ?string.

$connect = new SupertabConnect(apiKey: 'stc_live_your_api_key');

$result = $connect->verifyAndRecord(
    token: $token,
    resourceUrl: 'https://example.com/article/my-slug',
    userAgent: $_SERVER['HTTP_USER_AGENT'] ?? null,
    requestHeaders: getallheaders() ?: [],
);

if (! $result->valid) {
    http_response_code(401);
    echo $result->error;
    exit;
}

SupertabConnect::fetchLicenseXml() (static)

Fetches the RSL license XML for a merchant system from the Supertab Connect API.

Parameter Type Required Default Description
merchantSystemUrn string Yes Your merchant system URN (urn:supertab:system:...)
baseUrl ?string No null Per-call override (does not change the global default)
httpClient ?HttpClientInterface No null Inject a custom HTTP client

Returns: string (the raw XML body). Throws SupertabConnectException on failure.

$xml = SupertabConnect::fetchLicenseXml(
    merchantSystemUrn: 'urn:supertab:system:your_system_id',
);

header('Content-Type: application/rsl+xml');
echo $xml;

SupertabConnect::obtainLicenseToken() (static)

Obtains a license token for accessing a protected resource using the OAuth2 client_credentials flow.

Parameter Type Required Default Description
clientId string Yes OAuth2 client ID
clientSecret string Yes OAuth2 client secret
resourceUrl string Yes Full URL of the protected resource
debug bool No false Emit debug logs
httpClient ?HttpClientInterface No null Inject a custom HTTP client

Returns: string (the access token). Throws SupertabConnectException on failure.

The SDK handles the full RSL flow automatically:

  1. Fetches {origin}/license.xml from the resource URL
  2. Parses content blocks and finds the best matching URL pattern (exact > path pattern > wildcard by specificity)
  3. POSTs to the token endpoint using OAuth2 client_credentials
  4. Caches the token in memory (keyed by clientId:resourceUrl, reused until 30s before expiry)

SupertabConnect::setBaseUrl() (static)

Sets the global default base URL for all API requests. Useful for sandbox/testing environments. This affects all subsequent calls (both instance and static methods).

SupertabConnect::setBaseUrl('https://api-connect.sbx.supertab.co');

SupertabConnect::getBaseUrl() (static)

Returns the current global default base URL.

SupertabConnect::setAnalyticsBaseUrl() (static)

Sets the global base URL of the analytics ingest relay (default https://ingest-connect.supertab.co). Independent of setBaseUrl() — token/JWKS/verify traffic is unaffected.

SupertabConnect::setAnalyticsBaseUrl('https://ingest-connect.sbx.supertab.co');

SupertabConnect::getAnalyticsBaseUrl() (static)

Returns the current base URL of the analytics ingest relay.

SupertabConnect::resetInstance() (static)

Clears the singleton instance, allowing a new one to be created with different configuration.

Self-Report Status Endpoint

handleRequest() also serves GET /.well-known/supertab/status, which lets the Supertab backend observe an SDK's live health by comparing configured-vs-confirmed settings. The branch short-circuits at the top of handleRequest() — before token verification, bot detection, and analytics — so a probe never looks like real traffic or emits an event.

A request carrying a valid, backend-minted challenge (Authorization: Bearer <jwt>, an ES256 token with purpose: "status-probe" and aud equal to the request origin) gets the live running config back:

{ "runtime": null, "sdkVersion": "v1.4.0", "component": { "kind": "php-sdk", "version": "v1.4.0" }, "enforcement": "enforce", "eventReporting": true }

Any other request (missing, malformed, expired, or wrong-audience challenge) gets a minimal {"supertab": true} with a 404 status. Both responses set Cache-Control: no-store. Challenge verification reuses the same platform JWKS the SDK already fetches for license tokens — no extra configuration.

Because the endpoint is served through handleRequest(), no extra wiring is needed: the dispatch shown above (emit status/body/headers for any non-ALLOW HandlerResult) already returns the probe response. The result is a RespondResult (action: RESPOND).

Result Types

HandlerResult (returned by handleRequest())

Property Type Description
action HandlerAction ALLOW, BLOCK, or RESPOND
headers array<string, string> Response headers

BlockResult and RespondResult both add status: int and body: string.

use Supertab\Connect\Enum\HandlerAction;

foreach ($result->headers as $name => $value) {
    header("{$name}: {$value}");
}

if ($result->action !== HandlerAction::ALLOW) {
    http_response_code($result->status);
    echo $result->body;
    exit;
}

// AllowResult — serve your content

VerificationResult (returned by verify())

Property Type Description
valid bool Whether the token is valid
error ?string Human-readable reason if invalid

Debug Logging

Pass debug: true to the constructor or static methods to log internal steps via error_log():

[SupertabConnect] Fetching license.xml from https://example.com/license.xml
[SupertabConnect] Found 2 content block(s)
[SupertabConnect] Best match: https://example.com/* (server: https://api-connect.supertab.co)
[SupertabConnect] Token obtained and cached