unified-keys 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,12 +5,22 @@
5
5
  ## 特性
6
6
 
7
7
  - ✅ **明文可重复显示**:list/get 返回完整明文(Unkey 只给前缀)
8
- - ✅ **完整使用记录**:每次 verify/refund/create/delete 写 usage_log(Unkey 无)
9
- - ✅ **verify 原子扣减**:`UPDATE ... WHERE credits>0 RETURNING`,并发安全
8
+ - ✅ **完整使用记录**:每次 check/verify/refund/create/delete 写 usage_log(Unkey 无)
9
+ - ✅ **check / verify 分离**:check 公开只读校验,verify 管理(带 rootKey)扣减
10
+ - ✅ **原子扣减**:`UPDATE ... WHERE credits>0 RETURNING`,并发安全
10
11
  - ✅ **多 app 命名空间**:appId(slug)区分项目,首次创建 key 自动注册
11
12
  - ✅ **零依赖**:纯 fetch,Worker/Node/Next.js 通用
12
13
  - ✅ **TS 强类型**
13
14
 
15
+ ## 0.6.0 破坏性变更(安全修复)
16
+
17
+ `autoDelete` 和 `decrement` 不再由 verify 调用方传参——以前公开 verify 接受这两个参数,任何拿到 key 明文的人都能删别人的 key 或白嫖额度。现在:
18
+
19
+ - **`autoDelete`** 移到 `createKey`(预设为 key 属性),verify 扣减到 0 时按 key 自身策略删
20
+ - **`POST /v1/keys/check`**(公开,无鉴权):纯只读校验,不扣减、不删。探活 / 前端校验用这个
21
+ - **`POST /v1/keys/verify`**(管理,需 `Bearer KEYS_ROOT_KEY`):扣减 + autoDelete 读 key 属性
22
+ - 顶层 `verifyKey()` 保留但 `@deprecated`,内部改调 check(只读);扣减请用 `client.verifyKey()`
23
+
14
24
  ## 安装
15
25
 
16
26
  ```bash
@@ -20,67 +30,84 @@ pnpm add unified-keys
20
30
  ## 快速开始
21
31
 
22
32
  ```ts
23
- import { createClient, verifyKey } from "unified-keys";
33
+ import { createClient, checkKey } from "unified-keys";
24
34
 
25
35
  const client = createClient({
26
36
  baseUrl: "https://keys.932324.xyz",
27
37
  rootKey: process.env.KEYS_ROOT_KEY,
28
38
  });
29
39
 
30
- // 创建 key(明文返回)
40
+ // 创建 key(明文返回;autoDelete 预设为 key 属性)
31
41
  const { keys } = await client.createKey({
32
42
  appId: "my-app",
33
43
  credits: 5,
44
+ maxUses: 5,
45
+ autoDelete: "3d", // 用尽后 3 天软删(true 立即软删)
34
46
  externalId: "user-123",
35
47
  meta: { plan: "pro" },
36
48
  });
37
- console.log(keys[0].key); // 完整明文
38
49
 
