Skip to content

feat: humanish MVP — two-tier credential broker (broker + box + google.whoami) - #1

Open
thedotmack wants to merge 13 commits into
mainfrom
feat/humanish-mvp-server
Open

feat: humanish MVP — two-tier credential broker (broker + box + google.whoami)#1
thedotmack wants to merge 13 commits into
mainfrom
feat/humanish-mvp-server

Conversation

@thedotmack

@thedotmack thedotmack commented May 18, 2026

Copy link
Copy Markdown
Owner

Summary

humanish MVP — two-tier credential broker. Code complete and live in production. All 8 phases of plans/2026-05-17-humanish-server.md + post-MVP hardening (tests, CI, dashboard polish, security review, deploy adjustments).

Deployed

Both originally provisioned during this branch:

  • KV namespace humanish-broker.KV (id 1d108315895e4a4896a6bbe040cbdc84)
  • Fly app humanish-box, 3 GB volume humanish_data, shared IPv4 + dedicated IPv6, sjc region
  • Secrets set: HUMANISH_MASTER_KEY + BOX_SHARED_SECRET identical on both Worker and Fly; RESEND_API_KEY is a re_PLACEHOLDER_ value — broker logs magic links to wrangler tail until operator wires a real Resend key

End-to-end verified live

Check Result
GET / landing 200, 2,979 bytes
POST /api/auth/request allowlisted email 200 + magic link logged to wrangler tail
POST /api/auth/request non-allowlisted 403 not_allowed
GET /api/auth/verify?token=… 302 → /dashboard, humanish_session cookie set HttpOnly+Secure+SameSite=Lax
GET /api/me (with session) 200 {email:thedotmack@gmail.com}
GET /api/profiles (proxied to box) 200 {profiles:[]}
POST /api/mint 200 {slug,url}
GET /:slug (first visit) 200 with skill markdown, all placeholders substituted, single unique bearer
GET /:slug (second visit) 410 dead-slug page
POST /api/actions/google-personal/google.whoami (no profile saved yet) 404 profile_not_found (box correctly rejects)
Replay same bearer 401 expired
Bearer for shape {} called with {"extra":"field"} 400 payload_shape_mismatch
Box /healthz no auth 200 {ok:true,ts:…}
Box /profiles no auth 401 unauthorized
Box /profiles with BOX_SHARED_SECRET 200 {profiles:[]}

Test + verification gates passed

  • pnpm -r typecheck — broker + box clean
  • pnpm check:allowlist — broker ACTION_ALLOWLIST and box ACTION_REGISTRY in sync
  • pnpm -r test35 vitest tests passing (9 crypto AES-GCM round-trip / AAD tampering / IV uniqueness, 17 bearer scope + payload_shape_hash, 9 SlugBox single-use)
  • pnpm audit --prod — no known vulnerabilities
  • wrangler deploy --dry-run — bundle 162.51 KiB / 43.72 KiB gzip
  • docker build -f packages/box/Dockerfile . — builds clean
  • OWASP top-10 review (A01–A10) — PASS on every category, zero findings
  • All 8 anti-pattern grep guards from plan §7 — zero violations (slug-box atomicity, no secrets in pages/templates, no payload logging, no allow-top-navigation, slug page has no fetch(), AAD on both encrypt + decrypt, no child_process.exec, action runner bind validated)
  • GitHub Actions CI workflow added (typecheck + test + broker dry-run + box docker build, concurrency cancels in-flight)

What's left for live use by the operator

  1. Sign up for Resend, verify a sending domain, run wrangler secret put RESEND_API_KEY with a real key. Until then, magic links are logged to wrangler tail instead of emailed.
  2. Open the dashboard, walk through the Add profile flow to drop a real Google storageState into /data/profiles/google-personal.bin.
  3. Mint a slug, paste into a Claude chat, ask "which Google account am I signed in as?" — the full §7 8-step E2E. The previous live test got everything to the box, so once a profile exists the action should return {email, checked_at} in ~10s.

Known caveats (documented in README)

  • Neko v2 legacy mode is active. The upstream ghcr.io/m1k1o/neko/chromium image bakes NEKO_MEMBER_MULTIUSER_USER_PASS (v2) at build time, which silently disables our chat/filetransfer plugin-disable config. For single-user MVP this is not a security defect (dashboard sandboxed iframe contains the threat) — needs an image fork to fully fix.
  • Action runner exposed publicly (not Flycast-only as the plan originally said). Workers can't reach *.flycast; the real defense is BOX_SHARED_SECRET (constant-time compare) + TLS termination at fly-proxy. Bearer + TLS = same threat model as the plan's intent.
  • RESEND_API_KEY placeholder triggers a magic-link bypass that logs via wrangler tail. Replace before sharing the broker URL.

Commits

