Search by

ghayma / sdk

bzn.reda

PHP client for the Ghayma platform: admin (project key) and stateless end-user auth (app slug + server key).

v0.4.0 2026-10-09 19:36 UTC

This package is auto-updated.

Last update: 2026-10-09 19:38:35 UTC


README

PHP client for the Ghayma platform. Two entry classes over the two published contracts:

  • Ghayma — the admin client, authenticated with a project API key (gsk_…). Sub-clients ->auth, ->storage, ->databases. Server-side only.
  • GhaymaAuth — the stateless end-user auth client for one app, keyed by an app slug and an optional server key (ghs_…). Registers, signs in and manages an app's end users.

Requires PHP 8.2+.

Install

composer require ghayma/sdk

The SDK talks HTTP through the PSR-18 / PSR-17 interfaces and never hard-depends on a client. It discovers any installed implementation via php-http/discovery. If your app doesn't already ship one, add Guzzle and nyholm:

composer require guzzlehttp/guzzle nyholm/psr7

You can also pass your own client and factories explicitly (see Bring your own HTTP client).

Admin client (Ghayma)

Authenticate with a project API key from Project → Settings → API keys.

use Ghayma\Sdk\Ghayma;

$ghayma = new Ghayma('gsk_your_project_key');

// Storage
$ghayma->storage->uploadObject('bucket-id', 'images/photo.jpg', $bytes, 'image/jpeg');
$file = $ghayma->storage->downloadObject('bucket-id', 'images/photo.jpg');   // ObjectContent
$link = $ghayma->storage->presignDownload('bucket-id', 'images/photo.jpg');  // PresignedUrl

// Databases
$creds = $ghayma->databases->credentials('db-id');  // your site's own: host, port, username, password, internalUrl, level

// Auth apps — administer an app's end users
$page = $ghayma->auth->listUsers('auth-app-id', page: 1, limit: 20);         // UserPage
$ghayma->auth->setAppMetadata('auth-app-id', 'user-id', ['roles' => ['admin']]);

->auth, ->storage and ->databases return typed, immutable DTOs (Ghayma\Sdk\Model\*). See examples/admin.php.

Credentials: your site's own

->databases->credentials() and ->storage->credentials() answer a site's key (the GHAYMA_API_KEY in the site's variables) with that site's own connection credential, the one its runtime already carries in DATABASE_URL / MONGODB_URI or STORAGE_*, at the connection's level (read-only/connect for a database, read/read-write for a bucket); credential is always connection. A database's own login and a bucket's owner key are never handed out.

Answer status / errorCode Exception
The connection has no credential of its own yet; retry in a few minutes 409 no_own_credential GhaymaException
The bucket is still being provisioned 409 credentials_not_ready GhaymaException
A project-wide key has no connection of its own 403 site_key_required ForbiddenException
The site is not connected to that database or bucket 403 not_connected ForbiddenException
An account token instead of a site key: a database's own login and a bucket's owner key are shown to no one 410 shared_credentials_retired GhaymaException