39
- // 验证 + 扣减(用户持有 key)
40
- const result = await verifyKey(
41
- { baseUrl: "https://keys.932324.xyz" },
42
- { key: userKey, decrement: true, context: { username: "alice" } }
43
- );
50
+ // 公开只读校验(无需 rootKey,不扣减)
51
+ const r = await checkKey({ key: userKey, context: { ip: "1.2.3.4" } });
52
+
53
+ // 扣减(需 rootKey,走 client)
54
+ const v = await client.verifyKey({ key: userKey, context: { ip: "1.2.3.4" } });
44
55
  ```
45
56
 
46
57
  ## API
47
58
 
48
59
  ### `createClient({ baseUrl, rootKey })` → client
49
- 管理客户端。`rootKey` 用于管理 API(verify 端点不需要)。
50
60
 
51
- ### `client.createKey({ appId, prefix?, length?, credits?, maxUses?, meta?, externalId?, expiresAt?, count? })`
52
- 创建 key。返回 `{ keys: [{ key, keyId }] }`,`key` 是完整明文(`<prefix>_<base62 length>`)。`appId` 首次自动注册。`length` 默认 8(短期邀请码等);**长期付费 key 传 16**(≈95 bit 防爆破)。
61
+ 管理客户端。`rootKey` 用于管理 API(含 verify 扣减;check 公开不需要)。
62
+
63
+ ### `client.createKey({ appId, prefix?, length?, credits?, maxUses?, meta?, externalId?, expiresAt?, count?, autoDelete?, rateLimit?, autoBan? })`
64
+
65
+ 创建 key。返回 `{ keys: [{ key, keyId }] }`,`key` 是完整明文。`appId` 首次自动注册。`length` 默认 8;**长期付费 key 传 16**。
66
+
67
+ `autoDelete`(预设为 key 属性,verify 用尽时执行,不再由 verify 传参):`true` 立即软删;`"3m"`/`"2h"`/`"3d"` 延迟软删(set deleteAt,Cron 每 10min 清理过期 key,保留记录供 admin 查)。
68
+
69
+ ### `checkKey({ key, context? })` / `client.checkKey(...)`
53
70
 
54
- ### `verifyKey({ key, decrement?, autoDelete?, context? })`
55
- 公开验证(无需 rootKey,baseUrl 内置默认)。`decrement: true` 扣减 + 写 verify log;`false` 只查。`autoDelete`:扣减后额度用尽(remaining=0)自动删——`true` 立即删;`"3m"`/`"2h"`/`"3d"` 延迟删(set deleteAt,Cron 每 10min 清理过期 key,保留记录供 admin 查看一段时间)。SDK 前置 `isValidKeyFormat` 校验,格式不对直接返回不查 DB。
56
- 返回 `{ valid, found, keyId, remaining, maxUses, meta, externalId, appId, enabled, expiresAt, createdAt }`。
71
+ 公开只读校验(无需 rootKey,baseUrl 内置默认)。**不扣减、不删**,写 check log。返回 `{ valid, found, keyId, remaining, maxUses, meta, externalId, appId, ... }`。SDK 前置 `isValidKeyFormat` 校验。
72
+
73
+ ### `client.verifyKey({ key, context? })`
74
+
75
+ 管理扣减(需 rootKey)。原子扣减 1,写 verify log;扣到 0 时按 key 的 `autoDelete` 属性删除。返回同 check。`code`(valid=false 时):`RATE_LIMITED`(429) / `KEY_PAUSED`、`KEY_BANNED`(423) / `KEY_DISABLED` / `KEY_EXPIRED` / `KEY_EXHAUSTED`。
57
76
 
58
77
  ### `client.listKeys({ appId, limit?, offset? })`
78
+
59
79
  列表,含完整明文。返回 `{ keys: KeyRecord[], total }`。
60
80
 
61
81
  ### `client.getKey(keyId)`
82
+
62
83
  详情(明文 + 最近 50 条 usage_logs)。
63
84
 
64
85
  ### `client.getKeyUsageLogs(keyId, { limit? })`
86
+
65
87
  使用记录。返回 `{ logs: UsageLog[] }`。
66
88
 
67
89
  ### `client.updateCredits(keyId, { operation, value, context? })`
90
+
68
91
  调整额度。`operation: "increment" | "decrement" | "set"`。increment 写 `refund` 事件。
69
92
 
70
93
  ### `client.updateKey(keyId, { meta?, enabled?, expiresAt? })`
94
+
71
95
  更新元数据/禁用。
72
96
 
73
97
  ### `client.deleteKey(keyId)` / `client.deleteKeys(keyIds)`
74
98
 
99
+ soft delete(`_delete=true`,明文 + meta 永久保留可反查)。
100
+
75
101
  ## usage_log event 类型
76
102
 
77
103
  | event | 触发 | creditsBefore/After |
78
104
  |---|---|---|
79
- | `verify` | verify decrement:true 扣减 | |
105
+ | `check` | check 端点 / verify 遇无限 key | |
106
+ | `verify` | verify 扣减 | ✅ |
80
107
  | `refund` | updateCredits increment | ✅ |
81
108
  | `create` | createKey | after = 初始额度 |
82
- | `delete` | deleteKey | — |
83
- | `update` | updateKey / updateCredits(decrement/set) | — |
109
+ | `soft_delete` | deleteKey / autoDelete 用尽 | — |
110
+ | `rate_limited` / `auto_pause` / `auto_ban` / `pause` / `unpause` / `ban` / `unban` / `update` | 对应操作 | — |
84
111
 
85
112
  ## License
86
113
 
package/SKILL.md CHANGED
@@ -1,160 +1,201 @@
1
1
  ---
2
2
  name: unified-keys
3
- description: 当项目需要管理 API key(创建、验证扣减额度、查询使用记录、管理端显示明文、用尽自动删除)时使用。自建通用 key 服务 keys.932324.xyz,明文存储 + 完整使用记录,npm 包 unified-keys。
3
+ description: 当项目需要管理 API key(创建、只读校验、扣减验证、查询使用记录、管理端显示明文、用尽自动软删、DO 限流、自动暂停/封禁、明文反查)时使用。自建通用 key 服务 keys.932324.xyz,明文存储 + 完整使用记录 + 永久痕迹,npm 包 unified-keys。check 公开只读 / verify 管理扣减分离。
4
4
  ---
5
5
 
6
6
  # unified-keys
7
7
 
8
- 自建通用 API key 管理 SDK(替代 Unkey)。**明文存储 + 明文返回 + 完整使用记录 + 用尽自动删除**。服务跑在 Cloudflare Worker(`keys.932324.xyz`),数据存 D1。
8
+ 自建通用 API key 管理 SDK(替代 Unkey)。**明文存储 + 明文返回 + 完整使用记录 + soft delete 永久痕迹 + DO 滑动窗口限流 + 渐进式/直接自动封禁 + 反查**。服务跑在 Cloudflare Worker(`keys.932324.xyz`),数据存 D1。
9
+
10
+ ## 0.6.0 安全修复(check/verify 拆分)
11
+
12
+ `autoDelete` 和 `decrement` 不再由 verify 调用方传——以前公开 verify 接受这俩参数,任何拿到 key 明文的人都能删别人的 key(`{key, decrement:true, autoDelete:true}`)或白嫖额度(`decrement:false`)。现在:
13
+
14
+ - **`POST /v1/keys/check`**(公开,无鉴权):纯只读校验,**不扣减、不删**。写 `check` event。探活 / 前端校验 / 只验证不扣减场景(如 shadcn-proxy)。
15
+ - **`POST /v1/keys/verify`**(管理,需 `Bearer KEYS_ROOT_KEY`):扣减 + autoDelete 读 key 属性。写 `verify` event。
16
+ - **`autoDelete`** 在 createKey 时预设为 key 属性(`auto_delete` 列),verify 扣到 0 时执行;调用方不再传。
17
+ - 顶层 `verifyKey()` 保留但 `@deprecated`,内部改调 check(只读);扣减用 `client.verifyKey()`(带 rootKey)。
9
18
 
10
19
  ## 何时用
11
20
 
12
21
  - 项目要发 API key / 邀请码 / 访问凭证给用户
13
22
  - 需要 verify key + 原子扣减额度(credits)
14
- - 想看每个 key 的使用历史(谁、何时、扣了几次、退了几次)
23
+ - 需要「只读校验 key 有效性」不扣减(前端探活、whoami)
24
+ - 想看每个 key 的使用历史(谁、何时、扣了几次、退了几次、何时被软删/暂停/封禁)
15
25
  - 想在管理端显示**完整 key 明文**(Unkey 做不到,只给前缀)
16
- - 一次性 key 用完自动删(避免 DB 堆积无用 key)
17
- - 多项目共用一个 key 服务(appId 命名空间隔离)
26
+ - **限流** / **自动封禁** / **永久痕迹(soft delete 反查)** / 多项目命名空间
18
27
 
19
28
  ## 安装
20
29
 
21
30
  ```bash
