Vol. XV / Issue 01

The McKinnie Dispatch

Filed from the experiment desk

Experiment Fork story Trust model

A distribution problem

You should not have to run an executable to get a poster.

SteamPanno turns a Steam library into a visual poster. The original ships as a downloadable binary. A Reddit thread found the obvious fault line. This fork was about working out what the safer alternative actually required.

SteamPanno turns a Steam library into a visual poster. The original ships as a binary. The Reddit thread found the problem in the first reply.

Not because the developer was malicious. The project is open source, the intent looks honest, and the poster idea is genuinely good. But "you can audit the source if you want" is not a real answer for a one-time creative tool. Most users are not going to build from source, reason through Steam API access, and run a personal risk calculation to get a poster. They are going to look at the download page and say no.

The problem was not the code. It was the distribution shape. A binary that touches a Steam identity, even lightly, is the wrong packaging for something that is supposed to be fun and low-stakes.

The use case is: give me a visual of my library. The trust ask is: run this executable from the internet. Those two things are not matched.

I forked it not to compete with the original. The idea was already done. The question I wanted to work through was practical: what does moving this to a server actually require, and what does that change about the trust model?

The obvious first answer is a static site. That stops working the moment you try to sign in.

Steam's sign-in is OpenID 2.0. The flow sends the user to Steam's login page and back to a callback URL you control. That callback has to post back to Steam to verify the assertion, extract the SteamID, and issue a session. GitHub Pages cannot receive that return trip. The verification logic has nowhere to run. The moment you need a real OpenID callback, you need a real server.

Exhibit A: The callback requirement Building the login redirect is the easy part. The return trip, Steam posting back to your callback URL and your server verifying the assertion against Steam's endpoint, is why this is not a static-site problem.
export function steamLoginUrl(returnTo: string, realm: string): string {
  const p = new URLSearchParams({
    "openid.ns": "http://specs.openid.net/auth/2.0",
    "openid.mode": "checkid_setup",
    "openid.return_to": returnTo,
    "openid.realm": realm,
    "openid.identity": "http://specs.openid.net/auth/2.0/identifier_select",
    "openid.claimed_id": "http://specs.openid.net/auth/2.0/identifier_select"
  });

  return `https://steamcommunity.com/openid/login?${p.toString()}`;
}

Once you have a real server and a working callback, the session is product surface, not just implementation glue. The user just handed you their Steam identity. The session cookie needs httpOnly and secure flags or it is not doing its job. httpOnly keeps the token off JavaScript. secure keeps it on HTTPS. sameSite narrows cross-site cookie behavior. None of those are optional when the user has just authenticated.

Exhibit B: Session as product surface If the user signs in, the cookie flags are not optional. These three are the difference between server-side auth and just relocating the same problem to a different layer.
const token = await makeSession(steamid);
const isProd = process.env.NODE_ENV === "production";

res.cookie("session", token, {
  httpOnly: true,
  sameSite: "lax",
  secure: isProd,
  maxAge: 7 * 24 * 60 * 60 * 1000
});

The Steam API key cannot live in client code or a committed config file. The session secret has the same requirement. Render handles both without you committing either one: SESSION_SECRET gets generateValue: true so the host creates it on first deploy, and STEAM_API_KEY gets sync: false so you set it once in the dashboard. Neither ends up in version control.

Exhibit C: Secrets out of version control The host generates the session key on first deploy. You set the API key once in the dashboard. Version control sees neither.
services:
  - type: web
    name: steampanno-web
    env: docker
    dockerfilePath: webapp/Dockerfile
    envVars:
      - key: SESSION_SECRET
        generateValue: true
      - key: STEAM_API_KEY
        sync: false

GitHub Actions runs typecheck, builds the Docker image on every push to main, and pushes to GHCR. Not comprehensive coverage, but it keeps the container buildable and the TypeScript honest.

The stopping point was Steam developer/API identity setup. Getting the hosted path clean meant registering and configuring the API key like a real product dependency, not treating it like a throwaway local secret. For a side experiment whose point I had already worked out, it was enough friction to stop. The fork showed what the web path required. I did not need it to ship to know what it demonstrated.

Moving a tool from binary to browser does not just change the deployment shape. It changes the trust question the user is being asked. Instead of "will I run this executable from a stranger," the question becomes "what does this site need from my Steam account, and what does it actually keep?" That is a better-shaped question. Not risk-free, but answerable. The poster idea was fine from the start.

The distribution model was the product decision that needed to change.