Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
5 changes: 3 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.
4 changes: 3 additions & 1 deletion src/pages/api/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
29 changes: 17 additions & 12 deletions src/pages/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -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 <details> 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 <details>.
const tokenSignin = import.meta.env.DEV;
const TokenWrap = oauth ? "details" : "div";

// ?login=<reason> is set by /api/auth/callback when "Sign in with daily.dev" doesn't pan out.
const LOGIN_ERRORS: Record<string, string> = {
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";
Expand Down Expand Up @@ -80,16 +83,18 @@ const name = profile?.name || profile?.username || "stranger";
<p class="fine">No Plus needed. Your keys ride in a secure cookie for this session; we keep nothing.</p>
</>
)}
<TokenWrap class="token-form">
{oauth && <summary class="fine">Rather paste an API token?</summary>}
<p class="fine">
Paste your daily.dev API token (<a href="https://app.daily.dev/settings/api" target="_blank" rel="noopener">Settings → API</a>).
It stays in a secure cookie, only for your session.
</p>
<label for="token" class="sr-only">daily.dev API token</label>
<input type="password" id="token" placeholder="daily.dev API token" autocomplete="off" />
<button class={oauth ? "btn-ghost w-full" : "btn-primary"} id="signin">Sign in</button>
</TokenWrap>
{tokenSignin && (
<TokenWrap class="token-form">
{oauth && <summary class="fine">Rather paste an API token?</summary>}
<p class="fine">
Paste your daily.dev API token (<a href="https://app.daily.dev/settings/api" target="_blank" rel="noopener">Settings → API</a>).
It stays in a secure cookie, only for your session.
</p>
<label for="token" class="sr-only">daily.dev API token</label>
<input type="password" id="token" placeholder="daily.dev API token" autocomplete="off" />
<button class={oauth ? "btn-ghost w-full" : "btn-primary"} id="signin">Sign in</button>
</TokenWrap>
)}
<p class="error" id="login-error" role="alert" aria-live="assertive">{loginError}</p>
</div>

Expand Down
5 changes: 3 additions & 2 deletions tests/e2e/auth.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <details> 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 <details>.
async function openTokenForm(page: Page) {
await page.goto("/");
await page.getByText("Rather paste an API token?").click();
Expand Down
Loading