I've saved the no-emoji rule to core memory so it applies across all projects. Here is the re-drafted plan, cleaned of emoji and decorative unicode.
Local Admin Development Environment for urthreads
Context
The project has no working local admin testing path. wrangler dev alone cannot drive the admin dashboard because of two compounding root causes, both discovered and half-fixed in the previous session (whose uncommitted changes were then reverted, so the working tree is clean today):
- Cookie dropped over HTTP.
src/worker-security.mjs:355-364always emits__Host-urthreads_admin_sessionplusSecure. Browsers silently dropSecurecookies over plain HTTP, so the session cookie set byPOST /admin/sessiononhttp://localhost:8787never sticks. (The previous session proved this fix works end-to-end via curl: login -> cookie ->{"authenticated":true}.) admin_key_expiredfootgun, the actual blocker the user kept hitting.getAdminKeyAccess(worker-security.mjs:462) checksisAdminKeyExpiredbefore comparing the key, andverifyAdminSessionToken(line 423) re-checks key expiry on every request.isAdminKeyExpired(236-250) fails closed: empty/never-> not expired, but unparseable -> expired, and a past date -> expired. The expiry value is written by theadmin-keyCLI (src/admin-key.js:477-480) into.env/wrangler.tomland sits in.dev.vars, so a stale/pastADMIN_API_KEY_EXPIRES_ATmakes even the correct key return{"error":"Admin access is required.","reason":"admin_key_expired"}andweb/dashboard.js:549-551then tells the user to "deploy again", which is wrong for a local worker.
Compounding gaps: no npm run dev/setup:dev scripts, no automated .dev.vars creation, no local D1 schema init step, CONTRIBUTING.md:36-40 only says wrangler dev, and package.json has zero dependencies.
Goal: npm install && npm run setup:dev && npm run dev is the complete local admin testing workflow, with no Cloudflare account, no CLOUDFLARE_API_TOKEN, no manual secret or schema management, and with production cookie/expiry behavior provably unchanged.
Security constraint (explicit): every dev-only behavior must be strictly gated so it cannot leak into a real deployment. Gates used: (a) request-origin detection (http + loopback hostname) for the cookie; (b) .dev.vars is gitignored (.gitignore:18-19) and is the only file setup:dev writes; (c) production expiry semantics unchanged.
User decision: add wrangler as a devDependency (pinned current stable) so npm install alone yields a runnable dev environment.
Phase 1 - Localhost-aware admin session cookie (root cause 1)
Goal: http://localhost, http://127.0.0.1, http://[::1] get a plain urthreads_admin_session cookie without Secure. Everything else gets the byte-identical __Host-urthreads_admin_session plus Secure.
Tasks
-
src/worker-security.mjs- addisLocalHttpOrigin(request)(nearsafeParseUrl, ~line 21)new URL(request.url); returnfalseon parse failure.- Return
falseunlessurl.protocol === "http:". - Return
trueiff hostname (lowercased, brackets stripped) islocalhost,127.0.0.1, or::1.
-
src/worker-security.mjs:9- two cookie-name constants- Keep
ADMIN_SESSION_COOKIE_NAME = "__Host-urthreads_admin_session"(production). - Add
ADMIN_SESSION_COOKIE_NAME_LOCAL = "urthreads_admin_session". - Add
getAdminSessionCookieName(request)-> local name iffisLocalHttpOrigin(request), else prod name.
- Keep
-
src/worker-security.mjs:355-364-buildAdminSessionCookie- Cookie name from
getAdminSessionCookieName(request). - Omit
"Secure"from the attribute array whenisLocalHttpOrigin(request)is true. - Keep
HttpOnly,Path=/,Max-Age,SameSitein both paths.buildExpiredAdminSessionCookie(366) delegates, so it inherits the fix.
- Cookie name from
-
src/worker-security.mjs:326-332-getAdminSessionCookie- Look for prod name first, then local name; return first match;
""if neither.
- Look for prod name first, then local name; return first match;
-
Tests -
test/worker-security.test.mjs(helpers exist:req()builds https URLs,parseSetCookie,baseEnv; add areqLocal(path)helper buildinghttp://localhost:8787)- Localhost request -> cookie name
urthreads_admin_session, nosecureattr,httponly/path/max-agepresent. - HTTPS request ->
__Host-plussecure(make the existing test at line 309 explicit). getAdminSessionCookiefinds a localhost-name cookie, finds a prod-name cookie, returns""when neither.
- Localhost request -> cookie name
-
Tests -
test/worker-admin-flow.test.mjs(mockedenv.DB, distinct CF-Connecting-IP per test)- POST valid key to
http://localhost:8787/admin/session-> 200,Set-Cookienameurthreads_admin_session, noSecure,HttpOnlyplusPath=/present. - Follow-up GET
/admin/sessionwith that cookie -> 200{authenticated:true}. - Existing HTTPS
__Host-tests (lines ~162-181) unchanged.
- POST valid key to
Key files: src/worker-security.mjs:9,21,326-364, test/worker-security.test.mjs, test/worker-admin-flow.test.mjs
Phase 2 - Kill the local admin_key_expired footgun (root cause 2)
Goal: the local worker can never reject the correct key as expired due to a stale env value, and the dashboard stops giving "deploy again" advice for localhost.
Tasks
-
src/setup-dev.js(new, Phase 3) must ALWAYS normalize expiry in.dev.vars- Whether the key is reused or generated,
.dev.varsis written with exactly:ADMIN_API_KEY=<key> ADMIN_API_KEY_EXPIRES_AT= ADMIN_API_KEY_EXPIRES_AT=(empty) means never-expires perisAdminKeyExpired(worker-security.mjs:240-242). The previous attempt's reuse path left a stale expiry in place, which is what kept producingadmin_key_expired; do not repeat it.- After writing, re-read
.dev.varsand validate with the same semantics as the worker (empty/unset/never-> OK; parseable -> warn if in the past; unparseable -> error). Print a warning when a non-empty expiry was overwritten.
- Whether the key is reused or generated,
-
web/dashboard.js:545-556- localhost-aware expired message- When
state.workerUrlishttp://plus loopback (reuse the hostname check fromcanAttemptCookieSession, lines 135-153), render: "The local worker says this admin key is expired. Runnpm run setup:devto reset.dev.vars, then restartnpm run dev." - Keep the existing "deployed Worker ... deploy again" message for all other URLs.
- When
-
Tests
test/worker-security.test.mjs: explicitisAdminKeyExpiredcases -"", unset,"never","none"-> not expired; unparseable string -> expired; past ISO -> expired (past-date case exists at line ~470; add the empty/unparseable ones).test/setup-dev.test.js(Phase 3): a.dev.varscontaining a staleADMIN_API_KEY_EXPIRES_AT=2020-...is rewritten to empty; a key present is reused, not regenerated.
-
No change to production semantics: unparseable/past -> expired stays (fail-closed).
verifyAdminSessionToken's per-request key-expiry check (line 423) is untouched; with empty expiry it is a no-op locally, and in production key rotation still kills sessions.
Key files: src/setup-dev.js (new), web/dashboard.js:135-153,545-556, test/worker-security.test.mjs, test/setup-dev.test.js
Phase 3 - npm run setup:dev plus npm run dev one-command setup
Tasks
-
New
src/setup-dev.js(Node built-ins only; mirrorsrc/setup-env.jsreadline pattern; importgenerateAdminApiKeyplus clipboard helper fromsrc/admin-key.js)- Read D1
database_namefromwrangler.toml[[d1_databases]](current value:test). Missing file/binding -> helpful error pointing atnpm run setup:env; never auto-editwrangler.toml. .dev.vars: reuse existingADMIN_API_KEYif present, else generate; always write emptyADMIN_API_KEY_EXPIRES_AT=(per Phase 2).- Local D1 schema:
npx wrangler d1 execute <database_name> --local --file=src/schema.sqlviachild_process.execSync; catch errors with actionable messages (wrangler missing -> point atnpm install).src/schema.sqlis idempotent (allCREATE TABLE IF NOT EXISTS, verified). - Print the admin key, copy to clipboard when available, and print next steps including "restart
npm run devif it was already running" (wrangler reads.dev.varsat startup).
- Read D1
-
package.json- Add
"dev": "wrangler dev"and"setup:dev": "node src/setup-dev.js"toscripts(line 42 block). - Append
&& node --check src/setup-dev.jsto thecheckscript. - Add
wrangleras a devDependency, pinned to the current stable v4 (confirm the exact version during implementation withnpm view wrangler version; do not guess). This creates the firstpackage-lock.json; commit it so installs are reproducible.
- Add
-
test/setup-dev.test.js(new) - all file ops inos.tmpdir(); never touch the realwrangler.toml/.dev.vars:- Reads D1 database name from a temp
wrangler.toml. - Writes key plus empty expiry when
.dev.varsabsent; reuses key but overwrites stale expiry when present. - Missing D1 binding -> prompt asked (mock the prompt) or clean exit.
- Reads D1 database name from a temp
Key files: src/setup-dev.js (new), package.json (plus new package-lock.json), test/setup-dev.test.js (new)
Phase 4 - admin-key CLI local-dev hint
src/admin-key.js:500-507next-steps: after thewrangler secret putline, add:Informational only; the CLI still never writesFor local development (no Cloudflare account needed): run: npm run setup:dev.dev.vars(that stayssetup-dev.js's job).- Test in
test/admin-key.test.js: the next-steps output mentionssetup:devwhen the key is not stored as a secret.
Key files: src/admin-key.js:500-507, test/admin-key.test.js
Phase 5 - Documentation
CONTRIBUTING.md:36-52- replace the barewrangler devblock with:Plus: dashboard URLnpm install npm run setup:dev # creates .dev.vars (admin key, never-expiring), initializes local D1 npm run dev # wrangler dev on http://localhost:8787 # Terminal 2: python3 -m http.server 8000
http://localhost:8000/web/index.html, example URLhttp://localhost:8000/examples/multi-page-test/index.html?worker=http://localhost:8787, note thatsetup:devprints the admin key to paste into the dashboard, and thatALLOWED_ORIGINSalready includeshttp://localhost:8000(wrangler.toml:38).README.md:144-153- framewrangler devas step 2 afternpm run setup:dev.web/DASHBOARD.md- add a "Local testing" section with the same workflow.
Key files: CONTRIBUTING.md:36-52, README.md:144-153, web/DASHBOARD.md
What stays unchanged (security guarantees)
- Production cookie: HTTPS plus real hostname ->
__Host-urthreads_admin_sessionplusSecure,HttpOnly,Path=/,SameSite, byte-identical to today.isLocalHttpOriginrequires protocolhttp:AND a loopback hostname; a deployed worker is only reachable over HTTPS on its own hostname, so the branch cannot be triggered remotely. - Expiry semantics: empty = never, unparseable/past = expired (fail-closed), unchanged. Only
.dev.vars(local, gitignored) is affected bysetup:dev. - No remote surface:
setup:devnever callswrangler secret put,wrangler deploy, or remote D1;d1 execute --localwrites only the local sqlite replica. NoCLOUDFLARE_API_TOKENanywhere. - Untouched: session token format plus HMAC (
createAdminSessionToken/verifyAdminSessionToken), CSRF (checkCsrf), rate limiting, D1 auth tables, CORS. - Secrets:
.dev.varsis gitignored (.gitignore:18-19); the generated key is only printed to the user's own terminal/clipboard.
Implementation order
- Phase 1 (cookie) and Phase 2 (expiry normalization plus dashboard message) - both touch the login path; implement together so the e2e test exercises both fixes. Files:
worker-security.mjs,web/dashboard.js. - Phase 3 (setup-dev + package.json + wrangler devDependency + tests) - depends on Phase 1/2 being correct (setup is useless if login fails).
- Phase 4 (CLI hint) - independent, small.
- Phase 5 (docs) - last, after verification.
Recommended agents: Phase 1+2 backend-engineer; Phase 3 software-engineer-agent; Phase 4 software-engineer-agent; Phase 5 technical-writer. (code-reviewer on the combined diff before merge.)
Verification
npm test- all existing tests pass plus new localhost-cookie, expiry, and setup-dev tests (expect ~205+ existing assertions plus new ones; exit 0).npm run check- all files parse, includingsrc/setup-dev.js.- Manual end-to-end (the real proof):
npm install # installs pinned wrangler devDependency npm run setup:dev # prints admin key + next steps npm run dev # wrangler dev on http://localhost:8787 # Terminal 2: python3 -m http.server 8000 # Browser: http://localhost:8000/web/index.html # Worker URL: http://localhost:8787, admin key: <printed> # Expect authenticated admin view - NOT "Session expired" # Example site: http://localhost:8000/examples/multi-page-test/index.html?worker=http://localhost:8787
- curl proof of the two fixes:
curl -si -X POST http://localhost:8787/admin/session \ -H 'Content-Type: application/json' -H 'Origin: http://localhost:8000' \ -d '{"adminKey":"<key>"}' # Expect 200, Set-Cookie: urthreads_admin_session=...; (no Secure) curl -si http://localhost:8787/admin/session -H "Cookie: urthreads_admin_session=<token>" # Expect 200 {"authenticated":true}
- DevTools: Application -> Cookies ->
http://localhost:8787-> cookie namedurthreads_admin_session,HttpOnly, noSecure. - Production regression: existing HTTPS tests (
test/worker-admin-flow.test.mjs~162-181) still assert__Host-urthreads_admin_sessionplusSecure, and pass unchanged.
Assumptions
- Wrangler 4
wrangler devruns locally by default and reads.dev.varsat startup (env changes require a restart; setup-dev's next-steps says so). .dev.varspluswrangler.tomlstay gitignored; the user's existingwrangler.toml(D1 nametest,ALLOWED_ORIGINSincl.http://localhost:8000, empty expiry in all[vars]) is left untouched by setup:dev.src/schema.sqlis safe to re-run (allCREATE TABLE IF NOT EXISTS).- The dashboard's
canAttemptCookieSessionalready permits HTTP for loopback, so no dashboard fetch change is needed; only the error-message text changes. - wrangler devDependency version: pin current stable v4, confirmed via
npm view wrangler versionat implementation time (no version guessed in this plan).