Skip to content

Yukaii/hn-auth-proxy

Repository files navigation

hn-auth-proxy

Cloudflare Worker proxy for Hacker News clients that need a service-friendly auth layer.

The Worker logs in to Hacker News with the normal acct / pw form flow, stores the returned HN user cookie server-side in Cloudflare KV, and returns a signed JWT for browser or PWA clients. The JWT contains only the HN username and an internal session id.

Endpoints

  • GET /docs serves Swagger UI.
  • GET /openapi.json serves the generated OpenAPI 3.1 document.
  • POST /auth/login with { "username": "...", "password": "..." } returns a bearer JWT.
  • GET /auth/me returns the current JWT-backed session.
  • GET /auth/submissions returns the current user's submitted HN stories as parsed JSON.
  • GET /auth/comments returns the current user's HN comments page as parsed JSON.
  • GET /auth/upvoted?type=submissions|comments returns the current user's upvoted HN submissions or comments as parsed JSON.
  • GET /auth/favorites?type=submissions|comments returns the current user's favorite HN submissions or comments as parsed JSON.
  • POST /auth/logout deletes the server-side HN session.
  • GET /v0/* proxies the official Firebase Hacker News API.
  • /hn/* proxies news.ycombinator.com/* with the stored HN cookie injected. Send Authorization: Bearer <token>.

Frontend

The frontend project that consumes this API is Yukaii/ykhn.

Local Setup

pnpm install
cp .dev.vars.example .dev.vars
pnpm dev

Set JWT_SECRET in .dev.vars to a long random value.

Login attempts are rate-limited per client IP using LOGIN_RATE_LIMIT_MAX and LOGIN_RATE_LIMIT_WINDOW_SECONDS from wrangler.toml.

Checks

pnpm format
pnpm lint
pnpm typecheck
pnpm test
pnpm check

The project uses Oxlint and Oxfmt. pnpm check runs formatting checks, linting, TypeScript, and tests.

OpenAPI

OpenAPI is generated by @hono/zod-openapi from route definitions and Zod schemas in src/index.ts. The raw upstream proxy endpoints are registered in the same OpenAPI registry because they intentionally pass through non-JSON upstream responses.

Logged-in HN Actions

The /hn/* endpoint is a cookie-injecting proxy for Hacker News web endpoints. It does not invent a separate typed API for HN actions; clients should fetch the HN page, parse the action URL that Hacker News generated for the logged-in user, then call that URL through /hn/*.

All logged-in calls need the JWT returned by /auth/login:

curl -H "Authorization: Bearer $TOKEN" \
  "https://hn-api.yukai.dev/hn/item?id=48080800"

Voting

To vote on an item:

  1. Fetch the item page through /hn/item?id=<item-id>.
  2. Parse the HN vote link from the returned HTML.
  3. Call the parsed link through /hn/<vote-link>.

HN vote links include an account/session-specific auth parameter, so clients should not construct them manually.

Example shape:

vote?id=48080800&how=up&auth=<hn-auth-token>&goto=item%3Fid%3D48080800
vote?id=48080800&how=un&auth=<hn-auth-token>&goto=item%3Fid%3D48080800

Call it through the proxy:

curl -i -H "Authorization: Bearer $TOKEN" \
  "https://hn-api.yukai.dev/hn/vote?id=48080800&how=up&auth=<hn-auth-token>&goto=item%3Fid%3D48080800"

A successful vote or unvote currently returns HN's normal redirect response, usually 302 back to the goto page. The deployed API has been tested with an upvote followed by an unvote; after upvoting, the item page exposed how=un, and after unvoting it exposed how=up again.

Replying

To show a reply form:

  1. Fetch the item page through /hn/item?id=<story-id>.
  2. Parse an actual reply?... link from the returned HTML.
  3. Call that parsed reply link through /hn/<reply-link>.

Reply links are for comments, not the story root. Calling /hn/reply?id=<story-id> may return an empty page. A valid reply URL looks like:

reply?id=<comment-id>&goto=item%3Fid%3D<story-id>%23<comment-id>

The reply page returns HN's normal comment form, including hidden parent, goto, hmac, and a textarea. Creating posts and deleting comments are intentionally not supported/documented yet.

Parsed account lists

The API exposes typed JSON wrappers around HN's logged-in account pages:

curl -H "Authorization: Bearer $TOKEN" \
  "https://hn-api.yukai.dev/auth/submissions?page=1"

curl -H "Authorization: Bearer $TOKEN" \
  "https://hn-api.yukai.dev/auth/comments?page=1"

curl -H "Authorization: Bearer $TOKEN" \
  "https://hn-api.yukai.dev/auth/upvoted?type=submissions&page=2"

curl -H "Authorization: Bearer $TOKEN" \
  "https://hn-api.yukai.dev/auth/upvoted?type=comments&page=2"

curl -H "Authorization: Bearer $TOKEN" \
  "https://hn-api.yukai.dev/auth/favorites?type=submissions&page=1"

curl -H "Authorization: Bearer $TOKEN" \
  "https://hn-api.yukai.dev/auth/favorites?type=comments&page=1"

These wrap HN's pages:

  • submitted?id=<username>&p=<page>
  • threads?id=<username>&p=<page>
  • upvoted?id=<username>&p=<page>
  • upvoted?id=<username>&comments=t&p=<page>
  • favorites?id=<username>&p=<page>
  • favorites?id=<username>&comments=t&p=<page>

Submission-list responses look like:

{
  "user": "pg",
  "page": 2,
  "items": [
    {
      "id": 37144985,
      "rank": 1,
      "title": "Htmx is part of the GitHub Accelerator",
      "url": "https://htmx.org/posts/2023-06-06-htmx-github-accelerator/",
      "site": "htmx.org",
      "score": 1109,
      "by": "jjdeveloper",
      "age": "on Aug 16, 2023",
      "time": 1692181172,
      "comments": 487,
      "itemUrl": "item?id=37144985"
    }
  ],
  "nextPage": 3,
  "nextUrl": "upvoted?id=pg&p=3"
}

Comment-list responses look like:

{
  "user": "pg",
  "page": 1,
  "items": [
    {
      "id": 37023160,
      "by": "jph",
      "age": "on Aug 6, 2023",
      "time": 1691336757,
      "text": "How to add a date to HN title?",
      "textHtml": "How to add a date to HN title?",
      "parentUrl": "item?id=37022911",
      "contextUrl": "item?id=37022911#37023160",
      "itemUrl": "item?id=37023160",
      "story": {
        "id": 37022911,
        "title": "I went to 50 different dentists",
        "url": "item?id=37022911"
      }
    }
  ],
  "nextPage": 2,
  "nextUrl": "upvoted?id=pg&comments=t&p=2"
}

Real Hacker News Integration Test

The real login test is opt-in so normal test runs do not require credentials.

cp test/.env.hn.example test/.env.hn

Fill in HN_USERNAME and HN_PASSWORD, then run:

pnpm test

test/.env.hn is ignored by git.

Cloudflare Setup

Create a KV namespace:

pnpm exec wrangler kv namespace create HN_SESSIONS
pnpm exec wrangler kv namespace create HN_SESSIONS --preview

Put the returned ids in wrangler.toml, then set the production JWT secret:

pnpm exec wrangler secret put JWT_SECRET
pnpm deploy

Notes

This is an unofficial Hacker News integration. The official Firebase API is read-only; logged-in actions go through the HN website and may change if Hacker News changes its form fields or cookie behavior.

About

Cloudflare Worker proxy for Hacker News clients that need a service-friendly auth layer

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors