unified-keys 0.4.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?, credits?, maxUses?, meta?, externalId?, expiresAt?, count? })`
52
- 创建 key。返回 `{ keys: [{ key, keyId }] }`,`key` 是完整明文。`appId` 首次自动注册。
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,159 +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
- externalId: "user-123", // 应用层关联业务实体(可选)
42
- meta: { plan: "pro" }, // 任意元数据(可选)
43
- count: 1, // 批量创建数
44
- });
45
- // keys[0].key = 完整明文 `<prefix>_<8位base62>`
46
-
47
- // 验证 + 扣减(用户持有明文 key,服务端验证)
48
- const result = await verifyKey({
49
- key: userKey,
50
- decrement: true, // 扣减(默认 true)
51
- autoDelete: true, // 扣减后用尽(remaining=0)自动删 key(一次性 key 用)
52
- 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 },
53
56
  });
54
- // { valid, found, keyId, remaining, maxUses, meta, externalId, appId, enabled, expiresAt, createdAt }
55
- // found=false: key 不存在;valid=false found=true: 存在但禁用/过期/额度耗尽
56
57
 
57
- // 列表(含完整明文 区别于 Unkey)
58
- 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)
59
61
 
60
- // 详情(明文 + 使用记录)
61
- 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
62
68
 
63
- // 查使用记录(核心特性)
64
- const { logs } = await client.getKeyUsageLogs(keyId, { limit: 50 });
65
- // 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" });
66
71
 
67
- // 调整额度(refund 恢复)
68
- await client.updateCredits(keyId, {
69
- operation: "increment", // increment | decrement | set
70
- value: 1,
71
- context: { reason: "refund" },
72
- });
72
+ // 列表(含明文)/ 详情 / 使用记录
73
+ const { keys: all, total } = await client.listKeys({ appId: "my-app", limit: 200 });
74
+ const { key, usageLogs } = await client.getKey(keyId);
73
75
 
74
- // 更新元数据 / 禁用
76
+ // 调整额度(refund 恢复)/ 更新元数据
77
+ await client.updateCredits(keyId, { operation: "increment", value: 1 });
75
78
  await client.updateKey(keyId, { meta: { plan: "enterprise" }, enabled: false });
76
79
 
77
- // 删除
80
+ // 删除(soft delete:_delete=true,明文 + meta 永久保留可反查)
78
81
  await client.deleteKey(keyId);
79
- await client.deleteKeys([keyId1, keyId2]);
80
82
 
81
- // 手输校验(isValidKeyFormat 库内置,与 verify 前置校验同源)
83
+ // 手动管控
84
+ await client.pauseKey(keyId, { durationSec: 3600, reason: "manual" });
85
+ await client.banKey(keyId, { reason: "abuse" });
86
+
87
+ // 手输校验
82
88
  if (!isValidKeyFormat(inputKey)) return invalid();
83
89
  ```
84
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
+
85
113
  ## 特性(区别于 Unkey)
86
114
 
87
115
  | | unified-keys | Unkey |
88
116
  |---|---|---|
89
- | 明文显示 | ✅ 列表/详情返回完整明文 | ❌ 只给前缀 |
90
- | 使用记录 | ✅ 每次 verify/refund/create/deleteusage_log | ❌ 只给当前 remaining |
117
+ | 明文显示 | ✅ 列表/详情/反查返回完整明文 | ❌ 只给前缀 |
118
+ | 使用记录 | ✅ 每次 check/verify/refund/create/soft_delete/rate_limited/pause/banlog | ❌ 只给 remaining |
91
119
  | verify 延迟 | <5ms(D1 binding 零网络跳) | 50-100ms(公网) |
92
- | 用尽自动删 | ✅ verify `autoDelete` 选项 | ❌ |
93
- | 前置格式校验 | ✅ SDK `isValidKeyFormat`,格式不对不查 DB | ❌ |
94
- | 自托管 | ✅ 自己的 D1 | ❌ SaaS |
95
- | 原子扣减 | ✅ `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 | ❌ |
96
127
 
97
- ## verify 选项
128
+ ## 自动封禁(autoBan)
98
129
 
99
- | 选项 | 默认 | 说明 |
100
- |---|---|---|
101
- | `decrement` | true | true 扣减 + 写 verify log;false 只查 |
102
- | `autoDelete` | false | 扣减后 remaining=0 自动删:`true` 立即删;`"3m"`/`"2h"`/`"3d"` 延迟删(set deleteAt,Cron 每 10min 清理过期 key,保留记录供 admin 查看一段时间) |
103
- | `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
+ ```
104
136
 
105
- **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` 不重置。
106
144
 
