Skip to content

v1.2.0

Latest

Choose a tag to compare

@3M1RY33T 3M1RY33T released this 26 Aug 21:36
· 3 commits to main since this release
b82d203

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):

  1. Cookie dropped over HTTP. src/worker-security.mjs:355-364 always emits __Host-urthreads_admin_session plus Secure. Browsers silently drop Secure cookies over plain HTTP, so the session cookie set by POST /admin/session on http://localhost:8787 never sticks. (The previous session proved this fix works end-to-end via curl: login -> cookie -> {"authenticated":true}.)
  2. admin_key_expired footgun, the actual blocker the user kept hitting. getAdminKeyAccess (worker-security.mjs:462) checks isAdminKeyExpired before comparing the key, and verifyAdminSessionToken (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 the admin-key CLI (src/admin-key.js:477-480) into .env/wrangler.toml and sits in .dev.vars, so a stale/past ADMIN_API_KEY_EXPIRES_AT makes even the correct key return {"error":"Admin access is required.","reason":"admin_key_expired"} and web/dashboard.js:549-551 then 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

  1. src/worker-security.mjs - add isLocalHttpOrigin(request) (near safeParseUrl, ~line 21)

    • new URL(request.url); return false on parse failure.
    • Return false unless url.protocol === "http:".
    • Return true iff hostname (lowercased, brackets stripped) is localhost, 127.0.0.1, or ::1.
  2. 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 iff isLocalHttpOrigin(request), else prod name.
  3. src/worker-security.mjs:355-364 - buildAdminSessionCookie

    • Cookie name from getAdminSessionCookieName(request).
    • Omit "Secure" from the attribute array when isLocalHttpOrigin(request) is true.
    • Keep HttpOnly, Path=/, Max-Age, SameSite in both paths. buildExpiredAdminSessionCookie (366) delegates, so it inherits the fix.
  4. src/worker-security.mjs:326-332 - getAdminSessionCookie

    • Look for prod name first, then local name; return first match; "" if neither.
  5. Tests - test/worker-security.test.mjs (helpers exist: req() builds https URLs, parseSetCookie, baseEnv; add a reqLocal(path) helper building http://localhost:8787)

    • Localhost request -> cookie name urthreads_admin_session, no secure attr, httponly/path/max-age present.
    • HTTPS request -> __Host- plus secure (make the existing test at line 309 explicit).
    • getAdminSessionCookie finds a localhost-name cookie, finds a prod-name cookie, returns "" when neither.
  6. Tests - test/worker-admin-flow.test.mjs (mocked env.DB, distinct CF-Connecting-IP per test)

    • POST valid key to http://localhost:8787/admin/session -> 200, Set-Cookie name urthreads_admin_session, no Secure, HttpOnly plus Path=/ present.
    • Follow-up GET /admin/session with that cookie -> 200 {authenticated:true}.
    • Existing HTTPS __Host- tests (lines ~162-181) unchanged.

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

  1. src/setup-dev.js (new, Phase 3) must ALWAYS normalize expiry in .dev.vars

    • Whether the key is reused or generated, .dev.vars is written with exactly:
      ADMIN_API_KEY=<key>
      ADMIN_API_KEY_EXPIRES_AT=
      
    • ADMIN_API_KEY_EXPIRES_AT= (empty) means never-expires per isAdminKeyExpired (worker-security.mjs:240-242). The previous attempt's reuse path left a stale expiry in place, which is what kept producing admin_key_expired; do not repeat it.
    • After writing, re-read .dev.vars and 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.
  2. web/dashboard.js:545-556 - localhost-aware expired message

    • When state.workerUrl is http:// plus loopback (reuse the hostname check from canAttemptCookieSession, lines 135-153), render: "The local worker says this admin key is expired. Run npm run setup:dev to reset .dev.vars, then restart npm run dev."
    • Keep the existing "deployed Worker ... deploy again" message for all other URLs.
  3. Tests

    • test/worker-security.test.mjs: explicit isAdminKeyExpired cases - "", 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.vars containing a stale ADMIN_API_KEY_EXPIRES_AT=2020-... is rewritten to empty; a key present is reused, not regenerated.
  4. 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

  1. New src/setup-dev.js (Node built-ins only; mirror src/setup-env.js readline pattern; import generateAdminApiKey plus clipboard helper from src/admin-key.js)

    • Read D1 database_name from wrangler.toml [[d1_databases]] (current value: test). Missing file/binding -> helpful error pointing at npm run setup:env; never auto-edit wrangler.toml.
    • .dev.vars: reuse existing ADMIN_API_KEY if present, else generate; always write empty ADMIN_API_KEY_EXPIRES_AT= (per Phase 2).
    • Local D1 schema: npx wrangler d1 execute <database_name> --local --file=src/schema.sql via child_process.execSync; catch errors with actionable messages (wrangler missing -> point at npm install). src/schema.sql is idempotent (all CREATE TABLE IF NOT EXISTS, verified).
    • Print the admin key, copy to clipboard when available, and print next steps including "restart npm run dev if it was already running" (wrangler reads .dev.vars at startup).
  2. package.json

    • Add "dev": "wrangler dev" and "setup:dev": "node src/setup-dev.js" to scripts (line 42 block).
    • Append && node --check src/setup-dev.js to the check script.
    • Add wrangler as a devDependency, pinned to the current stable v4 (confirm the exact version during implementation with npm view wrangler version; do not guess). This creates the first package-lock.json; commit it so installs are reproducible.
  3. test/setup-dev.test.js (new) - all file ops in os.tmpdir(); never touch the real wrangler.toml/.dev.vars:

    • Reads D1 database name from a temp wrangler.toml.
    • Writes key plus empty expiry when .dev.vars absent; reuses key but overwrites stale expiry when present.
    • Missing D1 binding -> prompt asked (mock the prompt) or clean exit.

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-507 next-steps: after the wrangler secret put line, add:
    For local development (no Cloudflare account needed):
      run: npm run setup:dev
    
    Informational only; the CLI still never writes .dev.vars (that stays setup-dev.js's job).
  • Test in test/admin-key.test.js: the next-steps output mentions setup:dev when 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 bare wrangler dev block with:
    npm 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
    Plus: dashboard URL http://localhost:8000/web/index.html, example URL http://localhost:8000/examples/multi-page-test/index.html?worker=http://localhost:8787, note that setup:dev prints the admin key to paste into the dashboard, and that ALLOWED_ORIGINS already includes http://localhost:8000 (wrangler.toml:38).
  • README.md:144-153 - frame wrangler dev as step 2 after npm 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_session plus Secure, HttpOnly, Path=/, SameSite, byte-identical to today. isLocalHttpOrigin requires protocol http: 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 by setup:dev.
  • No remote surface: setup:dev never calls wrangler secret put, wrangler deploy, or remote D1; d1 execute --local writes only the local sqlite replica. No CLOUDFLARE_API_TOKEN anywhere.
  • Untouched: session token format plus HMAC (createAdminSessionToken/verifyAdminSessionToken), CSRF (checkCsrf), rate limiting, D1 auth tables, CORS.
  • Secrets: .dev.vars is gitignored (.gitignore:18-19); the generated key is only printed to the user's own terminal/clipboard.

Implementation order

  1. 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.
  2. Phase 3 (setup-dev + package.json + wrangler devDependency + tests) - depends on Phase 1/2 being correct (setup is useless if login fails).
  3. Phase 4 (CLI hint) - independent, small.
  4. 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

  1. npm test - all existing tests pass plus new localhost-cookie, expiry, and setup-dev tests (expect ~205+ existing assertions plus new ones; exit 0).
  2. npm run check - all files parse, including src/setup-dev.js.
  3. 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
  4. 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}
  5. DevTools: Application -> Cookies -> http://localhost:8787 -> cookie named urthreads_admin_session, HttpOnly, no Secure.
  6. Production regression: existing HTTPS tests (test/worker-admin-flow.test.mjs ~162-181) still assert __Host-urthreads_admin_session plus Secure, and pass unchanged.

Assumptions

  • Wrangler 4 wrangler dev runs locally by default and reads .dev.vars at startup (env changes require a restart; setup-dev's next-steps says so).
  • .dev.vars plus wrangler.toml stay gitignored; the user's existing wrangler.toml (D1 name test, ALLOWED_ORIGINS incl. http://localhost:8000, empty expiry in all [vars]) is left untouched by setup:dev.
  • src/schema.sql is safe to re-run (all CREATE TABLE IF NOT EXISTS).
  • The dashboard's canAttemptCookieSession already 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 version at implementation time (no version guessed in this plan).