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