11 commits, each phase atomically reviewable:

  • 5937eb8 scaffold
  • e4e84a0 broker security core (auth/SlugBox/bearer/routes)
  • 54a5169 box (Dockerfile/Fly/crypto/runner)
  • c5f1469 broker templates wired via wrangler Text bundles
  • c1d00b9 dashboard SPA
  • f329710 google.whoami action
  • ed185e3 Phase 8 hardening (mint rate limit / audit rotation / allowlist guard / README)
  • 0fd16b2 plan docs
  • 0ee87bb tests + CI
  • 9a6c3af production deploy + dashboard polish + skill template replaceAll bug
  • 8899da1 neko v3 env fallback + README updated for actual deployed state

🤖 Generated with Claude Code

thedotmack and others added 13 commits May 17, 2026 15:52
Maps byoky's credential broker architecture (encrypted vault, scoped gifts,
WebSocket relay, native-messaging bridge, custom-fetch SDK) onto humanish's
cookie-jar plan. Identifies crypto.ts, proxy-utils.ts, and gift.ts as the
highest-leverage modules to lift for the MVP, with the fork-and-extend path
recommended for the multi-tenant cloud version.

Also gitignores .scratch/ so cloned upstream repos don't drift into commits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 1 of plans/2026-05-17-humanish-server.md. Adds:
- pnpm workspace root with broker + box/action-runner
- broker: Cloudflare Worker stub with wrangler.toml, SlugBox DO stub, env types
- box: Fastify action-runner stub, Dockerfile + fly.toml placeholders

Typecheck passes; no source outside packages/.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Phase 2 of plans/2026-05-17-humanish-server.md.

- SlugBox Durable Object: atomic get-then-delete for single-use slugs
  (no intervening await — CI grep guard enforces this)
- Resend magic-link auth + sha256-hashed session cookies (30 day TTL)
- Bearer issuance scoped to (profile, action_id, payload_shape_hash, ttl);
  payloadShapeHash hashes sorted-key shape + leaf type names, not values
- Worker routes: /, /dashboard, /api/auth/{request,verify,logout},
  /api/me, /api/profiles*, /api/mint, /:slug, /api/actions/:p/:a
- EFF Long wordlist: 7776 words, 7772 hyphen-free for slugs
- CSP-locked slug page (connect-src 'none', no fetch escape hatch)

Phase 5 templates remain inline placeholders in worker.ts pending Phase 5 wiring.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Phase 3 of plans/2026-05-17-humanish-server.md.

- Dockerfile: neko/chromium base + Node 20 + action runner
- fly.toml: Flycast-only :7654 (action runner, no public handlers),
  public :8080→:443 (neko WebRTC), 2gb shared-cpu-2x, mounted /data
- neko.yaml: single-user multiuser provider, chat+filetransfer disabled,
  webcam/mic disabled (operator must verify env-var interpolation syntax)
- AES-GCM crypto: [12B IV][ct+tag] with AAD = profile name (defends
  against filename swap)
- profile-store: atomic write via .tmp + rename, 0o600, name regex
  ^[a-z][a-z0-9-]{0,63}$
- audit log: tab-separated, scrubbed metadata only (no payload)
- Fastify server: bound [::1]:7654 (NEVER 0.0.0.0/[::]), constant-time
  shared-secret bearer check, scrubbed error messages
- google.whoami: PHASE_6_PLACEHOLDER throw

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… rules

Phase 5 of plans/2026-05-17-humanish-server.md.

- pages/slug.html.template: minimal styled page with copy-to-clipboard,
  no fetch() escape hatch (CI grep guard enforces)
- pages/dead-slug.html: 410-page for already-consumed slugs
- pages/landing.html: sign-in form posting to /api/auth/request
- skill-templates/google.whoami.md.template: full skill markdown with
  curl example + failure mode table (5 placeholders for substitution)
- wrangler.toml: three [[rules]] Text bundles, most-specific first
- env.d.ts: module declarations for *.html / *.template / *.md.template
- worker.ts: replaced PHASE_5_PLACEHOLDER inline strings with imports

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Phase 4 of plans/2026-05-17-humanish-server.md.

- ~25 KB vanilla JS, no framework, no build step
- Header: brand + signed-in email + logout
- Profiles section: list with relative mtime, Reauth + Delete (with confirm)
- "+ Log in to a new service": sandboxed neko iframe
  (allow-same-origin allow-scripts allow-forms ONLY — never
  allow-top-navigation; CI grep guard enforces)
- Save profile button in dashboard chrome ABOVE iframe (iframe is
  hostile content boundary)
- Mint section: profile + action + TTL slider; on success
  navigator.clipboard.writeText(url) + paste prompt
- All DOM construction via textContent / createElement (no innerHTML
  interpolation)
- No third-party scripts, no analytics

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Phase 6 of plans/2026-05-17-humanish-server.md.

- chromium.launch headless with Docker-safe args (--no-sandbox,
  --disable-dev-shm-usage); never connectOverCDP from actions
