一句话:密钥保险库保存你的应用需要、但绝不能下发到浏览器的私有密钥——Stripe secret key、Resend / Twilio / ElevenLabs 的 API key,以及任何第三方凭证。你把每一个都用环境变量风格的 NAME 存储一次;你的函数在运行时于服务端以 ctx.secrets.NAME 读取它。浏览器永远看不到这个值。密钥用 AES-256-GCM 静态加密,主密钥存放在服务器环境里(而非数据库),控制台只列出名称——值是写入即不可回读的。
这和 anon key(匿名密钥)互为对照。anon key 是设计上就公开的——它会出现在你的客户端打包产物里,由行级安全负责守门。密钥则恰恰相反:它们正是那些必须保持私密的东西,而保险库的存在就是为了让这个值只活在服务端,由函数使用。一旦你发现自己想把厂商的 secret key 放到浏览器够得着的地方,答案几乎总是「把它放进保险库,改从函数里读取」。
大多数应用最终都会用到一个不能暴露的凭证。要扣款,你需要你的 Stripe secret key。要发送事务性邮件,你需要一个 Resend key。要发短信,你需要一个 Twilio token;要合成语音,需要一个 ElevenLabs key。这些都不能下发到浏览器——任何打开开发者工具的人都能拿到它们,进而花你的钱、或冒充你发邮件。
保险库通过给每个私有密钥在服务器上安一个家来解决这个问题。你存储它一次,你的服务端代码在运行时读取它,它永远不会越过线路传到客户端。这就是全部的思路:这个值只存在于一个浏览器看不见的地方。
每个密钥都以环境变量风格的 NAME(大写字母、数字和下划线——例如 STRIPE_SECRET_KEY)和一个 value(值)存储。这个值用 AES-256-GCM 静态加密。加密主密钥存放在服务器环境里,而非数据库中,这带来两个值得记牢的后果:
每个后端最多 32 个密钥。每个值最大 16 KB。名称必须匹配 [A-Z][A-Z0-9_]{0,63}——以一个大写字母开头,后面再跟最多 63 个大写字母、数字或下划线。
密钥在 Cloud 控制台里管理——仅限所有者——位于 lingcode.dev/backends.html → 你的后端 → Secrets。在那里你可以添加、更新和删除密钥。
有一点要提前知道:列表只会显示名称。一旦存储,值就永远不会被返回——你可以覆盖一个密钥,但无法把它读回来。如果你忘了某个值是什么,重新设置它,而不是试图把它找回来。
底层是一组以所有者身份认证的端点,方便你对它们写脚本:
GET /api/cloud/account/backends/<backend-id>/secrets # 仅列出名称(NAMES)
PUT /api/cloud/account/backends/<backend-id>/secrets/<KEY> # 添加或更新
DELETE /api/cloud/account/backends/<backend-id>/secrets/<KEY> # 删除
# PUT 请求体:
{ "value": "sk_live_..." }
设置密钥只完成了一半——重点是用上它。密钥在服务端从函数里读取,而不在其他任何地方。这有三种展开方式。
每个内置函数都声明它需要哪些密钥名称,然后在你调用时于服务端读取它们。你设置密钥,再调用内置函数——你从不自己传入这个 key。例如:
stripe-checkout 需要 STRIPE_SECRET_KEYresend-byo 需要 RESEND_API_KEYelevenlabs-tts 需要 ELEVENLABS_API_KEYtwilio-sms 需要 TWILIO_ACCOUNT_SID 和 TWILIO_AUTH_TOKEN所以收一笔款的端到端闭环是:在控制台里设置 STRIPE_SECRET_KEY,然后调用 stripe-checkout 内置函数。内置函数在服务器上读取这个密钥;你的客户端只会看到它返回的 checkout URL。
如果你编写自己的自定义函数,密钥会作为 ctx.secrets 出现在处理函数的上下文里:
export default async function handler(input, ctx) {
const key = ctx.secrets.STRIPE_SECRET_KEY;
// 用 key 调用厂商 API ……
return { ok: true };
}
和内置函数是同一套模型,只是改成手写:你在控制台里设置 STRIPE_SECRET_KEY,当函数在服务器上运行时,它就以 ctx.secrets.STRIPE_SECRET_KEY 的形式出现。这个值永远不会离开沙箱。
http-fetch 内置函数可以用 {{SECRET_NAME}} 占位符语法,把一个密钥插值进请求头或请求体。这样你就能向任意一个在白名单内的 API 发起带认证的请求——完全无需编写自定义函数:
{
"url": "https://api.example.com/v1/things",
"method": "GET",
"headers": {
"Authorization": "Bearer {{EXAMPLE_API_KEY}}"
}
}
{{EXAMPLE_API_KEY}} 占位符会在请求发出之前于服务端替换成密钥的值。浏览器发送的是请求的形状,而非这个 key。
保险库防范两类具体威胁。它抵御数据库外泄——主密钥不在数据库里,所以一份被盗的导出不会交出你的密钥。它也抵御篡改——GCM 认证标签意味着被修改过的密文会解密失败,而不是返回一个被破坏的值。
它在 v1 里不做的事:存在一个单一的部署级主密钥。没有按用户派生密钥,也没有内置的密钥轮换。把保险库当作厂商密钥的正确归宿——它确实是——而如果某个 key 泄露了,请到厂商那边轮换它(签发一个新的 Stripe key、吊销旧的),再到控制台里更新这个密钥。