feat: humanish MVP — two-tier credential broker (broker + box + google.whoami) - #1
Open
thedotmack wants to merge 13 commits into
Open
feat: humanish MVP — two-tier credential broker (broker + box + google.whoami)#1thedotmack wants to merge 13 commits into
thedotmack wants to merge 13 commits into
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
info@copterlabs.com, Flythedotmack@gmail.comBoth originally provisioned during this branch:
humanish-broker.KV(id1d108315895e4a4896a6bbe040cbdc84)humanish-box, 3 GB volumehumanish_data, shared IPv4 + dedicated IPv6, sjc regionHUMANISH_MASTER_KEY+BOX_SHARED_SECRETidentical on both Worker and Fly;RESEND_API_KEYis are_PLACEHOLDER_value — broker logs magic links towrangler tailuntil operator wires a real Resend keyEnd-to-end verified live
GET /landingPOST /api/auth/requestallowlisted emailwrangler tailPOST /api/auth/requestnon-allowlistednot_allowedGET /api/auth/verify?token=…humanish_sessioncookie set HttpOnly+Secure+SameSite=LaxGET /api/me(with session){email:thedotmack@gmail.com}GET /api/profiles(proxied to box){profiles:[]}POST /api/mint{slug,url}GET /:slug(first visit)GET /:slug(second visit)POST /api/actions/google-personal/google.whoami(no profile saved yet)profile_not_found(box correctly rejects)expired{}called with{"extra":"field"}payload_shape_mismatch/healthzno auth{ok:true,ts:…}/profilesno authunauthorized/profileswithBOX_SHARED_SECRET{profiles:[]}Test + verification gates passed
pnpm -r typecheck— broker + box cleanpnpm check:allowlist— brokerACTION_ALLOWLISTand boxACTION_REGISTRYin syncpnpm -r test— 35 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 vulnerabilitieswrangler deploy --dry-run— bundle 162.51 KiB / 43.72 KiB gzipdocker build -f packages/box/Dockerfile .— builds cleanallow-top-navigation, slug page has nofetch(), AAD on both encrypt + decrypt, nochild_process.exec, action runner bind validated)What's left for live use by the operator
wrangler secret put RESEND_API_KEYwith a real key. Until then, magic links are logged towrangler tailinstead of emailed.storageStateinto/data/profiles/google-personal.bin.{email, checked_at}in ~10s.Known caveats (documented in README)
ghcr.io/m1k1o/neko/chromiumimage bakesNEKO_MEMBER_MULTIUSER_USER_PASS(v2) at build time, which silently disables ourchat/filetransferplugin-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.*.flycast; the real defense isBOX_SHARED_SECRET(constant-time compare) + TLS termination at fly-proxy. Bearer + TLS = same threat model as the plan's intent.RESEND_API_KEYplaceholder triggers a magic-link bypass that logs viawrangler tail. Replace before sharing the broker URL.Commits
11 commits, each phase atomically reviewable:
5937eb8scaffolde4e84a0broker security core (auth/SlugBox/bearer/routes)54a5169box (Dockerfile/Fly/crypto/runner)c5f1469broker templates wired via wrangler Text bundlesc1d00b9dashboard SPAf329710google.whoami actioned185e3Phase 8 hardening (mint rate limit / audit rotation / allowlist guard / README)0fd16b2plan docs0ee87bbtests + CI9a6c3afproduction deploy + dashboard polish + skill templatereplaceAllbug8899da1neko v3 env fallback + README updated for actual deployed state🤖 Generated with Claude Code