Ctrl K
ComunidadGuías…

CLAUDE.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

CLIPLINK is a zero-auth, ephemeral cross-device clipboard: create a room, share a short code (random, or chosen), and text sent from one device appears — and is auto-copied — on every other device in the room. Files move peer-to-peer over WebRTC and never touch the server.

Read [`AGENTS.md`](AGENTS.md) first: **this is Next.js 16, which has breaking changes from what is likely in your training data.** Consult `node_modules/next/dist/docs/` before writing framework code.

## Commands

```bash
pnpm dev          # dev server
pnpm build        # next build --webpack
pnpm start        # serve the production build
pnpm lint         # eslint
pnpm lint:fix     # eslint --fix
pnpm type-check   # tsc --noEmit
```

pnpm is the package manager — do not create a `package-lock.json`. The app has **no test suite**; do not invent a root `pnpm test` or `pnpm format`. The workspace packages do: `pnpm -F @thebkht/rtc-file-transfer test` and `pnpm -F @thebkht/cliplink test` (Node test runner). CI runs the package tests and builds, then lint, type-check, and build.

## Architecture

```
app/                      App Router: pages + API routes
  layout.tsx              Root layout, metadata, theme provider
  page.tsx                Landing + room entry
  manifest.json           PWA manifest
  rooms/route.ts          POST — create room
  rooms/[code]/route.ts   GET — fetch room
  rooms/[code]/clips/     POST — send clip; GET — poll clips after id; DELETE — erase clips
  rooms/[code]/socket/    GET — WebSocket upgrade (clips + WebRTC signaling)
proxy.ts                  Content-Security-Policy, with a per-request script nonce
components/
  cliplink-app.tsx        The room experience (client component)
  cliplink/               Room UI, sheets and hooks
  ui/                     shadcn components (Base UI), restyled to Apple conventions
lib/cliplink/             All domain logic
lib/utils.ts              cn() — clsx + tailwind-merge
packages/
  rtc-file-transfer/      @thebkht/rtc-file-transfer — published to npm
  cliplink/               @thebkht/cliplink — the room protocol, plus src/cli/
```

There is **no** `src/`, `hooks/`, `store/`, or `server/` directory.

### `lib/cliplink/`

Several files here are now thin re-exports of `@thebkht/cliplink`, which holds
everything the CLI shares with the app — `crypto.ts`, `types.ts`,
`validation.ts`, `room-code.ts`, `qr.ts`, `http.ts`, `ws.ts` and
`encrypted-transport.ts`. Edit the package, not the shim. The app keeps
importing from `@/lib/cliplink/...` either way.

| File | Responsibility |
| --- | --- |
| `types.ts` | Re-export: shared types, including the `TransportClient` interface |
| `constants.ts` | App limits and timing — caps, rate limits. Wire limits re-export from the package; file-transfer tuning lives in `rtc-file-transfer` |
| `origin.ts` | Where this tab's room API lives, as a function so SSR can evaluate the module |
| `storage.ts` | Room/clip persistence behind a `StorageAdapter`; Redis or in-memory |
| `redis.ts` | Upstash REST client, returns `null` when unconfigured |
| `pubsub.ts` | `ioredis` pub/sub fan-out, with a process-local `EventEmitter` fallback |
| `ws.ts` | Re-export: the WebSocket transport, bound to this tab's origin |
| `http.ts` | Re-export: the room REST API, bound to this tab's origin |
| `rate-limit.ts` | Atomic per-IP limiting via `@upstash/ratelimit`; fails open |
| `erase.ts` | Server-only: checks a presented erase token against the hash a room was created with |
| `device.ts` | This browser's display name, and the mark by which it recognises its own clips |
| `validation.ts` | Re-export: input validation — hand-written, **not** Zod |
| `room-code.ts`, `session.ts`, `clipboard.ts`, `format.ts`, `errors.ts` | Focused helpers |

### Things that are easy to get wrong

