PHP 8.2+ Guzzle-based client mirroring the Python SDK surface: - Client::healthz / run / uploadFile / createToken / listTokens / revokeToken - Readonly value objects: RunRequest, RunResult, FileToken, AppToken - Exception hierarchy: ForgeException (abstract) -> ApiException -> AuthException, plus TransportException - camelCase PHP <-> snake_case wire conversion at the boundary - Streamed multipart uploads via fopen($path, 'r') - Injectable GuzzleHttp\ClientInterface (MockHandler-friendly) - HTTP timeout = subprocess timeout + 30s margin - 15 PHPUnit tests, 61 assertions, no live network - README with Laravel + WordPress integration snippets - MIT license, no Sulkta-specific assumptions
81 lines
2.5 KiB
PHP
81 lines
2.5 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Clawdforge;
|
|
|
|
/**
|
|
* A row from `GET /admin/tokens` or the result of `POST /admin/tokens`.
|
|
*
|
|
* `$token` is the plaintext bearer string, returned ONLY at create time.
|
|
* On list responses it is `null` — the server stores only a sha256 hash.
|
|
* Persist the plaintext immediately when minting; you cannot recover it.
|
|
*/
|
|
final readonly class AppToken
|
|
{
|
|
/**
|
|
* @param list<string> $ipCidrs CIDR allowlist for this token
|
|
*/
|
|
public function __construct(
|
|
public string $name,
|
|
public ?string $token = null,
|
|
public array $ipCidrs = [],
|
|
public ?int $createdAt = null,
|
|
public ?int $lastUsed = null,
|
|
public bool $enabled = true,
|
|
) {
|
|
}
|
|
|
|
/**
|
|
* Build from `POST /admin/tokens` response (includes plaintext token).
|
|
*
|
|
* @param array<string, mixed> $payload
|
|
*/
|
|
public static function fromCreateResponse(array $payload): self
|
|
{
|
|
$cidrs = $payload['ip_cidrs'] ?? [];
|
|
return new self(
|
|
name: (string) ($payload['name'] ?? ''),
|
|
token: isset($payload['token']) ? (string) $payload['token'] : null,
|
|
ipCidrs: self::normalizeCidrs($cidrs),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Build from a `GET /admin/tokens` row (no plaintext token).
|
|
*
|
|
* @param array<string, mixed> $row
|
|
*/
|
|
public static function fromListRow(array $row): self
|
|
{
|
|
return new self(
|
|
name: (string) ($row['name'] ?? ''),
|
|
token: null,
|
|
ipCidrs: self::normalizeCidrs($row['ip_cidrs'] ?? ''),
|
|
createdAt: isset($row['created_at']) ? (int) $row['created_at'] : null,
|
|
lastUsed: isset($row['last_used']) && $row['last_used'] !== null ? (int) $row['last_used'] : null,
|
|
enabled: (bool) ($row['enabled'] ?? true),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Server's `list_tokens` returns ip_cidrs as a comma-joined string; the
|
|
* create endpoint returns it as a list. Normalize both into list<string>.
|
|
*
|
|
* @param mixed $raw
|
|
* @return list<string>
|
|
*/
|
|
private static function normalizeCidrs(mixed $raw): array
|
|
{
|
|
if (is_array($raw)) {
|
|
return array_values(array_filter(
|
|
array_map(static fn ($v): string => (string) $v, $raw),
|
|
static fn (string $s): bool => $s !== '',
|
|
));
|
|
}
|
|
if (is_string($raw) && $raw !== '') {
|
|
return array_values(array_filter(explode(',', $raw), static fn (string $s): bool => $s !== ''));
|
|
}
|
|
return [];
|
|
}
|
|
}
|