107
145
  ## event 类型(usage_log)
108
146
 
109
- - `verify`:verify 扣减(含 creditsBefore/After + context)
110
- - `refund`:updateCredits increment 恢复额度
111
- - `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`。
112
148
 
113
149
  ## 集成示例
114
150
 
115
- ### Next.js Route Handler(服务端 verify + 一次性 key 用尽自动删)
151
+ ### Next.js Route Handler(扣减,带 rootKey client)
116
152
  ```ts
117
- import { verifyKey } from "unified-keys";
153
+ import { createClient } from "unified-keys";
154
+ const client = createClient({ rootKey: process.env.KEYS_ROOT_KEY });
118
155
  export async function POST(req: Request) {
119
156
  const { key } = await req.json();
120
- const r = await verifyKey({
121
- key,
122
- decrement: true,
123
- autoDelete: true, // 用完自动删
124
- context: { ip: req.headers.get("x-forwarded-for") },
125
- });
126
- 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 });
127
161
  // ... 授权逻辑
128
162
  }
129
163
  ```
130
164
 
131
- ### Next.js Server Component(只读查询,不扣减)
165
+ ### 公开只读校验(探活 / whoami,无需 rootKey)
132
166
  ```ts
133
- import { verifyKey } from "unified-keys";
134
- const info = await verifyKey({ key: params.key, decrement: false });
135
- if (!info.found || !info.valid) notFound();
136
- // info.remaining / info.meta / info.expiresAt
167
+ import { checkKey } from "unified-keys";
168
+ const r = await checkKey({ key }); // 不扣减,前端可直调
137
169
  ```
138
170
 
139
- ### Cloudflare Worker(管理端)
171
+ ### admin 反查(明文 key → 注册用户)
140
172
  ```ts
141
- import { createClient } from "unified-keys";
142
- const client = createClient({ rootKey: c.env.KEY_SERVICE_ROOT_KEY });
143
- await client.createKey({ appId: "my-app", credits: 1 });
173
+ const { found, key, recentLogs } = await client.lookupKey({ plaintext: "sk_xxx" });
144
174
  ```
145
175
 
146
176
  ## 服务端点
147
177
 
148
- - **公开 verify**:`POST https://keys.932324.xyz/v1/keys/verify`(无需鉴权,任何持有 key 的人可调)
149
- - **管理 API**:`https://keys.932324.xyz/v1/*`(需 `Authorization: Bearer <ROOT_KEY>`)
150
- - 健康检查:`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`
151
183
 
152
- ROOT_KEY 联系服务管理员获取。
184
+ ROOT_KEY 联系服务管理员获取(本项目在 `packages/infra/.env` 的 `KEYS_ROOT_KEY`;4 应用共用)。
153
185
 
154
186
  ## 实现位置(本项目内)
155
187
 