22
31
  pnpm add unified-keys
23
- # 或 npm i unified-keys
24
32
  ```
25
33
 
26
34
  ## 核心 API
27
35
 
28
- **baseUrl 内置默认**(`https://keys.932324.xyz`),无需传。
36
+ baseUrl 内置默认(`https://keys.932324.xyz`),无需传。
29
37
 
30
38
  ```ts
31
- import { createClient, isValidKeyFormat, verifyKey } from "unified-keys";
39
+ import { createClient, checkKey, isValidKeyFormat } from "unified-keys";
32
40
 
33
- // 管理客户端(需 rootKey;baseUrl 内置)
41
+ // 管理客户端(需 rootKey,verify 扣减 + 管理 API 都要)
34
42
  const client = createClient({ rootKey: process.env.KEYS_ROOT_KEY });
35
43
 
36
- // 创建 key(明文返回;appId = 项目 slug,首次自动注册)
44
+ // 创建 key(明文返回;appId 首次自动注册;autoDelete 预设为 key 属性)
37
45
  const { keys } = await client.createKey({
38
46
  appId: "my-app",
39
- credits: 5, // 初始剩余次数
40
- maxUses: 5, // 总额度(0 = 无限)
41
- length: 8, // body 位数(默认 8;长期付费 key 用 16 防爆破)
42
- externalId: "user-123", // 应用层关联业务实体(可选)
43
- meta: { plan: "pro" }, // 任意元数据(可选)
44
- count: 1, // 批量创建数
45
- });
46
- // keys[0].key = 完整明文 `<prefix>_<8位base62>`
47
-
48
- // 验证 + 扣减(用户持有明文 key,服务端验证)
49
- const result = await verifyKey({
50
- key: userKey,
51
- decrement: true, // 扣减(默认 true)
52
- autoDelete: true, // 扣减后用尽(remaining=0)自动删 key(一次性 key 用)
53
- context: { username: "alice", ip: "1.2.3.4" }, // 写 usage_log
47
+ credits: 5, // 初始剩余次数
48
+ maxUses: 5, // 总额度(0 = 无限)
49
+ autoDelete: "3d", // 用尽后延迟软删(true 立即软删;"3m"/"2h"/"3d" 延迟;不传=不删)
50
+ length: 8, // body 位数(默认 8;长期付费 key 用 16 防爆破)
51
+ prefix: "sk", // 可选前缀,生成 `<prefix>_<body>`
52
+ externalId: "user-123", // 应用层关联业务实体(可选)
53
+ meta: { plan: "pro" }, // 任意元数据(可选)
54
+ rateLimit: { max: 60, windowSec: 60 }, // 限流(max 不传 = 不限速)
55
+ autoBan: { threshold: 10, windowSec: 600, maxViolations: 3, durationSec: 3600 },
54
56
  });
55
- // { valid, found, keyId, remaining, maxUses, meta, externalId, appId, enabled, expiresAt, createdAt }
56
- // found=false: key 不存在;valid=false found=true: 存在但禁用/过期/额度耗尽
57
57
 
58
- // 列表(含完整明文 区别于 Unkey)
59
- const { keys: all, total } = await client.listKeys({ appId: "my-app", limit: 200 });
58
+ // 公开只读校验(无需 rootKey,不扣减不删)—— 探活 / 前端校验 / shadcn-proxy
59
+ const c = await checkKey({ key: userKey, context: { ip: "1.2.3.4" } });
60
+ // 或 client.checkKey({...})(client 不需要 rootKey 也能 check,但通常已有 client)
60
61
 
61
- // 详情(明文 + 使用记录)
62
- const { key, usageLogs } = await client.getKey(keyId);
62
+ // 扣减(需 rootKey,走 client)—— 真正消费额度
63
+ const r = await client.verifyKey({ key: userKey, context: { ip: "1.2.3.4" } });
64
+ // 返回: { valid, found, keyId, remaining, maxUses, meta, appId, banStatus,
65
+ // ratelimited?, code?, resetIn? }
66
+ // code(valid=false 时): RATE_LIMITED(429) / KEY_PAUSED(423) / KEY_BANNED(423)
67
+ // / KEY_DISABLED / KEY_EXPIRED / KEY_EXHAUSTED
63
68
 
64
- // 查使用记录(核心特性)
65
- const { logs } = await client.getKeyUsageLogs(keyId, { limit: 50 });
66
- // logs: [{ event: "verify"|"refund"|"create"|"delete"|"update"|"disable", creditsBefore, creditsAfter, context, createdAt }]
69
+ // 反查明文 key → keyId + 历史(★ 含已软删的 key,排查注册用户用)
70
+ const { found, key, recentLogs } = await client.lookupKey({ plaintext: "sk_xxx" });
67
71
 
68
- // 调整额度(refund 恢复)
69
- await client.updateCredits(keyId, {
70
- operation: "increment", // increment | decrement | set
71
- value: 1,
72
- context: { reason: "refund" },
73
- });
72
+ // 列表(含明文)/ 详情 / 使用记录
73
+ const { keys: all, total } = await client.listKeys({ appId: "my-app", limit: 200 });
74
+ const { key, usageLogs } = await client.getKey(keyId);
74
75
 
75
- // 更新元数据 / 禁用
76
+ // 调整额度(refund 恢复)/ 更新元数据
77
+ await client.updateCredits(keyId, { operation: "increment", value: 1 });
76
78
  await client.updateKey(keyId, { meta: { plan: "enterprise" }, enabled: false });
77
79
 
78
- // 删除
80
+ // 删除(soft delete:_delete=true,明文 + meta 永久保留可反查)
79
81
  await client.deleteKey(keyId);
80
- await client.deleteKeys([keyId1, keyId2]);
81
82
 
82
- // 手输校验(isValidKeyFormat 库内置,与 verify 前置校验同源)
83
+ // 手动管控
84
+ await client.pauseKey(keyId, { durationSec: 3600, reason: "manual" });
85
+ await client.banKey(keyId, { reason: "abuse" });
86
+
87
+ // 手输校验
83
88
  if (!isValidKeyFormat(inputKey)) return invalid();
84
89
  ```
