@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 ADDED
@@ -0,0 +1,154 @@
1
+ # @55387.ai/uniauth-server
2
+
3
+ UniAuth 后端 SDK,用于在 Node.js 后端服务中验证用户令牌。
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ npm install @55387.ai/uniauth-server
9
+ # or
10
+ pnpm add @55387.ai/uniauth-server
11
+ ```
12
+
13
+ ## 快速开始
14
+
15
+ ```typescript
16
+ import { UniAuthServer } from '@55387.ai/uniauth-server';
17
+
18
+ const auth = new UniAuthServer({
19
+ baseUrl: 'https://sso.55387.xyz',
20
+ clientId: 'your-client-id',
21
+ clientSecret: 'your-client-secret',
22
+ });
23
+
24
+ // 验证令牌
25
+ const payload = await auth.verifyToken(accessToken);
26
+ console.log('User ID:', payload.sub);
27
+ ```
28
+
29
+ ## Express 中间件
30
+
31
+ ```typescript
32
+ import express from 'express';
33
+ import { UniAuthServer } from '@uniauth/server-sdk';
34
+
35
+ const app = express();
36
+ const auth = new UniAuthServer({ ... });
37
+
38
+ // 保护 API 路由
39
+ app.use('/api/*', auth.middleware());
40
+
41
+ // 在路由中使用用户信息
42
+ app.get('/api/profile', (req, res) => {
43
+ res.json({ user: req.user, payload: req.authPayload });
44
+ });
45
+ ```
46
+
47
+ ## Hono 中间件
48
+
49
+ ```typescript
50
+ import { Hono } from 'hono';
51
+ import { UniAuthServer } from '@uniauth/server-sdk';
52
+
53
+ const app = new Hono();
54
+ const auth = new UniAuthServer({ ... });
55
+
56
+ // 保护 API 路由
57
+ app.use('/api/*', auth.honoMiddleware());
58
+
59
+ // 在路由中使用用户信息
60
+ app.get('/api/profile', (c) => {
61
+ const user = c.get('user');
62
+ return c.json({ user });
63
+ });
64
+ ```
65
+
66
+ ## OAuth2 Token Introspection (RFC 7662)
67
+
68
+ ```typescript
69
+ // 内省令牌(资源服务器标准验证方式)
70
+ const result = await auth.introspectToken(accessToken);
71
+
72
+ if (result.active) {
73
+ console.log('Token 有效');
74
+ console.log('用户:', result.sub);
75
+ console.log('权限:', result.scope);
76
+ } else {
77
+ console.log('Token 无效或已过期');
78
+ }
79
+ ```
80
+
81
+ ## API 参考
82
+
83
+ ### 初始化选项
84
+
85
+ ```typescript
86
+ interface UniAuthServerConfig {
87
+ baseUrl: string; // UniAuth 服务地址
88
+ clientId: string; // OAuth2 客户端 ID
89
+ clientSecret: string; // OAuth2 客户端密钥
90
+ jwtPublicKey?: string; // JWT 公钥(用于本地验证)
91
+ }
92
+ ```
93
+
94
+ ### 方法
95
+
96
+ | 方法 | 说明 |
97
+ |------|------|
98
+ | `verifyToken(token)` | 验证访问令牌 |
99
+ | `introspectToken(token)` | RFC 7662 令牌内省 |
100
+ | `isTokenActive(token)` | 检查令牌是否有效 |
101
+ | `getUser(userId)` | 获取用户信息 |
102
+ | `middleware()` | Express/Connect 中间件 |
103
+ | `honoMiddleware()` | Hono 中间件 |
104
+ | `clearCache()` | 清除令牌缓存 |
105
+
106
+ ### 类型
107
+
108
+ ```typescript
109
+ interface TokenPayload {
110
+ sub: string; // 用户 ID
111
+ iss?: string; // 签发者
112
+ aud?: string | string[]; // 受众
113
+ exp: number; // 过期时间
114
+ iat: number; // 签发时间
115
+ scope?: string; // 权限范围
116
+ phone?: string; // 手机号
117
+ email?: string; // 邮箱
118
+ }
119
+
120
+ interface UserInfo {
121
+ id: string;
122
+ phone?: string;
123
+ email?: string;
124
+ nickname?: string;
125
+ avatar_url?: string;
126
+ phone_verified?: boolean;
127
+ email_verified?: boolean;
128
+ }
129
+ ```
130
+
131
+ ## 错误处理
132
+
133
+ ```typescript
134
+ import { ServerAuthError, ServerErrorCode } from '@55387.ai/uniauth-server';
135
+
136
+ try {
137
+ await auth.verifyToken(token);
138
+ } catch (error) {
139
+ if (error instanceof ServerAuthError) {
140
+ switch (error.code) {
141
+ case ServerErrorCode.INVALID_TOKEN:
142
+ // 令牌无效
143
+ break;
144
+ case ServerErrorCode.TOKEN_EXPIRED:
145
+ // 令牌已过期
146
+ break;
147
+ }
148
+ }
149
+ }
150
+ ```
151
+
152
+ ## License
153
+
154
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,378 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ ServerAuthError: () => ServerAuthError,
34
+ ServerErrorCode: () => ServerErrorCode,
35
+ UniAuthServer: () => UniAuthServer,
36
+ default: () => index_default
37
+ });
38
+ module.exports = __toCommonJS(index_exports);
39
+ var jose = __toESM(require("jose"), 1);
40
+ var ServerErrorCode = {
41
+ INVALID_TOKEN: "INVALID_TOKEN",
42
+ TOKEN_EXPIRED: "TOKEN_EXPIRED",
43
+ VERIFICATION_FAILED: "VERIFICATION_FAILED",
44
+ USER_NOT_FOUND: "USER_NOT_FOUND",
45
+ UNAUTHORIZED: "UNAUTHORIZED",
46
+ NO_PUBLIC_KEY: "NO_PUBLIC_KEY",
47
+ NETWORK_ERROR: "NETWORK_ERROR",
48
+ INTERNAL_ERROR: "INTERNAL_ERROR"
49
+ };
50
+ var ServerAuthError = class _ServerAuthError extends Error {
51
+ code;
52
+ statusCode;
53
+ constructor(code, message, statusCode = 401) {
54
+ super(message);
55
+ this.name = "ServerAuthError";
56
+ this.code = code;
57
+ this.statusCode = statusCode;
58
+ if (Error.captureStackTrace) {
59
+ Error.captureStackTrace(this, _ServerAuthError);
60
+ }
61
+ }
62
+ };
63
+ var UniAuthServer = class {
64
+ config;
65
+ tokenCache = /* @__PURE__ */ new Map();
66
+ constructor(config) {
67
+ this.config = {
68
+ ...config,
69
+ clientId: config.clientId || config.appKey || "",
70
+ clientSecret: config.clientSecret || config.appSecret || ""
71
+ };
72
+ }
73
+ // ============================================
74
+ // Token Verification
75
+ // ============================================
76
+ /**
77
+ * Verify access token
78
+ * 验证访问令牌
79
+ *
80
+ * @param token - JWT access token
81
+ * @returns Token payload if valid
82
+ * @throws ServerAuthError if token is invalid
83
+ */
84
+ async verifyToken(token) {
85
+ const cached = this.tokenCache.get(token);
86
+ if (cached && cached.expiresAt > Date.now()) {
87
+ return cached.payload;
88
+ }
89
+ try {
90
+ const response = await fetch(`${this.config.baseUrl}/api/v1/auth/verify`, {
91
+ method: "POST",
92
+ headers: {
93
+ "Content-Type": "application/json",
94
+ "X-App-Key": this.config.clientId,
95
+ "X-App-Secret": this.config.clientSecret
96
+ },
97
+ body: JSON.stringify({ token })
98
+ });
99
+ if (!response.ok) {
100
+ const errorResponse = await response.json();
101
+ throw new ServerAuthError(
102
+ errorResponse.error?.code || ServerErrorCode.INVALID_TOKEN,
103
+ errorResponse.error?.message || "Invalid token",
104
+ 401
105
+ );
106
+ }
107
+ const data = await response.json();
108
+ const payload = data.data;
109
+ const cacheExpiry = Math.min(payload.exp * 1e3, Date.now() + 60 * 1e3);
110
+ this.tokenCache.set(token, { payload, expiresAt: cacheExpiry });
111
+ return payload;
112
+ } catch (error) {
113
+ if (error instanceof ServerAuthError) {
114
+ throw error;
115
+ }
116
+ if (this.config.jwtPublicKey) {
117
+ return this.verifyTokenLocally(token);
118
+ }
119
+ throw new ServerAuthError(
120
+ ServerErrorCode.VERIFICATION_FAILED,
121
+ error instanceof Error ? error.message : "Token verification failed",
122
+ 401
123
+ );
124
+ }
125
+ }
126
+ /**
127
+ * Verify token locally using JWT public key
128
+ * 使用 JWT 公钥本地验证令牌
129
+ */
130
+ async verifyTokenLocally(token) {
131
+ if (!this.config.jwtPublicKey) {
132
+ throw new ServerAuthError(ServerErrorCode.NO_PUBLIC_KEY, "JWT public key not configured", 500);
133
+ }
134
+ try {
135
+ const publicKey = await jose.importSPKI(this.config.jwtPublicKey, "RS256");
136
+ const { payload } = await jose.jwtVerify(token, publicKey);
137
+ return {
138
+ sub: payload.sub,
139
+ iss: payload.iss,
140
+ aud: payload.aud,
141
+ iat: payload.iat,
142
+ exp: payload.exp,
143
+ scope: payload.scope,
144
+ azp: payload.azp,
145
+ phone: payload.phone,
146
+ email: payload.email
147
+ };
148
+ } catch (error) {
149
+ throw new ServerAuthError(
150
+ ServerErrorCode.INVALID_TOKEN,
151
+ error instanceof Error ? error.message : "Invalid token",
152
+ 401
153
+ );
154
+ }
155
+ }
156
+ // ============================================
157
+ // OAuth2 Token Introspection (RFC 7662)
158
+ // ============================================
159
+ /**
160
+ * Introspect a token (RFC 7662)
161
+ * 内省令牌(RFC 7662 标准)
162
+ *
163
+ * This is the standard way for resource servers to validate tokens.
164
+ *
165
+ * @param token - The token to introspect
166
+ * @param tokenTypeHint - Optional hint about the token type ('access_token' or 'refresh_token')
167
+ * @returns Introspection result
168
+ *
169
+ * @example
170
+ * ```typescript
171
+ * const result = await auth.introspectToken(accessToken);
172
+ * if (result.active) {
173
+ * console.log('Token is valid, user:', result.sub);
174
+ * }
175
+ * ```
176
+ */
177
+ async introspectToken(token, tokenTypeHint) {
178
+ try {
179
+ const credentials = Buffer.from(`${this.config.clientId}:${this.config.clientSecret}`).toString("base64");
180
+ const body = { token };
181
+ if (tokenTypeHint) {
182
+ body.token_type_hint = tokenTypeHint;
183
+ }
184
+ const response = await fetch(`${this.config.baseUrl}/api/v1/oauth2/introspect`, {
185
+ method: "POST",
186
+ headers: {
187
+ "Content-Type": "application/json",
188
+ "Authorization": `Basic ${credentials}`
189
+ },
190
+ body: JSON.stringify(body)
191
+ });
192
+ const result = await response.json();
193
+ return result;
194
+ } catch (error) {
195
+ return { active: false };
196
+ }
197
+ }
198
+ /**
199
+ * Check if a token is active
200
+ * 检查令牌是否有效
201
+ *
202
+ * @param token - The token to check
203
+ * @returns true if token is active
204
+ */
205
+ async isTokenActive(token) {
206
+ const result = await this.introspectToken(token);
207
+ return result.active;
208
+ }
209
+ // ============================================
210
+ // User Management
211
+ // ============================================
212
+ /**
213
+ * Get user info by ID
214
+ * 根据 ID 获取用户信息
215
+ */
216
+ async getUser(userId) {
217
+ const response = await fetch(`${this.config.baseUrl}/api/v1/admin/users/${userId}`, {
218
+ method: "GET",
219
+ headers: {
220
+ "Content-Type": "application/json",
221
+ "X-App-Key": this.config.clientId,
222
+ "X-App-Secret": this.config.clientSecret
223
+ }
224
+ });
225
+ if (!response.ok) {
226
+ const errorResponse = await response.json();
227
+ throw new ServerAuthError(
228
+ errorResponse.error?.code || ServerErrorCode.USER_NOT_FOUND,
229
+ errorResponse.error?.message || "User not found",
230
+ response.status
231
+ );
232
+ }
233
+ const data = await response.json();
234
+ return data.data;
235
+ }
236
+ // ============================================
237
+ // Express/Connect Middleware
238
+ // ============================================
239
+ /**
240
+ * Express/Connect middleware for authentication
241
+ * Express/Connect 认证中间件
242
+ *
243
+ * @example
244
+ * ```typescript
245
+ * import express from 'express';
246
+ *
247
+ * const app = express();
248
+ * app.use('/api/*', auth.middleware());
249
+ *
250
+ * app.get('/api/profile', (req, res) => {
251
+ * res.json({ user: req.user });
252
+ * });
253
+ * ```
254
+ */
255
+ middleware() {
256
+ return async (req, res, next) => {
257
+ const authHeader = req.headers["authorization"];
258
+ if (!authHeader || typeof authHeader !== "string" || !authHeader.startsWith("Bearer ")) {
259
+ res.status(401).json({
260
+ success: false,
261
+ error: {
262
+ code: ServerErrorCode.UNAUTHORIZED,
263
+ message: "Authorization header is required / \u9700\u8981\u6388\u6743\u5934"
264
+ }
265
+ });
266
+ return;
267
+ }
268
+ const token = authHeader.substring(7);
269
+ try {
270
+ const payload = await this.verifyToken(token);
271
+ req.authPayload = payload;
272
+ try {
273
+ req.user = await this.getUser(payload.sub);
274
+ } catch {
275
+ }
276
+ next();
277
+ } catch (error) {
278
+ const authError = error instanceof ServerAuthError ? error : new ServerAuthError(
279
+ ServerErrorCode.UNAUTHORIZED,
280
+ "Authentication failed",
281
+ 401
282
+ );
283
+ res.status(authError.statusCode).json({
284
+ success: false,
285
+ error: {
286
+ code: authError.code,
287
+ message: authError.message
288
+ }
289
+ });
290
+ }
291
+ };
292
+ }
293
+ // ============================================
294
+ // Hono Middleware
295
+ // ============================================
296
+ /**
297
+ * Hono middleware for authentication
298
+ * Hono 认证中间件
299
+ *
300
+ * @example
301
+ * ```typescript
302
+ * import { Hono } from 'hono';
303
+ *
304
+ * const app = new Hono();
305
+ * app.use('/api/*', auth.honoMiddleware());
306
+ *
307
+ * app.get('/api/profile', (c) => {
308
+ * const user = c.get('user');
309
+ * return c.json({ user });
310
+ * });
311
+ * ```
312
+ */
313
+ honoMiddleware() {
314
+ return async (c, next) => {
315
+ const authHeader = c.req.header("authorization") || c.req.header("Authorization");
316
+ if (!authHeader || !authHeader.startsWith("Bearer ")) {
317
+ return c.json({
318
+ success: false,
319
+ error: {
320
+ code: ServerErrorCode.UNAUTHORIZED,
321
+ message: "Authorization header is required / \u9700\u8981\u6388\u6743\u5934"
322
+ }
323
+ }, 401);
324
+ }
325
+ const token = authHeader.substring(7);
326
+ try {
327
+ const payload = await this.verifyToken(token);
328
+ c.set("authPayload", payload);
329
+ try {
330
+ const user = await this.getUser(payload.sub);
331
+ c.set("user", user);
332
+ } catch {
333
+ }
334
+ await next();
335
+ } catch (error) {
336
+ const authError = error instanceof ServerAuthError ? error : new ServerAuthError(
337
+ ServerErrorCode.UNAUTHORIZED,
338
+ "Authentication failed",
339
+ 401
340
+ );
341
+ return c.json({
342
+ success: false,
343
+ error: {
344
+ code: authError.code,
345
+ message: authError.message
346
+ }
347
+ }, authError.statusCode);
348
+ }
349
+ };
350
+ }
351
+ // ============================================
352
+ // Utility Methods
353
+ // ============================================
354
+ /**
355
+ * Clear token cache
356
+ * 清除令牌缓存
357
+ */
358
+ clearCache() {
359
+ this.tokenCache.clear();
360
+ }
361
+ /**
362
+ * Get cache statistics
363
+ * 获取缓存统计
364
+ */
365
+ getCacheStats() {
366
+ return {
367
+ size: this.tokenCache.size,
368
+ entries: this.tokenCache.size
369
+ };
370
+ }
371
+ };
372
+ var index_default = UniAuthServer;
373
+ // Annotate the CommonJS export names for ESM import in node:
374
+ 0 && (module.exports = {
375
+ ServerAuthError,
376
+ ServerErrorCode,
377
+ UniAuthServer
378
+ });