Install
$ agentstack add skill-stealth-factory-skills-vercel-deployment-password-gate ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ● Network access Used
- ● Filesystem access Used
- ✓ Shell / process execution No
- ● Environment & secrets Used
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Vercel deployment password gate
A free, portable password wall for preview deployments by default — and for production too, opt-in (see "Gating production too"). Humans see a brandable unlock form once (a signed 1-year cookie keeps them in); automation passes with named bypass tokens via header or query parameter. In the default preview-only posture, production ships no middleware function at all (Mode B) or a one-boolean short-circuit (Mode A), so the gate's production cost is zero. Only a scrypt hash of the password is stored; unlock cookies are keyed per-credential, so rotating the password or removing a bypass token revokes exactly the cookies it issued.
What this reimplements
A free, DIY reimplementation of Vercel's Advanced Deployment Protection add-on — $150/mo on Pro (30-day minimum before you can cancel), included on Enterprise, and apparently not sold on Hobby at all — rebuilt in one middleware file. Vercel bundles three features into the add-on — all three are supported here:
| Advanced Deployment Protection | This skill | | --- | --- | | Password Protection | ✅ Unlock form + DEPLOY_GATE_PASSWORD_HASH (scrypt), mirroring the platform's semantics: enter once per deployment URL, and changing the password invalidates the cookies it issued. One difference: Vercel's change takes effect on existing deployments immediately; ours applies to new builds, so redeploy to revoke now. | | Private Production Deployments (password on the production domain too) | ✅ Opt-in — previews by default, production via "Gating production too". Trade-off: the middleware then runs in prod, so the zero-prod-cost property is gone. | | Deployment Protection Exceptions (unprotect specific preview domains) | ✅ DEPLOY_GATE_UNPROTECTED_HOSTS — comma-separated hosts that skip the gate and are public. Same axis as Vercel's (the domain); the matcher is a different thing (path-level: /api, static assets). See "Unprotect specific domains". |
Plus an equivalent of Protection Bypass for Automation: named, individually revocable tokens, same header / query-param / set-cookie UX. This is parity, not an improvement — Vercel's own bypass also supports multiple named, individually revocable secrets ("You can create multiple bypass secrets per project", docs updated 2026-04-30), and theirs additionally clears Firewall and bot-protection challenges, which a DIY token cannot. The reason to use ours is that it works with our gate; if you're on platform protection, use theirs.
Not reimplemented: Shareable Links (a per-recipient bypass token is the closest analogue — no TTL), Trusted IPs/Passport (out of scope), and Vercel Authentication, which can't be — that session lives on vercel.com.
Two things $150/mo can't buy you. The unlock page is yours — Vercel's password screen is Vercel-branded with no documented theming hook (its whole config surface across dashboard, API, and Terraform is deploymentType + password), so matching a client's brand, logo, and design system is only possible DIY. And on Hobby, where the docs indicate the add-on isn't sold, this is very likely the only password option at all.
What the platform does better: it runs before your code (protects static assets and every route, nothing to misconfigure), it can't fail open on a missing env var, and it's Vercel's problem to maintain. This gate is a speed bump, not auth — see the gotcha of the same name.
Release-method agnostic: everything keys off the Vercel environment (VERCEL_TARGET_ENV), never branch names — it composes with any promotion/branch/deploy model.
Step 0 — check whether you need this at all
| Need | Right tool | | --- | --- | | Only the Vercel team views previews | Vercel Authentication (Deployment Protection → Standard). Free on all plans, zero code, team members pass invisibly via their Vercel login. Prefer this when it fits. | | External stakeholders | Vercel Authentication + Shareable Links (all plans; Hobby is capped at one link per account, Pro+ lifts the cap). Still zero code. | | Anyone-with-a-password, for free | This skill. Vercel's Password Protection is Enterprise-only, or Pro + $150/mo for the Advanced Deployment Protection add-on (which you must keep ≥30 days before you can cancel) — both verified against Vercel's docs 2026-07-17; re-check pricing. On Hobby it appears unbuyable (the docs list it as "Enterprise, or a paid add-on for Pro", and say Hobby gets only Vercel Authentication) — inferred from the plan listings, not stated outright, so the DIY gate is very likely the only password option there. | | The password page must carry your/your client's branding | This skill. Vercel's password screen is Vercel's — deploymentType + password is its whole documented config surface, with no theming hook. This gate renders your own HTML. | | Non-Next framework, static export, or SPA on Vercel | Still this skill — use the framework-agnostic template via Vercel Routing Middleware (see "Pick your template"), which runs platform-level before the app or static assets. |
A DIY gate cannot detect "is this visitor logged into Vercel" — that session lives on vercel.com and is only checkable by platform-level Vercel Authentication, which runs before your code. Don't try to hybridize; pick per the table.
Already on Vercel Authentication, need limited third-party access?
The DIY gate can't help here (the platform wall blocks third parties before your code runs). Use the platform's own bypass methods instead:
- Shareable Links — the purpose-built answer. Minted per deployment
URL/alias with optional TTL, individually revocable, no shared secret. Create from the deployment's Share dialog in the dashboard, or via API (PATCH /aliases/{id}/protection-bypass, ttl). Available on Hobby too, but capped at one link per account there; Pro+ lifts the cap (verified 2026-07-17).
- Protection Bypass for Automation — secrets in a crafted URL:
https:///?x-vercel-protection-bypass=&x-vercel-set-bypass-cookie=true persists a bypass cookie. You can create multiple named secrets per project, each revocable independently (docs updated 2026-04-30), and they also clear Firewall/bot-protection challenges. Caveat: secrets in URLs end up in logs, so prefer the header where the caller supports it.
Pick your template
Two templates, identical behavior, env vars, and helper scripts — pick by framework:
| Project | Template | Installs as | | --- | --- | --- | | Next.js | [templates/deploy-gate.ts](./templates/deploy-gate.ts) — no deps beyond next/server + node:crypto | proxy.ts (Mode B) or lib/deploy-gate.ts (Mode A) | | Anything else on Vercel (SvelteKit, Nuxt, Astro, Remix, static/SPA) | [templates/deploy-gate.vercel.ts](./templates/deploy-gate.vercel.ts) — uses Vercel Routing Middleware; one dep (@vercel/functions); config.runtime must stay "nodejs" (edge is the default and lacks node:crypto) | root middleware.ts, next to package.json |
The skill's mechanics (VERCEL_TARGET_ENV gating, env-var management via vercel env, build-time strip) are Vercel-platform-wide, not framework-specific.
How the gate works (both modes)
Single self-contained file:
- Gates every remote non-production Vercel deployment —
preview**and any
custom environment (e.g. a named "staging"). The signal is VERCEL_TARGET_ENV (falling back to VERCEL_ENV when absent), NOT VERCEL_ENV: VERCEL_ENV only ever reports production/preview/development and collapses every custom environment into one of those buckets, so a custom target can read VERCEL_ENV=production and slip through ungated. VERCEL_TARGET_ENV carries the custom name. It fails open when that value is production or development; when it's unset, it fails open only for a genuine local dev server (NODE_ENV === "development") or** when nothing is configured — a Vercel deploy with System Env Vars disabled also reads unset but runs NODE_ENV=production, so a configured one there still gates (see the Mode B lifecycle note).
- No valid cookie → responds
401with an inline HTML password form (no extra
routes/pages added to the app). Form POSTs to /__deploy-unlock.
- Human auth:
DEPLOY_GATE_PASSWORD_HASHstoress2::(scrypt, memory-hard)
— never the plaintext. Submitted passwords are run through scrypt and compared constant-time. (Legacy fallback: a plaintext DEPLOY_GATE_PASSWORD also works.)
- Automation auth (mimics Vercel's Protection Bypass for Automation):
DEPLOY_GATE_BYPASS_TOKENS stores JSON {"":"", ...} — plaintext by design (automation must read tokens back; they're generated random, never human-reused). Send a token via the x-deploy-gate-bypass header (passes through + sets the cookie) or query parameter (303 redirect to the cleaned URL — token stripped from the address bar — with the cookie set, so one crafted link = click-once access for a service that can't set headers). Bypass accepts tokens only — the human password never works in the header or query param: verifying a password costs a memory-hard scrypt run, so accepting it per-request would hand attackers a CPU-DoS amplifier (and passwords don't belong in URLs). The password unlocks solely via the form.
- Unlock cookies are HMACs keyed on the credential that minted them:
rotating the password kills password-issued cookies; removing a bypass token kills that token's cookies. maxAge 1 year → "unlocks once, stays unlocked".
- Absent config fails open (a fresh clone never bricks its previews);
present-but-malformed DEPLOY_GATE_PASSWORD_HASH fails CLOSED (503) — a typo must not silently publish a preview the operator meant to protect. A legacy DEPLOY_GATE_PASSWORD longer than the 256-char cap also fails closed (it would hash into a config the unlock form's length cap can never match — gated with no way in), and the 503 body names both causes. Malformed bypass-token JSON is ignored with a warning (password still works).
Why a cookie, not localStorage: the decision happens server-side in the proxy before any JavaScript runs; the token must travel with the request. localStorage physically cannot gate SSR. Same "enter once" UX.
Env-var names (and the legacy aliases)
All config vars use the DEPLOY_GATE_ prefix, because the gate protects production too — the old PREVIEW_-prefixed names implied preview-only and were misleading once production gating landed:
| Purpose | Current name | Legacy alias (still honoured) | | --- | --- | --- | | Password hash | DEPLOY_GATE_PASSWORD_HASH | PREVIEW_PASSWORD_HASH | | Plaintext password (legacy scheme) | DEPLOY_GATE_PASSWORD | PREVIEW_PASSWORD | | Automation bypass tokens | DEPLOY_GATE_BYPASS_TOKENS | PREVIEW_GATE_BYPASS_TOKENS | | Unprotected-host allowlist | DEPLOY_GATE_UNPROTECTED_HOSTS | (new — no alias) |
The gate reads the current name first and falls back to the legacy alias with a one-time warning if only the old one is set — a rename can't fail open, because absent config is intentionally fail-open (an existing install that still has PREVIEW_PASSWORD_HASH keeps working; migrate at leisure). The current name wins if both are set. To migrate, add the DEPLOY_GATE_* var and remove the PREVIEW_* one; the alias support is a courtesy, not a permanent contract.
Mode A — app already has middleware.ts / proxy.ts
The middleware function already runs on every matched request, so the gate adds one env-var boolean in production — no new invocations, no meaningful cost.
> ⚠️ If the host file is legacy middleware.ts, migrate it to proxy.ts > first (npx @next/codemod@canary middleware-to-proxy . — the tag Next's proxy docs use for this codemod, or rename the file > + the exported function and fix test imports). middleware.ts runs on the > Edge runtime even in Next 16, where node:crypto does not exist — the > gate 500s every request. Caught in a real install (piaf-web, Next 16.2.7, > 2026-07-17): Error: Failed to load external module node:crypto.
- Copy [
templates/deploy-gate.ts](./templates/deploy-gate.ts) **next to your
host proxy file** and import it relatively. If the host proxy is at the project root (proxy.ts), put the helper at lib/deploy-gate.ts; if the host uses a src/ layout (src/proxy.ts), put it at src/lib/deploy-gate.ts — a root ./lib/deploy-gate import from src/proxy.ts resolves to src/lib/… and won't find a root lib/, so the build fails.
- Wire it into the existing
proxy()/middleware()function — check first,
attach the cookie to whatever response the pipeline produces last. Pass the request protocol so the unlock cookie persists on a local http:// dev run:
```ts import { previewGate, withUnlockCookie } from "./lib/deploy-gate";
export async function proxy(request: NextRequest) { const gate = await previewGate(request); if (gate.block) return gate.block;
// Strip the bypass token from the headers forwarded upstream, so your app / // request logging never sees it (Mode B's proxy.ts does this for you). Pass // THESE cleaned headers into your pipeline — don't reuse the raw request. const headers = new Headers(request.headers); headers.delete("x-deploy-gate-bypass"); const cleaned = new NextRequest(request.nextUrl, { headers, method: request.method, body: request.body, duplex: "half", // required whenever a (stream) body is forwarded, else POSTs throw });
const response = await yourExistingLogic(cleaned); const secure = request.nextUrl.protocol === "https:"; return gate.setCookie ? withUnlockCookie(response, gate.setCookie, secure) : response; } ```
(If your host logic doesn't take a request argument — it reads globals or NextResponse.next()s — set the cleaned headers on the continue-response instead: NextResponse.next({ request: { headers } }).)
The setCookie path matters: a header-bypassed request must CONTINUE through the host pipeline (i18n redirects, rewrites, analytics cookies) — returning a bare pass-through from the gate would skip all of it (caught by review on the piaf-web install).
- Check the host matcher: it must not exclude
/__deploy-unlock, and if it
excludes /api (piaf-web's did), decide deliberately — un-gated API routes on previews are usually a hole. Include /api in the matcher and skip only the host's page-routing logic for API paths.
- Do NOT wire up the removal script — the host middleware must ship to
production for its own duties.
Mode B — app has NO middleware (strict zero prod cost)
The gate file is the app's real, checked-in proxy.ts; a build step strips it from production builds only:
- Copy [
templates/deploy-gate.ts](./templates/deploy-gate.ts) toproxy.ts
at the same level as your app/pages directory — the project root, or src/proxy.ts if the app uses a src/ directory. ⚠️ Next only loads the proxy at that level: a root proxy.ts in a src/-layout app is silently ignored, and every preview then deploys ungated even with the password set — the worst failure mode, because nothing errors. (It's checked in — it IS the middleware; lintable, typecheckable.) If the app customises pageExtensions (e.g. .page.ts), Next expects the proxy named to match — proxy.page.ts — per Next's proxy docs; a plain proxy.ts is ignored (same silent-ungate). Name the file accordingly and point the removal script's CANDIDATES at it.
- Copy [
templates/remove-proxy-on-prod.mjs](./templates/remove-proxy-on-prod.mjs)
to `scripts/remove-
…
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: stealth-factory
- Source: stealth-factory/skills
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.