85
90
 
91
+ ## check vs verify(0.6.0 起拆分)
92
+
93
+ | | `checkKey` | `client.verifyKey` |
94
+ |---|---|---|
95
+ | 鉴权 | 公开(无需 rootKey) | 管理(需 rootKey) |
96
+ | 扣减 | ❌ 不扣 | ✅ 原子扣减 1 |
97
+ | autoDelete | ❌ 不删 | ✅ 扣到 0 按 key.autoDelete 删 |
98
+ | event | `check` | `verify`(无限 key 也写 `check`) |
99
+ | 场景 | 探活 / 前端校验 / shadcn-proxy | 真正消费额度(notify-me/github-inviter/user-dashboard 兑换) |
100
+
101
+ **`autoDelete`(createKey 预设,verify 执行)**:`true` 立即 soft delete;`"3m"`/`"2h"`/`"3d"` 延迟(set deleteAt,Cron 每 10min 清理);不传 = 不删(无限 key / 长期 key)。
102
+
103
+ **返回 `code`**(valid=false 时):`RATE_LIMITED`(429) / `KEY_PAUSED`、`KEY_BANNED`(423) / `KEY_DISABLED` / `KEY_EXPIRED` / `KEY_EXHAUSTED`。
104
+
105
+ **SDK 对 429/423 不 throw**(业务码,body 含 valid/code/ratelimited),返回 body 让调用方读:
106
+ ```ts
107
+ const r = await client.verifyKey({ key });
108
+ if (r.ratelimited || r.code === "RATE_LIMITED") return res.status(429).json({ error: "请求过于频繁" });
109
+ if (r.code === "KEY_PAUSED" || r.code === "KEY_BANNED") return res.status(423).json({ error: "Key 已暂停/封禁" });
110
+ if (!r.valid) return res.status(403).json({ error: "Key 无效" });
111
+ ```
112
+
86
113
  ## 特性(区别于 Unkey)
87
114
 
88
115
  | | unified-keys | Unkey |
89
116
  |---|---|---|
90
- | 明文显示 | ✅ 列表/详情返回完整明文 | ❌ 只给前缀 |
91
- | 使用记录 | ✅ 每次 verify/refund/create/deleteusage_log | ❌ 只给当前 remaining |
117
+ | 明文显示 | ✅ 列表/详情/反查返回完整明文 | ❌ 只给前缀 |
118
+ | 使用记录 | ✅ 每次 check/verify/refund/create/soft_delete/rate_limited/pause/banlog | ❌ 只给 remaining |
92
119
  | verify 延迟 | <5ms(D1 binding 零网络跳) | 50-100ms(公网) |
93
- | 用尽自动删 | ✅ verify `autoDelete` 选项 | ❌ |
94
- | 前置格式校验 | ✅ SDK `isValidKeyFormat`,格式不对不查 DB | ❌ |
95
- | 自托管 | ✅ 自己的 D1 | ❌ SaaS |
96
- | 原子扣减 | ✅ `UPDATE ... WHERE credits>0 RETURNING` | |
120
+ | check/verify 拆分 | ✅ 公开只读 vs 管理扣减(避免滥用) | ❌ 单端点 |
121
+ | 用尽自动删 | ✅ autoDelete(createKey 预设,立即 / 延迟软删) | ❌ |
122
+ | **永久痕迹** | ✅ soft delete(`_delete=true`),明文 + meta 永久可反查 | ❌ 真删丢痕迹 |
123
+ | **限流** | ✅ Durable Object 滑动窗口 | |
124
+ | **自动封禁** | ✅ 渐进式 / 直接 | ❌ |
125
+ | **反查** | ✅ 明文 → keyId + 历史(含已软删) | ❌ |
126
+ | 前置格式校验 | ✅ SDK isValidKeyFormat | ❌ |
97
127
 
98
- ## verify 选项
128
+ ## 自动封禁(autoBan)
99
129
 
100
- | 选项 | 默认 | 说明 |
101
- |---|---|---|
102
- | `decrement` | true | true 扣减 + 写 verify log;false 只查 |
103
- | `autoDelete` | false | 扣减后 remaining=0 自动删:`true` 立即删;`"3m"`/`"2h"`/`"3d"` 延迟删(set deleteAt,Cron 每 10min 清理过期 key,保留记录供 admin 查看一段时间) |
104
- | `context` | - | 写入 usage_log({username, ip, source, ...}) |
130
+ 两种模式,按 `mode` 选择:
131
+
132
+ **渐进式**(默认,`mode: "progressive"`):违规累计,达 maxViolations 永久 ban,前 N-1 pause。
133
+ ```jsonc
134
+ { "threshold": 10, "windowSec": 600, "maxViolations": 3, "durationSec": 3600 }
135
+ ```
105
136
 
106
- **SDK 前置校验**:verify 前先 `isValidKeyFormat(key)`,格式不对直接返回 `{valid:false, found:false}`,不查 DB(省时间)。应用也可单独用 `isValidKeyFormat` 做手输校验。
137
+ **直接**(`mode: "direct"`):单次达阈值即 action(不累计违规计数)。
138
+ ```jsonc
139
+ { "mode": "direct", "action": "ban", "threshold": 10, "windowSec": 600 } // 一次违规永久 ban
140
+ { "mode": "direct", "action": "pause", "threshold": 10, "durationSec": 3600 } // 一次违规 pause
141
+ ```
142
+
143
+ `unban` 重置违规计数;`unpause` 不重置。
107
144
 
