# site-pin-gate (Agent Skill)

> A shared-PIN gate in front of a whole Next.js site, from the proxy layer: one environment variable arms it, an unlock page sets an HMAC cookie, and the app behind it stays untouched.

Page: https://timerise.ai/skills/site-pin-gate

Agent Skill `site-pin-gate`: https://github.com/timerise-ai/site-pin-gate. Category: Infrastructure & security. Runs on Next.js; backend: No data store, one cookie; attempt store, locale resolver and bypass list behind seams.

Latest release: v0.5.0 (2026-10-15).

Install: npx skills add timerise-ai/site-pin-gate. Timerise builds it on a brief; the price comes from the brief.

A site that is not ready for the public still has to be seen: by the client, by investors, by a reviewer. User accounts are too much for that. A shared PIN is enough, as long as the gate itself is safe.

The dangerous parts of a PIN gate are small and easy to get wrong: where the unlock page redirects, and how the cookie is made. An open redirect or a guessable cookie turns a locked preview into a public one.

This skill puts the gate in front of the whole site from the proxy layer. One environment variable arms it. The unlock page sets a cookie derived from a server-side secret. Comparisons run in constant time, failed attempts get a budget, and the app behind it never knows the gate exists.

Put a shared-PIN gate in front of an entire Next.js site from the proxy/middleware layer: one env var arms it, a self-contained unlock page sets an HMAC-derived HttpOnly cookie, and the app behind it stays untouched. Use when: (1) a pre-launch, staging, client-preview or investor-preview site must be hidden from the public and from crawlers without adding user accounts, (2) an existing PIN or password gate in middleware.ts or proxy.ts needs auditing, (3) the user mentions: site PIN, password-protect the site, access code, pre-launch gate, coming-soon lock, staging password, SITE_PIN, "hide the site behind a PIN", "lock the preview deployment", "temporary password page", "keep webhooks working". Carries the return path resolved against the request origin, the 303 unlock redirect, the cookie token keyed by a server-side secret, constant-time comparisons, an attempt budget answering 429 with Retry-After, and the suite that holds them. Next.js App Router, proxy.ts or middleware.ts; the attempt store, locale resolver and a webhook bypass list are seams. Not user auth, not per-route authorization.

This skill was written by the engineer who has shipped this module. The earlier implementation it was audited against was a temporary gate on a marketing site kept private ahead of its launch. The templates hold the properties a gate has to hold: an unlock that can only return to the site's own origin, a redirect the browser follows as a GET so the PIN is never sent twice, a cookie that is opaque to anyone without the server's secret, comparisons that take the same time whatever the input, a request body that is answered rather than thrown, and a failed-attempt budget that answers 429. The behaviour contract and the handler suite state each one; [`references/provenance.md`](references/provenance.md) has the record.

What it builds:
- One env var arms it
- An unlock page sets an HMAC cookie
- Origin-resolved return path
- Attempt budget answering 429
- A bypass list that keeps webhooks and cron working
- The host's own locale resolver with right-to-left layout

When to use:
- A public-by-default Next.js site that must be hidden for a while: before launch, during a client or investor review, on a staging domain. Everyone who should see it can be told one PIN. The gate sits in `proxy.ts` (Next 16) or `middleware.ts` (Next 13 to 15) and is removed by unsetting one env var.

Non-negotiables:
- The return path is resolved against the request origin , never checked with `startsWith('/')`. A path that begins with a slash can still leave the site, even after one resolve (`/a/..//evil.example` normalises to `//evil.example`), so the sanitiser resolves the value, compares origins and checks the result again. Five off-origin path cases are in the suite.
- The unlock redirect is a 303. A 307 tells the browser to repeat the request with its method and body, so the PIN would be posted again to the landing page, and once more through any locale redirect. The status is asserted on the success path.
- The cookie is an HMAC keyed by a server-side secret , not the PIN and not an unkeyed hash of it. A cookie an operator can read is a cookie an operator's laptop can leak. A token derived without the secret is rejected in the suite.
- Both comparisons are constant-time over equal-length digests, so neither the PIN check nor the cookie check leaks its answer through how long it took.
- A body that is not a form is answered, never thrown. `request.formData()` rejects anything that is not form-encoded, and an unhandled rejection in the proxy is a stack trace instead of a rejected attempt. The 400 path is in the suite.
- The matcher is chosen on purpose, and the unlock path sits outside every locale prefix and app route. Whatever the matcher excludes is public, and so is whatever a bypass list beside a shared matcher names, including the sitemap and OG images unless you say otherwise. Build assets stay outside it: gating `_next/static` hides nothing from a crawler, which never holds the cookie. A colliding unlock path would never render, because the gate answers it before routing.

Not this:
- Real users with real accounts: The host's auth: Clerk, NextAuth, Supabase Auth. A shared PIN cannot be revoked for one person
- Per-route or per-tenant authorization: The host's authorization layer; this gate is all-or-nothing by path matcher
- Locking preview deployments for team members on Vercel, with no webhooks to receive: Vercel Deployment Protection, which does it with no code. It refuses webhooks and other machine callers; the comparison is in `references/operations.md`
- Protecting an API consumed by machines: An API key; a cookie and an HTML form are the wrong shape

What to tell your agent:
- Hide this whole site behind a PIN until launch. One environment variable turns it on, and search engines must not index anything while it is on.
- Lock our staging deployment for a client review with a shared PIN, and slow down anyone who keeps guessing wrong.
- Audit the password gate in our middleware.ts.

Agent eval results (newest per agent and prompt):
- Gemini CLI (gemini-3.8-flash) on v0.5.0, 2026-10-15: pass (No data store)
- Codex CLI (gpt-6.1-sol) on v0.5.0, 2026-10-15: pass (No data store)
- Claude Code (claude-opus-5-5) on v0.5.0, 2026-10-15: pass (No data store)

Recent releases:
- v0.5.0 (2026-10-15): cookieMaxAgeSeconds: null issues a session cookie: no Max-Age, so the browser drops it on close and the PIN is asked again on the next browser session. The 30-day default is unchanged. adaptation.md carries the variant and what a session cookie does not do (open windows, session restore, cookies already issued); one handler test, and the unlock test now asserts the default Max-Age. The suite is 41 tests.
- v0.4.0 (2026-10-05): From applying the skill to a second host: the staging site of a multi-vendor marketplace, with an existing proxy, nine UI locales including Arabic, and inbound webhooks from six kinds of provider. provenance.md records it as source B.
- v0.3.7 (2026-09-27): Fix release, from scoring the prompt-1 agent eval runs against 0.3.6.
