@forinda/kickjs-auth 1.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Felix Orinda
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,496 @@
1
+ import { AppAdapter, AdapterMiddleware, Container } from '@forinda/kickjs-core';
2
+
3
+ declare const AUTH_META: {
4
+ readonly AUTHENTICATED: symbol;
5
+ readonly PUBLIC: symbol;
6
+ readonly ROLES: symbol;
7
+ readonly STRATEGY: symbol;
8
+ };
9
+ /**
10
+ * The authenticated user object attached to the request.
11
+ * Extend this via module augmentation for your app's user shape.
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * declare module '@forinda/kickjs-auth' {
16
+ * interface AuthUser {
17
+ * id: string
18
+ * email: string
19
+ * roles: string[]
20
+ * }
21
+ * }
22
+ * ```
23
+ */
24
+ interface AuthUser {
25
+ [key: string]: any;
26
+ }
27
+ /**
28
+ * Abstract authentication strategy. Implement this to support any
29
+ * auth mechanism: JWT, API keys, OAuth, sessions, SAML, etc.
30
+ *
31
+ * @example
32
+ * ```ts
33
+ * class SessionStrategy implements AuthStrategy {
34
+ * name = 'session'
35
+ * async validate(req) {
36
+ * const session = req.session
37
+ * if (!session?.userId) return null
38
+ * return { id: session.userId, roles: session.roles }
39
+ * }
40
+ * }
41
+ * ```
42
+ */
43
+ interface AuthStrategy {
44
+ /** Unique name for this strategy (e.g., 'jwt', 'api-key', 'session') */
45
+ name: string;
46
+ /**
47
+ * Extract and validate credentials from the request.
48
+ * Return the authenticated user, or null if authentication fails.
49
+ *
50
+ * @param req - Express request object
51
+ * @returns The authenticated user, or null
52
+ */
53
+ validate(req: any): Promise<AuthUser | null> | AuthUser | null;
54
+ }
55
+ interface AuthAdapterOptions {
56
+ /**
57
+ * Authentication strategies to use, in order of precedence.
58
+ * The first strategy that returns a user wins.
59
+ *
60
+ * @example
61
+ * ```ts
62
+ * new AuthAdapter({
63
+ * strategies: [
64
+ * new JwtStrategy({ secret: process.env.JWT_SECRET! }),
65
+ * new ApiKeyStrategy({ keys: { 'sk-123': { name: 'CI bot', roles: ['api'] } } }),
66
+ * ],
67
+ * })
68
+ * ```
69
+ */
70
+ strategies: AuthStrategy[];
71
+ /**
72
+ * Default behavior for routes without @Authenticated or @Public.
73
+ * - `'protected'` — all routes require auth unless marked @Public (default)
74
+ * - `'open'` — all routes are public unless marked @Authenticated
75
+ */
76
+ defaultPolicy?: 'protected' | 'open';
77
+ /**
78
+ * Custom handler for unauthorized requests.
79
+ * Default: responds with 401 JSON error.
80
+ */
81
+ onUnauthorized?: (req: any, res: any) => void;
82
+ /**
83
+ * Custom handler for forbidden requests (role mismatch).
84
+ * Default: responds with 403 JSON error.
85
+ */
86
+ onForbidden?: (req: any, res: any) => void;
87
+ }
88
+
89
+ /**
90
+ * Mark a controller or method as requiring authentication.
91
+ * When applied to a controller, all methods require auth unless overridden with @Public.
92
+ *
93
+ * Optionally specify which strategy to use (defaults to trying all registered strategies).
94
+ *
95
+ * @example
96
+ * ```ts
97
+ * @Controller('/users')
98
+ * @Authenticated() // All routes require auth
99
+ * class UserController {
100
+ * @Get('/')
101
+ * list(ctx) { ... } // Protected
102
+ *
103
+ * @Get('/public-count')
104
+ * @Public() // Override: this route is open
105
+ * count(ctx) { ... }
106
+ * }
107
+ *
108
+ * // Strategy-specific:
109
+ * @Authenticated('api-key')
110
+ * @Get('/webhook')
111
+ * webhook(ctx) { ... }
112
+ * ```
113
+ */
114
+ declare function Authenticated(strategy?: string): ClassDecorator & MethodDecorator;
115
+ /**
116
+ * Mark a method as publicly accessible, bypassing authentication.
117
+ * Use inside an @Authenticated controller to exempt specific routes.
118
+ *
119
+ * @example
120
+ * ```ts
121
+ * @Controller('/auth')
122
+ * @Authenticated()
123
+ * class AuthController {
124
+ * @Post('/login')
125
+ * @Public() // No auth required
126
+ * login(ctx) { ... }
127
+ *
128
+ * @Get('/me') // Auth required (inherits from controller)
129
+ * me(ctx) { ... }
130
+ * }
131
+ * ```
132
+ */
133
+ declare function Public(): MethodDecorator;
134
+ /**
135
+ * Require specific roles to access a route.
136
+ * The authenticated user must have at least one of the specified roles.
137
+ * Implies @Authenticated — no need to add both.
138
+ *
139
+ * The user object must have a `roles` property (string array).
140
+ *
141
+ * @example
142
+ * ```ts
143
+ * @Get('/admin/dashboard')
144
+ * @Roles('admin', 'superadmin')
145
+ * dashboard(ctx) { ... }
146
+ *
147
+ * @Delete('/:id')
148
+ * @Roles('admin')
149
+ * deleteUser(ctx) { ... }
150
+ * ```
151
+ */
152
+ declare function Roles(...roles: string[]): MethodDecorator;
153
+
154
+ /** DI token to resolve the current authenticated user from the container */
155
+ declare const AUTH_USER: unique symbol;
156
+ /**
157
+ * Authentication adapter — plugs into the KickJS lifecycle to protect
158
+ * routes based on @Authenticated, @Public, and @Roles decorators.
159
+ *
160
+ * Supports multiple strategies (JWT, API key, custom) with first-match semantics.
161
+ *
162
+ * @example
163
+ * ```ts
164
+ * import { AuthAdapter, JwtStrategy, ApiKeyStrategy } from '@forinda/kickjs-auth'
165
+ *
166
+ * bootstrap({
167
+ * modules: [...],
168
+ * adapters: [
169
+ * new AuthAdapter({
170
+ * strategies: [
171
+ * new JwtStrategy({ secret: process.env.JWT_SECRET! }),
172
+ * new ApiKeyStrategy({ keys: { 'sk-123': { name: 'Bot', roles: ['api'] } } }),
173
+ * ],
174
+ * defaultPolicy: 'protected', // secure by default
175
+ * }),
176
+ * ],
177
+ * })
178
+ * ```
179
+ */
180
+ declare class AuthAdapter implements AppAdapter {
181
+ private options;
182
+ name: string;
183
+ private strategies;
184
+ private defaultPolicy;
185
+ private onUnauthorized;
186
+ private onForbidden;
187
+ private routeControllers;
188
+ constructor(options: AuthAdapterOptions);
189
+ onRouteMount(controllerClass: any, mountPath: string): void;
190
+ middleware(): AdapterMiddleware[];
191
+ beforeStart(app: any, _container: Container): void;
192
+ private createAuthMiddleware;
193
+ private authenticate;
194
+ private resolveHandler;
195
+ private pathMatches;
196
+ private isAuthRequired;
197
+ private getRequiredRoles;
198
+ private getStrategyName;
199
+ }
200
+
201
+ interface JwtStrategyOptions {
202
+ /** JWT secret key for HS256 or public key for RS256 */
203
+ secret: string | Buffer;
204
+ /**
205
+ * Algorithm (default: 'HS256').
206
+ * Common values: 'HS256', 'HS384', 'HS512', 'RS256', 'RS384', 'RS512'
207
+ */
208
+ algorithms?: string[];
209
+ /** Where to read the token from (default: 'header') */
210
+ tokenFrom?: 'header' | 'query' | 'cookie';
211
+ /** Header name (default: 'authorization') */
212
+ headerName?: string;
213
+ /** Query parameter name when tokenFrom='query' (default: 'token') */
214
+ queryParam?: string;
215
+ /** Cookie name when tokenFrom='cookie' (default: 'jwt') */
216
+ cookieName?: string;
217
+ /** Token prefix in header (default: 'Bearer') */
218
+ headerPrefix?: string;
219
+ /**
220
+ * Transform the decoded JWT payload into your AuthUser shape.
221
+ * By default, returns the payload as-is.
222
+ *
223
+ * @example
224
+ * ```ts
225
+ * mapPayload: (payload) => ({
226
+ * id: payload.sub,
227
+ * email: payload.email,
228
+ * roles: payload.roles || [],
229
+ * })
230
+ * ```
231
+ */
232
+ mapPayload?: (payload: any) => AuthUser;
233
+ }
234
+ /**
235
+ * JWT authentication strategy.
236
+ * Validates Bearer tokens using `jsonwebtoken`.
237
+ *
238
+ * Requires `jsonwebtoken` as a peer dependency:
239
+ * ```bash
240
+ * pnpm add jsonwebtoken @types/jsonwebtoken
241
+ * ```
242
+ *
243
+ * @example
244
+ * ```ts
245
+ * new JwtStrategy({
246
+ * secret: process.env.JWT_SECRET!,
247
+ * mapPayload: (payload) => ({
248
+ * id: payload.sub,
249
+ * email: payload.email,
250
+ * roles: payload.roles ?? ['user'],
251
+ * }),
252
+ * })
253
+ * ```
254
+ */
255
+ declare class JwtStrategy implements AuthStrategy {
256
+ name: string;
257
+ private jwt;
258
+ private options;
259
+ constructor(options: JwtStrategyOptions);
260
+ private ensureJwt;
261
+ validate(req: any): Promise<AuthUser | null>;
262
+ private extractToken;
263
+ }
264
+
265
+ interface ApiKeyUser {
266
+ /** Display name for the API key holder */
267
+ name: string;
268
+ /** Roles granted to this API key */
269
+ roles?: string[];
270
+ /** Additional metadata */
271
+ [key: string]: any;
272
+ }
273
+ interface ApiKeyStrategyOptions {
274
+ /**
275
+ * Map of API keys to their associated users.
276
+ * Keys are the raw API key strings.
277
+ *
278
+ * @example
279
+ * ```ts
280
+ * keys: {
281
+ * 'sk-live-abc123': { name: 'Production Bot', roles: ['api', 'write'] },
282
+ * 'sk-test-xyz789': { name: 'Test Runner', roles: ['api'] },
283
+ * }
284
+ * ```
285
+ */
286
+ keys?: Record<string, ApiKeyUser>;
287
+ /**
288
+ * Async function to validate an API key.
289
+ * Use when keys are stored in a database or external service.
290
+ * Takes precedence over the static `keys` map.
291
+ *
292
+ * @example
293
+ * ```ts
294
+ * validate: async (key) => {
295
+ * const row = await db.apiKeys.findUnique({ where: { key } })
296
+ * if (!row || row.revokedAt) return null
297
+ * return { name: row.name, roles: row.roles }
298
+ * }
299
+ * ```
300
+ */
301
+ validate?: (key: string) => Promise<AuthUser | null> | AuthUser | null;
302
+ /**
303
+ * Where to read the API key from (default: 'header').
304
+ * Checks all specified locations in order.
305
+ */
306
+ from?: Array<'header' | 'query'>;
307
+ /** Header name (default: 'x-api-key') */
308
+ headerName?: string;
309
+ /** Query parameter name (default: 'api_key') */
310
+ queryParam?: string;
311
+ }
312
+ /**
313
+ * API key authentication strategy.
314
+ * Validates keys from headers or query parameters against a static map
315
+ * or async validator function.
316
+ *
317
+ * @example
318
+ * ```ts
319
+ * // Static keys
320
+ * new ApiKeyStrategy({
321
+ * keys: {
322
+ * 'sk-prod-123': { name: 'CI Bot', roles: ['api'] },
323
+ * },
324
+ * })
325
+ *
326
+ * // Database lookup
327
+ * new ApiKeyStrategy({
328
+ * validate: async (key) => {
329
+ * const record = await db.apiKeys.findByKey(key)
330
+ * return record ? { name: record.name, roles: record.roles } : null
331
+ * },
332
+ * })
333
+ * ```
334
+ */
335
+ declare class ApiKeyStrategy implements AuthStrategy {
336
+ name: string;
337
+ private options;
338
+ constructor(options: ApiKeyStrategyOptions);
339
+ validate(req: any): Promise<AuthUser | null>;
340
+ private extractKey;
341
+ }
342
+
343
+ /** Supported OAuth providers with pre-configured endpoints */
344
+ type OAuthProvider = 'google' | 'github' | 'discord' | 'microsoft' | 'custom';
345
+ /** Provider-specific endpoint configuration */
346
+ interface OAuthEndpoints {
347
+ authorizeUrl: string;
348
+ tokenUrl: string;
349
+ userInfoUrl: string;
350
+ /** Scopes to request (space-separated or array) */
351
+ scopes?: string[] | string;
352
+ }
353
+ interface OAuthStrategyOptions {
354
+ /** OAuth provider preset or 'custom' for manual endpoint config */
355
+ provider: OAuthProvider;
356
+ /** OAuth client ID */
357
+ clientId: string;
358
+ /** OAuth client secret */
359
+ clientSecret: string;
360
+ /** Callback URL for the OAuth flow (e.g. 'http://localhost:3000/auth/google/callback') */
361
+ callbackUrl: string;
362
+ /** Custom endpoints (required when provider is 'custom') */
363
+ endpoints?: OAuthEndpoints;
364
+ /** Override default scopes for the provider */
365
+ scopes?: string[];
366
+ /**
367
+ * Transform the provider's user profile into your AuthUser.
368
+ * Called after successfully exchanging the code for a token and fetching user info.
369
+ *
370
+ * @example
371
+ * ```ts
372
+ * mapProfile: (profile) => ({
373
+ * id: profile.id,
374
+ * email: profile.email,
375
+ * name: profile.name,
376
+ * avatar: profile.picture,
377
+ * roles: ['user'],
378
+ * })
379
+ * ```
380
+ */
381
+ mapProfile?: (profile: any, tokens: OAuthTokens) => AuthUser | Promise<AuthUser>;
382
+ }
383
+ interface OAuthTokens {
384
+ accessToken: string;
385
+ refreshToken?: string;
386
+ tokenType: string;
387
+ expiresIn?: number;
388
+ scope?: string;
389
+ }
390
+ /**
391
+ * Built-in OAuth 2.0 strategy with pre-configured providers.
392
+ * No Passport dependency — KickJS handles the entire OAuth flow.
393
+ *
394
+ * Supports: Google, GitHub, Discord, Microsoft, or any custom OAuth 2.0 provider.
395
+ *
396
+ * This strategy validates the callback request (the one with `?code=...`),
397
+ * exchanges the code for tokens, fetches the user profile, and returns it.
398
+ *
399
+ * You need two routes:
400
+ * 1. **Redirect** — sends user to provider's login page
401
+ * 2. **Callback** — receives the code and authenticates
402
+ *
403
+ * @example
404
+ * ```ts
405
+ * import { OAuthStrategy } from '@forinda/kickjs-auth'
406
+ *
407
+ * const googleAuth = new OAuthStrategy({
408
+ * provider: 'google',
409
+ * clientId: process.env.GOOGLE_CLIENT_ID!,
410
+ * clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
411
+ * callbackUrl: 'http://localhost:3000/auth/google/callback',
412
+ * mapProfile: (profile) => ({
413
+ * id: profile.id,
414
+ * email: profile.email,
415
+ * name: profile.name,
416
+ * avatar: profile.picture,
417
+ * roles: ['user'],
418
+ * }),
419
+ * })
420
+ *
421
+ * // In your controller:
422
+ * @Get('/auth/google')
423
+ * @Public()
424
+ * loginWithGoogle(ctx: RequestContext) {
425
+ * return ctx.res.redirect(googleAuth.getAuthorizationUrl())
426
+ * }
427
+ *
428
+ * @Get('/auth/google/callback')
429
+ * @Public()
430
+ * async googleCallback(ctx: RequestContext) {
431
+ * const user = await googleAuth.validate(ctx.req)
432
+ * if (!user) return ctx.res.status(401).json({ error: 'Auth failed' })
433
+ * const token = issueJwt(user)
434
+ * return ctx.json({ token, user })
435
+ * }
436
+ * ```
437
+ */
438
+ declare class OAuthStrategy implements AuthStrategy {
439
+ name: string;
440
+ private options;
441
+ private endpoints;
442
+ constructor(options: OAuthStrategyOptions);
443
+ /**
444
+ * Get the authorization URL to redirect the user to the OAuth provider.
445
+ * Pass an optional `state` parameter for CSRF protection.
446
+ */
447
+ getAuthorizationUrl(state?: string): string;
448
+ /**
449
+ * Validate the callback request — exchange the authorization code for
450
+ * tokens and fetch the user profile.
451
+ */
452
+ validate(req: any): Promise<AuthUser | null>;
453
+ /** Exchange authorization code for access/refresh tokens */
454
+ private exchangeCode;
455
+ /** Fetch user profile from the provider's userinfo endpoint */
456
+ private fetchUserInfo;
457
+ }
458
+
459
+ /**
460
+ * Bridge that wraps any Passport.js strategy into a KickJS AuthStrategy.
461
+ * This lets you use the full Passport ecosystem (500+ strategies) without
462
+ * changing your KickJS auth setup.
463
+ *
464
+ * Requires `passport` as a dependency in your project:
465
+ * ```bash
466
+ * pnpm add passport
467
+ * ```
468
+ *
469
+ * @example
470
+ * ```ts
471
+ * import { Strategy as GoogleStrategy } from 'passport-google-oauth20'
472
+ * import { PassportBridge } from '@forinda/kickjs-auth'
473
+ *
474
+ * const google = new PassportBridge('google', new GoogleStrategy({
475
+ * clientID: process.env.GOOGLE_CLIENT_ID!,
476
+ * clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
477
+ * callbackURL: '/auth/google/callback',
478
+ * }, (accessToken, refreshToken, profile, done) => {
479
+ * // Find or create user from profile
480
+ * const user = await findOrCreateUser(profile)
481
+ * done(null, user)
482
+ * }))
483
+ *
484
+ * new AuthAdapter({
485
+ * strategies: [jwtStrategy, google],
486
+ * })
487
+ * ```
488
+ */
489
+ declare class PassportBridge implements AuthStrategy {
490
+ name: string;
491
+ private passportStrategy;
492
+ constructor(name: string, passportStrategy: any);
493
+ validate(req: any): Promise<AuthUser | null>;
494
+ }
495
+
496
+ export { AUTH_META, AUTH_USER, ApiKeyStrategy, type ApiKeyStrategyOptions, type ApiKeyUser, AuthAdapter, type AuthAdapterOptions, type AuthStrategy, type AuthUser, Authenticated, JwtStrategy, type JwtStrategyOptions, type OAuthEndpoints, type OAuthProvider, OAuthStrategy, type OAuthStrategyOptions, type OAuthTokens, PassportBridge, Public, Roles };
package/dist/index.js ADDED
@@ -0,0 +1,487 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
3
+
4
+ // src/index.ts
5
+ import "reflect-metadata";
6
+
7
+ // src/types.ts
8
+ import "reflect-metadata";
9
+ var AUTH_META = {
10
+ AUTHENTICATED: /* @__PURE__ */ Symbol("auth:authenticated"),
11
+ PUBLIC: /* @__PURE__ */ Symbol("auth:public"),
12
+ ROLES: /* @__PURE__ */ Symbol("auth:roles"),
13
+ STRATEGY: /* @__PURE__ */ Symbol("auth:strategy")
14
+ };
15
+
16
+ // src/decorators.ts
17
+ import "reflect-metadata";
18
+ function Authenticated(strategy) {
19
+ return (target, propertyKey) => {
20
+ if (propertyKey) {
21
+ Reflect.defineMetadata(AUTH_META.AUTHENTICATED, true, target.constructor, propertyKey);
22
+ if (strategy) {
23
+ Reflect.defineMetadata(AUTH_META.STRATEGY, strategy, target.constructor, propertyKey);
24
+ }
25
+ } else {
26
+ Reflect.defineMetadata(AUTH_META.AUTHENTICATED, true, target);
27
+ if (strategy) {
28
+ Reflect.defineMetadata(AUTH_META.STRATEGY, strategy, target);
29
+ }
30
+ }
31
+ };
32
+ }
33
+ __name(Authenticated, "Authenticated");
34
+ function Public() {
35
+ return (target, propertyKey) => {
36
+ Reflect.defineMetadata(AUTH_META.PUBLIC, true, target.constructor, propertyKey);
37
+ };
38
+ }
39
+ __name(Public, "Public");
40
+ function Roles(...roles) {
41
+ return (target, propertyKey) => {
42
+ Reflect.defineMetadata(AUTH_META.AUTHENTICATED, true, target.constructor, propertyKey);
43
+ Reflect.defineMetadata(AUTH_META.ROLES, roles, target.constructor, propertyKey);
44
+ };
45
+ }
46
+ __name(Roles, "Roles");
47
+
48
+ // src/adapter.ts
49
+ import "reflect-metadata";
50
+ import { Logger, HttpStatus, METADATA } from "@forinda/kickjs-core";
51
+ var log = Logger.for("AuthAdapter");
52
+ var AUTH_USER = /* @__PURE__ */ Symbol("AuthUser");
53
+ var AuthAdapter = class {
54
+ static {
55
+ __name(this, "AuthAdapter");
56
+ }
57
+ options;
58
+ name = "AuthAdapter";
59
+ strategies;
60
+ defaultPolicy;
61
+ onUnauthorized;
62
+ onForbidden;
63
+ // Collected route metadata for auth resolution
64
+ routeControllers = /* @__PURE__ */ new Map();
65
+ constructor(options) {
66
+ this.options = options;
67
+ this.strategies = options.strategies;
68
+ this.defaultPolicy = options.defaultPolicy ?? "protected";
69
+ this.onUnauthorized = options.onUnauthorized ?? ((_req, res) => {
70
+ res.status(HttpStatus.UNAUTHORIZED).json({
71
+ statusCode: HttpStatus.UNAUTHORIZED,
72
+ error: "Unauthorized",
73
+ message: "Authentication required"
74
+ });
75
+ });
76
+ this.onForbidden = options.onForbidden ?? ((_req, res) => {
77
+ res.status(HttpStatus.FORBIDDEN).json({
78
+ statusCode: HttpStatus.FORBIDDEN,
79
+ error: "Forbidden",
80
+ message: "Insufficient permissions"
81
+ });
82
+ });
83
+ }
84
+ onRouteMount(controllerClass, mountPath) {
85
+ this.routeControllers.set(mountPath, controllerClass);
86
+ }
87
+ middleware() {
88
+ return [
89
+ {
90
+ handler: this.createAuthMiddleware(),
91
+ phase: "beforeRoutes"
92
+ }
93
+ ];
94
+ }
95
+ beforeStart(app, _container) {
96
+ const strategyNames = this.strategies.map((s) => s.name).join(", ");
97
+ log.info(`Auth enabled [${strategyNames}] (default: ${this.defaultPolicy})`);
98
+ }
99
+ // ── Core Auth Middleware ─────────────────────────────────────────────
100
+ createAuthMiddleware() {
101
+ return async (req, res, next) => {
102
+ const { controllerClass, handlerName } = this.resolveHandler(req);
103
+ const authRequired = this.isAuthRequired(controllerClass, handlerName);
104
+ if (!authRequired) {
105
+ return next();
106
+ }
107
+ const strategyName = this.getStrategyName(controllerClass, handlerName);
108
+ const user = await this.authenticate(req, strategyName);
109
+ if (!user) {
110
+ return this.onUnauthorized(req, res);
111
+ }
112
+ req.user = user;
113
+ const requiredRoles = this.getRequiredRoles(controllerClass, handlerName);
114
+ if (requiredRoles && requiredRoles.length > 0) {
115
+ const userRoles = user.roles ?? [];
116
+ const hasRole = requiredRoles.some((role) => userRoles.includes(role));
117
+ if (!hasRole) {
118
+ return this.onForbidden(req, res);
119
+ }
120
+ }
121
+ next();
122
+ };
123
+ }
124
+ // ── Strategy Execution ──────────────────────────────────────────────
125
+ async authenticate(req, strategyName) {
126
+ const strategies = strategyName ? this.strategies.filter((s) => s.name === strategyName) : this.strategies;
127
+ for (const strategy of strategies) {
128
+ try {
129
+ const user = await strategy.validate(req);
130
+ if (user) return user;
131
+ } catch (err) {
132
+ log.debug(`Strategy ${strategy.name} failed: ${err.message}`);
133
+ }
134
+ }
135
+ return null;
136
+ }
137
+ // ── Metadata Resolution ─────────────────────────────────────────────
138
+ resolveHandler(req) {
139
+ const route = req.route;
140
+ if (!route) {
141
+ return {
142
+ controllerClass: void 0,
143
+ handlerName: void 0
144
+ };
145
+ }
146
+ for (const [mountPath, controllerClass] of this.routeControllers) {
147
+ if (req.baseUrl?.startsWith(mountPath) || req.path?.startsWith(mountPath)) {
148
+ const routes = Reflect.getMetadata(METADATA.ROUTES, controllerClass) ?? [];
149
+ const matched = routes.find((r) => r.method === req.method && this.pathMatches(r.path, req.route?.path ?? req.path));
150
+ if (matched) {
151
+ return {
152
+ controllerClass,
153
+ handlerName: matched.handlerName
154
+ };
155
+ }
156
+ }
157
+ }
158
+ return {
159
+ controllerClass: void 0,
160
+ handlerName: void 0
161
+ };
162
+ }
163
+ pathMatches(routePath, requestPath) {
164
+ const norm = /* @__PURE__ */ __name((p) => p.replace(/\/+$/, "") || "/", "norm");
165
+ return norm(routePath) === norm(requestPath);
166
+ }
167
+ isAuthRequired(controllerClass, handlerName) {
168
+ if (!controllerClass) {
169
+ return this.defaultPolicy === "protected";
170
+ }
171
+ if (handlerName) {
172
+ const isPublic = Reflect.getMetadata(AUTH_META.PUBLIC, controllerClass, handlerName);
173
+ if (isPublic) return false;
174
+ const methodAuth = Reflect.getMetadata(AUTH_META.AUTHENTICATED, controllerClass, handlerName);
175
+ if (methodAuth !== void 0) return methodAuth;
176
+ }
177
+ const classAuth = Reflect.getMetadata(AUTH_META.AUTHENTICATED, controllerClass);
178
+ if (classAuth !== void 0) return classAuth;
179
+ return this.defaultPolicy === "protected";
180
+ }
181
+ getRequiredRoles(controllerClass, handlerName) {
182
+ if (!controllerClass || !handlerName) return void 0;
183
+ return Reflect.getMetadata(AUTH_META.ROLES, controllerClass, handlerName);
184
+ }
185
+ getStrategyName(controllerClass, handlerName) {
186
+ if (!controllerClass) return void 0;
187
+ if (handlerName) {
188
+ const methodStrategy = Reflect.getMetadata(AUTH_META.STRATEGY, controllerClass, handlerName);
189
+ if (methodStrategy) return methodStrategy;
190
+ }
191
+ return Reflect.getMetadata(AUTH_META.STRATEGY, controllerClass);
192
+ }
193
+ };
194
+
195
+ // src/strategies/jwt.strategy.ts
196
+ var JwtStrategy = class {
197
+ static {
198
+ __name(this, "JwtStrategy");
199
+ }
200
+ name = "jwt";
201
+ jwt;
202
+ options;
203
+ constructor(options) {
204
+ this.options = options;
205
+ }
206
+ async ensureJwt() {
207
+ if (this.jwt) return;
208
+ try {
209
+ const mod = await import("jsonwebtoken");
210
+ this.jwt = mod.default ?? mod;
211
+ } catch {
212
+ throw new Error('JwtStrategy requires "jsonwebtoken" package. Install: pnpm add jsonwebtoken');
213
+ }
214
+ }
215
+ async validate(req) {
216
+ await this.ensureJwt();
217
+ const token = this.extractToken(req);
218
+ if (!token) return null;
219
+ try {
220
+ const payload = this.jwt.verify(token, this.options.secret, {
221
+ algorithms: this.options.algorithms ?? [
222
+ "HS256"
223
+ ]
224
+ });
225
+ return this.options.mapPayload ? this.options.mapPayload(payload) : payload;
226
+ } catch {
227
+ return null;
228
+ }
229
+ }
230
+ extractToken(req) {
231
+ const from = this.options.tokenFrom ?? "header";
232
+ if (from === "header") {
233
+ const headerName = this.options.headerName ?? "authorization";
234
+ const prefix = this.options.headerPrefix ?? "Bearer";
235
+ const header = req.headers?.[headerName] ?? req.headers?.[headerName.toLowerCase()];
236
+ if (!header || typeof header !== "string") return null;
237
+ if (!header.startsWith(`${prefix} `)) return null;
238
+ return header.slice(prefix.length + 1);
239
+ }
240
+ if (from === "query") {
241
+ const param = this.options.queryParam ?? "token";
242
+ return req.query?.[param] ?? null;
243
+ }
244
+ if (from === "cookie") {
245
+ const cookieName = this.options.cookieName ?? "jwt";
246
+ return req.cookies?.[cookieName] ?? null;
247
+ }
248
+ return null;
249
+ }
250
+ };
251
+
252
+ // src/strategies/api-key.strategy.ts
253
+ var ApiKeyStrategy = class {
254
+ static {
255
+ __name(this, "ApiKeyStrategy");
256
+ }
257
+ name = "api-key";
258
+ options;
259
+ constructor(options) {
260
+ this.options = options;
261
+ }
262
+ async validate(req) {
263
+ const key = this.extractKey(req);
264
+ if (!key) return null;
265
+ if (this.options.validate) {
266
+ return this.options.validate(key);
267
+ }
268
+ if (this.options.keys) {
269
+ const user = this.options.keys[key];
270
+ return user ?? null;
271
+ }
272
+ return null;
273
+ }
274
+ extractKey(req) {
275
+ const sources = this.options.from ?? [
276
+ "header"
277
+ ];
278
+ for (const source of sources) {
279
+ if (source === "header") {
280
+ const headerName = this.options.headerName ?? "x-api-key";
281
+ const value = req.headers?.[headerName] ?? req.headers?.[headerName.toLowerCase()];
282
+ if (value && typeof value === "string") return value;
283
+ }
284
+ if (source === "query") {
285
+ const param = this.options.queryParam ?? "api_key";
286
+ const value = req.query?.[param];
287
+ if (value && typeof value === "string") return value;
288
+ }
289
+ }
290
+ return null;
291
+ }
292
+ };
293
+
294
+ // src/strategies/oauth.strategy.ts
295
+ import { Logger as Logger2 } from "@forinda/kickjs-core";
296
+ var log2 = Logger2.for("OAuthStrategy");
297
+ var PROVIDER_ENDPOINTS = {
298
+ google: {
299
+ authorizeUrl: "https://accounts.google.com/o/oauth2/v2/auth",
300
+ tokenUrl: "https://oauth2.googleapis.com/token",
301
+ userInfoUrl: "https://www.googleapis.com/oauth2/v2/userinfo",
302
+ scopes: [
303
+ "openid",
304
+ "email",
305
+ "profile"
306
+ ]
307
+ },
308
+ github: {
309
+ authorizeUrl: "https://github.com/login/oauth/authorize",
310
+ tokenUrl: "https://github.com/login/oauth/access_token",
311
+ userInfoUrl: "https://api.github.com/user",
312
+ scopes: [
313
+ "read:user",
314
+ "user:email"
315
+ ]
316
+ },
317
+ discord: {
318
+ authorizeUrl: "https://discord.com/api/oauth2/authorize",
319
+ tokenUrl: "https://discord.com/api/oauth2/token",
320
+ userInfoUrl: "https://discord.com/api/users/@me",
321
+ scopes: [
322
+ "identify",
323
+ "email"
324
+ ]
325
+ },
326
+ microsoft: {
327
+ authorizeUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
328
+ tokenUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/token",
329
+ userInfoUrl: "https://graph.microsoft.com/v1.0/me",
330
+ scopes: [
331
+ "openid",
332
+ "email",
333
+ "profile"
334
+ ]
335
+ }
336
+ };
337
+ var OAuthStrategy = class {
338
+ static {
339
+ __name(this, "OAuthStrategy");
340
+ }
341
+ name;
342
+ options;
343
+ endpoints;
344
+ constructor(options) {
345
+ this.options = options;
346
+ this.name = `oauth-${options.provider}`;
347
+ if (options.provider === "custom") {
348
+ if (!options.endpoints) {
349
+ throw new Error('OAuthStrategy: "endpoints" required when provider is "custom"');
350
+ }
351
+ this.endpoints = options.endpoints;
352
+ } else {
353
+ this.endpoints = {
354
+ ...PROVIDER_ENDPOINTS[options.provider],
355
+ ...options.endpoints ?? {}
356
+ };
357
+ }
358
+ if (options.scopes) {
359
+ this.endpoints.scopes = options.scopes;
360
+ }
361
+ }
362
+ /**
363
+ * Get the authorization URL to redirect the user to the OAuth provider.
364
+ * Pass an optional `state` parameter for CSRF protection.
365
+ */
366
+ getAuthorizationUrl(state) {
367
+ const scopes = Array.isArray(this.endpoints.scopes) ? this.endpoints.scopes.join(" ") : this.endpoints.scopes ?? "";
368
+ const params = new URLSearchParams({
369
+ client_id: this.options.clientId,
370
+ redirect_uri: this.options.callbackUrl,
371
+ response_type: "code",
372
+ scope: scopes,
373
+ ...state ? {
374
+ state
375
+ } : {}
376
+ });
377
+ return `${this.endpoints.authorizeUrl}?${params.toString()}`;
378
+ }
379
+ /**
380
+ * Validate the callback request — exchange the authorization code for
381
+ * tokens and fetch the user profile.
382
+ */
383
+ async validate(req) {
384
+ const code = req.query?.code;
385
+ if (!code) return null;
386
+ try {
387
+ const tokens = await this.exchangeCode(code);
388
+ if (!tokens) return null;
389
+ const profile = await this.fetchUserInfo(tokens.accessToken);
390
+ if (!profile) return null;
391
+ if (this.options.mapProfile) {
392
+ return this.options.mapProfile(profile, tokens);
393
+ }
394
+ return profile;
395
+ } catch (err) {
396
+ log2.error({
397
+ err
398
+ }, `OAuth ${this.options.provider} callback failed`);
399
+ return null;
400
+ }
401
+ }
402
+ /** Exchange authorization code for access/refresh tokens */
403
+ async exchangeCode(code) {
404
+ const body = new URLSearchParams({
405
+ client_id: this.options.clientId,
406
+ client_secret: this.options.clientSecret,
407
+ code,
408
+ grant_type: "authorization_code",
409
+ redirect_uri: this.options.callbackUrl
410
+ });
411
+ const response = await fetch(this.endpoints.tokenUrl, {
412
+ method: "POST",
413
+ headers: {
414
+ "Content-Type": "application/x-www-form-urlencoded",
415
+ Accept: "application/json"
416
+ },
417
+ body: body.toString()
418
+ });
419
+ if (!response.ok) {
420
+ log2.error(`Token exchange failed: ${response.status} ${response.statusText}`);
421
+ return null;
422
+ }
423
+ const data = await response.json();
424
+ return {
425
+ accessToken: data.access_token,
426
+ refreshToken: data.refresh_token,
427
+ tokenType: data.token_type ?? "Bearer",
428
+ expiresIn: data.expires_in,
429
+ scope: data.scope
430
+ };
431
+ }
432
+ /** Fetch user profile from the provider's userinfo endpoint */
433
+ async fetchUserInfo(accessToken) {
434
+ const response = await fetch(this.endpoints.userInfoUrl, {
435
+ headers: {
436
+ Authorization: `Bearer ${accessToken}`,
437
+ Accept: "application/json"
438
+ }
439
+ });
440
+ if (!response.ok) {
441
+ log2.error(`User info fetch failed: ${response.status} ${response.statusText}`);
442
+ return null;
443
+ }
444
+ return response.json();
445
+ }
446
+ };
447
+
448
+ // src/strategies/passport.bridge.ts
449
+ var PassportBridge = class {
450
+ static {
451
+ __name(this, "PassportBridge");
452
+ }
453
+ name;
454
+ passportStrategy;
455
+ constructor(name, passportStrategy) {
456
+ this.name = name;
457
+ this.passportStrategy = passportStrategy;
458
+ }
459
+ validate(req) {
460
+ return new Promise((resolve) => {
461
+ const strategy = Object.create(this.passportStrategy);
462
+ strategy.success = (user) => resolve(user ?? null);
463
+ strategy.fail = () => resolve(null);
464
+ strategy.error = () => resolve(null);
465
+ strategy.pass = () => resolve(null);
466
+ strategy.redirect = () => resolve(null);
467
+ try {
468
+ strategy.authenticate(req, {});
469
+ } catch {
470
+ resolve(null);
471
+ }
472
+ });
473
+ }
474
+ };
475
+ export {
476
+ AUTH_META,
477
+ AUTH_USER,
478
+ ApiKeyStrategy,
479
+ AuthAdapter,
480
+ Authenticated,
481
+ JwtStrategy,
482
+ OAuthStrategy,
483
+ PassportBridge,
484
+ Public,
485
+ Roles
486
+ };
487
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/types.ts","../src/decorators.ts","../src/adapter.ts","../src/strategies/jwt.strategy.ts","../src/strategies/api-key.strategy.ts","../src/strategies/oauth.strategy.ts","../src/strategies/passport.bridge.ts"],"sourcesContent":["import 'reflect-metadata'\n\n// Types & interfaces\nexport { AUTH_META, type AuthUser, type AuthStrategy, type AuthAdapterOptions } from './types'\n\n// Decorators\nexport { Authenticated, Public, Roles } from './decorators'\n\n// Adapter\nexport { AuthAdapter, AUTH_USER } from './adapter'\n\n// Built-in strategies\nexport {\n JwtStrategy,\n ApiKeyStrategy,\n OAuthStrategy,\n PassportBridge,\n type JwtStrategyOptions,\n type ApiKeyStrategyOptions,\n type ApiKeyUser,\n type OAuthStrategyOptions,\n type OAuthProvider,\n type OAuthEndpoints,\n type OAuthTokens,\n} from './strategies'\n","import 'reflect-metadata'\n\n// ── Auth Metadata Keys ──────────────────────────────────────────────────\n\nexport const AUTH_META = {\n AUTHENTICATED: Symbol('auth:authenticated'),\n PUBLIC: Symbol('auth:public'),\n ROLES: Symbol('auth:roles'),\n STRATEGY: Symbol('auth:strategy'),\n} as const\n\n// ── AuthUser ────────────────────────────────────────────────────────────\n\n/**\n * The authenticated user object attached to the request.\n * Extend this via module augmentation for your app's user shape.\n *\n * @example\n * ```ts\n * declare module '@forinda/kickjs-auth' {\n * interface AuthUser {\n * id: string\n * email: string\n * roles: string[]\n * }\n * }\n * ```\n */\nexport interface AuthUser {\n [key: string]: any\n}\n\n// ── AuthStrategy Interface ──────────────────────────────────────────────\n\n/**\n * Abstract authentication strategy. Implement this to support any\n * auth mechanism: JWT, API keys, OAuth, sessions, SAML, etc.\n *\n * @example\n * ```ts\n * class SessionStrategy implements AuthStrategy {\n * name = 'session'\n * async validate(req) {\n * const session = req.session\n * if (!session?.userId) return null\n * return { id: session.userId, roles: session.roles }\n * }\n * }\n * ```\n */\nexport interface AuthStrategy {\n /** Unique name for this strategy (e.g., 'jwt', 'api-key', 'session') */\n name: string\n\n /**\n * Extract and validate credentials from the request.\n * Return the authenticated user, or null if authentication fails.\n *\n * @param req - Express request object\n * @returns The authenticated user, or null\n */\n validate(req: any): Promise<AuthUser | null> | AuthUser | null\n}\n\n// ── AuthAdapter Options ─────────────────────────────────────────────────\n\nexport interface AuthAdapterOptions {\n /**\n * Authentication strategies to use, in order of precedence.\n * The first strategy that returns a user wins.\n *\n * @example\n * ```ts\n * new AuthAdapter({\n * strategies: [\n * new JwtStrategy({ secret: process.env.JWT_SECRET! }),\n * new ApiKeyStrategy({ keys: { 'sk-123': { name: 'CI bot', roles: ['api'] } } }),\n * ],\n * })\n * ```\n */\n strategies: AuthStrategy[]\n\n /**\n * Default behavior for routes without @Authenticated or @Public.\n * - `'protected'` — all routes require auth unless marked @Public (default)\n * - `'open'` — all routes are public unless marked @Authenticated\n */\n defaultPolicy?: 'protected' | 'open'\n\n /**\n * Custom handler for unauthorized requests.\n * Default: responds with 401 JSON error.\n */\n onUnauthorized?: (req: any, res: any) => void\n\n /**\n * Custom handler for forbidden requests (role mismatch).\n * Default: responds with 403 JSON error.\n */\n onForbidden?: (req: any, res: any) => void\n}\n","import 'reflect-metadata'\nimport { AUTH_META } from './types'\n\n/**\n * Mark a controller or method as requiring authentication.\n * When applied to a controller, all methods require auth unless overridden with @Public.\n *\n * Optionally specify which strategy to use (defaults to trying all registered strategies).\n *\n * @example\n * ```ts\n * @Controller('/users')\n * @Authenticated() // All routes require auth\n * class UserController {\n * @Get('/')\n * list(ctx) { ... } // Protected\n *\n * @Get('/public-count')\n * @Public() // Override: this route is open\n * count(ctx) { ... }\n * }\n *\n * // Strategy-specific:\n * @Authenticated('api-key')\n * @Get('/webhook')\n * webhook(ctx) { ... }\n * ```\n */\nexport function Authenticated(strategy?: string): ClassDecorator & MethodDecorator {\n return (target: any, propertyKey?: string | symbol) => {\n if (propertyKey) {\n Reflect.defineMetadata(AUTH_META.AUTHENTICATED, true, target.constructor, propertyKey)\n if (strategy) {\n Reflect.defineMetadata(AUTH_META.STRATEGY, strategy, target.constructor, propertyKey)\n }\n } else {\n Reflect.defineMetadata(AUTH_META.AUTHENTICATED, true, target)\n if (strategy) {\n Reflect.defineMetadata(AUTH_META.STRATEGY, strategy, target)\n }\n }\n }\n}\n\n/**\n * Mark a method as publicly accessible, bypassing authentication.\n * Use inside an @Authenticated controller to exempt specific routes.\n *\n * @example\n * ```ts\n * @Controller('/auth')\n * @Authenticated()\n * class AuthController {\n * @Post('/login')\n * @Public() // No auth required\n * login(ctx) { ... }\n *\n * @Get('/me') // Auth required (inherits from controller)\n * me(ctx) { ... }\n * }\n * ```\n */\nexport function Public(): MethodDecorator {\n return (target: any, propertyKey: string | symbol) => {\n Reflect.defineMetadata(AUTH_META.PUBLIC, true, target.constructor, propertyKey)\n }\n}\n\n/**\n * Require specific roles to access a route.\n * The authenticated user must have at least one of the specified roles.\n * Implies @Authenticated — no need to add both.\n *\n * The user object must have a `roles` property (string array).\n *\n * @example\n * ```ts\n * @Get('/admin/dashboard')\n * @Roles('admin', 'superadmin')\n * dashboard(ctx) { ... }\n *\n * @Delete('/:id')\n * @Roles('admin')\n * deleteUser(ctx) { ... }\n * ```\n */\nexport function Roles(...roles: string[]): MethodDecorator {\n return (target: any, propertyKey: string | symbol) => {\n Reflect.defineMetadata(AUTH_META.AUTHENTICATED, true, target.constructor, propertyKey)\n Reflect.defineMetadata(AUTH_META.ROLES, roles, target.constructor, propertyKey)\n }\n}\n","import 'reflect-metadata'\nimport {\n Logger,\n HttpStatus,\n METADATA,\n type AppAdapter,\n type AdapterMiddleware,\n type Container,\n type RouteDefinition,\n} from '@forinda/kickjs-core'\n\nimport { AUTH_META, type AuthAdapterOptions, type AuthStrategy, type AuthUser } from './types'\n\nconst log = Logger.for('AuthAdapter')\n\n/** DI token to resolve the current authenticated user from the container */\nexport const AUTH_USER = Symbol('AuthUser')\n\n/**\n * Authentication adapter — plugs into the KickJS lifecycle to protect\n * routes based on @Authenticated, @Public, and @Roles decorators.\n *\n * Supports multiple strategies (JWT, API key, custom) with first-match semantics.\n *\n * @example\n * ```ts\n * import { AuthAdapter, JwtStrategy, ApiKeyStrategy } from '@forinda/kickjs-auth'\n *\n * bootstrap({\n * modules: [...],\n * adapters: [\n * new AuthAdapter({\n * strategies: [\n * new JwtStrategy({ secret: process.env.JWT_SECRET! }),\n * new ApiKeyStrategy({ keys: { 'sk-123': { name: 'Bot', roles: ['api'] } } }),\n * ],\n * defaultPolicy: 'protected', // secure by default\n * }),\n * ],\n * })\n * ```\n */\nexport class AuthAdapter implements AppAdapter {\n name = 'AuthAdapter'\n private strategies: AuthStrategy[]\n private defaultPolicy: 'protected' | 'open'\n private onUnauthorized: (req: any, res: any) => void\n private onForbidden: (req: any, res: any) => void\n\n // Collected route metadata for auth resolution\n private routeControllers = new Map<string, any>()\n\n constructor(private options: AuthAdapterOptions) {\n this.strategies = options.strategies\n this.defaultPolicy = options.defaultPolicy ?? 'protected'\n\n this.onUnauthorized =\n options.onUnauthorized ??\n ((_req, res) => {\n res.status(HttpStatus.UNAUTHORIZED).json({\n statusCode: HttpStatus.UNAUTHORIZED,\n error: 'Unauthorized',\n message: 'Authentication required',\n })\n })\n\n this.onForbidden =\n options.onForbidden ??\n ((_req, res) => {\n res.status(HttpStatus.FORBIDDEN).json({\n statusCode: HttpStatus.FORBIDDEN,\n error: 'Forbidden',\n message: 'Insufficient permissions',\n })\n })\n }\n\n onRouteMount(controllerClass: any, mountPath: string): void {\n this.routeControllers.set(mountPath, controllerClass)\n }\n\n middleware(): AdapterMiddleware[] {\n return [\n {\n handler: this.createAuthMiddleware(),\n phase: 'beforeRoutes',\n },\n ]\n }\n\n beforeStart(app: any, _container: Container): void {\n const strategyNames = this.strategies.map((s) => s.name).join(', ')\n log.info(`Auth enabled [${strategyNames}] (default: ${this.defaultPolicy})`)\n }\n\n // ── Core Auth Middleware ─────────────────────────────────────────────\n\n private createAuthMiddleware() {\n return async (req: any, res: any, next: any) => {\n // Find which controller + method handles this route\n const { controllerClass, handlerName } = this.resolveHandler(req)\n\n // Determine if this route needs auth\n const authRequired = this.isAuthRequired(controllerClass, handlerName)\n if (!authRequired) {\n return next()\n }\n\n // Determine which strategy to use\n const strategyName = this.getStrategyName(controllerClass, handlerName)\n\n // Try to authenticate\n const user = await this.authenticate(req, strategyName)\n if (!user) {\n return this.onUnauthorized(req, res)\n }\n\n // Attach user to request\n req.user = user\n\n // Check roles if required\n const requiredRoles = this.getRequiredRoles(controllerClass, handlerName)\n if (requiredRoles && requiredRoles.length > 0) {\n const userRoles: string[] = (user as any).roles ?? []\n const hasRole = requiredRoles.some((role) => userRoles.includes(role))\n if (!hasRole) {\n return this.onForbidden(req, res)\n }\n }\n\n next()\n }\n }\n\n // ── Strategy Execution ──────────────────────────────────────────────\n\n private async authenticate(req: any, strategyName?: string): Promise<AuthUser | null> {\n const strategies = strategyName\n ? this.strategies.filter((s) => s.name === strategyName)\n : this.strategies\n\n for (const strategy of strategies) {\n try {\n const user = await strategy.validate(req)\n if (user) return user\n } catch (err: any) {\n log.debug(`Strategy ${strategy.name} failed: ${err.message}`)\n }\n }\n\n return null\n }\n\n // ── Metadata Resolution ─────────────────────────────────────────────\n\n private resolveHandler(req: any): { controllerClass: any; handlerName: string | undefined } {\n // Express 5 stores matched route info\n const route = req.route\n if (!route) {\n return { controllerClass: undefined, handlerName: undefined }\n }\n\n // Try to find the controller from our collected route mounts\n for (const [mountPath, controllerClass] of this.routeControllers) {\n if (req.baseUrl?.startsWith(mountPath) || req.path?.startsWith(mountPath)) {\n const routes: RouteDefinition[] =\n Reflect.getMetadata(METADATA.ROUTES, controllerClass) ?? []\n const matched = routes.find(\n (r) => r.method === req.method && this.pathMatches(r.path, req.route?.path ?? req.path),\n )\n if (matched) {\n return { controllerClass, handlerName: matched.handlerName }\n }\n }\n }\n\n return { controllerClass: undefined, handlerName: undefined }\n }\n\n private pathMatches(routePath: string, requestPath: string): boolean {\n // Normalize trailing slashes and compare\n const norm = (p: string) => p.replace(/\\/+$/, '') || '/'\n return norm(routePath) === norm(requestPath)\n }\n\n private isAuthRequired(controllerClass: any, handlerName?: string): boolean {\n if (!controllerClass) {\n // No controller found — apply default policy\n return this.defaultPolicy === 'protected'\n }\n\n // Method-level @Public always wins\n if (handlerName) {\n const isPublic = Reflect.getMetadata(AUTH_META.PUBLIC, controllerClass, handlerName)\n if (isPublic) return false\n\n // Method-level @Authenticated\n const methodAuth = Reflect.getMetadata(AUTH_META.AUTHENTICATED, controllerClass, handlerName)\n if (methodAuth !== undefined) return methodAuth\n }\n\n // Class-level @Authenticated\n const classAuth = Reflect.getMetadata(AUTH_META.AUTHENTICATED, controllerClass)\n if (classAuth !== undefined) return classAuth\n\n // Default policy\n return this.defaultPolicy === 'protected'\n }\n\n private getRequiredRoles(controllerClass: any, handlerName?: string): string[] | undefined {\n if (!controllerClass || !handlerName) return undefined\n return Reflect.getMetadata(AUTH_META.ROLES, controllerClass, handlerName)\n }\n\n private getStrategyName(controllerClass: any, handlerName?: string): string | undefined {\n if (!controllerClass) return undefined\n\n // Method-level strategy\n if (handlerName) {\n const methodStrategy = Reflect.getMetadata(AUTH_META.STRATEGY, controllerClass, handlerName)\n if (methodStrategy) return methodStrategy\n }\n\n // Class-level strategy\n return Reflect.getMetadata(AUTH_META.STRATEGY, controllerClass)\n }\n}\n","import type { AuthStrategy, AuthUser } from '../types'\n\nexport interface JwtStrategyOptions {\n /** JWT secret key for HS256 or public key for RS256 */\n secret: string | Buffer\n\n /**\n * Algorithm (default: 'HS256').\n * Common values: 'HS256', 'HS384', 'HS512', 'RS256', 'RS384', 'RS512'\n */\n algorithms?: string[]\n\n /** Where to read the token from (default: 'header') */\n tokenFrom?: 'header' | 'query' | 'cookie'\n\n /** Header name (default: 'authorization') */\n headerName?: string\n\n /** Query parameter name when tokenFrom='query' (default: 'token') */\n queryParam?: string\n\n /** Cookie name when tokenFrom='cookie' (default: 'jwt') */\n cookieName?: string\n\n /** Token prefix in header (default: 'Bearer') */\n headerPrefix?: string\n\n /**\n * Transform the decoded JWT payload into your AuthUser shape.\n * By default, returns the payload as-is.\n *\n * @example\n * ```ts\n * mapPayload: (payload) => ({\n * id: payload.sub,\n * email: payload.email,\n * roles: payload.roles || [],\n * })\n * ```\n */\n mapPayload?: (payload: any) => AuthUser\n}\n\n/**\n * JWT authentication strategy.\n * Validates Bearer tokens using `jsonwebtoken`.\n *\n * Requires `jsonwebtoken` as a peer dependency:\n * ```bash\n * pnpm add jsonwebtoken @types/jsonwebtoken\n * ```\n *\n * @example\n * ```ts\n * new JwtStrategy({\n * secret: process.env.JWT_SECRET!,\n * mapPayload: (payload) => ({\n * id: payload.sub,\n * email: payload.email,\n * roles: payload.roles ?? ['user'],\n * }),\n * })\n * ```\n */\nexport class JwtStrategy implements AuthStrategy {\n name = 'jwt'\n private jwt: any\n private options: JwtStrategyOptions\n\n constructor(options: JwtStrategyOptions) {\n this.options = options\n }\n\n private async ensureJwt(): Promise<void> {\n if (this.jwt) return\n try {\n const mod: any = await import('jsonwebtoken')\n this.jwt = mod.default ?? mod\n } catch {\n throw new Error('JwtStrategy requires \"jsonwebtoken\" package. Install: pnpm add jsonwebtoken')\n }\n }\n\n async validate(req: any): Promise<AuthUser | null> {\n await this.ensureJwt()\n\n const token = this.extractToken(req)\n if (!token) return null\n\n try {\n const payload = this.jwt.verify(token, this.options.secret, {\n algorithms: this.options.algorithms ?? ['HS256'],\n })\n\n return this.options.mapPayload ? this.options.mapPayload(payload) : payload\n } catch {\n return null\n }\n }\n\n private extractToken(req: any): string | null {\n const from = this.options.tokenFrom ?? 'header'\n\n if (from === 'header') {\n const headerName = this.options.headerName ?? 'authorization'\n const prefix = this.options.headerPrefix ?? 'Bearer'\n const header = req.headers?.[headerName] ?? req.headers?.[headerName.toLowerCase()]\n if (!header || typeof header !== 'string') return null\n if (!header.startsWith(`${prefix} `)) return null\n return header.slice(prefix.length + 1)\n }\n\n if (from === 'query') {\n const param = this.options.queryParam ?? 'token'\n return req.query?.[param] ?? null\n }\n\n if (from === 'cookie') {\n const cookieName = this.options.cookieName ?? 'jwt'\n return req.cookies?.[cookieName] ?? null\n }\n\n return null\n }\n}\n","import type { AuthStrategy, AuthUser } from '../types'\n\nexport interface ApiKeyUser {\n /** Display name for the API key holder */\n name: string\n /** Roles granted to this API key */\n roles?: string[]\n /** Additional metadata */\n [key: string]: any\n}\n\nexport interface ApiKeyStrategyOptions {\n /**\n * Map of API keys to their associated users.\n * Keys are the raw API key strings.\n *\n * @example\n * ```ts\n * keys: {\n * 'sk-live-abc123': { name: 'Production Bot', roles: ['api', 'write'] },\n * 'sk-test-xyz789': { name: 'Test Runner', roles: ['api'] },\n * }\n * ```\n */\n keys?: Record<string, ApiKeyUser>\n\n /**\n * Async function to validate an API key.\n * Use when keys are stored in a database or external service.\n * Takes precedence over the static `keys` map.\n *\n * @example\n * ```ts\n * validate: async (key) => {\n * const row = await db.apiKeys.findUnique({ where: { key } })\n * if (!row || row.revokedAt) return null\n * return { name: row.name, roles: row.roles }\n * }\n * ```\n */\n validate?: (key: string) => Promise<AuthUser | null> | AuthUser | null\n\n /**\n * Where to read the API key from (default: 'header').\n * Checks all specified locations in order.\n */\n from?: Array<'header' | 'query'>\n\n /** Header name (default: 'x-api-key') */\n headerName?: string\n\n /** Query parameter name (default: 'api_key') */\n queryParam?: string\n}\n\n/**\n * API key authentication strategy.\n * Validates keys from headers or query parameters against a static map\n * or async validator function.\n *\n * @example\n * ```ts\n * // Static keys\n * new ApiKeyStrategy({\n * keys: {\n * 'sk-prod-123': { name: 'CI Bot', roles: ['api'] },\n * },\n * })\n *\n * // Database lookup\n * new ApiKeyStrategy({\n * validate: async (key) => {\n * const record = await db.apiKeys.findByKey(key)\n * return record ? { name: record.name, roles: record.roles } : null\n * },\n * })\n * ```\n */\nexport class ApiKeyStrategy implements AuthStrategy {\n name = 'api-key'\n private options: ApiKeyStrategyOptions\n\n constructor(options: ApiKeyStrategyOptions) {\n this.options = options\n }\n\n async validate(req: any): Promise<AuthUser | null> {\n const key = this.extractKey(req)\n if (!key) return null\n\n // Async validator takes precedence\n if (this.options.validate) {\n return this.options.validate(key)\n }\n\n // Static key lookup\n if (this.options.keys) {\n const user = this.options.keys[key]\n return user ?? null\n }\n\n return null\n }\n\n private extractKey(req: any): string | null {\n const sources = this.options.from ?? ['header']\n\n for (const source of sources) {\n if (source === 'header') {\n const headerName = this.options.headerName ?? 'x-api-key'\n const value = req.headers?.[headerName] ?? req.headers?.[headerName.toLowerCase()]\n if (value && typeof value === 'string') return value\n }\n\n if (source === 'query') {\n const param = this.options.queryParam ?? 'api_key'\n const value = req.query?.[param]\n if (value && typeof value === 'string') return value\n }\n }\n\n return null\n }\n}\n","import { Logger } from '@forinda/kickjs-core'\nimport type { AuthStrategy, AuthUser } from '../types'\n\nconst log = Logger.for('OAuthStrategy')\n\n/** Supported OAuth providers with pre-configured endpoints */\nexport type OAuthProvider = 'google' | 'github' | 'discord' | 'microsoft' | 'custom'\n\n/** Provider-specific endpoint configuration */\nexport interface OAuthEndpoints {\n authorizeUrl: string\n tokenUrl: string\n userInfoUrl: string\n /** Scopes to request (space-separated or array) */\n scopes?: string[] | string\n}\n\n/** Pre-configured OAuth provider endpoints */\nconst PROVIDER_ENDPOINTS: Record<Exclude<OAuthProvider, 'custom'>, OAuthEndpoints> = {\n google: {\n authorizeUrl: 'https://accounts.google.com/o/oauth2/v2/auth',\n tokenUrl: 'https://oauth2.googleapis.com/token',\n userInfoUrl: 'https://www.googleapis.com/oauth2/v2/userinfo',\n scopes: ['openid', 'email', 'profile'],\n },\n github: {\n authorizeUrl: 'https://github.com/login/oauth/authorize',\n tokenUrl: 'https://github.com/login/oauth/access_token',\n userInfoUrl: 'https://api.github.com/user',\n scopes: ['read:user', 'user:email'],\n },\n discord: {\n authorizeUrl: 'https://discord.com/api/oauth2/authorize',\n tokenUrl: 'https://discord.com/api/oauth2/token',\n userInfoUrl: 'https://discord.com/api/users/@me',\n scopes: ['identify', 'email'],\n },\n microsoft: {\n authorizeUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',\n tokenUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',\n userInfoUrl: 'https://graph.microsoft.com/v1.0/me',\n scopes: ['openid', 'email', 'profile'],\n },\n}\n\nexport interface OAuthStrategyOptions {\n /** OAuth provider preset or 'custom' for manual endpoint config */\n provider: OAuthProvider\n\n /** OAuth client ID */\n clientId: string\n\n /** OAuth client secret */\n clientSecret: string\n\n /** Callback URL for the OAuth flow (e.g. 'http://localhost:3000/auth/google/callback') */\n callbackUrl: string\n\n /** Custom endpoints (required when provider is 'custom') */\n endpoints?: OAuthEndpoints\n\n /** Override default scopes for the provider */\n scopes?: string[]\n\n /**\n * Transform the provider's user profile into your AuthUser.\n * Called after successfully exchanging the code for a token and fetching user info.\n *\n * @example\n * ```ts\n * mapProfile: (profile) => ({\n * id: profile.id,\n * email: profile.email,\n * name: profile.name,\n * avatar: profile.picture,\n * roles: ['user'],\n * })\n * ```\n */\n mapProfile?: (profile: any, tokens: OAuthTokens) => AuthUser | Promise<AuthUser>\n}\n\nexport interface OAuthTokens {\n accessToken: string\n refreshToken?: string\n tokenType: string\n expiresIn?: number\n scope?: string\n}\n\n/**\n * Built-in OAuth 2.0 strategy with pre-configured providers.\n * No Passport dependency — KickJS handles the entire OAuth flow.\n *\n * Supports: Google, GitHub, Discord, Microsoft, or any custom OAuth 2.0 provider.\n *\n * This strategy validates the callback request (the one with `?code=...`),\n * exchanges the code for tokens, fetches the user profile, and returns it.\n *\n * You need two routes:\n * 1. **Redirect** — sends user to provider's login page\n * 2. **Callback** — receives the code and authenticates\n *\n * @example\n * ```ts\n * import { OAuthStrategy } from '@forinda/kickjs-auth'\n *\n * const googleAuth = new OAuthStrategy({\n * provider: 'google',\n * clientId: process.env.GOOGLE_CLIENT_ID!,\n * clientSecret: process.env.GOOGLE_CLIENT_SECRET!,\n * callbackUrl: 'http://localhost:3000/auth/google/callback',\n * mapProfile: (profile) => ({\n * id: profile.id,\n * email: profile.email,\n * name: profile.name,\n * avatar: profile.picture,\n * roles: ['user'],\n * }),\n * })\n *\n * // In your controller:\n * @Get('/auth/google')\n * @Public()\n * loginWithGoogle(ctx: RequestContext) {\n * return ctx.res.redirect(googleAuth.getAuthorizationUrl())\n * }\n *\n * @Get('/auth/google/callback')\n * @Public()\n * async googleCallback(ctx: RequestContext) {\n * const user = await googleAuth.validate(ctx.req)\n * if (!user) return ctx.res.status(401).json({ error: 'Auth failed' })\n * const token = issueJwt(user)\n * return ctx.json({ token, user })\n * }\n * ```\n */\nexport class OAuthStrategy implements AuthStrategy {\n name: string\n private options: OAuthStrategyOptions\n private endpoints: OAuthEndpoints\n\n constructor(options: OAuthStrategyOptions) {\n this.options = options\n this.name = `oauth-${options.provider}`\n\n if (options.provider === 'custom') {\n if (!options.endpoints) {\n throw new Error('OAuthStrategy: \"endpoints\" required when provider is \"custom\"')\n }\n this.endpoints = options.endpoints\n } else {\n this.endpoints = {\n ...PROVIDER_ENDPOINTS[options.provider],\n ...(options.endpoints ?? {}),\n }\n }\n\n // Override scopes if provided\n if (options.scopes) {\n this.endpoints.scopes = options.scopes\n }\n }\n\n /**\n * Get the authorization URL to redirect the user to the OAuth provider.\n * Pass an optional `state` parameter for CSRF protection.\n */\n getAuthorizationUrl(state?: string): string {\n const scopes = Array.isArray(this.endpoints.scopes)\n ? this.endpoints.scopes.join(' ')\n : (this.endpoints.scopes ?? '')\n\n const params = new URLSearchParams({\n client_id: this.options.clientId,\n redirect_uri: this.options.callbackUrl,\n response_type: 'code',\n scope: scopes,\n ...(state ? { state } : {}),\n })\n\n return `${this.endpoints.authorizeUrl}?${params.toString()}`\n }\n\n /**\n * Validate the callback request — exchange the authorization code for\n * tokens and fetch the user profile.\n */\n async validate(req: any): Promise<AuthUser | null> {\n const code = req.query?.code\n if (!code) return null\n\n try {\n // Exchange code for tokens\n const tokens = await this.exchangeCode(code)\n if (!tokens) return null\n\n // Fetch user profile\n const profile = await this.fetchUserInfo(tokens.accessToken)\n if (!profile) return null\n\n // Map profile to AuthUser\n if (this.options.mapProfile) {\n return this.options.mapProfile(profile, tokens)\n }\n\n return profile\n } catch (err: any) {\n log.error({ err }, `OAuth ${this.options.provider} callback failed`)\n return null\n }\n }\n\n /** Exchange authorization code for access/refresh tokens */\n private async exchangeCode(code: string): Promise<OAuthTokens | null> {\n const body = new URLSearchParams({\n client_id: this.options.clientId,\n client_secret: this.options.clientSecret,\n code,\n grant_type: 'authorization_code',\n redirect_uri: this.options.callbackUrl,\n })\n\n const response = await fetch(this.endpoints.tokenUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n Accept: 'application/json',\n },\n body: body.toString(),\n })\n\n if (!response.ok) {\n log.error(`Token exchange failed: ${response.status} ${response.statusText}`)\n return null\n }\n\n const data: any = await response.json()\n\n return {\n accessToken: data.access_token,\n refreshToken: data.refresh_token,\n tokenType: data.token_type ?? 'Bearer',\n expiresIn: data.expires_in,\n scope: data.scope,\n }\n }\n\n /** Fetch user profile from the provider's userinfo endpoint */\n private async fetchUserInfo(accessToken: string): Promise<any | null> {\n const response = await fetch(this.endpoints.userInfoUrl, {\n headers: {\n Authorization: `Bearer ${accessToken}`,\n Accept: 'application/json',\n },\n })\n\n if (!response.ok) {\n log.error(`User info fetch failed: ${response.status} ${response.statusText}`)\n return null\n }\n\n return response.json()\n }\n}\n","import type { AuthStrategy, AuthUser } from '../types'\n\n/**\n * Bridge that wraps any Passport.js strategy into a KickJS AuthStrategy.\n * This lets you use the full Passport ecosystem (500+ strategies) without\n * changing your KickJS auth setup.\n *\n * Requires `passport` as a dependency in your project:\n * ```bash\n * pnpm add passport\n * ```\n *\n * @example\n * ```ts\n * import { Strategy as GoogleStrategy } from 'passport-google-oauth20'\n * import { PassportBridge } from '@forinda/kickjs-auth'\n *\n * const google = new PassportBridge('google', new GoogleStrategy({\n * clientID: process.env.GOOGLE_CLIENT_ID!,\n * clientSecret: process.env.GOOGLE_CLIENT_SECRET!,\n * callbackURL: '/auth/google/callback',\n * }, (accessToken, refreshToken, profile, done) => {\n * // Find or create user from profile\n * const user = await findOrCreateUser(profile)\n * done(null, user)\n * }))\n *\n * new AuthAdapter({\n * strategies: [jwtStrategy, google],\n * })\n * ```\n */\nexport class PassportBridge implements AuthStrategy {\n name: string\n private passportStrategy: any\n\n constructor(name: string, passportStrategy: any) {\n this.name = name\n this.passportStrategy = passportStrategy\n }\n\n validate(req: any): Promise<AuthUser | null> {\n return new Promise((resolve) => {\n // Passport strategies call done(err, user, info)\n // We wrap the authenticate flow manually without needing passport.initialize()\n const strategy = Object.create(this.passportStrategy)\n\n strategy.success = (user: any) => resolve(user ?? null)\n strategy.fail = () => resolve(null)\n strategy.error = () => resolve(null)\n strategy.pass = () => resolve(null)\n strategy.redirect = () => resolve(null)\n\n try {\n strategy.authenticate(req, {})\n } catch {\n resolve(null)\n }\n })\n }\n}\n"],"mappings":"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","names":["AUTH_META","AUTHENTICATED","Symbol","PUBLIC","ROLES","STRATEGY","Authenticated","strategy","target","propertyKey","Reflect","defineMetadata","AUTH_META","AUTHENTICATED","STRATEGY","Public","PUBLIC","Roles","roles","ROLES","Logger","HttpStatus","METADATA","log","Logger","for","AUTH_USER","Symbol","AuthAdapter","name","strategies","defaultPolicy","onUnauthorized","onForbidden","routeControllers","Map","options","_req","res","status","HttpStatus","UNAUTHORIZED","json","statusCode","error","message","FORBIDDEN","onRouteMount","controllerClass","mountPath","set","middleware","handler","createAuthMiddleware","phase","beforeStart","app","_container","strategyNames","map","s","join","info","req","next","handlerName","resolveHandler","authRequired","isAuthRequired","strategyName","getStrategyName","user","authenticate","requiredRoles","getRequiredRoles","length","userRoles","roles","hasRole","some","role","includes","filter","strategy","validate","err","debug","route","undefined","baseUrl","startsWith","path","routes","Reflect","getMetadata","METADATA","ROUTES","matched","find","r","method","pathMatches","routePath","requestPath","norm","p","replace","isPublic","AUTH_META","PUBLIC","methodAuth","AUTHENTICATED","classAuth","ROLES","methodStrategy","STRATEGY","JwtStrategy","name","jwt","options","ensureJwt","mod","default","Error","validate","req","token","extractToken","payload","verify","secret","algorithms","mapPayload","from","tokenFrom","headerName","prefix","headerPrefix","header","headers","toLowerCase","startsWith","slice","length","param","queryParam","query","cookieName","cookies","ApiKeyStrategy","name","options","validate","req","key","extractKey","keys","user","sources","from","source","headerName","value","headers","toLowerCase","param","queryParam","query","Logger","log","Logger","for","PROVIDER_ENDPOINTS","google","authorizeUrl","tokenUrl","userInfoUrl","scopes","github","discord","microsoft","OAuthStrategy","name","options","endpoints","provider","Error","getAuthorizationUrl","state","Array","isArray","join","params","URLSearchParams","client_id","clientId","redirect_uri","callbackUrl","response_type","scope","toString","validate","req","code","query","tokens","exchangeCode","profile","fetchUserInfo","accessToken","mapProfile","err","error","body","client_secret","clientSecret","grant_type","response","fetch","method","headers","Accept","ok","status","statusText","data","json","access_token","refreshToken","refresh_token","tokenType","token_type","expiresIn","expires_in","Authorization","PassportBridge","name","passportStrategy","validate","req","Promise","resolve","strategy","Object","create","success","user","fail","error","pass","redirect","authenticate"]}
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "@forinda/kickjs-auth",
3
+ "version": "1.1.0",
4
+ "description": "Pluggable authentication for KickJS — JWT, API key, and custom strategies",
5
+ "keywords": [
6
+ "kickjs",
7
+ "auth",
8
+ "authentication",
9
+ "jwt",
10
+ "api-key",
11
+ "guard",
12
+ "middleware",
13
+ "typescript",
14
+ "decorators",
15
+ "strategy-pattern"
16
+ ],
17
+ "type": "module",
18
+ "main": "dist/index.js",
19
+ "types": "dist/index.d.ts",
20
+ "exports": {
21
+ ".": {
22
+ "import": "./dist/index.js",
23
+ "types": "./dist/index.d.ts"
24
+ }
25
+ },
26
+ "files": [
27
+ "dist"
28
+ ],
29
+ "dependencies": {
30
+ "reflect-metadata": "^0.2.2",
31
+ "@forinda/kickjs-core": "1.1.0"
32
+ },
33
+ "peerDependencies": {
34
+ "jsonwebtoken": "^9.0.0"
35
+ },
36
+ "peerDependenciesMeta": {
37
+ "jsonwebtoken": {
38
+ "optional": true
39
+ }
40
+ },
41
+ "devDependencies": {
42
+ "@types/jsonwebtoken": "^9.0.9",
43
+ "@types/node": "^24.5.2",
44
+ "jsonwebtoken": "^9.0.2",
45
+ "tsup": "^8.5.0",
46
+ "typescript": "^5.9.2",
47
+ "vitest": "^3.2.4"
48
+ },
49
+ "publishConfig": {
50
+ "access": "public"
51
+ },
52
+ "license": "MIT",
53
+ "author": "Felix Orinda",
54
+ "engines": {
55
+ "node": ">=20.0"
56
+ },
57
+ "homepage": "https://forinda.github.io/kick-js/",
58
+ "repository": {
59
+ "type": "git",
60
+ "url": "https://github.com/forinda/kick-js.git",
61
+ "directory": "packages/auth"
62
+ },
63
+ "bugs": {
64
+ "url": "https://github.com/forinda/kick-js/issues"
65
+ },
66
+ "scripts": {
67
+ "build": "tsup",
68
+ "dev": "tsup --watch",
69
+ "test": "vitest run",
70
+ "typecheck": "tsc --noEmit",
71
+ "clean": "rm -rf dist .turbo"
72
+ }
73
+ }