108
145
  ## event 类型(usage_log)
109
146
 
110
- - `verify`:verify 扣减(含 creditsBefore/After + context)
111
- - `refund`:updateCredits increment 恢复额度
112
- - `create` / `delete`(含 autoDelete 触发的 exhausted_autodelete)/ `update` / `disable`
147
+ `check`(只读探活 / verify 遇无限 key)/ `verify`(扣减)/ `refund`(恢复额度)/ `create` / `soft_delete`(含 autoDelete 触发的)/ `rate_limited` / `auto_pause` / `auto_ban` / `pause` / `unpause` / `ban` / `unban` / `update`。
113
148
 
114
149
  ## 集成示例
115
150
 
116
- ### Next.js Route Handler(服务端 verify + 一次性 key 用尽自动删)
151
+ ### Next.js Route Handler(扣减,带 rootKey client)
117
152
  ```ts
118
- import { verifyKey } from "unified-keys";
153
+ import { createClient } from "unified-keys";
154
+ const client = createClient({ rootKey: process.env.KEYS_ROOT_KEY });
119
155
  export async function POST(req: Request) {
120
156
  const { key } = await req.json();
121
- const r = await verifyKey({
122
- key,
123
- decrement: true,
124
- autoDelete: true, // 用完自动删
125
- context: { ip: req.headers.get("x-forwarded-for") },
126
- });
127
- if (!r.valid) return Response.json({ error: "invalid key" }, { status: 403 });
157
+ const r = await client.verifyKey({ key, context: { ip: req.headers.get("x-forwarded-for") } });
158
+ if (r.ratelimited || r.code === "RATE_LIMITED") return Response.json({ error: "请求过于频繁" }, { status: 429 });
159
+ if (r.code === "KEY_PAUSED" || r.code === "KEY_BANNED") return Response.json({ error: "Key 已暂停/封禁" }, { status: 423 });
160
+ if (!r.valid) return Response.json({ error: "Key 无效或已使用" }, { status: 403 });
128
161
  // ... 授权逻辑
129
162
  }
130
163
  ```
131
164
 
132
- ### Next.js Server Component(只读查询,不扣减)
165
+ ### 公开只读校验(探活 / whoami,无需 rootKey)
133
166
  ```ts
134
- import { verifyKey } from "unified-keys";
135
- const info = await verifyKey({ key: params.key, decrement: false });
136
- if (!info.found || !info.valid) notFound();
137
- // info.remaining / info.meta / info.expiresAt
167
+ import { checkKey } from "unified-keys";
168
+ const r = await checkKey({ key }); // 不扣减,前端可直调
138
169
  ```
139
170
 
140
- ### Cloudflare Worker(管理端)
171
+ ### admin 反查(明文 key → 注册用户)
141
172
  ```ts
142
- import { createClient } from "unified-keys";
143
- const client = createClient({ rootKey: c.env.KEY_SERVICE_ROOT_KEY });
144
- await client.createKey({ appId: "my-app", credits: 1 });
173
+ const { found, key, recentLogs } = await client.lookupKey({ plaintext: "sk_xxx" });
145
174
  ```
146
175
 
147
176
  ## 服务端点
148
177
 
149
- - **公开 verify**:`POST https://keys.932324.xyz/v1/keys/verify`(无需鉴权,任何持有 key 的人可调)
150
- - **管理 API**:`https://keys.932324.xyz/v1/*`(需 `Authorization: Bearer <ROOT_KEY>`)
151
- - 健康检查:`GET https://keys.932324.xyz/v1/health`
178
+ - **公开 check**:`POST https://keys.932324.xyz/v1/keys/check`(无鉴权,只读校验,不扣不删)
179
+ - **管理 verify**:`POST /v1/keys/verify`(需 `Bearer KEYS_ROOT_KEY`,扣减 + autoDelete 读 key 属性)
180
+ - **管理 API**:`/v1/*`(createKey/list/get/credits/update/delete/lookup/pause/ban 等,需 rootKey)
181
+ - **反查**:`POST /v1/keys/lookup` `{plaintext}`
182
+ - 健康检查:`GET /v1/health`
152
183
 
153
- ROOT_KEY 联系服务管理员获取。
184
+ ROOT_KEY 联系服务管理员获取(本项目在 `packages/infra/.env` 的 `KEYS_ROOT_KEY`;4 应用共用)。
154
185
 
155
186
  ## 实现位置(本项目内)
156
187
 
