exec('rm -rf ' . escapeshellarg($dir))); $now = 1_000_000; $tokens = new AgentTokens("{$dir}/agent-tokens", "{$dir}/agent-tickets", static function () use (&$now): int { return $now; }); // --- Tokens ---------------------------------------------------------------------------------------- $made = $tokens->create('alice', ' Claude Code ', 30); check( 'a token is made with its name trimmed, an expiry in days, and the token itself in the answer', preg_match('/^fwa_[0-9a-f]{64}$/', $made['token']) === 1 && $made['name'] === 'Claude Code' && $made['expiresAt'] === $now + 30 * 86400 && $made['usedAt'] === null, $made, ); $files = glob("{$dir}/agent-tokens/*.json"); $stored = implode('', array_map('file_get_contents', $files)); check( 'on disk it is one file under the hash of the token, 0640, and the token is nowhere in it', count($files) === 1 && basename($files[0]) === hash('sha256', $made['token']) . '.json' && (fileperms($files[0]) & 0777) === 0640 && !str_contains($stored, substr($made['token'], 4)), [$files, $stored], ); $listed = $tokens->listOf('Alice'); check( 'its owner lists it, under any spelling of the name, without the token', count($listed) === 1 && $listed[0]['id'] === $made['id'] && !isset($listed[0]['token']) && !isset($listed[0]['username']), $listed, ); check('another account lists nothing', $tokens->listOf('bob') === []); check('the token names its account', $tokens->verify($made['token']) === 'alice'); check('and its use is written down', $tokens->listOf('alice')[0]['usedAt'] === $now); $now += 30; $tokens->verify($made['token']); check('but not again within the minute', $tokens->listOf('alice')[0]['usedAt'] === $now - 30); $now += 60; $tokens->verify($made['token']); check('after it, again', $tokens->listOf('alice')[0]['usedAt'] === $now); $wrong = 'fwa_' . str_repeat('0', 64); check('an unknown token names nobody', $tokens->verify($wrong) === null); check('nor does one of the wrong shape', $tokens->verify(substr($made['token'], 4)) === null && $tokens->verify($made['token'] . '0') === null); check('nor a path', $tokens->verify('fwa_../../users') === null); check('another account cannot revoke it', $tokens->revoke('bob', $made['id']) === false && $tokens->verify($made['token']) === 'alice'); check('its owner can', $tokens->revoke('alice', $made['id']) === true); check('and then it names nobody', $tokens->verify($made['token']) === null && $tokens->listOf('alice') === []); check('revoking it twice finds nothing', $tokens->revoke('alice', $made['id']) === false); $short = $tokens->create('alice', 'one day', 1); $now += 86400 - 1; check('a token works until the second before it expires', $tokens->verify($short['token']) === 'alice'); $now += 1; check('then it names nobody', $tokens->verify($short['token']) === null); check('and is gone from the list and the disk', $tokens->listOf('alice') === [] && glob("{$dir}/agent-tokens/*.json") === []); $long = $tokens->create('alice', 'long', 10_000); check('a token lasts at most a year', $long['expiresAt'] === $now + AgentTokens::MAX_DAYS * 86400, $long); $tokens->create('bob', 'bob', 5); check('ending an account’s tokens ends its own alone', $tokens->endAllOf('ALICE') === 1 && count($tokens->listOf('bob')) === 1); for ($i = 0; $i < AgentTokens::MAX_PER_ACCOUNT; $i++) { $tokens->create('carol', "t{$i}", 5); } $refused = null; try { $tokens->create('carol', 'one too many', 5); } catch (TooManyAgentTokensException $e) { $refused = $e->getMessage(); } check('an account has at most ' . AgentTokens::MAX_PER_ACCOUNT . ' tokens', $refused !== null && count($tokens->listOf('carol')) === AgentTokens::MAX_PER_ACCOUNT, $refused); check('a name is 1–60 characters', AgentTokens::isValidName('x') && !AgentTokens::isValidName(' ') && !AgentTokens::isValidName(str_repeat('x', 61))); // --- Tickets --------------------------------------------------------------------------------------- $ticket = $tokens->issueTicket('alice'); $ticketFiles = glob("{$dir}/agent-tickets/*.json"); check( 'a ticket is kept under its hash, 0640', preg_match('/^[0-9a-f]{64}$/', $ticket) === 1 && count($ticketFiles) === 1 && basename($ticketFiles[0]) === hash('sha256', $ticket) . '.json' && (fileperms($ticketFiles[0]) & 0777) === 0640, $ticketFiles, ); check('redeemed, it names its account', $tokens->redeemTicket($ticket) === 'alice'); check('a second time, nobody', $tokens->redeemTicket($ticket) === null); check('and nothing of it is left', glob("{$dir}/agent-tickets/*") === []); $late = $tokens->issueTicket('alice'); $now += AgentTokens::TICKET_SECONDS + 1; check('a ticket older than a minute names nobody', $tokens->redeemTicket($late) === null); check('an agent token is no ticket', $tokens->redeemTicket($long['token']) === null); $stale = $tokens->issueTicket('bob'); $now += AgentTokens::TICKET_SECONDS + 1; touch("{$dir}/agent-tickets/" . hash('sha256', $stale) . '.json', $now - AgentTokens::TICKET_SECONDS - 1); $tokens->issueTicket('bob'); check('issuing a ticket sweeps the ones nobody redeemed', count(glob("{$dir}/agent-tickets/*.json")) === 1); echo $failed === 0 ? "ALL PASSED – {$passed} checks\n" : "{$failed} FAILED, {$passed} passed\n"; exit($failed === 0 ? 0 : 1);