unified-keys 0.1.0 → 0.3.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 +2 -2
- package/SKILL.md +52 -39
- package/package.json +1 -1
- package/src/client.ts +6 -7
- package/src/types.ts +6 -2
package/README.md
CHANGED
|
@@ -51,8 +51,8 @@ const result = await verifyKey(
|
|
|
51
51
|
### `client.createKey({ appId, prefix?, credits?, maxUses?, meta?, externalId?, expiresAt?, count? })`
|
|
52
52
|
创建 key。返回 `{ keys: [{ key, keyId }] }`,`key` 是完整明文。`appId` 首次自动注册。
|
|
53
53
|
|
|
54
|
-
### `verifyKey({
|
|
55
|
-
公开验证(无需 rootKey
|
|
54
|
+
### `verifyKey({ key, decrement?, autoDelete?, context? })`
|
|
55
|
+
公开验证(无需 rootKey,baseUrl 内置默认)。`decrement: true` 扣减 + 写 verify log;`false` 只查。`autoDelete: true` 扣减后额度用尽(remaining=0)自动删 key(一次性 key,避免 DB 堆积)。SDK 前置 `isValidKeyFormat` 校验,格式不对直接返回不查 DB。
|
|
56
56
|
返回 `{ valid, found, keyId, remaining, maxUses, meta, externalId, appId, enabled, expiresAt, createdAt }`。
|
|
57
57
|
|
|
58
58
|
### `client.listKeys({ appId, limit?, offset? })`
|
package/SKILL.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: unified-keys
|
|
3
|
-
description: 当项目需要管理 API key
|
|
3
|
+
description: 当项目需要管理 API key(创建、验证扣减额度、查询使用记录、管理端显示明文、用尽自动删除)时使用。自建通用 key 服务 keys.932324.xyz,明文存储 + 完整使用记录,npm 包 unified-keys。
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# unified-keys
|
|
7
7
|
|
|
8
|
-
自建通用 API key 管理 SDK(替代 Unkey)。**明文存储 + 明文返回 +
|
|
8
|
+
自建通用 API key 管理 SDK(替代 Unkey)。**明文存储 + 明文返回 + 完整使用记录 + 用尽自动删除**。服务跑在 Cloudflare Worker(`keys.932324.xyz`),数据存 D1。
|
|
9
9
|
|
|
10
10
|
## 何时用
|
|
11
11
|
|
|
@@ -13,6 +13,7 @@ description: 当项目需要管理 API key(创建、验证扣减额度、查
|
|
|
13
13
|
- 需要 verify key + 原子扣减额度(credits)
|
|
14
14
|
- 想看每个 key 的使用历史(谁、何时、扣了几次、退了几次)
|
|
15
15
|
- 想在管理端显示**完整 key 明文**(Unkey 做不到,只给前缀)
|
|
16
|
+
- 一次性 key 用完自动删(避免 DB 堆积无用 key)
|
|
16
17
|
- 多项目共用一个 key 服务(appId 命名空间隔离)
|
|
17
18
|
|
|
18
19
|
## 安装
|
|
@@ -22,24 +23,15 @@ pnpm add unified-keys
|
|
|
22
23
|
# 或 npm i unified-keys
|
|
23
24
|
```
|
|
24
25
|
|
|
25
|
-
## 服务端点
|
|
26
|
-
|
|
27
|
-
- **公开 verify**:`POST https://keys.932324.xyz/v1/keys/verify`(无需鉴权,任何持有 key 的人可调)
|
|
28
|
-
- **管理 API**:`https://keys.932324.xyz/v1/*`(需 `Authorization: Bearer <ROOT_KEY>`)
|
|
29
|
-
- 健康检查:`GET https://keys.932324.xyz/v1/health`
|
|
30
|
-
|
|
31
|
-
ROOT_KEY 联系服务管理员获取。
|
|
32
|
-
|
|
33
26
|
## 核心 API
|
|
34
27
|
|
|
28
|
+
**baseUrl 内置默认**(`https://keys.932324.xyz`),无需传。
|
|
29
|
+
|
|
35
30
|
```ts
|
|
36
|
-
import { createClient, verifyKey } from "unified-keys";
|
|
31
|
+
import { createClient, isValidKeyFormat, verifyKey } from "unified-keys";
|
|
37
32
|
|
|
38
|
-
// 管理客户端(需 rootKey
|
|
39
|
-
const client = createClient({
|
|
40
|
-
baseUrl: "https://keys.932324.xyz",
|
|
41
|
-
rootKey: process.env.KEYS_ROOT_KEY,
|
|
42
|
-
});
|
|
33
|
+
// 管理客户端(需 rootKey;baseUrl 内置)
|
|
34
|
+
const client = createClient({ rootKey: process.env.KEYS_ROOT_KEY });
|
|
43
35
|
|
|
44
36
|
// 创建 key(明文返回;appId = 项目 slug,首次自动注册)
|
|
45
37
|
const { keys } = await client.createKey({
|
|
@@ -50,13 +42,15 @@ const { keys } = await client.createKey({
|
|
|
50
42
|
meta: { plan: "pro" }, // 任意元数据(可选)
|
|
51
43
|
count: 1, // 批量创建数
|
|
52
44
|
});
|
|
53
|
-
// keys[0].key =
|
|
45
|
+
// keys[0].key = 完整明文 `<prefix>_<8位base62>`
|
|
54
46
|
|
|
55
47
|
// 验证 + 扣减(用户持有明文 key,服务端验证)
|
|
56
|
-
const result = await verifyKey(
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
|
53
|
+
});
|
|
60
54
|
// { valid, found, keyId, remaining, maxUses, meta, externalId, appId, enabled, expiresAt, createdAt }
|
|
61
55
|
// found=false: key 不存在;valid=false found=true: 存在但禁用/过期/额度耗尽
|
|
62
56
|
|
|
@@ -68,7 +62,7 @@ const { key, usageLogs } = await client.getKey(keyId);
|
|
|
68
62
|
|
|
69
63
|
// 查使用记录(核心特性)
|
|
70
64
|
const { logs } = await client.getKeyUsageLogs(keyId, { limit: 50 });
|
|
71
|
-
// logs: [{ event: "verify"|"refund"|"create"|"delete"|"update", creditsBefore, creditsAfter, context, createdAt }]
|
|
65
|
+
// logs: [{ event: "verify"|"refund"|"create"|"delete"|"update"|"disable", creditsBefore, creditsAfter, context, createdAt }]
|
|
72
66
|
|
|
73
67
|
// 调整额度(refund 恢复)
|
|
74
68
|
await client.updateCredits(keyId, {
|
|
@@ -83,35 +77,52 @@ await client.updateKey(keyId, { meta: { plan: "enterprise" }, enabled: false });
|
|
|
83
77
|
// 删除
|
|
84
78
|
await client.deleteKey(keyId);
|
|
85
79
|
await client.deleteKeys([keyId1, keyId2]);
|
|
80
|
+
|
|
81
|
+
// 手输校验(isValidKeyFormat 库内置,与 verify 前置校验同源)
|
|
82
|
+
if (!isValidKeyFormat(inputKey)) return invalid();
|
|
86
83
|
```
|
|
87
84
|
|
|
88
85
|
## 特性(区别于 Unkey)
|
|
89
86
|
|
|
90
87
|
| | unified-keys | Unkey |
|
|
91
88
|
|---|---|---|
|
|
92
|
-
| 明文显示 | ✅ 列表/详情返回完整明文 | ❌
|
|
89
|
+
| 明文显示 | ✅ 列表/详情返回完整明文 | ❌ 只给前缀 |
|
|
93
90
|
| 使用记录 | ✅ 每次 verify/refund/create/delete 写 usage_log | ❌ 只给当前 remaining |
|
|
94
91
|
| verify 延迟 | <5ms(D1 binding 零网络跳) | 50-100ms(公网) |
|
|
92
|
+
| 用尽自动删 | ✅ verify `autoDelete` 选项 | ❌ |
|
|
93
|
+
| 前置格式校验 | ✅ SDK `isValidKeyFormat`,格式不对不查 DB | ❌ |
|
|
95
94
|
| 自托管 | ✅ 自己的 D1 | ❌ SaaS |
|
|
96
95
|
| 原子扣减 | ✅ `UPDATE ... WHERE credits>0 RETURNING` | ✅ |
|
|
97
96
|
|
|
97
|
+
## verify 选项
|
|
98
|
+
|
|
99
|
+
| 选项 | 默认 | 说明 |
|
|
100
|
+
|---|---|---|
|
|
101
|
+
| `decrement` | true | true 扣减 + 写 verify log;false 只查 |
|
|
102
|
+
| `autoDelete` | false | 扣减后 remaining=0 自动删 key(一次性 key,避免 DB 堆积无用 key) |
|
|
103
|
+
| `context` | - | 写入 usage_log({username, ip, source, ...}) |
|
|
104
|
+
|
|
105
|
+
**SDK 前置校验**:verify 前先 `isValidKeyFormat(key)`,格式不对直接返回 `{valid:false, found:false}`,不查 DB(省时间)。应用也可单独用 `isValidKeyFormat` 做手输校验。
|
|
106
|
+
|
|
98
107
|
## event 类型(usage_log)
|
|
99
108
|
|
|
100
109
|
- `verify`:verify 扣减(含 creditsBefore/After + context)
|
|
101
110
|
- `refund`:updateCredits increment 恢复额度
|
|
102
|
-
- `create` / `delete
|
|
111
|
+
- `create` / `delete`(含 autoDelete 触发的 exhausted_autodelete)/ `update` / `disable`
|
|
103
112
|
|
|
104
113
|
## 集成示例
|
|
105
114
|
|
|
106
|
-
### Next.js Route Handler(服务端 verify
|
|
115
|
+
### Next.js Route Handler(服务端 verify + 一次性 key 用尽自动删)
|
|
107
116
|
```ts
|
|
108
117
|
import { verifyKey } from "unified-keys";
|
|
109
118
|
export async function POST(req: Request) {
|
|
110
119
|
const { key } = await req.json();
|
|
111
|
-
const r = await verifyKey(
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
120
|
+
const r = await verifyKey({
|
|
121
|
+
key,
|
|
122
|
+
decrement: true,
|
|
123
|
+
autoDelete: true, // 用完自动删
|
|
124
|
+
context: { ip: req.headers.get("x-forwarded-for") },
|
|
125
|
+
});
|
|
115
126
|
if (!r.valid) return Response.json({ error: "invalid key" }, { status: 403 });
|
|
116
127
|
// ... 授权逻辑
|
|
117
128
|
}
|
|
@@ -120,10 +131,7 @@ export async function POST(req: Request) {
|
|
|
120
131
|
### Next.js Server Component(只读查询,不扣减)
|
|
121
132
|
```ts
|
|
122
133
|
import { verifyKey } from "unified-keys";
|
|
123
|
-
const info = await verifyKey(
|
|
124
|
-
{ baseUrl: "https://keys.932324.xyz" },
|
|
125
|
-
{ key: params.key, decrement: false }
|
|
126
|
-
);
|
|
134
|
+
const info = await verifyKey({ key: params.key, decrement: false });
|
|
127
135
|
if (!info.found || !info.valid) notFound();
|
|
128
136
|
// info.remaining / info.meta / info.expiresAt
|
|
129
137
|
```
|
|
@@ -131,16 +139,21 @@ if (!info.found || !info.valid) notFound();
|
|
|
131
139
|
### Cloudflare Worker(管理端)
|
|
132
140
|
```ts
|
|
133
141
|
import { createClient } from "unified-keys";
|
|
134
|
-
const client = createClient({
|
|
135
|
-
baseUrl: c.env.KEY_SERVICE_URL,
|
|
136
|
-
rootKey: c.env.KEY_SERVICE_ROOT_KEY,
|
|
137
|
-
});
|
|
142
|
+
const client = createClient({ rootKey: c.env.KEY_SERVICE_ROOT_KEY });
|
|
138
143
|
await client.createKey({ appId: "my-app", credits: 1 });
|
|
139
144
|
```
|
|
140
145
|
|
|
146
|
+
## 服务端点
|
|
147
|
+
|
|
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`
|
|
151
|
+
|
|
152
|
+
ROOT_KEY 联系服务管理员获取。
|
|
153
|
+
|
|
141
154
|
## 实现位置(本项目内)
|
|
142
155
|
|
|
143
156
|
- 服务端:`apps/key-service`(Hono Worker → keys.932324.xyz)
|
|
144
|
-
- SDK:`packages/keys
|
|
157
|
+
- SDK:`packages/keys`(npm `unified-keys`)
|
|
145
158
|
- schema:`packages/db/src/schemas/key-service.ts`(key_apps / key_records / key_usage_logs)
|
|
146
|
-
-
|
|
159
|
+
- 4 应用全迁移:github-inviter / user-dashboard / notify-me 完整迁移;shadcn-proxy 新用新服务 + Unkey 格式路由 fallback(旧 key 保持可用)
|
package/package.json
CHANGED
package/src/client.ts
CHANGED
|
@@ -20,6 +20,8 @@ import type {
|
|
|
20
20
|
VerifyResult,
|
|
21
21
|
} from "./types";
|
|
22
22
|
|
|
23
|
+
const DEFAULT_BASE_URL = "https://keys.932324.xyz";
|
|
24
|
+
|
|
23
25
|
const KEY_FORMAT = /^[a-zA-Z0-9_-]{8,128}$/;
|
|
24
26
|
|
|
25
27
|
/**
|
|
@@ -31,7 +33,7 @@ export function isValidKeyFormat(key: string): boolean {
|
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
export function createClient(config: KeyServiceClientConfig): KeyServiceClient {
|
|
34
|
-
const baseUrl = config.baseUrl.replace(/\/$/, "");
|
|
36
|
+
const baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
35
37
|
const rootKey = config.rootKey;
|
|
36
38
|
|
|
37
39
|
async function request<T>(
|
|
@@ -118,15 +120,12 @@ export function createClient(config: KeyServiceClientConfig): KeyServiceClient {
|
|
|
118
120
|
}
|
|
119
121
|
|
|
120
122
|
/**
|
|
121
|
-
* 公开 verify 端点,无需 rootKey
|
|
123
|
+
* 公开 verify 端点,无需 rootKey,baseUrl 内置默认。
|
|
122
124
|
* 适合「用户持有明文 key、服务端验证并扣减」的场景。
|
|
123
125
|
*/
|
|
124
|
-
export async function verifyKey(
|
|
125
|
-
config: { baseUrl: string },
|
|
126
|
-
input: VerifyInput
|
|
127
|
-
): Promise<VerifyResult> {
|
|
126
|
+
export async function verifyKey(input: VerifyInput): Promise<VerifyResult> {
|
|
128
127
|
if (!isValidKeyFormat(input.key)) return { valid: false, found: false };
|
|
129
|
-
const baseUrl =
|
|
128
|
+
const baseUrl = DEFAULT_BASE_URL;
|
|
130
129
|
const res = await fetch(`${baseUrl}/v1/keys/verify`, {
|
|
131
130
|
method: "POST",
|
|
132
131
|
headers: { "Content-Type": "application/json" },
|
package/src/types.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/** unified-keys SDK 类型定义 */
|
|
2
2
|
|
|
3
3
|
export interface KeyServiceClientConfig {
|
|
4
|
-
/**
|
|
5
|
-
|
|
4
|
+
/**
|
|
5
|
+
* 服务端点,默认 https://keys.932324.xyz(自建服务固定,通常无需传)
|
|
6
|
+
*/
|
|
7
|
+
baseUrl?: string;
|
|
6
8
|
/** 管理 API 需要;verify 端点不需要 */
|
|
7
9
|
rootKey?: string;
|
|
8
10
|
}
|
|
@@ -122,6 +124,8 @@ export interface GetKeyResult {
|
|
|
122
124
|
}
|
|
123
125
|
|
|
124
126
|
export interface VerifyInput {
|
|
127
|
+
/** 扣减后额度用尽(remaining=0)时自动删除 key,避免 DB 堆积无用 key */
|
|
128
|
+
autoDelete?: boolean;
|
|
125
129
|
/** 上下文,写入 usage_log(如 {username, source, ip}) */
|
|
126
130
|
context?: Record<string, unknown>;
|
|
127
131
|
/** 默认 true;false 时只查不扣(写 verify log 但不扣减) */
|