157
- - 服务端:`apps/key-service`(Hono Worker → keys.932324.xyz
158
- - SDK:`packages/keys`(npm `unified-keys`)
159
- - schema:`packages/db/src/schemas/key-service.ts`(key_apps / key_records / key_usage_logs)
160
- - 4 应用全迁移:github-inviter / user-dashboard / notify-me 完整迁移;shadcn-proxy 新用新服务 + Unkey 格式路由 fallback(旧 key 保持可用)
188
+ - 服务端:`apps/key-service`(Hono Worker → keys.932324.xyz;D1 binding `KEY_SERVICE_DB` + Durable Object `RATE_LIMITER`)
189
+ - SDK:`packages/keys`(npm `unified-keys@0.6.0`,monorepo workspace 直接用 src)
190
+ - schema:`packages/db/src/schemas/key-service.ts`
191
+ - `key_records`:含 `auto_delete`(用尽策略) / `_delete`(soft delete) / `rate_limit_*` / `ban_*`+`paused_until`+`auto_ban_count` / `delete_at`(延迟软删触发时间)
192
+ - `key_usage_logs`:完整事件流(含 keyId 外键,soft delete 不级联删)
193
+ - migrations:`packages/infra/migrations/keys/0001` ~ `0005`(0005 加 auto_delete 列 + 按 appId 回填;alchemy deploy 自动跑)
194
+
195
+ ## 注意事项
196
+
197
+ - **SDK 429/423 不 throw**:check/verify 限流/封禁时返回 429/423 + JSON body,SDK 不抛异常,调用方读 result.code/result.ratelimited。
198
+ - **check/verify 必须分清**:探活用 check(公开,不扣),消费额度用 verify(管理,扣)。verify 需 rootKey,公开侧(浏览器/无 rootKey 的服务)只能 check。
199
+ - **autoDelete 是 key 属性**:createKey 时预设,verify 扣到 0 自动执行;调用方无法通过 verify 传参删别人的 key(0.6.0 安全修复)。
200
+ - **soft delete 不丢痕迹**:deleteKey/autoDelete 只 set `_delete=true`,明文 + meta + usage_logs 永久保留,lookup 可反查。
201
+ - **D1 datetime 拼接**:服务端 `datetime('now', '+N seconds')` 必须用 `sql.raw(String(N))`,不能 `${N}` 参数化。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unified-keys",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "自建通用 API key 管理 SDK(明文存储 + 明文返回 + 完整使用记录),调用 keys.932324.xyz 服务",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
package/src/client.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  import type {
2
+ BanInput,
2
3
  BatchDeleteResult,
4
+ CheckInput,
5
+ CheckResult,
3
6
  CreateAppInput,
4
7
  CreateAppResult,
5
8
  CreateKeyInput,
@@ -12,6 +15,10 @@ import type {
12
15
  ListAppsResult,
13
16
  ListKeysInput,
14
17
  ListKeysResult,
18
+ LookupInput,
19
+ LookupResult,
20
+ PauseInput,
21
+ PauseResult,
15
22
  UpdateCreditsInput,
16
23
  UpdateCreditsResult,
17
24
  UpdateKeyInput,
@@ -65,7 +72,8 @@ export function createClient(config: KeyServiceClientConfig): KeyServiceClient {
65
72
  `unified-keys ${method} ${path}: ${res.status} ${text.slice(0, 200)}`
66
73
  );
67
74
  }
68
- if (!res.ok) {
75
+ // 429/423 是 verify 端点的业务码(限流/封禁,body 含 valid/code),返回 body 让调用方读,不 throw
76
+ if (!res.ok && res.status !== 429 && res.status !== 423) {
69
77
  const msg = (data as { error?: string }).error ?? `${res.status}`;
70
78
  throw new Error(`unified-keys ${method} ${path}: ${msg}`);
71
79
  }
@@ -107,26 +115,50 @@ export function createClient(config: KeyServiceClientConfig): KeyServiceClient {
107
115
  `/v1/keys/${keyId}/usage-logs?limit=${limit}`
108
116
  );
109
117
  },
118
+ checkKey: (input: CheckInput) => {
119
+ if (!isValidKeyFormat(input.key)) {
120
+ return Promise.resolve({ valid: false, found: false } as CheckResult);
121
+ }
122
+ return request<CheckResult>("POST", "/v1/keys/check", {
123
+ body: input,
124
+ auth: false,
125
+ });
126
+ },
110
127
  verifyKey: (input: VerifyInput) => {
111
128
  if (!isValidKeyFormat(input.key)) {
112
129
  return Promise.resolve({ valid: false, found: false } as VerifyResult);
113
130
  }
131
+ // verify 改为管理端点(需 rootKey,扣减 + autoDelete 读 key 属性)
114
132
  return request<VerifyResult>("POST", "/v1/keys/verify", {
115
133
  body: input,
116
- auth: false,
134
+ auth: true,
117
135
  });
118
136
  },
137
+ lookupKey: (input: LookupInput) =>
138
+ request<LookupResult>("POST", "/v1/keys/lookup", { body: input }),
139
+ pauseKey: (keyId: string, input?: PauseInput) =>
140
+ request<PauseResult>("POST", `/v1/keys/${keyId}/pause`, {
141
+ body: input ?? {},
142
+ }),
143
+ unpauseKey: (keyId: string) =>
144
+ request<DeleteResult>("POST", `/v1/keys/${keyId}/unpause`, { body: {} }),
145
+ banKey: (keyId: string, input?: BanInput) =>
146
+ request<DeleteResult>("POST", `/v1/keys/${keyId}/ban`, {
147
+ body: input ?? {},
148
+ }),
149
+ unbanKey: (keyId: string) =>
150
+ request<DeleteResult>("POST", `/v1/keys/${keyId}/unban`, { body: {} }),
119
151
  };
120
152
  }
121
153
 
122
154
  /**
123
- * 公开 verify 端点,无需 rootKey,baseUrl 内置默认。
124
- * 适合「用户持有明文 key、服务端验证并扣减」的场景。
155
+ * 公开 check 端点(只读校验),无需 rootKey,baseUrl 内置默认。
156
+ * 不扣减、不删;适合「探活 / 前端校验 key 有效性 / 只验证不扣减」场景。
125
157
  */
