clawdforge/clients/php/src/AppToken.php
Kayos 1cff9b89d2 clients/php: initial PHP SDK for clawdforge
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
2026-04-28 22:41:02 -07:00

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 [];
}
}