Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adopt standard Seam SDK patterns #289

Closed
wants to merge 13 commits into from
2 changes: 1 addition & 1 deletion .gitattributes
Original file line number Diff line number Diff line change
@@ -1 +1 @@
src/Objects/**/*.php linguist-generated
src/Routes/**/*.php linguist-generated
4 changes: 2 additions & 2 deletions .github/workflows/generate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ name: Generate

on:
push:
branches-ignore:
- main
branches:
- 'no-branch-will-match-this-pattern'
workflow_dispatch: {}

jobs:
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ Check out [the documentation](https://docs.seam.co) or the usage below.
## Usage

```php
$seam = new Seam\SeamClient("YOUR_API_KEY");
use Seam\Seam;

$seam = new Seam("YOUR_API_KEY");

# Create a Connect Webview to login to a provider
$connect_webview = $seam->connect_webviews->create(
Expand Down
197 changes: 197 additions & 0 deletions src/Auth.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
<?php

namespace Seam;

class SeamInvalidTokenError extends \Exception
{
public function __construct(string $message)
{
parent::__construct("Seam received an invalid token: {$message}");
}
}

class Auth
{
const TOKEN_PREFIX = "seam_";
const ACCESS_TOKEN_PREFIX = "seam_at";
const JWT_PREFIX = "ey";
const CLIENT_SESSION_TOKEN_PREFIX = "seam_cst";
const PUBLISHABLE_KEY_TOKEN_PREFIX = "seam_pk";

public static function getAuthHeaders(
?string $api_key = null,
?string $personal_access_token = null,
?string $workspace_id = null
): array {
if (
Options::isSeamOptionsWithApiKey($api_key, $personal_access_token)
) {
return self::getAuthHeadersForApiKey($api_key);
}

if (
Options::isSeamOptionsWithPersonalAccessToken(
$personal_access_token,
$api_key,
$workspace_id
)
) {
return self::getAuthHeadersForPersonalAccessToken(
$personal_access_token,
$workspace_id
);
}

throw new SeamInvalidOptionsError(
"Must specify an api_key or personal_access_token. " .
"Attempted reading configuration from the environment, " .
"but the environment variable SEAM_API_KEY is not set."
);
}

public static function getAuthHeadersForApiKey(string $api_key): array
{
if (self::isClientSessionToken($api_key)) {
throw new SeamInvalidTokenError(
"A Client Session Token cannot be used as an api_key"
);
}

if (self::isJwt($api_key)) {
throw new SeamInvalidTokenError(
"A JWT cannot be used as an api_key"
);
}

if (self::isAccessToken($api_key)) {
throw new SeamInvalidTokenError(
"An Access Token cannot be used as an api_key"
);
}

if (self::isPublishableKey($api_key)) {
throw new SeamInvalidTokenError(
"A Publishable Key cannot be used as an api_key"
);
}

if (!self::isSeamToken($api_key)) {
throw new SeamInvalidTokenError(
"Unknown or invalid api_key format, expected token to start with " .
self::TOKEN_PREFIX
);
}

return ["Authorization" => "Bearer {$api_key}"];
}

public static function getAuthHeadersForPersonalAccessToken(
string $personal_access_token,
string $workspace_id
): array {
if (self::isJwt($personal_access_token)) {
throw new SeamInvalidTokenError(
"A JWT cannot be used as a personal_access_token"
);
}

if (self::isClientSessionToken($personal_access_token)) {
throw new SeamInvalidTokenError(
"A Client Session Token cannot be used as a personal_access_token"
);
}

if (self::isPublishableKey($personal_access_token)) {
throw new SeamInvalidTokenError(
"A Publishable Key cannot be used as a personal_access_token"
);
}

if (!self::isAccessToken($personal_access_token)) {
throw new SeamInvalidTokenError(
"Unknown or invalid personal_access_token format, expected token to start with " .
self::ACCESS_TOKEN_PREFIX
);
}

return [
"Authorization" => "Bearer {$personal_access_token}",
"Seam-Workspace-Id" => $workspace_id,
];
}

public static function getAuthHeadersForMultiWorkspacePersonalAccessToken(
string $personal_access_token
): array {
if (self::isJwt($personal_access_token)) {
throw new SeamInvalidTokenError(
"A JWT cannot be used as a personal_access_token"
);
}

if (self::isClientSessionToken($personal_access_token)) {
throw new SeamInvalidTokenError(
"A Client Session Token cannot be used as a personal_access_token"
);
}

if (self::isPublishableKey($personal_access_token)) {
throw new SeamInvalidTokenError(
"A Publishable Key cannot be used as a personal_access_token"
);
}

if (!self::isAccessToken($personal_access_token)) {
throw new SeamInvalidTokenError(
"Unknown or invalid personal_access_token format, expected token to start with " .
self::ACCESS_TOKEN_PREFIX
);
}

return ["Authorization" => "Bearer {$personal_access_token}"];
}

public static function isAccessToken(string $token): bool
{
return str_starts_with($token, self::ACCESS_TOKEN_PREFIX);
}

public static function isJwt(string $token): bool
{
return str_starts_with($token, self::JWT_PREFIX);
}

public static function isSeamToken(string $token): bool
{
return str_starts_with($token, self::TOKEN_PREFIX);
}

public static function isApiKey(string $token): bool
{
return !self::isClientSessionToken($token) &&
!self::isJwt($token) &&
!self::isAccessToken($token) &&
!self::isPublishableKey($token) &&
self::isSeamToken($token);
}

public static function isClientSessionToken(string $token): bool
{
return str_starts_with($token, self::CLIENT_SESSION_TOKEN_PREFIX);
}

public static function isPublishableKey(string $token): bool
{
return str_starts_with($token, self::PUBLISHABLE_KEY_TOKEN_PREFIX);
}

public static function isConsoleSessionToken(string $token): bool
{
return self::isJwt($token);
}

public static function isPersonalAccessToken(string $token): bool
{
return self::isAccessToken($token);
}
}
9 changes: 9 additions & 0 deletions src/Config.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

namespace Seam;

class Config
{
const LTS_VERSION = "1.0.0";
const DEFAULT_ENDPOINT = "https://connect.getseam.com";
}
2 changes: 1 addition & 1 deletion src/Exceptions/ActionAttemptError.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace Seam;

use Seam\Objects\ActionAttempt;
use Seam\Routes\Objects\ActionAttempt;

class ActionAttemptError extends \Exception
{
Expand Down
2 changes: 1 addition & 1 deletion src/Exceptions/ActionAttemptFailedError.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace Seam;

use Seam\Objects\ActionAttempt;
use Seam\Routes\Objects\ActionAttempt;

class ActionAttemptFailedError extends ActionAttemptError
{
Expand Down
2 changes: 1 addition & 1 deletion src/Exceptions/ActionAttemptTimeoutError.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace Seam;

use Seam\Objects\ActionAttempt;
use Seam\Routes\Objects\ActionAttempt;

class ActionAttemptTimeoutError extends ActionAttemptError
{
Expand Down
110 changes: 110 additions & 0 deletions src/Http.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
<?php

namespace Seam;

use GuzzleHttp\Client as GuzzleHttpClient;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Exception\RequestException;
use Psr\Http\Message\ResponseInterface;
use Seam\HttpApiError;
use Seam\HttpUnauthorizedError;
use Seam\HttpInvalidInputError;

class Http
{
public static function createClient(array $config): GuzzleHttpClient
{
$baseUrl = $config["base_url"];
$headers = $config["headers"] ?? [];
$timeout = $config["timeout"] ?? 60.0;

$handlerStack = HandlerStack::create();
$handlerStack->push(self::createErrorHandlingMiddleware());

$clientOptions = [
"base_uri" => $baseUrl,
"timeout" => $timeout,
"headers" => $headers,
"handler" => $handlerStack,
"http_errors" => false,
];

$guzzleOptions = $config["guzzle_options"] ?? [];
$clientOptions = array_merge($clientOptions, $guzzleOptions);

return new GuzzleHttpClient($clientOptions);
}

public static function createErrorHandlingMiddleware(): callable
{
return function (callable $nextHandler) {
return function ($request, array $options) use ($nextHandler) {
return $nextHandler($request, $options)->then(function (
ResponseInterface $response
) use ($request) {
$statusCode = $response->getStatusCode();
$requestId = trim(
$response->getHeaderLine("seam-request-id")
);
$body = (string) $response->getBody();
$decodedBody = json_decode($body);

if ($statusCode === 401) {
throw new HttpUnauthorizedError($requestId);
}

if (!self::isApiErrorResponse($response, $decodedBody)) {
if ($statusCode >= 400) {
throw RequestException::create($request, $response);
}

return $response;
}

$errorData = $decodedBody->error;
if (
isset($errorData->type) &&
$errorData->type === "invalid_input"
) {
throw new HttpInvalidInputError(
$errorData,
$statusCode,
$requestId
);
}

throw new HttpApiError($errorData, $statusCode, $requestId);
});
};
};
}

public static function isApiErrorResponse(
ResponseInterface $response,
$decodedBody
): bool {
$contentType = $response->getHeaderLine("Content-Type");
if (stripos($contentType, "application/json") !== 0) {
return false;
}

if (!is_object($decodedBody) || !isset($decodedBody->error)) {
return false;
}

$error = $decodedBody->error;
if (!is_object($error)) {
return false;
}

if (!isset($error->type) || !is_string($error->type)) {
return false;
}

if (!isset($error->message) || !is_string($error->message)) {
return false;
}

return true;
}
}
Loading
Loading