From your laptop, reach a database with ghayma connect --local, and a bucket with ghayma env pull (your site's STORAGE_* variables; the S3 endpoint is public). A system outside Ghayma gets a credential of its own: ghayma access add database <name> --name <principal> (or bucket).

End-user auth client (GhaymaAuth)

GhaymaAuth holds no session state — a PHP request is stateless, so the caller passes the accessToken (or refreshToken) it stored. There is no token storage, no auto-refresh timer.

use Ghayma\Sdk\GhaymaAuth;
use Ghayma\Sdk\Model\LoginSuccess;
use Ghayma\Sdk\Model\TwoFaRequired;
use Ghayma\Sdk\Model\TwoFaEnrollmentRequired;

$auth = new GhaymaAuth(appSlug: 'my-app', serverKey: getenv('GHAYMA_AUTH_SERVER_KEY') ?: null);
$clientIp = $_SERVER['HTTP_X_REAL_IP'] ?? null;

$result = $auth->login('user@example.com', $password, $clientIp);

if ($result instanceof LoginSuccess) {
    $session = $result->session;         // store $session->accessToken / ->refreshToken yourself
} elseif ($result instanceof TwoFaRequired) {
    $session = $auth->verify2fa($result->challengeToken, $code, $clientIp);
} elseif ($result instanceof TwoFaEnrollmentRequired) {
    $enrollment  = $auth->enrollTotp($result->enrollToken);   // render $enrollment->otpauthUri as a QR
    $confirmation = $auth->confirmTotp($code, $result->enrollToken);
}

// Later, on a subsequent request, with the tokens you stored:
$user = $auth->getUser($accessToken);
$pair = $auth->refresh($refreshToken, $clientIp);

login() returns a LoginResult (LoginSuccess | TwoFaRequired | TwoFaEnrollmentRequired); register() returns a RegisterResult (RegisterSuccess | VerificationRequired). Match with instanceof. See examples/auth.php.

Server key and client IP

When you construct GhaymaAuth with a server key, pass the real end-user IP so the auth service applies its rate limits to that user rather than to your server. The key and the IP travel together or not at all (a forwarded IP is only honoured from a caller that proves itself with the key), and the IP is validated as a literal before it is sent:

$auth = new GhaymaAuth(appSlug: 'my-app', serverKey: 'ghs_…');
$auth->login($email, $password, $_SERVER['HTTP_X_REAL_IP'] ?? null);   // sends X-Ghayma-Server-Key + X-Ghayma-Client-IP

On Ghayma, the visitor's IP is in X-Real-IP, which the platform's edge overwrites; if your server is also reachable another way (another host, your own proxy), make sure that path overwrites it too, or don't forward it.

clientIp is accepted on the rate-limited entry points: register, login, verify2fa, refresh, forgotPassword, resetPassword, verifyResetToken, resendVerification, exchangeCode, signInWithIdToken. Never expose the server key to a browser — build GhaymaAuth in server code only.

OAuth

Build a provider sign-in URL for a login page, then redirect the browser to it:

$url = $auth->googleAuthUrl('https://app.example.com/auth/callback');
// PKCE (mobile / SPA): pass the code_challenge; the callback returns a one-time code.
$url = $auth->googleAuthUrl('https://app.example.com/auth/callback', $codeChallenge);

Complete a native or PKCE sign-in server-side:

$session = $auth->exchangeCode($code, $codeVerifier, $clientIp);        // PKCE one-time code
$session = $auth->signInWithIdToken($googleIdToken, $nonce, $clientIp); // native ID token

The redirect_uri must match one of the app's Allowed Origins, and native client IDs must be registered under Native client IDs, both in the console. See the mobile OAuth guide: https://docs.ghayma.cloud/guides/oauth-mobile.

signInWithIdToken() refuses a Google account whose email address Google has not verified: a 403 ForbiddenException with errorCode email_not_verified, through the usual error mapping.

Second factor on OAuth sign-in

When the app's 2FA policy applies to the user, exchangeCode() and signInWithIdToken() create no session and throw a TwoFactorRequiredException. Its result is the same pending step login() returns, finished the same way:

use Ghayma\Sdk\Exception\TwoFactorRequiredException;
use Ghayma\Sdk\Model\TwoFaEnrollmentRequired;
use Ghayma\Sdk\Model\TwoFaRequired;

try {
    $session = $auth->exchangeCode($code, $codeVerifier, $clientIp);
} catch (TwoFactorRequiredException $e) {
    if ($e->result instanceof TwoFaRequired) {
        $session = $auth->verify2fa($e->result->challengeToken, $totpCode, $clientIp);
    } elseif ($e->result instanceof TwoFaEnrollmentRequired) {
        $enrollment   = $auth->enrollTotp($e->result->enrollToken);            // render $enrollment->otpauthUri as a QR
        $confirmation = $auth->confirmTotp($totpCode, $e->result->enrollToken); // ->session, ->recoveryCodes
    }
}

$e->errorCode is two_fa_required or two_fa_enrollment_required, and $e->status is 200. The exception extends GhaymaException, so a broad catch (GhaymaException $e) still stops the sign-in; catch TwoFactorRequiredException first to offer the second factor.

Errors

Every failure is a Ghayma\Sdk\Exception\GhaymaException carrying status, errorCode and (on a 429) retryAfter. Catch the base type broadly, or a subclass to branch:

Exception Raised when
UnauthorizedException 401 — missing, malformed or expired credentials
ForbiddenException 403 — valid credentials, not entitled to the route or action
NotFoundException 404 — no such resource for these credentials
RateLimitedException 429 — too many requests; retryAfter holds the cooldown in seconds when known
InvalidGrantException an expired or already-spent token/code (code: invalid_grant)
InvalidTokenException a token that failed verification (code: invalid_token)
NetworkException a transport failure before any HTTP status (status 0)
TwoFactorRequiredException an OAuth sign-in stopped at the second factor (status 200); result holds the pending step, see Second factor on OAuth sign-in
use Ghayma\Sdk\Exception\RateLimitedException;
use Ghayma\Sdk\Exception\GhaymaException;

try {
    $auth->login($email, $password, $clientIp);
} catch (RateLimitedException $e) {
    // back off for $e->retryAfter seconds
} catch (GhaymaException $e) {
    // $e->status, $e->errorCode, $e->getMessage()
}

Bring your own HTTP client

The constructors accept an optional PSR-18 client and PSR-17 request/stream factories; when omitted, they are discovered:

$ghayma = new Ghayma('gsk_…', 'https://api.ghayma.tech', $psr18Client, $requestFactory, $streamFactory);
$auth   = new GhaymaAuth('my-app', 'https://auth.ghayma.tech', 'ghs_…', $psr18Client, $requestFactory, $streamFactory);

Laravel

No facade or companion package is required — bind the clients in a service provider:

use Ghayma\Sdk\Ghayma;
use Ghayma\Sdk\GhaymaAuth;

public function register(): void
{
    $this->app->singleton(Ghayma::class, fn () => new Ghayma(config('services.ghayma.api_key')));

    $this->app->singleton(GhaymaAuth::class, fn () => new GhaymaAuth(
        appSlug: config('services.ghayma.app_slug'),
        serverKey: config('services.ghayma.server_key'),
    ));
}

Then type-hint Ghayma or GhaymaAuth anywhere the container resolves. Laravel's Guzzle satisfies the PSR-18/17 discovery. Forward the visitor's IP with $request->header('X-Real-IP').

Contracts and conformance

Every wire shape comes from the two vendored OpenAPI contracts in spec/: runtime.v1.yaml (the project-key surface) and auth.v1.yaml (the end-user auth surface). tool/conformance.sh boots a Prism mock of each and runs the SDK against them; tool/sync-spec.sh refreshes the vendored copies from the served documents.

License

MIT © Throct