diff --git a/.env.example b/.env.example index d3f4eba..ecd4e81 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,5 @@ -# All variables are optional: without them the app runs with personal-token -# sign-in and the client-side demo mode. +# All variables are optional: without them a deployment only offers the +# client-side demo (`pnpm dev` also offers personal-token sign-in). # "Sign in with daily.dev" OAuth app (daily.dev Settings → API → OAuth apps). # Register this redirect URI on the app: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b44cb6f..8ca3b6c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -46,8 +46,9 @@ map. In short: (oxfmt is still alpha and does not format `.astro` files yet, so keep those tidy by hand.) 5. **Test your change**: `pnpm test` runs the Playwright E2E suite against the - demo flow. Also exercise it manually with `pnpm dev` (and real mode if you - have a daily.dev API token), and run `pnpm build` to confirm the production build passes. + demo flow. Also exercise it manually with `pnpm dev` (and real mode by + pasting a daily.dev API token, which only `pnpm dev` accepts), and run + `pnpm build` to confirm the production build passes. All of the above (`lint`, `format:check`, `build`, `test`) run automatically on every pull request via GitHub Actions, and must pass before a PR can be merged. diff --git a/README.md b/README.md index 2af37a5..6ea54ae 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,9 @@ Your bookmarks pile up and rot — this forces a reckoning. One spin serves one ### Two ways to play - **Demo mode** — runs entirely in the browser on a fake bookmark pile. No account, no token required. This is the headline experience and always works. -- **Real mode** — **Sign in with daily.dev** (OAuth, authorization code + PKCE), or paste a daily.dev API token (Settings → API, no Plus subscription needed) as a fallback. Tokens are stored in httpOnly cookies and OAuth access tokens are refreshed server-side; every API call is proxied through Astro server routes, so no token ever touches client JS and there are no CORS issues. Bookmarks load 50 at a time (one API request), moving on to older ones as you clear each batch, to go easy on daily.dev's monthly API quota. Deletes hit the real `DELETE /bookmarks/{id}` endpoint. +- **Real mode** — **Sign in with daily.dev** (OAuth, authorization code + PKCE; no Plus subscription needed). Tokens are stored in httpOnly cookies and access tokens are refreshed server-side; every API call is proxied through Astro server routes, so no token ever touches client JS and there are no CORS issues. Bookmarks load 50 at a time (one API request), moving on to older ones as you clear each batch, to go easy on daily.dev's monthly API quota. Deletes hit the real `DELETE /bookmarks/{id}` endpoint. + +The deployed app never asks for a personal API token. Under `pnpm dev` only, a "paste an API token" form is available, since daily.dev's OAuth can't redirect to localhost. ## Run locally @@ -50,4 +52,4 @@ Targets Vercel out of the box (`@astrojs/vercel`, `output: "server"`). Push and ## API notes -daily.dev's Public API is REST + Bearer tokens (personal tokens or [OAuth app](https://docs.daily.dev/oauth-apps/) tokens), available to any daily.dev account. OAuth is optional: set `DAILY_OAUTH_CLIENT_ID` / `DAILY_OAUTH_CLIENT_SECRET` (see `.env.example`) to show the button. Endpoints used: `GET /bookmarks/` (cursor-paginated), `DELETE /bookmarks/{id}`, `GET /profile/`. Other resources exist (feeds, follows, tech stack) — candidates for future roulette modes. See `spike/spike.mjs` for a standalone API probe. +daily.dev's Public API is REST + Bearer tokens (personal tokens or [OAuth app](https://docs.daily.dev/oauth-apps/) tokens), available to any daily.dev account. Set `DAILY_OAUTH_CLIENT_ID` / `DAILY_OAUTH_CLIENT_SECRET` (see `.env.example`) to enable sign-in; without them a deployment only offers the demo. Endpoints used: `GET /bookmarks/` (cursor-paginated), `DELETE /bookmarks/{id}`, `GET /profile/`. Other resources exist (feeds, follows, tech stack) — candidates for future roulette modes. See `spike/spike.mjs` for a standalone API probe. diff --git a/src/pages/api/auth/index.ts b/src/pages/api/auth/index.ts index 91dedb9..ba329fb 100644 --- a/src/pages/api/auth/index.ts +++ b/src/pages/api/auth/index.ts @@ -3,8 +3,10 @@ import { validateToken } from "../../../lib/daily"; import { oauthEnabled, revokeRefreshToken } from "../../../lib/oauth"; import { setSession, clearSession, getRefreshToken } from "../../../lib/session"; -// POST { token } -> sign in with a personal token. +// POST { token } -> sign in with a personal token. Local dev only: OAuth can't +// complete on localhost, and the deployed app never accepts personal tokens. export const POST: APIRoute = async ({ request, cookies }) => { + if (!import.meta.env.DEV) return json({ error: "Not found" }, 404); const body = await request.json().catch(() => ({})); const token: string | undefined = body.token?.trim(); diff --git a/src/pages/index.astro b/src/pages/index.astro index 2eae2b4..5d5490c 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -5,14 +5,17 @@ import { oauthEnabled } from "../lib/oauth"; const profile = await currentUser(Astro.cookies); const oauth = oauthEnabled(); -// With OAuth on, the personal-token form tucks into a
as a fallback. +// Personal tokens are a local-dev convenience only (OAuth can't complete on +// localhost): the deployed app never asks for one, so they never pass through +// a hosted server. With OAuth on, the form tucks into a
. +const tokenSignin = import.meta.env.DEV; const TokenWrap = oauth ? "details" : "div"; // ?login= is set by /api/auth/callback when "Sign in with daily.dev" doesn't pan out. const LOGIN_ERRORS: Record = { denied: "You turned daily.dev down. No hard feelings, partner.", failed: "Sign-in with daily.dev misfired. Give it another shot.", - unavailable: "Sign in with daily.dev isn't saddled up here. Use an API token instead.", + unavailable: "Sign in with daily.dev isn't saddled up here. Try the Demo instead.", }; const loginError = LOGIN_ERRORS[Astro.url.searchParams.get("login") ?? ""] ?? ""; const name = profile?.name || profile?.username || "stranger"; @@ -80,16 +83,18 @@ const name = profile?.name || profile?.username || "stranger";

No Plus needed. Your keys ride in a secure cookie for this session; we keep nothing.

)} - - {oauth && Rather paste an API token?} -

- Paste your daily.dev API token (Settings → API). - It stays in a secure cookie, only for your session. -

- - - -
+ {tokenSignin && ( + + {oauth && Rather paste an API token?} +

+ Paste your daily.dev API token (Settings → API). + It stays in a secure cookie, only for your session. +

+ + + +
+ )} diff --git a/tests/e2e/auth.spec.ts b/tests/e2e/auth.spec.ts index dbe12bf..c82f337 100644 --- a/tests/e2e/auth.spec.ts +++ b/tests/e2e/auth.spec.ts @@ -12,8 +12,9 @@ import { test, expect, type Page } from "@playwright/test"; // Playwright context has no cookies, so the redirect is already // deterministic without mocking anything. -// With OAuth configured (see playwright.config.ts), the token form lives in a -// collapsed
fallback. +// The token form only exists under the dev server (the deployed app is +// OAuth-only), which is what Playwright runs. With OAuth configured (see +// playwright.config.ts), it lives in a collapsed
. async function openTokenForm(page: Page) { await page.goto("/"); await page.getByText("Rather paste an API token?").click();