- Three-tier email extraction: meta[og:email] property, meta[og:email] name,
  a[aria-label*="@"] regex parse
- /ServiceLogin or /signin URL → ActionAuthExpiredError (server → 503)
- try/finally browser.close() guarantees no leaked Chromium
- 30s budget via Promise.race timeout; input + output validated by zod schemas
- storageState parsed in-memory, never written to disk

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…guard, README

- broker: 30 mints/hour per session sliding window in KV
- broker: expired-profile marker (KV) set on box 503; profiles list decorates
  with status:"expired"; new POST /api/profiles/:name/clear-expired
- box: audit log daily rotation (audit-YYYY-MM-DD.log) + 30-day prune
- scripts/check-action-allowlist.mjs: parses broker ACTION_ALLOWLIST + box
  action ids, fails CI on divergence; wired into broker typecheck script
- README: full security-model docs + setup runbook + operating notes

Deferred to follow-up:
- Dashboard yellow banner for expired profiles (broker API is in place)
- GET /api/audit dashboard view
- DO-based bearer state (KV consistency window is acceptable for MVP single-user)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The plan that drove this PR. Lives in plans/ so future contributors can
read the original architectural reasoning, anti-pattern guards, and risk
register alongside the implementation.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- packages/broker: vitest with in-memory KV + DO stubs
  (bearer.test: 17 tests, slug-box.test: 9 tests for atomic single-use)
- packages/box/action-runner: vitest for crypto round-trip,
  AAD tampering, ciphertext tampering, IV uniqueness, edge sizes
  (9 tests)
- root package.json: pnpm -r test wires both packages
- .github/workflows/ci.yml: typecheck, test, broker dry-run wrangler
  deploy, box docker build. Concurrency cancels in-flight runs.

@cloudflare/vitest-pool-workers requires vitest 4.x which would
force the whole workspace; using in-memory mocks per the plan's
fallback option (documented in test file headers).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Production wiring (deployed to humanish-broker.workers.dev + humanish-box.fly.dev):

- broker/wrangler.toml: real KV id, BOX_ORIGIN points to public Fly URL
  (Cloudflare Workers can't reach Fly's private *.flycast hostnames),
  COOKIE_DOMAIN matches the deployed workers.dev subdomain
- box/fly.toml: 8443 (public TLS) → 7654 internal action runner;
  defense is BOX_SHARED_SECRET bearer (not network isolation)
- box/Dockerfile: drop the entrypoint override that was bypassing
  Xorg init and panicking neko; install /etc/neko/supervisord/
  action-runner.conf so the runner starts alongside neko under the
  upstream image's supervisord
- box/action-runner.conf: priority 900 (after neko at 800) so CDP
  is bound by the time the runner attaches
- box/env.ts: LISTEN_HOST default "0.0.0.0" (was "::1" / "::");
  fly-proxy reaches the container via IPv4
- broker/auth.ts: detect placeholder RESEND_API_KEY and log the
  magic URL to `wrangler tail` so first sign-in works before the
  operator wires Resend (warning makes it loud)

Dashboard polish:
- pages/dashboard.html: yellow banner for expired profiles,
  Reauth wired to POST /api/profiles/:name/clear-expired before
  start-login {restore:true}; "Recent activity" section reads
  /api/audit (last 50 entries, no request_id)
- worker.ts: GET /api/audit proxies box GET /audit, parses TSV
- box/server.ts: GET /audit returns last 100 audit lines

Bug:
- worker.ts renderSkillMarkdown: use replaceAll so each
  {{BEARER}} / {{PROFILE}} occurrence is substituted (String.replace
  with a literal replaces only the first match)

End-to-end verified live: magic-link → session → mint → slug render
→ bearer call (404 profile_not_found is expected, no Google profile
saved yet) → replay 401 → shape mismatch 400.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
  (NEKO_MEMBER_MULTIUSER_USER_PASSWORD) so the runner doesn't crash
  when only v3 is set
neko.yaml: drop ${VAR} env-interpolation (Viper doesn't expand); all
  secrets now come from Fly secrets in NEKO_* env namespace
README: document the public-TLS-with-bearer architecture (corrects
  the plan's original 'Flycast-only' assumption), expanded runbook
  with IPv4 allocation + non-interactive `wrangler secret put`,
  known caveats section covers (1) neko v2 legacy mode triggered by
  upstream image defaults, (2) Resend placeholder bypass, (3) the
  0.0.0.0 bind change

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- ci.yml: bump node-version 20 → 22 (wrangler 4.92 requires Node 22+)
- box/Dockerfile: neko reads /etc/neko/neko.yaml not /etc/neko.yaml;
  fixing path means our screen/capture/plugins config now actually
  applies. (Chat + filetransfer still start because the upstream
  image's NEKO_MEMBER_MULTIUSER_USER_PASS env triggers v2 legacy
  mode regardless — tracked in README "known caveats".)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant