@broberg/ apikey (英语)
框架不可知性 输入 API- key 原始 为了兄弟会的舰队 它拥有危险到错误的位点——薄荷,常时核查,限速,和云浮式的授权级联——并离开 存储、租赁和请求文本解决方案带上你自己的 lookup。 。 。 .
由9-repo分舰队勘测(trail ^ carmem → cms → upmethers → vn)所设计:包裹从不强迫散发,租借模式,固定前缀,或限速后端.
npm i @broberg/apikey # exact-pin for prod-auth deps
核心项目@broberg/apikey(中文(简体) )
import { generateKey, hashKey, verifyKey, makeKeyPreview, hasScope } from "@broberg/apikey";
const raw = generateKey("trail"); // "trail_<64 hex>" — show ONCE
const stored = hashKey(raw); // sha256 — store this (hash-at-rest)
const preview = makeKeyPreview(raw); // "trail_0a1b2c3d" — display/grep anchor
// On each request — YOU do the DB read; the package does the constant-time compare:
verifyKey(presented, stored); // hashed (default): timingSafeEqual(sha256(presented), stored)
verifyKey(presented, stored, { hashed: false }); // plaintext-revealable (upmetrics-style)
hasScope(["content:*"], ["content:write"]); // true — exact / `*` / `area:*`
timingSafeEqual(a, b) 也导出 a !== b 代币支票.
速率限制——可插件存储
默认内相(单机). 对a 无国籍的多机 车队,通过一个共享商店,这样窗口就不会每台机器漏出:
import { SlidingWindowRateLimiter, type RateLimitStore } from "@broberg/apikey";
const limiter = new SlidingWindowRateLimiter({ windowMs: 60_000, max: 100 });
const { allowed, remaining, resetAt } = await limiter.check(clientKey);
// One limiter, per-key caps — pass a per-check `max` override (v0.1.1):
await limiter.check(clientKey, { max: keyRecord.rateLimitPerHour });
// Shared backend (Turso/Redis): implement one method.
const turso: RateLimitStore = {
async hit(key, now, windowMs) { /* … */ return { count, oldest }; },
};
new SlidingWindowRateLimiter({ windowMs: 60_000, max: 100, store: turso });
授权级联@broberg/apikey/authorize(中文(简体) )
可选的丰富级——允许相×资源-过滤相×CIDR相×TTL(型号为cms F134). 简单领养者跳过此选项并使用 hasScope。 。 。 .
import { evaluateToken, type TokenGrant } from "@broberg/apikey/authorize";
const grant: TokenGrant = {
permissions: ["deploy:trigger"],
resources: [{ scope: "site", effect: "include", targets: ["fysiodk"] }],
ipFilters: [{ mode: "in", cidrs: ["203.0.113.0/24"] }],
notBefore: Date.parse("2026-01-01"),
notAfter: Date.parse("2027-01-01"),
};
const decision = evaluateToken(grant, {
permission: "deploy:trigger",
resource: { scope: "site", target: "fysiodk" },
ip: "203.0.113.5",
});
// → { allowed: true } | { allowed: false, reason: "expired" | "permission_denied" | "resource_denied" | "ip_denied" }
连带命令: TTL ~ 权限 ~ 资源 (排除胜负+ CIDR(IPv4 + IPv6,0-dep). 没有过滤器的瞄准镜不受限制 .
租户选择者(铁路选择者-非赠与者)
import { selectTenant, TenantAccessError } from "@broberg/apikey/authorize";
// A `spansAll` key lets the owner pick any tenant they belong to via a header.
// A non-member slug is a HARD refuse — never a silent fall-back to home.
try {
const tenant = selectTenant({ requestedSlug, homeTenant, spansAll: true, isMember });
} catch (e) {
if (e instanceof TenantAccessError) return new Response(null, { status: 401 });
}
适配器
// Stack B — Hono
import { honoApiKeyMiddleware, honoRateLimit } from "@broberg/apikey/hono";
app.use("/api/*", honoApiKeyMiddleware({ lookup, authorize })); // 401/403; c.get("apiKey")
app.use("/api/*", honoRateLimit(limiter)); // 429 + Retry-After
// Stack A — Next.js (Web-standard Request/Response, edge-safe, no `next` dep)
import { withApiKeyAuth, nextRateLimit } from "@broberg/apikey/next";
export const POST = withApiKeyAuth(async (req, record) => Response.json({ ok: true }), { lookup });
你的错误合同,不是我们的(v0.2.0)
默认机构是 { error: "missing_api_key" } ——a. 国家 字符串时
API回答其他问题(如:. { error: { code, message } },该机器
客户端可以切换, 让它自己而不是重写中间软件 :
app.use("/api/*", honoApiKeyMiddleware({
lookup,
authorize,
onUnauthorized: (c, reason) => // reason: "missing" | "invalid"
c.json({ error: { code: `unauthorized_${reason}` } }, 401),
onForbidden: (c, record) =>
c.json({ error: { code: "forbidden", at: record.id } }, 403),
}));
app.use("/api/*", honoRateLimit(limiter, keyFn, {
onLimited: (c, r) => c.json({ error: { code: "rate_limited", retryAt: r.resetAt } }, 429),
}));
中间的软件仍然决定 什麽 发生了;只有钩子决定它是如何
写下来。 状态代码和 X-RateLimit-Remaining 编号 : Retry-After
标题不变。 解开钩子和反应 字节相同
(原始内容存档于2013-10-01). v. 1.1.
reason 值得采取行动的是: 401 (中文(简体) ) 表示 "我不知道你是谁" (无)
指使,未知指使,刚被撤销),通过取出指使来固定; 403 (韩语)
表示 "我知道你到底是谁, 这不是你的" 由
请求不同的角色。 反应是唯一能告诉
打电话的.
限速桶键. 没有
keyFn,在x-forwarded-for后退为"unknown"当信头缺席时。 在一个 在前面没有代理的回放服务意味着 每一个呼叫者份额 一个桶 - 通过一个keyFn代之以代号/记录id上的密钥.
每条路线的上限不同,1个限制(v0.3.0)
keyFn 可能返回 { key, max } 而不是一个赤裸的字符串,所以一个单一的限定器
每个路由执行不同的上限——您不需要每个上限的限制实例 :
app.use("/admin/*", honoRateLimit(limiter, (c) => ({ key: tokenId(c), max: 600 })));
app.use("/write/*", honoRateLimit(limiter, (c) => ({ key: tokenId(c), max: 300 })));
两套适配器 X-RateLimit-Limit 以及 X-RateLimit-Remaining —— 说
Limit 报告 有效 该请求的上限(每条路线) max 何时
),因为你无法解释的剩余数并不多用.
RateLimitResult.limit 直接驱动限制器时带相同的号码.
您的处理器( v0. 3. 0)
默认情况下 整体 查询记录是什么 c.set(contextKey, …) 存储
(Hono)和作为处理器的第二论点(Next)——包括:
无论你的存储行携带,例如 hash 栏。 大麻不是一个
可用证书,所以这不是证书泄露,但不止是
处理器需要,一个 c.json(caller) 后来它成为一个反应机构.
过 project 选择自己形状的呼叫者:
honoApiKeyMiddleware({ lookup, project: (r) => ({ id: r.id, role: r.role }) });
withApiKeyAuth(handler, { lookup, project: (r) => ({ id: r.id, role: r.role }) });
适配器不对称,明确声明: 页:1
onUnauthorized编号 :onForbidden编号 :onLimited上面有钩子 浩诺 仅限适配器. 下一个适配器 仍然释放内置的字符串体。 将问题归档是正确之举,如果 你需要他们在那里.
如果你把自己的429个尸体 引用同样的话
result。 。 。 . 信头来了 从中间的软件 和身体来自你onLimited,所以他们是 在两个地方生产。 打电话的人说 "每60个1" 在体内X-RateLimit-Limit说600无法判断该相信什么。 使用result.limit编号 :result.resetAt在身体中,而不是一个常数 你持有 - 包不能为您断言这一点,所以值得测试 你那边.
Retry-After 下层为 1 (自v.3.1起):与一个共享远程商店,该商店
绕行超过剩余窗口, 原始计算可能为负数
发射 Retry-After: 0 - 让客户重新尝试 马上 还在
有限.
lookup(presented) => record | null 是您的: 散列 + DB/ 文件系统读取, 您的存储, 您的租赁 。 包裹没看到你的店.
边界(它故意不做的事)
- 无存储 – 没有 DB/ CRUD 层; 您拥有 schema( Drizzle / libSQL / JSON) .
- 无要求-租户解决 - 这是你的代理/路由器;将结果输入
selectTenant。 。 。 . - 无捆绑的 Redis/ Turso - 船舶
RateLimitStore接口 + 仅内存. - 核心加密为节点/ Bun (单位:千美元)
node:crypto) (中文(简体) ). 在边缘处,通过网络加密的散列lookup;适配器本身是边缘安全.
麻省理工学院的一部分 broberg.ai 共享库存。 。 。 .