126
- export async function verifyKey(input: VerifyInput): Promise<VerifyResult> {
158
+ export async function checkKey(input: CheckInput): Promise<CheckResult> {
127
159
  if (!isValidKeyFormat(input.key)) return { valid: false, found: false };
128
160
  const baseUrl = DEFAULT_BASE_URL;
129
- const res = await fetch(`${baseUrl}/v1/keys/verify`, {
161
+ const res = await fetch(`${baseUrl}/v1/keys/check`, {
130
162
  method: "POST",
131
163
  headers: { "Content-Type": "application/json" },
132
164
  body: JSON.stringify(input),
@@ -136,11 +168,20 @@ export async function verifyKey(input: VerifyInput): Promise<VerifyResult> {
136
168
  try {
137
169
  data = text ? JSON.parse(text) : {};
138
170
  } catch {
139
- throw new Error(`unified-keys verify: ${res.status} ${text.slice(0, 200)}`);
171
+ throw new Error(`unified-keys check: ${res.status} ${text.slice(0, 200)}`);
140
172
  }
141
- if (!res.ok) {
173
+ if (!res.ok && res.status !== 429 && res.status !== 423) {
142
174
  const msg = (data as { error?: string }).error ?? `${res.status}`;
143
- throw new Error(`unified-keys verify: ${msg}`);
175
+ throw new Error(`unified-keys check: ${msg}`);
144
176
  }
145
- return data as VerifyResult;
177
+ return data as CheckResult;
178
+ }
179
+
180
+ /**
181
+ * @deprecated 0.6.0 起 verify 改为管理端点(需 rootKey),公开场景请用 `checkKey`(只读)。
182
+ * 此函数保留向后兼容,内部改为调公开 check 端点(行为:只读校验,不扣减)。
183
+ * 扣减请用 `client.verifyKey`(`createClient({ rootKey })`)。
184
+ */
185
+ export async function verifyKey(input: VerifyInput): Promise<VerifyResult> {
186
+ return checkKey(input);
146
187
  }
package/src/index.ts CHANGED
@@ -1,7 +1,11 @@
1
- export { createClient, isValidKeyFormat, verifyKey } from "./client";
1
+ export { checkKey, createClient, isValidKeyFormat, verifyKey } from "./client";
2
2
  export type {
3
3
  AppRecord,
4
+ AutoBanConfig,
5
+ BanInput,
4
6
  BatchDeleteResult,
7
+ CheckInput,
8
+ CheckResult,
5
9
  CreateAppInput,
6
10
  CreateAppResult,
7
11
  CreatedKey,
@@ -16,11 +20,17 @@ export type {
16
20
  ListAppsResult,
17
21
  ListKeysInput,
18
22
  ListKeysResult,
23
+ LookupInput,
24
+ LookupResult,
25
+ PauseInput,
26
+ PauseResult,
27
+ RateLimitConfig,
19
28
  UpdateCreditsInput,
20
29
  UpdateCreditsResult,
21
30
  UpdateKeyInput,
22
31
  UsageLog,
23
32
  UsageLogsResult,
33
+ VerifyErrorCode,
24
34
  VerifyInput,
25
35
  VerifyResult,
26
36
  } from "./types";
package/src/types.ts CHANGED
@@ -27,8 +27,39 @@ export interface ListAppsResult {
27
27
  apps: AppRecord[];
28
28
  }
29
29
 
30
+ /** 限流配置(max 配置后启用 RateLimiterDO 滑动窗口限流)*/
31
+ export interface RateLimitConfig {
32
+ /** 窗口内最大请求数 */
33
+ max: number;
34
+ /** 窗口秒数,默认 60 */
35
+ windowSec?: number;
36
+ }
37
+
38
+ /** 自动封禁配置(meta.autoBan)*/
39
+ export interface AutoBanConfig {
40
+ /** direct: 单次触发动作(pause/ban,默认 pause)*/
41
+ action?: "pause" | "ban";
42
+ /** pause 持续秒数,默认 3600(1h)*/
43
+ durationSec?: number;
44
+ /** progressive: 违规达此数 → 永久 ban(默认 3);前 N-1 次 pause */
45
+ maxViolations?: number;
46
+ /** 模式:progressive(渐进式,默认)/ direct(直接,单次达阈值即 action)*/
47
+ mode?: "progressive" | "direct";
48
+ /** 触发阈值(窗口内 rate_limited 次数 = 一次违规)*/
49
+ threshold: number;
50
+ /** 统计窗口秒数,默认 600(10min)*/
51
+ windowSec?: number;
52
+ }
53
+
30
54
  export interface CreateKeyInput {
31
55
  appId: string;
56
+ /** 自动封禁配置(存 meta.autoBan)*/
57
+ autoBan?: AutoBanConfig;
58
+ /**
59
+ * autoDelete(createKey 时预设为 key 属性,verify 扣减到 0 时执行;verify 端点不再接受调用方传参):
60
+ * true / "now" → 立即 soft delete;"3m" / "2h" / "3d" → set deleteAt,Cron 软删(保留记录供 admin 查一段时间)
61
+ */
62
+ autoDelete?: boolean | string;
32
63
  /** 批量创建数量,默认 1 */
33
64
  count?: number;
34
65
  /** 初始额度(剩余次数),默认 1 */
@@ -41,10 +72,16 @@ export interface CreateKeyInput {
41
72
  length?: number;
42
73
  /** 总额度(0=无限),默认等于 credits */
43
74
  maxUses?: number;
44
- /** 应用层任意元数据 */
75
+ /** 应用层任意元数据(count 个 key 共享) */
45
76
  meta?: Record<string, unknown>;
77
+ /**
78
+ * 批量创建时每个 key 的独立 meta(长度须 = count);不传则 count 个 key 共享 `meta`。
79
+ */
80
+ metas?: Record<string, unknown>[];
46
81
  /** 可选前缀,生成 `<prefix>_<rand>`;不传则纯 random */
47
82
  prefix?: string;
83
+ /** 限流配置(max 配置后启用滑动窗口限流)*/
84
+ rateLimit?: RateLimitConfig;
48
85
  }
49
86
 
50
87
  export interface CreatedKey {
@@ -59,16 +96,25 @@ export interface CreateKeyResult {
59
96
 
60
97
  export interface KeyRecord {
61
98
  appId: string;
99
+ /** autoDelete 策略(createKey 预设,verify 只读):null=不删 | "true"=用尽立即软删 | "3m"/"2h"/"3d"=延迟软删 */
100
+ autoDelete: string | null;
101
+ banReason: string | null;
102
+ banStatus: "active" | "paused" | "banned";
62
103
  createdAt: string;
63
104
  creditsRemaining: number;
105
+ /** soft delete 标志(true=已软删,verify/list 过滤但 admin 可查)*/
106
+ deleted: boolean;
64
107
  enabled: boolean;
65
108
  expiresAt: string | null;
66
109
  externalId: string | null;
67
110
  keyId: string;
68
111
  maxUses: number;
69
112
  meta: Record<string, unknown> | null;
113
+ pausedUntil: string | null;
70
114
  plaintext: string;
71
115
  prefix: string | null;
116
+ rateLimitMax: number | null;
117
+ rateLimitWindowSec: number | null;
72
118
  updatedAt: string;
73
119
  }
74
120
 
@@ -106,7 +152,7 @@ export interface UsageLog {
106
152
  createdAt: string;
107
153
  creditsAfter: number | null;
108
154
  creditsBefore: number | null;
109
- /** verify(扣减) / refund(恢复) / create / delete / update / disable */
155
+ /** check(只读) / verify(扣减) / refund / create / soft_delete / rate_limited / auto_pause / auto_ban / pause / unpause / ban / unban / update */
110
156
  event: string;
111
157
  id: string;
112
158
  keyId: string;
@@ -126,31 +172,52 @@ export interface GetKeyResult {
126
172
  }
127
173
 
128
174
  export interface VerifyInput {
129
- /**
130
- * 扣减后额度用尽(remaining=0)自动删除:true 立即删;
131
- * "3m"/"2h"/"3d" 等延迟删(set deleteAt,Cron 定期清理,保留记录一段时间)
132
- */
133
- autoDelete?: boolean | string;
134
175
  /** 上下文,写入 usage_log(如 {username, source, ip}) */
135
176
  context?: Record<string, unknown>;
136
- /** 默认 true;false 时只查不扣(写 verify log 但不扣减) */
137
- decrement?: boolean;
138
177
  /** 完整明文 key */
139
178
  key: string;
140
179
  }
141
180
 
181
+ /** 公开 check(只读校验)入参。check 不扣减、不删,无需 rootKey。 */
182
+ export interface CheckInput {
183
+ /** 上下文,写入 usage_log */
184
+ context?: Record<string, unknown>;
185
+ /** 完整明文 key */
186
+ key: string;
187
+ }
188
+
189
+ /** check 结果(结构与 VerifyResult 相同)。 */
190
+ export type CheckResult = VerifyResult;
191
+
192
+ /** verify 失败码 */
193
+ export type VerifyErrorCode =
194
+ | "RATE_LIMITED"
195
+ | "KEY_PAUSED"
196
+ | "KEY_BANNED"
197
+ | "KEY_DISABLED"
198
+ | "KEY_EXPIRED"
199
+ | "KEY_EXHAUSTED";
200
+
142
201
  export interface VerifyResult {
143
202
  appId?: string;
203
+ banStatus?: "active" | "paused" | "banned";
204
+ /** 失败码(valid=false 时)*/
205
+ code?: VerifyErrorCode;
144
206
  createdAt?: string;
145
207
  enabled?: boolean;
208
+ error?: string;
146
209
  expiresAt?: string | null;
147
210
  externalId?: string | null;
148
- /** key 是否存在(区别「不存在」与「存在但额度耗尽」) */
211
+ /** key 是否存在(区别「不存在」与「存在但额度耗尽/封禁」)*/
149
212
  found: boolean;
150
213
  keyId?: string;
151
214
  maxUses?: number;
152
215
  meta?: Record<string, unknown> | null;
216
+ /** 是否触发限流(429)*/
217
+ ratelimited?: boolean;
153
218
  remaining?: number;
219
+ /** 限流/暂停重置剩余秒数 */
220
+ resetIn?: number;
154
221
  valid: boolean;
155
222
  }
156
223
 
@@ -163,7 +230,37 @@ export interface BatchDeleteResult {
163
230
  failed: string[];
164
231
  }
165
232
 
233
+ // ===== lookup / pause / ban =====
234
+
235
+ export interface LookupInput {
236
+ plaintext: string;
237
+ }
238
+
239
+ export interface LookupResult {
240
+ found: boolean;
241
+ key?: KeyRecord;
242
+ recentLogs?: UsageLog[];
243
+ }
244
+
245
+ export interface PauseInput {
246
+ /** 暂停秒数,默认 3600(1h)*/
247
+ durationSec?: number;
248
+ reason?: string;
249
+ }
250
+
251
+ export interface PauseResult {
252
+ pausedUntil: string | null;
253
+ success: boolean;
254
+ }
255
+
256
+ export interface BanInput {
257
+ reason?: string;
258
+ }
259
+
166
260
  export interface KeyServiceClient {
261
+ banKey(keyId: string, input?: BanInput): Promise<DeleteResult>;
262
+ /** 公开 check(只读校验,不扣减不删,无需 rootKey)。 */
263
+ checkKey(input: CheckInput): Promise<CheckResult>;
167
264
  createApp(input: CreateAppInput): Promise<CreateAppResult>;
168
265
  createKey(input: CreateKeyInput): Promise<CreateKeyResult>;
169
266
  deleteKey(keyId: string): Promise<DeleteResult>;
@@ -175,6 +272,10 @@ export interface KeyServiceClient {
175
272
  ): Promise<UsageLogsResult>;
176
273
  listApps(): Promise<ListAppsResult>;
177
274
  listKeys(input: ListKeysInput): Promise<ListKeysResult>;
275
+ lookupKey(input: LookupInput): Promise<LookupResult>;
276
+ pauseKey(keyId: string, input?: PauseInput): Promise<PauseResult>;
277
+ unbanKey(keyId: string): Promise<DeleteResult>;
278
+ unpauseKey(keyId: string): Promise<DeleteResult>;
178
279
  updateCredits(
179
280
  keyId: string,
180
281
  input: UpdateCreditsInput