@55387.ai/uniauth-server 1.0.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 +154 -0
- package/dist/index.cjs +378 -0
- package/dist/index.d.cts +260 -0
- package/dist/index.d.ts +260 -0
- package/dist/index.js +341 -0
- package/package.json +43 -0
- package/src/index.ts +581 -0
- package/src/server.test.ts +231 -0
- package/tsconfig.json +15 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import * as jose from "jose";
|
|
3
|
+
var ServerErrorCode = {
|
|
4
|
+
INVALID_TOKEN: "INVALID_TOKEN",
|
|
5
|
+
TOKEN_EXPIRED: "TOKEN_EXPIRED",
|
|
6
|
+
VERIFICATION_FAILED: "VERIFICATION_FAILED",
|
|
7
|
+
USER_NOT_FOUND: "USER_NOT_FOUND",
|
|
8
|
+
UNAUTHORIZED: "UNAUTHORIZED",
|
|
9
|
+
NO_PUBLIC_KEY: "NO_PUBLIC_KEY",
|
|
10
|
+
NETWORK_ERROR: "NETWORK_ERROR",
|
|
11
|
+
INTERNAL_ERROR: "INTERNAL_ERROR"
|
|
12
|
+
};
|
|
13
|
+
var ServerAuthError = class _ServerAuthError extends Error {
|
|
14
|
+
code;
|
|
15
|
+
statusCode;
|
|
16
|
+
constructor(code, message, statusCode = 401) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = "ServerAuthError";
|
|
19
|
+
this.code = code;
|
|
20
|
+
this.statusCode = statusCode;
|
|
21
|
+
if (Error.captureStackTrace) {
|
|
22
|
+
Error.captureStackTrace(this, _ServerAuthError);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
var UniAuthServer = class {
|
|
27
|
+
config;
|
|
28
|
+
tokenCache = /* @__PURE__ */ new Map();
|
|
29
|
+
constructor(config) {
|
|
30
|
+
this.config = {
|
|
31
|
+
...config,
|
|
32
|
+
clientId: config.clientId || config.appKey || "",
|
|
33
|
+
clientSecret: config.clientSecret || config.appSecret || ""
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
// ============================================
|
|
37
|
+
// Token Verification
|
|
38
|
+
// ============================================
|
|
39
|
+
/**
|
|
40
|
+
* Verify access token
|
|
41
|
+
* 验证访问令牌
|
|
42
|
+
*
|
|
43
|
+
* @param token - JWT access token
|
|
44
|
+
* @returns Token payload if valid
|
|
45
|
+
* @throws ServerAuthError if token is invalid
|
|
46
|
+
*/
|
|
47
|
+
async verifyToken(token) {
|
|
48
|
+
const cached = this.tokenCache.get(token);
|
|
49
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
50
|
+
return cached.payload;
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
const response = await fetch(`${this.config.baseUrl}/api/v1/auth/verify`, {
|
|
54
|
+
method: "POST",
|
|
55
|
+
headers: {
|
|
56
|
+
"Content-Type": "application/json",
|
|
57
|
+
"X-App-Key": this.config.clientId,
|
|
58
|
+
"X-App-Secret": this.config.clientSecret
|
|
59
|
+
},
|
|
60
|
+
body: JSON.stringify({ token })
|
|
61
|
+
});
|
|
62
|
+
if (!response.ok) {
|
|
63
|
+
const errorResponse = await response.json();
|
|
64
|
+
throw new ServerAuthError(
|
|
65
|
+
errorResponse.error?.code || ServerErrorCode.INVALID_TOKEN,
|
|
66
|
+
errorResponse.error?.message || "Invalid token",
|
|
67
|
+
401
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
const data = await response.json();
|
|
71
|
+
const payload = data.data;
|
|
72
|
+
const cacheExpiry = Math.min(payload.exp * 1e3, Date.now() + 60 * 1e3);
|
|
73
|
+
this.tokenCache.set(token, { payload, expiresAt: cacheExpiry });
|
|
74
|
+
return payload;
|
|
75
|
+
} catch (error) {
|
|
76
|
+
if (error instanceof ServerAuthError) {
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
if (this.config.jwtPublicKey) {
|
|
80
|
+
return this.verifyTokenLocally(token);
|
|
81
|
+
}
|
|
82
|
+
throw new ServerAuthError(
|
|
83
|
+
ServerErrorCode.VERIFICATION_FAILED,
|
|
84
|
+
error instanceof Error ? error.message : "Token verification failed",
|
|
85
|
+
401
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Verify token locally using JWT public key
|
|
91
|
+
* 使用 JWT 公钥本地验证令牌
|
|
92
|
+
*/
|
|
93
|
+
async verifyTokenLocally(token) {
|
|
94
|
+
if (!this.config.jwtPublicKey) {
|
|
95
|
+
throw new ServerAuthError(ServerErrorCode.NO_PUBLIC_KEY, "JWT public key not configured", 500);
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
const publicKey = await jose.importSPKI(this.config.jwtPublicKey, "RS256");
|
|
99
|
+
const { payload } = await jose.jwtVerify(token, publicKey);
|
|
100
|
+
return {
|
|
101
|
+
sub: payload.sub,
|
|
102
|
+
iss: payload.iss,
|
|
103
|
+
aud: payload.aud,
|
|
104
|
+
iat: payload.iat,
|
|
105
|
+
exp: payload.exp,
|
|
106
|
+
scope: payload.scope,
|
|
107
|
+
azp: payload.azp,
|
|
108
|
+
phone: payload.phone,
|
|
109
|
+
email: payload.email
|
|
110
|
+
};
|
|
111
|
+
} catch (error) {
|
|
112
|
+
throw new ServerAuthError(
|
|
113
|
+
ServerErrorCode.INVALID_TOKEN,
|
|
114
|
+
error instanceof Error ? error.message : "Invalid token",
|
|
115
|
+
401
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// ============================================
|
|
120
|
+
// OAuth2 Token Introspection (RFC 7662)
|
|
121
|
+
// ============================================
|
|
122
|
+
/**
|
|
123
|
+
* Introspect a token (RFC 7662)
|
|
124
|
+
* 内省令牌(RFC 7662 标准)
|
|
125
|
+
*
|
|
126
|
+
* This is the standard way for resource servers to validate tokens.
|
|
127
|
+
*
|
|
128
|
+
* @param token - The token to introspect
|
|
129
|
+
* @param tokenTypeHint - Optional hint about the token type ('access_token' or 'refresh_token')
|
|
130
|
+
* @returns Introspection result
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* ```typescript
|
|
134
|
+
* const result = await auth.introspectToken(accessToken);
|
|
135
|
+
* if (result.active) {
|
|
136
|
+
* console.log('Token is valid, user:', result.sub);
|
|
137
|
+
* }
|
|
138
|
+
* ```
|
|
139
|
+
*/
|
|
140
|
+
async introspectToken(token, tokenTypeHint) {
|
|
141
|
+
try {
|
|
142
|
+
const credentials = Buffer.from(`${this.config.clientId}:${this.config.clientSecret}`).toString("base64");
|
|
143
|
+
const body = { token };
|
|
144
|
+
if (tokenTypeHint) {
|
|
145
|
+
body.token_type_hint = tokenTypeHint;
|
|
146
|
+
}
|
|
147
|
+
const response = await fetch(`${this.config.baseUrl}/api/v1/oauth2/introspect`, {
|
|
148
|
+
method: "POST",
|
|
149
|
+
headers: {
|
|
150
|
+
"Content-Type": "application/json",
|
|
151
|
+
"Authorization": `Basic ${credentials}`
|
|
152
|
+
},
|
|
153
|
+
body: JSON.stringify(body)
|
|
154
|
+
});
|
|
155
|
+
const result = await response.json();
|
|
156
|
+
return result;
|
|
157
|
+
} catch (error) {
|
|
158
|
+
return { active: false };
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Check if a token is active
|
|
163
|
+
* 检查令牌是否有效
|
|
164
|
+
*
|
|
165
|
+
* @param token - The token to check
|
|
166
|
+
* @returns true if token is active
|
|
167
|
+
*/
|
|
168
|
+
async isTokenActive(token) {
|
|
169
|
+
const result = await this.introspectToken(token);
|
|
170
|
+
return result.active;
|
|
171
|
+
}
|
|
172
|
+
// ============================================
|
|
173
|
+
// User Management
|
|
174
|
+
// ============================================
|
|
175
|
+
/**
|
|
176
|
+
* Get user info by ID
|
|
177
|
+
* 根据 ID 获取用户信息
|
|
178
|
+
*/
|
|
179
|
+
async getUser(userId) {
|
|
180
|
+
const response = await fetch(`${this.config.baseUrl}/api/v1/admin/users/${userId}`, {
|
|
181
|
+
method: "GET",
|
|
182
|
+
headers: {
|
|
183
|
+
"Content-Type": "application/json",
|
|
184
|
+
"X-App-Key": this.config.clientId,
|
|
185
|
+
"X-App-Secret": this.config.clientSecret
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
if (!response.ok) {
|
|
189
|
+
const errorResponse = await response.json();
|
|
190
|
+
throw new ServerAuthError(
|
|
191
|
+
errorResponse.error?.code || ServerErrorCode.USER_NOT_FOUND,
|
|
192
|
+
errorResponse.error?.message || "User not found",
|
|
193
|
+
response.status
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
const data = await response.json();
|
|
197
|
+
return data.data;
|
|
198
|
+
}
|
|
199
|
+
// ============================================
|
|
200
|
+
// Express/Connect Middleware
|
|
201
|
+
// ============================================
|
|
202
|
+
/**
|
|
203
|
+
* Express/Connect middleware for authentication
|
|
204
|
+
* Express/Connect 认证中间件
|
|
205
|
+
*
|
|
206
|
+
* @example
|
|
207
|
+
* ```typescript
|
|
208
|
+
* import express from 'express';
|
|
209
|
+
*
|
|
210
|
+
* const app = express();
|
|
211
|
+
* app.use('/api/*', auth.middleware());
|
|
212
|
+
*
|
|
213
|
+
* app.get('/api/profile', (req, res) => {
|
|
214
|
+
* res.json({ user: req.user });
|
|
215
|
+
* });
|
|
216
|
+
* ```
|
|
217
|
+
*/
|
|
218
|
+
middleware() {
|
|
219
|
+
return async (req, res, next) => {
|
|
220
|
+
const authHeader = req.headers["authorization"];
|
|
221
|
+
if (!authHeader || typeof authHeader !== "string" || !authHeader.startsWith("Bearer ")) {
|
|
222
|
+
res.status(401).json({
|
|
223
|
+
success: false,
|
|
224
|
+
error: {
|
|
225
|
+
code: ServerErrorCode.UNAUTHORIZED,
|
|
226
|
+
message: "Authorization header is required / \u9700\u8981\u6388\u6743\u5934"
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
const token = authHeader.substring(7);
|
|
232
|
+
try {
|
|
233
|
+
const payload = await this.verifyToken(token);
|
|
234
|
+
req.authPayload = payload;
|
|
235
|
+
try {
|
|
236
|
+
req.user = await this.getUser(payload.sub);
|
|
237
|
+
} catch {
|
|
238
|
+
}
|
|
239
|
+
next();
|
|
240
|
+
} catch (error) {
|
|
241
|
+
const authError = error instanceof ServerAuthError ? error : new ServerAuthError(
|
|
242
|
+
ServerErrorCode.UNAUTHORIZED,
|
|
243
|
+
"Authentication failed",
|
|
244
|
+
401
|
|
245
|
+
);
|
|
246
|
+
res.status(authError.statusCode).json({
|
|
247
|
+
success: false,
|
|
248
|
+
error: {
|
|
249
|
+
code: authError.code,
|
|
250
|
+
message: authError.message
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
// ============================================
|
|
257
|
+
// Hono Middleware
|
|
258
|
+
// ============================================
|
|
259
|
+
/**
|
|
260
|
+
* Hono middleware for authentication
|
|
261
|
+
* Hono 认证中间件
|
|
262
|
+
*
|
|
263
|
+
* @example
|
|
264
|
+
* ```typescript
|
|
265
|
+
* import { Hono } from 'hono';
|
|
266
|
+
*
|
|
267
|
+
* const app = new Hono();
|
|
268
|
+
* app.use('/api/*', auth.honoMiddleware());
|
|
269
|
+
*
|
|
270
|
+
* app.get('/api/profile', (c) => {
|
|
271
|
+
* const user = c.get('user');
|
|
272
|
+
* return c.json({ user });
|
|
273
|
+
* });
|
|
274
|
+
* ```
|
|
275
|
+
*/
|
|
276
|
+
honoMiddleware() {
|
|
277
|
+
return async (c, next) => {
|
|
278
|
+
const authHeader = c.req.header("authorization") || c.req.header("Authorization");
|
|
279
|
+
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
280
|
+
return c.json({
|
|
281
|
+
success: false,
|
|
282
|
+
error: {
|
|
283
|
+
code: ServerErrorCode.UNAUTHORIZED,
|
|
284
|
+
message: "Authorization header is required / \u9700\u8981\u6388\u6743\u5934"
|
|
285
|
+
}
|
|
286
|
+
}, 401);
|
|
287
|
+
}
|
|
288
|
+
const token = authHeader.substring(7);
|
|
289
|
+
try {
|
|
290
|
+
const payload = await this.verifyToken(token);
|
|
291
|
+
c.set("authPayload", payload);
|
|
292
|
+
try {
|
|
293
|
+
const user = await this.getUser(payload.sub);
|
|
294
|
+
c.set("user", user);
|
|
295
|
+
} catch {
|
|
296
|
+
}
|
|
297
|
+
await next();
|
|
298
|
+
} catch (error) {
|
|
299
|
+
const authError = error instanceof ServerAuthError ? error : new ServerAuthError(
|
|
300
|
+
ServerErrorCode.UNAUTHORIZED,
|
|
301
|
+
"Authentication failed",
|
|
302
|
+
401
|
|
303
|
+
);
|
|
304
|
+
return c.json({
|
|
305
|
+
success: false,
|
|
306
|
+
error: {
|
|
307
|
+
code: authError.code,
|
|
308
|
+
message: authError.message
|
|
309
|
+
}
|
|
310
|
+
}, authError.statusCode);
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
// ============================================
|
|
315
|
+
// Utility Methods
|
|
316
|
+
// ============================================
|
|
317
|
+
/**
|
|
318
|
+
* Clear token cache
|
|
319
|
+
* 清除令牌缓存
|
|
320
|
+
*/
|
|
321
|
+
clearCache() {
|
|
322
|
+
this.tokenCache.clear();
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Get cache statistics
|
|
326
|
+
* 获取缓存统计
|
|
327
|
+
*/
|
|
328
|
+
getCacheStats() {
|
|
329
|
+
return {
|
|
330
|
+
size: this.tokenCache.size,
|
|
331
|
+
entries: this.tokenCache.size
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
var index_default = UniAuthServer;
|
|
336
|
+
export {
|
|
337
|
+
ServerAuthError,
|
|
338
|
+
ServerErrorCode,
|
|
339
|
+
UniAuthServer,
|
|
340
|
+
index_default as default
|
|
341
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@55387.ai/uniauth-server",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "UniAuth Server SDK - Token verification for Node.js backends",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.mjs",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js",
|
|
13
|
+
"types": "./dist/index.d.ts"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsup src/index.ts --format cjs,esm --dts --clean",
|
|
21
|
+
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
|
|
22
|
+
"test": "vitest run",
|
|
23
|
+
"test:watch": "vitest",
|
|
24
|
+
"lint": "eslint src --ext .ts",
|
|
25
|
+
"clean": "rm -rf dist"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"jose": "^5.9.6"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/node": "^22.10.2",
|
|
32
|
+
"tsup": "^8.3.5",
|
|
33
|
+
"typescript": "^5.7.2",
|
|
34
|
+
"vitest": "^2.1.8"
|
|
35
|
+
},
|
|
36
|
+
"keywords": [
|
|
37
|
+
"auth",
|
|
38
|
+
"authentication",
|
|
39
|
+
"middleware",
|
|
40
|
+
"jwt",
|
|
41
|
+
"sdk"
|
|
42
|
+
]
|
|
43
|
+
}
|