- **Two transports, one interface.** WebSocket is primary, polling is the fallback; both implement `TransportClient` so the UI's retry/backoff state machine is written once. Changes to connection behavior belong behind that interface, not in the component.
- **Everything degrades without credentials.** `getRedis()` returns `null` and storage falls back to memory; pub/sub falls back to a process-local bus. The app must always boot and work single-process with no env file — this is the documented contributor path. Never introduce a hard requirement on an env var.
- **The protocol is a published package.** Encryption, wire types, validation and both transports live in `packages/cliplink` (`@thebkht/cliplink`), consumed via `workspace:*`, with `lib/cliplink/` re-exporting them. The `cliplink` CLI is `src/cli/` in that same package, which is why the protocol may not assume a browser: transports take their origin as a parameter, and `window` appears nowhere in it. Wire protocol v1 is fixed — the tests pin ciphertexts from earlier builds, because a tab loaded before a deploy keeps talking to one loaded after it.
- **File transfer is a published package.** WebRTC offer/accept, chunking, backpressure, and stall detection live in `packages/rtc-file-transfer`, consumed via `workspace:*`. Keep it free of cliplink specifics (env vars, toast copy, rooms) and runtime dependencies; the wire protocol is v1 and must stay compatible with deployed clients. Its `exports` point at `src/` in the workspace and are rewritten to `dist/` by `publishConfig` at publish time.
- **Files never reach the server.** The socket route relays signaling envelopes only. Any change that buffers or proxies file bytes server-side is a design violation.
- **TTL is refreshed on write, not on read.** `appendClip` and `touchRoom` extend a room's life; polling must not. This was a deliberate fix — do not reintroduce read-side refresh.
- **Deleting is the client's decision and the key holder's right.** The server checks an erase token against a hash set when the room was created and never afterwards; it cannot tell a clip delivered from a clip read, so a one-time clip is deleted by the device that reads it. Polls and socket backlogs only add, which is why the server counts deletions (`eraseGen`) and the encrypted transport reconciles. Deletes do not refresh the TTL.
- **A socket outlives its function only by handing off.** On Vercel a socket dies with the function holding it, at the duration limit. The socket route reads its deadline and sends `reconnect` 30 s ahead; the transport in `ws.ts` opens a replacement and closes the old one with code 4001 once the new one is ready, and the route publishes no `peer-left` for a socket closed that way. Do not "fix" a drop every five minutes with a longer `maxDuration`.
- **Rate limiting fails open** by design. Preserve that on Redis errors.
- **No env var may be required at build time.** CI builds with no credentials at all.
- **The CSP depends on a per-request nonce.** `proxy.ts` generates it and the root layout reads it through `headers()`, so every page must stay dynamically rendered — a statically rendered page has no nonce and its scripts are blocked. The matcher covers pages only: keep `/rooms` and `/share-target` out of it, and allow any new origin or resource type in the policy there.

### Stack

Next.js 16 App Router · React 19 · TypeScript · Tailwind CSS v4 · Upstash Redis (`@upstash/redis`, `@upstash/ratelimit`) · `ioredis` for pub/sub · `ws` · `next-themes` · shadcn/ui on Base UI (`@base-ui/react`), Sonner for toasts, `lucide-react` · WebRTC data channels.

There is **no** Prisma, NextAuth, Vercel AI SDK, or Zod in this project. Do not add a dependency for something the codebase already does by hand.

## Conventions

- TypeScript throughout; no new `any`.
- Tailwind utility classes, canonical names, no arbitrary values where a token exists.
- Colours are OKLCH tokens in `app/globals.css` on shadcn's names (`primary`, `muted-foreground`, …) plus `link`, `success` and `warning`. `--primary` is the filled-button blue; use `text-link` for blue text. The system font stack is deliberate — no web fonts.
- `shadcn` rewrites `lib/utils.ts` and imports `cn` from a package called `cn` on init/add; point imports back at `@/lib/utils`.
- Shared logic in `lib/cliplink/`, UI in `components/`, routes in `app/`.
- Commit messages: imperative subject, no `Co-Authored-By` trailers.
- Run `pnpm lint && pnpm type-check && pnpm build` before declaring work done.