@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/src/index.ts ADDED
@@ -0,0 +1,581 @@
1
+ /**
2
+ * UniAuth Server SDK
3
+ * 统一认证后端 SDK
4
+ *
5
+ * Usage:
6
+ * ```typescript
7
+ * import { UniAuthServer } from '@uniauth/server-sdk';
8
+ *
9
+ * const auth = new UniAuthServer({
10
+ * baseUrl: 'https://auth.example.com',
11
+ * clientId: 'your-client-id',
12
+ * clientSecret: 'your-client-secret',
13
+ * });
14
+ *
15
+ * // Verify token
16
+ * const payload = await auth.verifyToken(accessToken);
17
+ *
18
+ * // Introspect token (RFC 7662)
19
+ * const introspectResult = await auth.introspectToken(accessToken);
20
+ *
21
+ * // Express middleware
22
+ * app.use('/api/*', auth.middleware());
23
+ *
24
+ * // Hono middleware
25
+ * app.use('/api/*', auth.honoMiddleware());
26
+ * ```
27
+ */
28
+
29
+ import * as jose from 'jose';
30
+
31
+ // ============================================
32
+ // Types
33
+ // ============================================
34
+
35
+ export interface UniAuthServerConfig {
36
+ /** API base URL */
37
+ baseUrl: string;
38
+ /** OAuth2 Client ID (also used as appKey) */
39
+ clientId: string;
40
+ /** OAuth2 Client Secret (also used as appSecret) */
41
+ clientSecret: string;
42
+ /** JWT public key (optional, for local verification) */
43
+ jwtPublicKey?: string;
44
+ /** @deprecated Use clientId instead */
45
+ appKey?: string;
46
+ /** @deprecated Use clientSecret instead */
47
+ appSecret?: string;
48
+ }
49
+
50
+ export interface TokenPayload {
51
+ /** User ID or Client ID (for M2M) */
52
+ sub: string;
53
+ /** Issuer */
54
+ iss?: string;
55
+ /** Audience */
56
+ aud?: string | string[];
57
+ /** Issued at timestamp */
58
+ iat: number;
59
+ /** Expiration timestamp */
60
+ exp: number;
61
+ /** Scopes */
62
+ scope?: string;
63
+ /** Authorized party (client_id that requested this token) */
64
+ azp?: string;
65
+ /** Phone number (optional) */
66
+ phone?: string;
67
+ /** Email address (optional) */
68
+ email?: string;
69
+ }
70
+
71
+ export interface UserInfo {
72
+ id: string;
73
+ phone?: string | null;
74
+ email?: string | null;
75
+ nickname?: string | null;
76
+ avatar_url?: string | null;
77
+ phone_verified?: boolean;
78
+ email_verified?: boolean;
79
+ created_at?: string;
80
+ updated_at?: string;
81
+ }
82
+
83
+ export interface VerifyResult {
84
+ valid: boolean;
85
+ payload?: TokenPayload;
86
+ error?: string;
87
+ }
88
+
89
+ /**
90
+ * RFC 7662 Token Introspection Response
91
+ * 令牌内省响应
92
+ */
93
+ export interface IntrospectionResult {
94
+ /** Whether the token is active */
95
+ active: boolean;
96
+ /** Scopes associated with this token */
97
+ scope?: string;
98
+ /** Client ID that requested the token */
99
+ client_id?: string;
100
+ /** Username or user identifier */
101
+ username?: string;
102
+ /** Token type (usually "Bearer") */
103
+ token_type?: string;
104
+ /** Expiration timestamp */
105
+ exp?: number;
106
+ /** Issued at timestamp */
107
+ iat?: number;
108
+ /** Not before timestamp */
109
+ nbf?: number;
110
+ /** Subject (user ID or client ID) */
111
+ sub?: string;
112
+ /** Audience */
113
+ aud?: string | string[];
114
+ /** Issuer */
115
+ iss?: string;
116
+ /** JWT ID */
117
+ jti?: string;
118
+ }
119
+
120
+ /**
121
+ * Error codes for UniAuth Server SDK
122
+ * UniAuth 服务端 SDK 错误码
123
+ */
124
+ export const ServerErrorCode = {
125
+ INVALID_TOKEN: 'INVALID_TOKEN',
126
+ TOKEN_EXPIRED: 'TOKEN_EXPIRED',
127
+ VERIFICATION_FAILED: 'VERIFICATION_FAILED',
128
+ USER_NOT_FOUND: 'USER_NOT_FOUND',
129
+ UNAUTHORIZED: 'UNAUTHORIZED',
130
+ NO_PUBLIC_KEY: 'NO_PUBLIC_KEY',
131
+ NETWORK_ERROR: 'NETWORK_ERROR',
132
+ INTERNAL_ERROR: 'INTERNAL_ERROR',
133
+ } as const;
134
+
135
+ export type ServerErrorCodeType = typeof ServerErrorCode[keyof typeof ServerErrorCode];
136
+
137
+ /**
138
+ * Custom error class for server SDK
139
+ * 服务端 SDK 自定义错误类
140
+ */
141
+ export class ServerAuthError extends Error {
142
+ code: ServerErrorCodeType | string;
143
+ statusCode: number;
144
+
145
+ constructor(code: ServerErrorCodeType | string, message: string, statusCode: number = 401) {
146
+ super(message);
147
+ this.name = 'ServerAuthError';
148
+ this.code = code;
149
+ this.statusCode = statusCode;
150
+
151
+ if (Error.captureStackTrace) {
152
+ Error.captureStackTrace(this, ServerAuthError);
153
+ }
154
+ }
155
+ }
156
+
157
+ // API Response types
158
+ interface ApiErrorResponse {
159
+ success: false;
160
+ error?: {
161
+ code?: string;
162
+ message?: string;
163
+ };
164
+ }
165
+
166
+ interface ApiSuccessResponse<T> {
167
+ success: true;
168
+ data: T;
169
+ }
170
+
171
+ // Express-compatible request/response types
172
+ interface ExpressRequest {
173
+ headers: Record<string, string | string[] | undefined>;
174
+ user?: UserInfo;
175
+ authPayload?: TokenPayload;
176
+ }
177
+
178
+ interface ExpressResponse {
179
+ status(code: number): ExpressResponse;
180
+ json(data: unknown): void;
181
+ }
182
+
183
+ type NextFunction = (error?: Error) => void;
184
+
185
+ // Hono-compatible types
186
+ interface HonoContext {
187
+ req: {
188
+ header(name: string): string | undefined;
189
+ };
190
+ set(key: string, value: unknown): void;
191
+ get(key: string): unknown;
192
+ json(data: unknown, status?: number): Response;
193
+ }
194
+
195
+ type HonoMiddlewareHandler = (c: HonoContext, next: () => Promise<void>) => Promise<Response | void>;
196
+
197
+ /**
198
+ * UniAuth Server SDK
199
+ * 统一认证后端 SDK
200
+ */
201
+ export class UniAuthServer {
202
+ private config: UniAuthServerConfig;
203
+ private tokenCache: Map<string, { payload: TokenPayload; expiresAt: number }> = new Map();
204
+
205
+ constructor(config: UniAuthServerConfig) {
206
+ // Support both new naming (clientId/clientSecret) and legacy (appKey/appSecret)
207
+ this.config = {
208
+ ...config,
209
+ clientId: config.clientId || config.appKey || '',
210
+ clientSecret: config.clientSecret || config.appSecret || '',
211
+ };
212
+ }
213
+
214
+ // ============================================
215
+ // Token Verification
216
+ // ============================================
217
+
218
+ /**
219
+ * Verify access token
220
+ * 验证访问令牌
221
+ *
222
+ * @param token - JWT access token
223
+ * @returns Token payload if valid
224
+ * @throws ServerAuthError if token is invalid
225
+ */
226
+ async verifyToken(token: string): Promise<TokenPayload> {
227
+ // Check cache first
228
+ const cached = this.tokenCache.get(token);
229
+ if (cached && cached.expiresAt > Date.now()) {
230
+ return cached.payload;
231
+ }
232
+
233
+ try {
234
+ // Verify with remote endpoint
235
+ const response = await fetch(`${this.config.baseUrl}/api/v1/auth/verify`, {
236
+ method: 'POST',
237
+ headers: {
238
+ 'Content-Type': 'application/json',
239
+ 'X-App-Key': this.config.clientId,
240
+ 'X-App-Secret': this.config.clientSecret,
241
+ },
242
+ body: JSON.stringify({ token }),
243
+ });
244
+
245
+ if (!response.ok) {
246
+ const errorResponse = await response.json() as ApiErrorResponse;
247
+ throw new ServerAuthError(
248
+ errorResponse.error?.code || ServerErrorCode.INVALID_TOKEN,
249
+ errorResponse.error?.message || 'Invalid token',
250
+ 401
251
+ );
252
+ }
253
+
254
+ const data = await response.json() as ApiSuccessResponse<TokenPayload>;
255
+ const payload = data.data;
256
+
257
+ // Cache the result (for 1 minute or until expiry, whichever is sooner)
258
+ const cacheExpiry = Math.min(payload.exp * 1000, Date.now() + 60 * 1000);
259
+ this.tokenCache.set(token, { payload, expiresAt: cacheExpiry });
260
+
261
+ return payload;
262
+ } catch (error) {
263
+ if (error instanceof ServerAuthError) {
264
+ throw error;
265
+ }
266
+
267
+ // Try local verification as fallback
268
+ if (this.config.jwtPublicKey) {
269
+ return this.verifyTokenLocally(token);
270
+ }
271
+
272
+ throw new ServerAuthError(
273
+ ServerErrorCode.VERIFICATION_FAILED,
274
+ error instanceof Error ? error.message : 'Token verification failed',
275
+ 401
276
+ );
277
+ }
278
+ }
279
+
280
+ /**
281
+ * Verify token locally using JWT public key
282
+ * 使用 JWT 公钥本地验证令牌
283
+ */
284
+ private async verifyTokenLocally(token: string): Promise<TokenPayload> {
285
+ if (!this.config.jwtPublicKey) {
286
+ throw new ServerAuthError(ServerErrorCode.NO_PUBLIC_KEY, 'JWT public key not configured', 500);
287
+ }
288
+
289
+ try {
290
+ const publicKey = await jose.importSPKI(this.config.jwtPublicKey, 'RS256');
291
+ const { payload } = await jose.jwtVerify(token, publicKey);
292
+
293
+ return {
294
+ sub: payload.sub as string,
295
+ iss: payload.iss as string,
296
+ aud: payload.aud as string | string[],
297
+ iat: payload.iat as number,
298
+ exp: payload.exp as number,
299
+ scope: payload.scope as string | undefined,
300
+ azp: payload.azp as string | undefined,
301
+ phone: payload.phone as string | undefined,
302
+ email: payload.email as string | undefined,
303
+ };
304
+ } catch (error) {
305
+ throw new ServerAuthError(
306
+ ServerErrorCode.INVALID_TOKEN,
307
+ error instanceof Error ? error.message : 'Invalid token',
308
+ 401
309
+ );
310
+ }
311
+ }
312
+
313
+ // ============================================
314
+ // OAuth2 Token Introspection (RFC 7662)
315
+ // ============================================
316
+
317
+ /**
318
+ * Introspect a token (RFC 7662)
319
+ * 内省令牌(RFC 7662 标准)
320
+ *
321
+ * This is the standard way for resource servers to validate tokens.
322
+ *
323
+ * @param token - The token to introspect
324
+ * @param tokenTypeHint - Optional hint about the token type ('access_token' or 'refresh_token')
325
+ * @returns Introspection result
326
+ *
327
+ * @example
328
+ * ```typescript
329
+ * const result = await auth.introspectToken(accessToken);
330
+ * if (result.active) {
331
+ * console.log('Token is valid, user:', result.sub);
332
+ * }
333
+ * ```
334
+ */
335
+ async introspectToken(token: string, tokenTypeHint?: 'access_token' | 'refresh_token'): Promise<IntrospectionResult> {
336
+ try {
337
+ // Use Basic Auth for client authentication
338
+ const credentials = Buffer.from(`${this.config.clientId}:${this.config.clientSecret}`).toString('base64');
339
+
340
+ const body: Record<string, string> = { token };
341
+ if (tokenTypeHint) {
342
+ body.token_type_hint = tokenTypeHint;
343
+ }
344
+
345
+ const response = await fetch(`${this.config.baseUrl}/api/v1/oauth2/introspect`, {
346
+ method: 'POST',
347
+ headers: {
348
+ 'Content-Type': 'application/json',
349
+ 'Authorization': `Basic ${credentials}`,
350
+ },
351
+ body: JSON.stringify(body),
352
+ });
353
+
354
+ const result = await response.json() as IntrospectionResult;
355
+ return result;
356
+ } catch (error) {
357
+ // On network error, return inactive
358
+ return { active: false };
359
+ }
360
+ }
361
+
362
+ /**
363
+ * Check if a token is active
364
+ * 检查令牌是否有效
365
+ *
366
+ * @param token - The token to check
367
+ * @returns true if token is active
368
+ */
369
+ async isTokenActive(token: string): Promise<boolean> {
370
+ const result = await this.introspectToken(token);
371
+ return result.active;
372
+ }
373
+
374
+ // ============================================
375
+ // User Management
376
+ // ============================================
377
+
378
+ /**
379
+ * Get user info by ID
380
+ * 根据 ID 获取用户信息
381
+ */
382
+ async getUser(userId: string): Promise<UserInfo> {
383
+ const response = await fetch(`${this.config.baseUrl}/api/v1/admin/users/${userId}`, {
384
+ method: 'GET',
385
+ headers: {
386
+ 'Content-Type': 'application/json',
387
+ 'X-App-Key': this.config.clientId,
388
+ 'X-App-Secret': this.config.clientSecret,
389
+ },
390
+ });
391
+
392
+ if (!response.ok) {
393
+ const errorResponse = await response.json() as ApiErrorResponse;
394
+ throw new ServerAuthError(
395
+ errorResponse.error?.code || ServerErrorCode.USER_NOT_FOUND,
396
+ errorResponse.error?.message || 'User not found',
397
+ response.status
398
+ );
399
+ }
400
+
401
+ const data = await response.json() as ApiSuccessResponse<UserInfo>;
402
+ return data.data;
403
+ }
404
+
405
+ // ============================================
406
+ // Express/Connect Middleware
407
+ // ============================================
408
+
409
+ /**
410
+ * Express/Connect middleware for authentication
411
+ * Express/Connect 认证中间件
412
+ *
413
+ * @example
414
+ * ```typescript
415
+ * import express from 'express';
416
+ *
417
+ * const app = express();
418
+ * app.use('/api/*', auth.middleware());
419
+ *
420
+ * app.get('/api/profile', (req, res) => {
421
+ * res.json({ user: req.user });
422
+ * });
423
+ * ```
424
+ */
425
+ middleware() {
426
+ return async (
427
+ req: ExpressRequest,
428
+ res: ExpressResponse,
429
+ next: NextFunction
430
+ ): Promise<void> => {
431
+ const authHeader = req.headers['authorization'];
432
+
433
+ if (!authHeader || typeof authHeader !== 'string' || !authHeader.startsWith('Bearer ')) {
434
+ res.status(401).json({
435
+ success: false,
436
+ error: {
437
+ code: ServerErrorCode.UNAUTHORIZED,
438
+ message: 'Authorization header is required / 需要授权头',
439
+ },
440
+ });
441
+ return;
442
+ }
443
+
444
+ const token = authHeader.substring(7);
445
+
446
+ try {
447
+ const payload = await this.verifyToken(token);
448
+
449
+ // Attach payload to request
450
+ req.authPayload = payload;
451
+
452
+ // Optionally fetch full user info
453
+ try {
454
+ req.user = await this.getUser(payload.sub);
455
+ } catch {
456
+ // User info not required for middleware to pass
457
+ }
458
+
459
+ next();
460
+ } catch (error) {
461
+ const authError = error instanceof ServerAuthError ? error : new ServerAuthError(
462
+ ServerErrorCode.UNAUTHORIZED,
463
+ 'Authentication failed',
464
+ 401
465
+ );
466
+ res.status(authError.statusCode).json({
467
+ success: false,
468
+ error: {
469
+ code: authError.code,
470
+ message: authError.message,
471
+ },
472
+ });
473
+ }
474
+ };
475
+ }
476
+
477
+ // ============================================
478
+ // Hono Middleware
479
+ // ============================================
480
+
481
+ /**
482
+ * Hono middleware for authentication
483
+ * Hono 认证中间件
484
+ *
485
+ * @example
486
+ * ```typescript
487
+ * import { Hono } from 'hono';
488
+ *
489
+ * const app = new Hono();
490
+ * app.use('/api/*', auth.honoMiddleware());
491
+ *
492
+ * app.get('/api/profile', (c) => {
493
+ * const user = c.get('user');
494
+ * return c.json({ user });
495
+ * });
496
+ * ```
497
+ */
498
+ honoMiddleware(): HonoMiddlewareHandler {
499
+ return async (c: HonoContext, next: () => Promise<void>): Promise<Response | void> => {
500
+ const authHeader = c.req.header('authorization') || c.req.header('Authorization');
501
+
502
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
503
+ return c.json({
504
+ success: false,
505
+ error: {
506
+ code: ServerErrorCode.UNAUTHORIZED,
507
+ message: 'Authorization header is required / 需要授权头',
508
+ },
509
+ }, 401);
510
+ }
511
+
512
+ const token = authHeader.substring(7);
513
+
514
+ try {
515
+ const payload = await this.verifyToken(token);
516
+
517
+ // Attach to context
518
+ c.set('authPayload', payload);
519
+
520
+ // Optionally fetch full user info
521
+ try {
522
+ const user = await this.getUser(payload.sub);
523
+ c.set('user', user);
524
+ } catch {
525
+ // User info not required for middleware to pass
526
+ }
527
+
528
+ await next();
529
+ } catch (error) {
530
+ const authError = error instanceof ServerAuthError ? error : new ServerAuthError(
531
+ ServerErrorCode.UNAUTHORIZED,
532
+ 'Authentication failed',
533
+ 401
534
+ );
535
+ return c.json({
536
+ success: false,
537
+ error: {
538
+ code: authError.code,
539
+ message: authError.message,
540
+ },
541
+ }, authError.statusCode);
542
+ }
543
+ };
544
+ }
545
+
546
+ // ============================================
547
+ // Utility Methods
548
+ // ============================================
549
+
550
+ /**
551
+ * Clear token cache
552
+ * 清除令牌缓存
553
+ */
554
+ clearCache(): void {
555
+ this.tokenCache.clear();
556
+ }
557
+
558
+ /**
559
+ * Get cache statistics
560
+ * 获取缓存统计
561
+ */
562
+ getCacheStats(): { size: number; entries: number } {
563
+ return {
564
+ size: this.tokenCache.size,
565
+ entries: this.tokenCache.size,
566
+ };
567
+ }
568
+ }
569
+
570
+ // ============================================
571
+ // Legacy Compatibility
572
+ // ============================================
573
+
574
+ /** @deprecated Use ServerAuthError instead */
575
+ export interface AuthError extends Error {
576
+ code: string;
577
+ statusCode: number;
578
+ }
579
+
580
+ // Default export
581
+ export default UniAuthServer;