156
- - 服务端:`apps/key-service`(Hono Worker → keys.932324.xyz
157
- - SDK:`packages/keys`(npm `unified-keys`)
158
- - schema:`packages/db/src/schemas/key-service.ts`(key_apps / key_records / key_usage_logs)
159
- - 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.4.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 */
@@ -37,12 +68,20 @@ export interface CreateKeyInput {
37
68
  expiresAt?: string;
38
69
  /** 应用层关联业务实体(如 github-inviter 的 configSlug) */
39
70
  externalId?: string;
71
+ /** key body 长度(base62 位数),默认 8;长期付费 key 建议 16(防爆破) */
72
+ length?: number;
40
73
  /** 总额度(0=无限),默认等于 credits */
41
74
  maxUses?: number;
42
- /** 应用层任意元数据 */
75
+ /** 应用层任意元数据(count 个 key 共享) */
43
76
  meta?: Record<string, unknown>;
77
+ /**
78
+ * 批量创建时每个 key 的独立 meta(长度须 = count);不传则 count 个 key 共享 `meta`。
79
+ */
80
+ metas?: Record<string, unknown>[];
44
81
  /** 可选前缀,生成 `<prefix>_<rand>`;不传则纯 random */
45
82
  prefix?: string;
83
+ /** 限流配置(max 配置后启用滑动窗口限流)*/
84
+ rateLimit?: RateLimitConfig;
46
85
  }
47
86
 
48
87
  export interface CreatedKey {
@@ -57,16 +96,25 @@ export interface CreateKeyResult {
57
96
 
58
97
  export interface KeyRecord {
59
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";
60
103
  createdAt: string;
61
104
  creditsRemaining: number;
105
+ /** soft delete 标志(true=已软删,verify/list 过滤但 admin 可查)*/
106
+ deleted: boolean;
62
107
  enabled: boolean;
63
108
  expiresAt: string | null;
64
109
  externalId: string | null;
65
110
  keyId: string;
66
111
  maxUses: number;
67
112
  meta: Record<string, unknown> | null;
113
+ pausedUntil: string | null;
68
114
  plaintext: string;
69
115
  prefix: string | null;
116
+ rateLimitMax: number | null;
117
+ rateLimitWindowSec: number | null;
70
118
  updatedAt: string;
71
119
  }
72
120
 
@@ -104,7 +152,7 @@ export interface UsageLog {
104
152
  createdAt: string;
105
153
  creditsAfter: number | null;
106
154
  creditsBefore: number | null;
107
- /** verify(扣减) / refund(恢复) / create / delete / update / disable */
155
+ /** check(只读) / verify(扣减) / refund / create / soft_delete / rate_limited / auto_pause / auto_ban / pause / unpause / ban / unban / update */
108
156
  event: string;
109
157
  id: string;
110
158
  keyId: string;
@@ -124,31 +172,52 @@ export interface GetKeyResult {
124
172
  }
125
173
 
126
174
  export interface VerifyInput {
127
- /**
128
- * 扣减后额度用尽(remaining=0)自动删除:true 立即删;
129
- * "3m"/"2h"/"3d" 等延迟删(set deleteAt,Cron 定期清理,保留记录一段时间)
130
- */
131
- autoDelete?: boolean | string;
132
175
  /** 上下文,写入 usage_log(如 {username, source, ip}) */
133
176
  context?: Record<string, unknown>;
134
- /** 默认 true;false 时只查不扣(写 verify log 但不扣减) */
135
- decrement?: boolean;
136
177
  /** 完整明文 key */
137
178
  key: string;
138
179
  }
139
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
+
140
201
  export interface VerifyResult {
141
202
  appId?: string;
203
+ banStatus?: "active" | "paused" | "banned";
204
+ /** 失败码(valid=false 时)*/
205
+ code?: VerifyErrorCode;
142
206
  createdAt?: string;
143
207
  enabled?: boolean;
208
+ error?: string;
144
209
  expiresAt?: string | null;
145
210
  externalId?: string | null;
146
- /** key 是否存在(区别「不存在」与「存在但额度耗尽」) */
211
+ /** key 是否存在(区别「不存在」与「存在但额度耗尽/封禁」)*/
147
212
  found: boolean;
148
213
  keyId?: string;
149
214
  maxUses?: number;
150
215
  meta?: Record<string, unknown> | null;
216
+ /** 是否触发限流(429)*/
217
+ ratelimited?: boolean;
151
218
  remaining?: number;
219
+ /** 限流/暂停重置剩余秒数 */
220
+ resetIn?: number;
152
221
  valid: boolean;
153
222
  }
154
223
 
@@ -161,7 +230,37 @@ export interface BatchDeleteResult {
161
230
  failed: string[];
162
231
  }
163
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
+
164
260
  export interface KeyServiceClient {
261
+ banKey(keyId: string, input?: BanInput): Promise<DeleteResult>;
262
+ /** 公开 check(只读校验,不扣减不删,无需 rootKey)。 */
263
+ checkKey(input: CheckInput): Promise<CheckResult>;
165
264
  createApp(input: CreateAppInput): Promise<CreateAppResult>;
166
265
  createKey(input: CreateKeyInput): Promise<CreateKeyResult>;
167
266
  deleteKey(keyId: string): Promise<DeleteResult>;
@@ -173,6 +272,10 @@ export interface KeyServiceClient {
173
272
  ): Promise<UsageLogsResult>;
174
273
  listApps(): Promise<ListAppsResult>;
175
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>;
176
279
  updateCredits(
177
280
  keyId: string,
178
281
  input: UpdateCreditsInput