candycore/candy-wish

SSH server middleware framework — port of charmbracelet/wish. Build TUIs that anyone can `ssh user@host` to run, with composable middleware (auth, logging, rate-limiting, ext-ssh2 client wrappers, mount a SugarCraft Program).

Maintainers

Package info

github.com/sugarcraft/candy-wish

Documentation

pkg:composer/candycore/candy-wish

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 1

Open Issues: 0

v0.2.0 2026-05-07 02:01 UTC

This package is not auto-updated.

Last update: 2026-05-08 01:46:22 UTC


README

candy-wish

CandyWish

CI codecov Packagist Version License PHP

PHP port of charmbracelet/wish — an SSH server middleware framework that lets you build TUIs anyone can ssh user@host to run.

composer require sugarcraft/candy-wish

Architecture

CandyWish leans on the host's OpenSSH daemon rather than implementing the SSH wire protocol from scratch. The deployment shape is:

[client] ─ssh─▶ [sshd] ─ForceCommand──▶ [php server.php] ──▶ [middleware stack] ──▶ [SugarCraft Program]

Each connection forks a fresh PHP process under sshd. The PHP entry script builds a Server, registers middleware, calls serve(), and returns when the user disconnects. This trades implementing SSH (key exchange, ciphers, host keys, fail2ban hooks, audit logs) for delegating it to the production-grade implementation already on every server.

Quickstart

1. Configure sshd

Add to /etc/ssh/sshd_config.d/wish.conf:

Match User wishuser
    ForceCommand /usr/bin/php /opt/wish/server.php
    AllowTcpForwarding no
    PermitTTY yes
    X11Forwarding no

Then systemctl reload sshd.

2. Write the entry script

<?php // /opt/wish/server.php
require '/opt/wish/vendor/autoload.php';

use SugarCraft\Wish\Server;
use SugarCraft\Wish\Middleware\Logger;
use SugarCraft\Wish\Middleware\Auth;
use SugarCraft\Wish\Middleware\RateLimit;
use SugarCraft\Wish\Middleware\BubbleTea;

Server::new()
    ->use(new Logger('/var/log/wish.jsonl'))
    ->use(new RateLimit('/var/lib/wish/buckets.json', burst: 5, ratePerSec: 0.5))
    ->use(new Auth(users: ['alice', 'bob']))
    ->use(new BubbleTea(fn($session) => new MyApp($session)))
    ->serve();

3. Connect

ssh wishuser@your-host

Middleware

Middleware Purpose
Logger One-line JSON event at session start + end, with elapsed time and connection meta.
Auth Username allowlist, public-key fingerprint allowlist (or both).
RateLimit Per-IP token-bucket persisted to a JSON state file with flock(LOCK_EX).
BubbleTea Terminal middleware. Mounts a SugarCraft Program for the connected user.

You can write your own — implement SugarCraft\Wish\Middleware:

final class HelloBanner implements Middleware
{
    public function handle(Session $s, callable $next): void
    {
        echo "Welcome, {$s->user}!\n";
        $next($s);
    }
}

Session metadata

Session::fromEnvironment() reads the standard sshd-supplied environment:

$s->user;        // 'alice'
$s->clientHost;  // '203.0.113.7'
$s->clientPort;  // 54321
$s->term;        // 'xterm-256color'
$s->cols;        // 120
$s->rows;        // 40
$s->tty;         // '/dev/pts/3'   (null when non-interactive)
$s->command;     // SSH_ORIGINAL_COMMAND if set
$s->isInteractive();
$s->toLogContext();

ext-ssh2

The PECL ssh2 extension is optional and used only if you want a middleware that opens outbound SSH connections from inside the session (e.g. SFTP file pickers, remote-control agents). Standard server-side use does not require it.

Status

Phase 9+ — first cut. Five middleware classes, 19 tests / 65 assertions, ready for v0 deployment.

See examples/hello-server.php for a runnable banner-only stack you can ForceCommand against.