关于 vercel
vercel 是面向应用测试的有状态 Vercel REST API 模拟器,提供 Vercel 风格的 JSON 响应和游标分页。它涵盖项目、部署、域名、环境变量、Blob、API 密钥及 OAuth 等接口。
主要功能
- 01模拟 Vercel REST API
- 02提供游标分页
- 03支持项目、部署和域名管理
- 04模拟环境变量与 Blob API
- 05支持团队作用域和 OAuth
适用场景
- 01测试 Vercel API 客户端
- 02验证项目创建与部署流程
- 03测试 Blob 文件操作
- 04检查 OAuth 与团队作用域请求
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ vercel-labs/ emulate --skill vercel安装后在 Claude Code 中输入/vercel确认已加载。
详细介绍
Vercel API 模拟器
全面支持状态管理的 Vercel REST API 模拟,采用 Vercel 风格的 JSON 响应和基于游标的分页。
启动
本地监听器默认绑定到 127.0.0.1。若要从容器或其他机器访问,请向 CLI 传入 --host 0.0.0.0,或向 createEmulator 传入 hostname: '0.0.0.0'。配置 --base-url 或 baseUrl,以设置这些客户端可访问的对外公布 URL。
# Vercel only
npx emulate --service vercel
# Default port
# http://localhost:4000
也可以通过编程方式启动:
import { createEmulator } from 'emulate'
const vercel = await createEmulator({ service: 'vercel', port: 4000 })
// vercel.url === 'http://localhost:4000'
身份验证
通过 Authorization: Bearer <token> 传入令牌。所有端点都接受 teamId 或 slug 查询参数,用于指定团队范围。
curl http://localhost:4000/v2/user \
-H "Authorization: Bearer test_token_admin"
团队范围的请求会根据 teamId 或 slug 查询参数解析账户。用户范围的请求会根据经过身份验证的用户解析账户。
将应用指向模拟器
环境变量
VERCEL_EMULATOR_URL=http://localhost:4000
Vercel SDK / 自定义 Fetch
const VERCEL_API = process.env.VERCEL_EMULATOR_URL ?? 'https://api.vercel.com'
const res = await fetch(`${VERCEL_API}/v10/projects`, {
headers: { Authorization: `Bearer ${token}` },
})
OAuth URL 映射
| 真实 Vercel URL | 模拟器 URL |
|---|---|
https://vercel.com/integrations/oauth/authorize | $VERCEL_EMULATOR_URL/oauth/authorize |
https://api.vercel.com/login/oauth/token | $VERCEL_EMULATOR_URL/login/oauth/token |
https://api.vercel.com/login/oauth/userinfo | $VERCEL_EMULATOR_URL/login/oauth/userinfo |
Auth.js / NextAuth.js
{
id: 'vercel',
name: 'Vercel',
type: 'oauth',
authorization: {
url: `${process.env.VERCEL_EMULATOR_URL}/oauth/authorize`,
},
token: {
url: `${process.env.VERCEL_EMULATOR_URL}/login/oauth/token`,
},
userinfo: {
url: `${process.env.VERCEL_EMULATOR_URL}/login/oauth/userinfo`,
},
clientId: process.env.VERCEL_CLIENT_ID,
clientSecret: process.env.VERCEL_CLIENT_SECRET,
profile(profile) {
return {
id: profile.sub,
name: profile.name,
email: profile.email,
image: profile.picture,
}
},
}
Seed 配置
tokens:
test_token_admin:
login: admin
scopes: []
vercel:
users:
- username: developer
name: Developer
email: [email protected]
teams:
- slug: my-team
name: My Team
description: Engineering team
projects:
- name: my-app
team: my-team
framework: nextjs
buildCommand: next build
outputDirectory: .next
rootDirectory: null
nodeVersion: "20.x"
envVars:
- key: DATABASE_URL
value: postgres://localhost/mydb
type: encrypted
target: [production, preview]
integrations:
- client_id: oac_abc123
client_secret: secret_abc123
name: My Vercel App
redirect_uris:
- http://localhost:3000/api/auth/callback/vercel
分页
使用 limit、since 和 until 查询参数进行基于游标的分页。响应包含一个 pagination 对象:
curl "http://localhost:4000/v10/projects?limit=10" \
-H "Authorization: Bearer $TOKEN"
API 端点
用户与团队
# Registration check
curl http://localhost:4000/registration
# Authenticated user
curl http://localhost:4000/v2/user -H "Authorization: Bearer $TOKEN"
# Update user
curl -X PATCH http://localhost:4000/v2/user \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "New Name", "email": "[email protected]"}'
# List teams (cursor paginated)
curl http://localhost:4000/v2/teams -H "Authorization: Bearer $TOKEN"
# Get team (by ID or slug)
curl http://localhost:4000/v2/teams/my-team -H "Authorization: Bearer $TOKEN"
# Create team
curl -X POST http://localhost:4000/v2/teams \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"slug": "new-team", "name": "New Team"}'
# Update team (name, slug, description)
curl -X PATCH http://localhost:4000/v2/teams/my-team \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "Updated Team", "description": "New description"}'
# List members
curl http://localhost:4000/v2/teams/my-team/members -H "Authorization: Bearer $TOKEN"
# Add member (by uid or email, with role)
curl -X POST "http://localhost:4000/v2/teams/team_id/members" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"email": "[email protected]", "role": "MEMBER"}'
角色:OWNER、MEMBER、DEVELOPER、VIEWER。
项目
# Create project (with optional env vars, git, and build config)
curl -X POST http://localhost:4000/v11/projects \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "my-app", "framework": "nextjs", "buildCommand": "next build", "outputDirectory": ".next", "nodeVersion": "20.x", "environmentVariables": [{"key": "API_KEY", "value": "secret", "type": "encrypted", "target": ["production"]}]}'
# List projects (search, cursor pagination)
curl "http://localhost:4000/v10/projects?search=my-app" \
-H "Authorization: Bearer $TOKEN"
# Get project (includes env vars)
curl http://localhost:4000/v9/projects/my-app \
-H "Authorization: Bearer $TOKEN"
# Update project (framework, buildCommand, devCommand, installCommand,
# outputDirectory, rootDirectory, nodeVersion, serverlessFunctionRegion,
# publicSource, autoAssignCustomDomains, gitForkProtection,
# commandForIgnoringBuildStep)
curl -X PATCH http://localhost:4000/v9/projects/my-app \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"framework": "remix"}'
# Delete project (cascades deployments, domains, env vars, protection bypasses)
curl -X DELETE http://localhost:4000/v9/projects/my-app \
-H "Authorization: Bearer $TOKEN"
# Promote aliases status
curl http://localhost:4000/v1/projects/my-app/promote/aliases \
-H "Authorization: Bearer $TOKEN"
# Protection bypass: generate, revoke, regenerate
curl -X PATCH http://localhost:4000/v1/projects/my-app/protection-bypass \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"generate": {"note": "CI preview", "scope": "deployment"}}'
# Revoke protection bypass secrets
curl -X PATCH http://localhost:4000/v1/projects/my-app/protection-bypass \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"revoke": ["secret_to_revoke"]}'
# Regenerate protection bypass secrets
curl -X PATCH http://localhost:4000/v1/projects/my-app/protection-bypass \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"regenerate": ["old_secret"]}'
部署
# Create deployment (auto-transitions to READY)
curl -X POST http://localhost:4000/v13/deployments \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "my-app", "target": "production", "meta": {"commit": "abc123"}, "regions": ["iad1"], "gitSource": {"type": "github", "ref": "main", "sha": "abc123", "repoId": "123", "org": "my-org", "repo": "my-app", "message": "Deploy", "authorName": "dev", "commitAuthorName": "dev"}}'
# Targets: "production", "preview", "staging"
# Get deployment (by ID or URL)
curl http://localhost:4000/v13/deployments/dpl_abc123 \
-H "Authorization: Bearer $TOKEN"
# List deployments (filter by projectId, app, target, state; cursor paginated)
curl "http://localhost:4000/v6/deployments?projectId=my-app&target=production&limit=10" \
-H "Authorization: Bearer $TOKEN"
# List deployments (filter by commit SHA)
curl "http://localhost:4000/v7/deployments?sha=abc123" \
-H "Authorization: Bearer $TOKEN"
# Delete deployment
curl -X DELETE http://localhost:4000/v13/deployments/dpl_abc123 \
-H "Authorization: Bearer $TOKEN"
# Cancel building deployment
curl -X PATCH http://localhost:4000/v12/deployments/dpl_abc123/cancel \
-H "Authorization: Bearer $TOKEN"
# List deployment aliases
curl http://localhost:4000/v2/deployments/dpl_abc123/aliases \
-H "Authorization: Bearer $TOKEN"
# Get build events/logs (supports direction, limit)
curl "http://localhost:4000/v3/deployments/dpl_abc123/events?direction=forward&limit=50" \
-H "Authorization: Bearer $TOKEN"
# List deployment files
curl http://localhost:4000/v6/deployments/dpl_abc123/files \
-H "Authorization: Bearer $TOKEN"
# Upload file (by SHA digest)
curl -X POST http://localhost:4000/v2/files \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/octet-stream" \
-H "x-vercel-digest: sha256hash" \
--data-binary @file.txt
域名
# Add domain (with optional redirect, gitBranch, customEnvironmentId)
curl -X POST http://localhost:4000/v10/projects/my-app/domains \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "example.com", "redirect": null, "redirectStatusCode": null, "gitBranch": null}'
# *.vercel.app domains are auto-verified
# List domains (cursor paginated)
curl http://localhost:4000/v9/projects/my-app/domains \
-H "Authorization: Bearer $TOKEN"
# Get, update, remove domain
curl http://localhost:4000/v9/projects/my-app/domains/example.com \
-H "Authorization: Bearer $TOKEN"
# Verify domain
curl -X POST http://localhost:4000/v9/projects/my-app/domains/example.com/verify \
-H "Authorization: Bearer $TOKEN"
重定向状态码:301、302、307、308。
环境变量
# List env vars (with decrypt option; filter by gitBranch, customEnvironmentId)
curl "http://localhost:4000/v10/projects/my-app/env?decrypt=true" \
-H "Authorization: Bearer $TOKEN"
# Create env vars (single, batch, or upsert)
curl -X POST "http://localhost:4000/v10/projects/my-app/env?upsert=true" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"key": "API_KEY", "value": "secret123", "type": "encrypted", "target": ["production", "preview"], "comment": "API key for service"}'
# Get env var
curl http://localhost:4000/v10/projects/my-app/env/env_abc123 \
-H "Authorization: Bearer $TOKEN"
# Update env var
curl -X PATCH http://localhost:4000/v9/projects/my-app/env/env_abc123 \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"value": "newsecret"}'
# Delete env var
curl -X DELETE http://localhost:4000/v9/projects/my-app/env/env_abc123 \
-H "Authorization: Bearer $TOKEN"
环境变量类型:system、encrypted、plain、secret、sensitive。
Blob
实现 @vercel/blob SDK 使用的 Vercel Blob API(put、head、list、del)。设置两个环境变量,即可将 SDK 指向模拟器:
VERCEL_BLOB_API_URL=http://localhost:4000/api/blob
BLOB_READ_WRITE_TOKEN=vercel_blob_rw_mystore_secret
接受任何形式为 vercel_blob_rw_<storeId>_<secret> 的令牌;存储 ID 会从令牌中解析。
import { put, head, list, del } from '@vercel/blob'
const blob = await put('avatars/user.png', data, { access: 'public' })
// blob.url serves the bytes from the emulator
await head(blob.url)
await list({ prefix: 'avatars/' })
await del(blob.url)
直接使用 HTTP:
BLOB_TOKEN="vercel_blob_rw_mystore_secret"
# Upload (honors x-add-random-suffix, x-allow-overwrite, x-content-type,
# x-cache-control-max-age, x-if-match headers)
curl -X PUT "http://localhost:4000/api/blob?pathname=docs/readme.txt" \
-H "Authorization: Bearer $BLOB_TOKEN" \
--data-binary @readme.txt
# Copy (x-if-match is compared with the source blob's ETag;
# an existing destination is always replaced)
curl -X PUT "http://localhost:4000/api/blob?pathname=docs/copy.txt&fromUrl=docs/readme.txt" \
-H "Authorization: Bearer $BLOB_TOKEN"
# Metadata (head)
curl "http://localhost:4000/api/blob?url=docs/readme.txt" \
-H "Authorization: Bearer $BLOB_TOKEN"
# List (prefix, limit, cursor, mode=folded; folded pages share one
# sorted cursor across folders and root blobs)
curl "http://localhost:4000/api/blob?prefix=docs/" \
-H "Authorization: Bearer $BLOB_TOKEN"
# Delete
curl -X POST http://localhost:4000/api/blob/delete \
-H "Authorization: Bearer $BLOB_TOKEN" \
-H "Content-Type: application/json" \
-d '{"urls": ["docs/readme.txt"]}'
# Start a multipart upload (returns { key, uploadId })
curl -X POST "http://localhost:4000/api/blob/mpu?pathname=docs/large.bin" \
-H "Authorization: Bearer $BLOB_TOKEN" \
-H "x-mpu-action: create"
# Serve content (public, no auth; ?download=1 forces attachment)
curl http://localhost:4000/blob/mystore/docs/readme.txt
上传和删除规则:
- 路径名前导的单个斜杠会被移除,因此
/docs/readme.txt会存储为docs/readme.txt。与真实服务一样,HTTP 请求中的//仍可使用,因此//a.txt会存储为/a.txt(@vercel/blobSDK 会在发送请求前拒绝//)。此规则同样适用于复制目标和分段上传。 - 内容类型依次取自
x-content-type、已知类型的路径名扩展名、请求的Content-Type标头,最后是application/octet-stream。 x-cache-control-max-age最小值为 60 秒(0会存储为public, max-age=60)。未提供或值无效时,默认值为 30 天。- 使用缺失或为空的
urls数组进行删除时,会返回 400Missing urls in request body。JSON 格式错误时会返回 400Cannot delete more than 1,000 urls at once or body is malformed;urls超过 1,000 个时会返回 400Cannot delete more than 1,000 urls at once;包含任何非字符串条目时会返回 400Some urls are malformed。以上情况均不会删除任何内容。
可以创建分段上传,但目前不支持上传分段和完成上传,这些操作会返回 400。目前不支持客户端(浏览器)上传。
API 密钥
# Create API key (optional teamId scope)
curl -X POST "http://localhost:4000/v1/api-keys?teamId=team_abc123" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "CI Deploy Key"}'
# List API keys (optional teamId filter)
curl "http://localhost:4000/v1/api-keys?teamId=team_abc123" \
-H "Authorization: Bearer $TOKEN"
# Delete API key
curl -X DELETE http://localhost:4000/v1/api-keys/ak_abc123 \
-H "Authorization: Bearer $TOKEN"
创建的 API 密钥会自动注册到令牌映射中,可作为 Bearer 令牌用于所有端点。
OAuth / 集成
# Authorize (browser flow, shows user picker)
# GET /oauth/authorize?client_id=...&redirect_uri=...&scope=...&state=...
# Token exchange (supports PKCE; accepts JSON or form-urlencoded)
curl -X POST http://localhost:4000/login/oauth/token \
-H "Content-Type: application/json" \
-d '{"client_id": "oac_abc123", "client_secret": "secret_abc123", "code": "<code>", "redirect_uri": "http://localhost:3000/api/auth/callback/vercel"}'
# User info (returns sub, email, email_verified, name, preferred_username, picture)
curl http://localhost:4000/login/oauth/userinfo \
-H "Authorization: Bearer $TOKEN"
常见模式
创建项目并部署
TOKEN="test_token_admin"
BASE="http://localhost:4000"
# Create project
curl -X POST $BASE/v11/projects \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "my-app", "framework": "nextjs"}'
# Add env var
curl -X POST $BASE/v10/projects/my-app/env \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"key": "DATABASE_URL", "value": "postgres://...", "type": "encrypted", "target": ["production"]}'
# Create deployment
curl -X POST $BASE/v13/deployments \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "my-app", "target": "production"}'
OAuth 集成流程
- 将用户重定向到
$VERCEL_EMULATOR_URL/oauth/authorize?client_id=...&redirect_uri=...&state=... - 用户在模拟器的界面中选择一个预置用户
- 模拟器重定向回来,并附带
?code=...&state=... - 通过
POST /login/oauth/token交换代码以获取令牌 - 通过
GET /login/oauth/userinfo获取用户信息
支持 PKCE。在授权请求中传递 code_challenge 和 code_challenge_method,然后在令牌交换时传递 code_verifier。
团队范围的请求
所有端点都接受 teamId 或 slug 查询参数:
curl "http://localhost:4000/v10/projects?teamId=team_abc123" \
-H "Authorization: Bearer $TOKEN"
curl "http://localhost:4000/v10/projects?slug=my-team" \
-H "Authorization: Bearer $TOKEN"
与内置模拟器一起使用自定义模拟器
使用 npx emulate init --custom inventory 搭建第三方 API 模拟器并进行测试。使用 emulate 中的 defineConfig 在 emulate.config.ts 中注册它,也可以同时注册 { emulator: "vercel" } 等内置条目。运行 npx emulate start --watch 可重新加载导入项,并在打印出的 /_emulate URL 中检查自定义状态。重新加载成功后,会将运行重置为种子数据。现有的扁平种子配置仍然有效;--config 用于选择指定文件。有关编写和测试第三方 API 模拟器的信息,请参阅 https://emulate.dev/docs/custom-emulators。