ttlSeconds = (int)$ttlSeconds; $dir = dirname($dbFile); if (!is_dir($dir)) { if (!mkdir($dir, 0700, true)) { throw new NextTokenException('Could not create token db dir: ' . $dir); } } $this->db = new PDO('sqlite:' . $dbFile); $this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $this->db->exec('PRAGMA journal_mode = WAL'); $this->db->exec('PRAGMA busy_timeout = 5000'); $this->init(); } private function init() { $this->db->exec( 'CREATE TABLE IF NOT EXISTS next_tokens ( token TEXT PRIMARY KEY, created_at INTEGER NOT NULL, expires_at INTEGER NOT NULL )' ); $this->db->exec( 'CREATE INDEX IF NOT EXISTS idx_next_tokens_expires ON next_tokens(expires_at)' ); $this->db->exec( 'CREATE TABLE IF NOT EXISTS next_token_words ( word TEXT NOT NULL, kind TEXT NOT NULL, language TEXT NOT NULL, PRIMARY KEY(word, kind, language) )' ); $this->migrateTokenWordsTable(); $this->db->exec( 'CREATE INDEX IF NOT EXISTS idx_next_token_words_kind ON next_token_words(kind, language)' ); $this->seedWords(); } private function migrateTokenWordsTable() { $stmt = $this->db->query('PRAGMA table_info(next_token_words)'); $pkCols = array(); foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) { if ((int)$row['pk'] > 0) { $pkCols[(int)$row['pk']] = $row['name']; } } ksort($pkCols); if (array_values($pkCols) === array('word', 'kind', 'language')) { return; } $this->db->beginTransaction(); try { $this->db->exec( 'CREATE TABLE next_token_words_new ( word TEXT NOT NULL, kind TEXT NOT NULL, language TEXT NOT NULL, PRIMARY KEY(word, kind, language) )' ); $this->db->exec( 'INSERT OR IGNORE INTO next_token_words_new (word, kind, language) SELECT word, kind, language FROM next_token_words' ); $this->db->exec('DROP TABLE next_token_words'); $this->db->exec('ALTER TABLE next_token_words_new RENAME TO next_token_words'); $this->db->commit(); } catch (Throwable $e) { if ($this->db->inTransaction()) { $this->db->rollBack(); } throw $e; } } public function create($ttlSeconds = null) { $ttl = $ttlSeconds === null ? $this->ttlSeconds : (int)$ttlSeconds; if ($ttl <= 0) { throw new NextTokenException('Invalid ttl.'); } $this->cleanup(); $now = time(); $stmt = $this->db->prepare( 'INSERT INTO next_tokens (token, created_at, expires_at) VALUES (:token, :created_at, :expires_at)' ); for ($i = 0; $i < 25; $i++) { $token = $this->newToken(); try { $stmt->execute(array( ':token' => $token, ':created_at' => $now, ':expires_at' => $now + $ttl, )); return $token; } catch (PDOException $e) { if ($e->getCode() !== '23000') { throw $e; } } } throw new NextTokenException('Could not create unique next token.'); } public function isValid($token) { if (!is_string($token) || !preg_match('/^[a-z0-9-]{5,160}$/', $token)) { return false; } $stmt = $this->db->prepare( 'SELECT expires_at FROM next_tokens WHERE token = :token' ); $stmt->execute(array(':token' => $token)); $row = $stmt->fetch(PDO::FETCH_ASSOC); if (!$row) { return false; } return time() <= (int)$row['expires_at']; } public function requireValid($token) { if (!$this->isValid($token)) { throw new NextTokenException('Invalid or expired next token.'); } return true; } public function cleanup() { $stmt = $this->db->prepare( 'DELETE FROM next_tokens WHERE expires_at < :now' ); $stmt->execute(array(':now' => time())); return $stmt->rowCount(); } public function currentTokens() { $stmt = $this->db->prepare( 'SELECT token, created_at, expires_at FROM next_tokens WHERE expires_at >= :now ORDER BY expires_at ASC, created_at ASC' ); $stmt->execute(array(':now' => time())); return $stmt->fetchAll(PDO::FETCH_ASSOC); } public function tokens() { $stmt = $this->db->query( 'SELECT token, created_at, expires_at FROM next_tokens ORDER BY expires_at ASC, created_at ASC' ); return $stmt->fetchAll(PDO::FETCH_ASSOC); } public function expired_html_response() { html_response(' Expired link

Expired or unauthorized link

This link is no longer valid. Please ask the human user for a new bootstrap link and restart the process from that link.

For the AI agent: do not try to invent a new next value. Only follow links that appear on authorized HTML pages.

', 200); } public function expired_text_response() { text_response( "Expired or unauthorized link.\n" . "Please ask the human user for a new bootstrap link.\n" . "Do not invent a new next value; only follow links from authorized HTML pages.\n", 200 ); } public function expired_response($kind = 'html') { if ($kind === 'html') { $this->expired_html_response(); } else { $this->expired_text_response(); } } public function check_valid_next($kind = 'html') { try { $this->requireValid($_GET['next'] ?? ''); } catch (Throwable $e) { $this->expired_response($kind); } } private function newToken() { $adjective = $this->randomWord('adjective'); $noun = $this->randomWord('noun'); return $adjective . '-' . $noun; } private function randomWord($kind) { $stmt = $this->db->prepare( 'SELECT word FROM next_token_words WHERE kind = :kind ORDER BY RANDOM() LIMIT 1' ); $stmt->execute(array(':kind' => $kind)); $row = $stmt->fetch(PDO::FETCH_ASSOC); if (!$row) { throw new NextTokenException('No token words available for: ' . $kind); } return (string)$row['word']; } private function seedWords() { $wordsFile = __DIR__ . '/config/next-token-words.php'; if (!is_file($wordsFile)) { throw new NextTokenException('Next token word seed file not found.'); } $words = require $wordsFile; if (!is_array($words)) { throw new NextTokenException('Invalid next token word seed file.'); } $stmt = $this->db->prepare( 'INSERT OR IGNORE INTO next_token_words (word, kind, language) VALUES (:word, :kind, :language)' ); foreach ($words as $language => $byKind) { if (!is_array($byKind)) { continue; } foreach ($byKind as $kind => $list) { if ($kind !== 'adjective' && $kind !== 'noun') { continue; } if (!is_array($list)) { continue; } foreach ($list as $word) { $word = trim((string)$word); if (!preg_match('/^[a-z0-9-]+$/', $word)) { continue; } $stmt->execute(array( ':word' => $word, ':kind' => $kind, ':language' => $language, )); } } } } }