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'); // host, port, user, password, internalUrl… // 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.
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); $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); } 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);
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, $request->ip()); // sends X-Ghayma-Server-Key + X-Ghayma-Client-IP
clientIp is accepted on the rate-limited entry points: register, login, forgotPassword, 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.
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) |
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 caller's IP with $request->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