ghayma / sdk
PHP client for the Ghayma platform: admin (project key) and stateless end-user auth (app slug + server key).
Requires
- php: >=8.2
- php-http/discovery: ^1.19
- psr/http-client: ^1.0
- psr/http-factory: ^1.0
- psr/http-message: ^2.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3
- guzzlehttp/guzzle: ^7
- nyholm/psr7: ^1.8
- phpstan/phpstan: ^2
- phpunit/phpunit: ^11
Suggests
- guzzlehttp/guzzle: A PSR-18 HTTP client, auto-discovered when installed
- nyholm/psr7: PSR-7/17 message and factory implementations, auto-discovered when installed
Provides
None
Conflicts
None
Replaces
None
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