@stonecrop/casl-middleware 0.7.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.
Files changed (66) hide show
  1. package/README.md +149 -0
  2. package/dist/casl-middleware.d.ts +158 -0
  3. package/dist/casl-middleware.js +40571 -0
  4. package/dist/casl-middleware.js.map +1 -0
  5. package/dist/casl_middleware.tsbuildinfo +1 -0
  6. package/dist/src/index.d.ts +6 -0
  7. package/dist/src/index.d.ts.map +1 -0
  8. package/dist/src/index.js +3 -0
  9. package/dist/src/middleware/ability.d.ts +55 -0
  10. package/dist/src/middleware/ability.d.ts.map +1 -0
  11. package/dist/src/middleware/ability.js +139 -0
  12. package/dist/src/middleware/graphql.d.ts +11 -0
  13. package/dist/src/middleware/graphql.d.ts.map +1 -0
  14. package/dist/src/middleware/graphql.js +120 -0
  15. package/dist/src/middleware/introspection.d.ts +71 -0
  16. package/dist/src/middleware/introspection.d.ts.map +1 -0
  17. package/dist/src/middleware/introspection.js +169 -0
  18. package/dist/src/middleware/jwt.d.ts +114 -0
  19. package/dist/src/middleware/jwt.d.ts.map +1 -0
  20. package/dist/src/middleware/jwt.js +291 -0
  21. package/dist/src/middleware/postgraphile.d.ts +7 -0
  22. package/dist/src/middleware/postgraphile.d.ts.map +1 -0
  23. package/dist/src/middleware/postgraphile.js +80 -0
  24. package/dist/src/middleware/yoga.d.ts +15 -0
  25. package/dist/src/middleware/yoga.d.ts.map +1 -0
  26. package/dist/src/middleware/yoga.js +32 -0
  27. package/dist/src/tsdoc-metadata.json +11 -0
  28. package/dist/src/types/index.d.ts +114 -0
  29. package/dist/src/types/index.d.ts.map +1 -0
  30. package/dist/src/types/index.js +0 -0
  31. package/dist/tests/ability.test.d.ts +2 -0
  32. package/dist/tests/ability.test.d.ts.map +1 -0
  33. package/dist/tests/ability.test.js +125 -0
  34. package/dist/tests/helpers/test-utils.d.ts +46 -0
  35. package/dist/tests/helpers/test-utils.d.ts.map +1 -0
  36. package/dist/tests/helpers/test-utils.js +92 -0
  37. package/dist/tests/introspection.test.d.ts +2 -0
  38. package/dist/tests/introspection.test.d.ts.map +1 -0
  39. package/dist/tests/introspection.test.js +368 -0
  40. package/dist/tests/jwt.test.d.ts +2 -0
  41. package/dist/tests/jwt.test.d.ts.map +1 -0
  42. package/dist/tests/jwt.test.js +371 -0
  43. package/dist/tests/middleware.test.d.ts +2 -0
  44. package/dist/tests/middleware.test.d.ts.map +1 -0
  45. package/dist/tests/middleware.test.js +184 -0
  46. package/dist/tests/postgraphile-plugin.test.d.ts +2 -0
  47. package/dist/tests/postgraphile-plugin.test.d.ts.map +1 -0
  48. package/dist/tests/postgraphile-plugin.test.js +56 -0
  49. package/dist/tests/setup.d.ts +2 -0
  50. package/dist/tests/setup.d.ts.map +1 -0
  51. package/dist/tests/setup.js +11 -0
  52. package/dist/tests/user-roles.test.d.ts +2 -0
  53. package/dist/tests/user-roles.test.d.ts.map +1 -0
  54. package/dist/tests/user-roles.test.js +157 -0
  55. package/dist/tests/yoga-plugin.test.d.ts +2 -0
  56. package/dist/tests/yoga-plugin.test.d.ts.map +1 -0
  57. package/dist/tests/yoga-plugin.test.js +47 -0
  58. package/package.json +91 -0
  59. package/src/index.ts +15 -0
  60. package/src/middleware/ability.ts +191 -0
  61. package/src/middleware/graphql.ts +157 -0
  62. package/src/middleware/introspection.ts +258 -0
  63. package/src/middleware/jwt.ts +394 -0
  64. package/src/middleware/postgraphile.ts +93 -0
  65. package/src/middleware/yoga.ts +39 -0
  66. package/src/types/index.ts +133 -0
@@ -0,0 +1,258 @@
1
+ import { GraphQLError } from 'graphql'
2
+ import type { Context, MiddlewareOptions } from '../types'
3
+
4
+ export interface IntrospectionConfig {
5
+ /**
6
+ * Whether to allow introspection queries at all
7
+ * @default true in development, false in production
8
+ */
9
+ enabled?: boolean
10
+
11
+ /**
12
+ * Roles that are allowed to introspect the schema
13
+ * If undefined, all authenticated users can introspect
14
+ */
15
+ allowedRoles?: string[]
16
+
17
+ /**
18
+ * Allow unauthenticated introspection
19
+ * @default false
20
+ */
21
+ allowAnonymous?: boolean
22
+
23
+ /**
24
+ * Custom function to determine if introspection is allowed
25
+ */
26
+ customCheck?: (context: Context) => boolean | Promise<boolean>
27
+
28
+ /**
29
+ * Types to hide from introspection based on user permissions
30
+ * Maps type names to required permissions
31
+ */
32
+ typePermissions?: Record<string, { action: string; subject: string }>
33
+
34
+ /**
35
+ * Fields to hide from introspection based on user permissions
36
+ * Maps "Type.field" to required permissions
37
+ */
38
+ fieldPermissions?: Record<string, { action: string; subject: string }>
39
+ }
40
+
41
+ /**
42
+ * Middleware to restrict GraphQL introspection based on user permissions
43
+ */
44
+ export const createIntrospectionMiddleware = (config: IntrospectionConfig = {}) => {
45
+ const {
46
+ enabled = process.env.NODE_ENV !== 'production',
47
+ allowedRoles,
48
+ allowAnonymous = false,
49
+ customCheck,
50
+ typePermissions = {},
51
+ fieldPermissions = {},
52
+ } = config
53
+
54
+ return async (resolve: any, root: any, args: any, context: Context, info: any) => {
55
+ // Check if this is an introspection query
56
+ const isIntrospection =
57
+ info.fieldName === '__schema' ||
58
+ info.fieldName === '__type' ||
59
+ info.parentType?.name === '__Schema' ||
60
+ info.parentType?.name === '__Type'
61
+
62
+ if (!isIntrospection) {
63
+ // Not an introspection query, continue normally
64
+ return resolve(root, args, context, info)
65
+ }
66
+
67
+ // Check if introspection is enabled
68
+ if (!enabled) {
69
+ throw new GraphQLError('Introspection is disabled')
70
+ }
71
+
72
+ // Custom check function
73
+ if (customCheck) {
74
+ const allowed = await customCheck(context)
75
+ if (!allowed) {
76
+ throw new GraphQLError('Introspection not allowed')
77
+ }
78
+ }
79
+
80
+ // Check authentication
81
+ if (!context.user && !allowAnonymous) {
82
+ throw new GraphQLError('Authentication required for introspection')
83
+ }
84
+
85
+ // Check role-based access
86
+ if (allowedRoles && allowedRoles.length > 0) {
87
+ const userRoles = context.user?.roles || []
88
+ const hasAllowedRole = allowedRoles.some(role => userRoles.includes(role))
89
+
90
+ if (!hasAllowedRole) {
91
+ throw new GraphQLError('Insufficient permissions for introspection')
92
+ }
93
+ }
94
+
95
+ // Check CASL-based permissions
96
+ if (context.ability) {
97
+ // Check if user can read schema
98
+ if (!context.ability.can('read', '__Schema')) {
99
+ throw new GraphQLError('Permission denied for schema introspection')
100
+ }
101
+ }
102
+
103
+ // Get the result
104
+ let result = await resolve(root, args, context, info)
105
+
106
+ // Filter the result based on permissions
107
+ if (result && (typePermissions || fieldPermissions)) {
108
+ result = filterIntrospectionResult(result, context, {
109
+ typePermissions,
110
+ fieldPermissions,
111
+ })
112
+ }
113
+
114
+ return result
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Filter introspection results based on user permissions
120
+ */
121
+ function filterIntrospectionResult(
122
+ result: any,
123
+ context: Context,
124
+ config: {
125
+ typePermissions?: Record<string, { action: string; subject: string }>
126
+ fieldPermissions?: Record<string, { action: string; subject: string }>
127
+ }
128
+ ): any {
129
+ if (!context.ability) return result
130
+
131
+ // Filter __schema result
132
+ if (result && result.types) {
133
+ result.types = result.types.filter((type: any) => {
134
+ // Check if user has permission to see this type
135
+ const permission = config.typePermissions?.[type.name]
136
+ if (permission) {
137
+ return context.ability!.can(permission.action, permission.subject)
138
+ }
139
+ return true // Show types without specific permissions
140
+ })
141
+
142
+ // Filter fields within types
143
+ result.types.forEach((type: any) => {
144
+ if (type.fields) {
145
+ type.fields = type.fields.filter((field: any) => {
146
+ const fieldKey = `${type.name}.${field.name}`
147
+ const permission = config.fieldPermissions?.[fieldKey]
148
+ if (permission) {
149
+ return context.ability!.can(permission.action, permission.subject)
150
+ }
151
+ return true
152
+ })
153
+ }
154
+ })
155
+ }
156
+
157
+ // Filter __type result
158
+ if (result && result.fields) {
159
+ const typeName = result.name
160
+ result.fields = result.fields.filter((field: any) => {
161
+ const fieldKey = `${typeName}.${field.name}`
162
+ const permission = config.fieldPermissions?.[fieldKey]
163
+ if (permission) {
164
+ return context.ability!.can(permission.action, permission.subject)
165
+ }
166
+ return true
167
+ })
168
+ }
169
+
170
+ return result
171
+ }
172
+
173
+ /**
174
+ * Postgraphile plugin for introspection control
175
+ */
176
+ export const createPostgraphileIntrospectionPlugin = (config: IntrospectionConfig) => {
177
+ return {
178
+ name: 'IntrospectionControlPlugin',
179
+ version: '1.0.0',
180
+
181
+ // Disable introspection in GraphiQL based on config
182
+ grafast: {
183
+ hooks: {
184
+ GraphQLSchema(schema: any) {
185
+ if (!config.enabled) {
186
+ // Remove introspection from schema
187
+ // This is a simplified approach - real implementation would be more complex
188
+ console.warn('Introspection control in Postgraphile requires custom implementation')
189
+ }
190
+ return schema
191
+ },
192
+ },
193
+ },
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Utility to create ability rules for introspection
199
+ */
200
+ export const createIntrospectionAbilityRules = (user?: {
201
+ roles?: string[]
202
+ }): Array<{ action: string; subject: string }> => {
203
+ const rules: Array<{ action: string; subject: string }> = []
204
+
205
+ if (!user) {
206
+ // Anonymous users cannot introspect
207
+ return rules
208
+ }
209
+
210
+ const roles = user.roles || []
211
+
212
+ // Admins can introspect everything
213
+ if (roles.includes('admin')) {
214
+ rules.push({ action: 'read', subject: '__Schema' })
215
+ rules.push({ action: 'read', subject: '__Type' })
216
+ return rules
217
+ }
218
+
219
+ // Developers can introspect
220
+ if (roles.includes('developer')) {
221
+ rules.push({ action: 'read', subject: '__Schema' })
222
+ rules.push({ action: 'read', subject: '__Type' })
223
+ return rules
224
+ }
225
+
226
+ // Regular users get limited introspection
227
+ if (roles.includes('user')) {
228
+ // They can see the schema but not all types
229
+ rules.push({ action: 'read', subject: '__Schema' })
230
+ // Specific types they can see would be added here
231
+ }
232
+
233
+ return rules
234
+ }
235
+
236
+ /**
237
+ * Example: Combine introspection with CASL middleware
238
+ */
239
+ export const createSecureGraphQLMiddleware = (options: {
240
+ casl?: MiddlewareOptions
241
+ introspection?: IntrospectionConfig
242
+ }) => {
243
+ const middlewares: any[] = []
244
+
245
+ // Add introspection control
246
+ if (options.introspection) {
247
+ middlewares.push(createIntrospectionMiddleware(options.introspection))
248
+ }
249
+
250
+ // Combine all middlewares
251
+ return (resolve: any, root: any, args: any, context: any, info: any) => {
252
+ const chain = middlewares.reduceRight(
253
+ (next, middleware) => () => middleware(next, root, args, context, info),
254
+ () => resolve(root, args, context, info)
255
+ )
256
+ return chain()
257
+ }
258
+ }
@@ -0,0 +1,394 @@
1
+ import jwt from 'jsonwebtoken'
2
+ import { AbilityBuilder, PureAbility } from '@casl/ability'
3
+ import type { Context, User } from '../types'
4
+ import { defaultAbilityBuilder } from './ability'
5
+
6
+ export interface JWTConfig {
7
+ enabled?: boolean
8
+ secret?: string
9
+ publicKey?: string
10
+ algorithms?: jwt.Algorithm[]
11
+ issuer?: string
12
+ audience?: string
13
+ extractUser?: (payload: any) => User | undefined
14
+ headerName?: string // Default: 'authorization'
15
+ tokenPrefix?: string // Default: 'Bearer '
16
+ optional?: boolean // If true, continues without error if no token
17
+ maxAge?: string // Maximum age of token (e.g., '1h', '7d')
18
+ }
19
+
20
+ export interface JWTPayload extends jwt.JwtPayload {
21
+ sub?: string // Subject (user id)
22
+ roles?: string[]
23
+ permissions?: Array<{
24
+ action: string
25
+ subject: string
26
+ conditions?: any
27
+ }>
28
+ [key: string]: any
29
+ }
30
+
31
+ /**
32
+ * Default user extractor from JWT payload
33
+ */
34
+ const defaultUserExtractor = (payload: JWTPayload): User | undefined => {
35
+ if (!payload.sub) return undefined
36
+
37
+ return {
38
+ id: payload.sub,
39
+ roles: payload.roles || [],
40
+ ...payload, // Include any additional claims
41
+ }
42
+ }
43
+
44
+ /**
45
+ * JWT middleware factory for GraphQL servers
46
+ *
47
+ * @example
48
+ * ```typescript
49
+ * // In Nuxt Yoga
50
+ * export default defineNuxtConfig({
51
+ * yoga: {
52
+ * middleware: [
53
+ * createJWTMiddleware({
54
+ * enabled: true,
55
+ * secret: process.env.JWT_SECRET,
56
+ * optional: true // Don't fail if no token
57
+ * })
58
+ * ]
59
+ * }
60
+ * })
61
+ *
62
+ * // In Postgraphile
63
+ * const jwtPlugin = createPostgraphileJWTPlugin({
64
+ * secret: process.env.JWT_SECRET,
65
+ * extractUser: (payload) => ({
66
+ * id: payload.user_id,
67
+ * roles: payload.user_roles
68
+ * })
69
+ * })
70
+ * ```
71
+ */
72
+ export const createJWTMiddleware = (config: JWTConfig = {}) => {
73
+ const {
74
+ enabled = true,
75
+ secret,
76
+ publicKey,
77
+ algorithms = ['HS256'] as jwt.Algorithm[],
78
+ issuer,
79
+ audience,
80
+ headerName = 'authorization',
81
+ tokenPrefix = 'Bearer ',
82
+ optional = false,
83
+ extractUser = defaultUserExtractor,
84
+ maxAge,
85
+ } = config
86
+
87
+ // Validate configuration
88
+ if (enabled && !secret && !publicKey) {
89
+ throw new Error('JWT middleware requires either secret or publicKey')
90
+ }
91
+
92
+ return async (context: Context, next: () => Promise<any>) => {
93
+ // Skip if JWT is disabled
94
+ if (!enabled) {
95
+ return next()
96
+ }
97
+
98
+ try {
99
+ // Extract token from request headers
100
+ const authHeader =
101
+ context.req?.headers?.get?.(headerName) ||
102
+ context.request?.headers?.get?.(headerName) ||
103
+ context.headers?.[headerName]
104
+
105
+ if (!authHeader) {
106
+ if (optional) {
107
+ return next()
108
+ }
109
+ throw new Error('No authorization header found')
110
+ }
111
+
112
+ // Remove token prefix
113
+ const token = authHeader.startsWith(tokenPrefix) ? authHeader.slice(tokenPrefix.length) : authHeader
114
+
115
+ // Prepare verification options
116
+ const verifyOptions: jwt.VerifyOptions = {
117
+ algorithms,
118
+ ...(issuer && { issuer }),
119
+ ...(audience && { audience }),
120
+ ...(maxAge && { maxAge }),
121
+ }
122
+
123
+ // Verify and decode token
124
+ const secretOrPublicKey = publicKey || secret!
125
+ const payload = jwt.verify(token, secretOrPublicKey, verifyOptions) as JWTPayload
126
+
127
+ // Extract user from payload
128
+ const user = extractUser(payload)
129
+
130
+ if (user) {
131
+ context.user = user
132
+ // Store the raw payload for potential use
133
+ context.jwtPayload = payload
134
+ }
135
+
136
+ // Continue to next middleware
137
+ return next()
138
+ } catch (error: any) {
139
+ if (optional) {
140
+ // Log error in development
141
+ if (process.env.NODE_ENV === 'development') {
142
+ console.warn('JWT verification failed (optional):', error.message)
143
+ }
144
+ // Continue without user if optional
145
+ return next()
146
+ }
147
+
148
+ // Re-throw with more specific error messages
149
+ if (error.name === 'TokenExpiredError') {
150
+ throw new Error('Token has expired')
151
+ } else if (error.name === 'JsonWebTokenError') {
152
+ throw new Error('Invalid token')
153
+ } else if (error.name === 'NotBeforeError') {
154
+ throw new Error('Token not active yet')
155
+ }
156
+
157
+ throw error
158
+ }
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Create a JWT token with user data
164
+ */
165
+ export const createJWT = (
166
+ user: User,
167
+ config: {
168
+ secret: string
169
+ expiresIn?: string | number
170
+ issuer?: string
171
+ audience?: string
172
+ additionalClaims?: Record<string, any>
173
+ }
174
+ ): string => {
175
+ const { secret, expiresIn = '1h', issuer, audience, additionalClaims = {} } = config
176
+
177
+ const payload: JWTPayload = {
178
+ sub: user.id,
179
+ roles: user.roles || [],
180
+ ...additionalClaims,
181
+ }
182
+
183
+ const signOptions: jwt.SignOptions = {}
184
+
185
+ // Add optional fields only if they exist
186
+ if (issuer !== undefined) signOptions.issuer = issuer
187
+ if (audience !== undefined) signOptions.audience = audience
188
+ if (expiresIn !== undefined) signOptions.expiresIn = expiresIn as any // Type cast to avoid TS issues
189
+
190
+ return jwt.sign(payload, secret, signOptions)
191
+ }
192
+
193
+ /**
194
+ * Integration with CASL ability builder
195
+ */
196
+ export const createJWTAbilityBuilder = (config: JWTConfig = {}) => {
197
+ return async (user?: User) => {
198
+ // If user has direct permissions in JWT, use those
199
+ const jwtPermissions = (user as any)?.permissions
200
+
201
+ if (jwtPermissions && Array.isArray(jwtPermissions)) {
202
+ // Build ability from JWT permissions
203
+ const { can, cannot, build } = new AbilityBuilder<PureAbility>(PureAbility as any)
204
+
205
+ jwtPermissions.forEach((permission: any) => {
206
+ if (permission.inverted) {
207
+ cannot(permission.action, permission.subject, permission.conditions)
208
+ } else {
209
+ can(permission.action, permission.subject, permission.conditions)
210
+ }
211
+ })
212
+
213
+ return build()
214
+ }
215
+
216
+ // Fall back to role-based abilities
217
+ return defaultAbilityBuilder(user)
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Postgraphile-specific JWT plugin
223
+ */
224
+ export const createPostgraphileJWTPlugin = (config: JWTConfig) => {
225
+ return {
226
+ name: 'JWTAuthPlugin',
227
+ version: '1.0.0',
228
+
229
+ // Hook into Postgraphile's context building
230
+ grafast: {
231
+ hooks: {
232
+ async context(ctx: any, build: any) {
233
+ const middleware = createJWTMiddleware(config)
234
+
235
+ // Create a simple context object that the middleware can work with
236
+ const context = {
237
+ req: ctx.req,
238
+ headers: ctx.req?.headers,
239
+ user: undefined,
240
+ jwtPayload: undefined,
241
+ }
242
+
243
+ // Run the JWT middleware
244
+ await middleware(context, async () => {})
245
+
246
+ // Add user to Postgraphile context
247
+ if (context.user) {
248
+ return { ...ctx, user: context.user, jwtPayload: context.jwtPayload }
249
+ }
250
+
251
+ return ctx
252
+ },
253
+ },
254
+ },
255
+ }
256
+ }
257
+
258
+ /**
259
+ * Express/Koa middleware for REST endpoints
260
+ */
261
+ export const createHTTPJWTMiddleware = (config: JWTConfig) => {
262
+ const jwtMiddleware = createJWTMiddleware(config)
263
+
264
+ // Express middleware
265
+ return async (req: any, res: any, next: any) => {
266
+ const context = {
267
+ req: {
268
+ headers: {
269
+ get: (name: string) => req.headers[name],
270
+ },
271
+ },
272
+ headers: req.headers,
273
+ user: undefined,
274
+ jwtPayload: undefined,
275
+ }
276
+
277
+ try {
278
+ await jwtMiddleware(context, async () => {})
279
+ req.user = context.user
280
+ req.jwtPayload = context.jwtPayload
281
+ next()
282
+ } catch (error: any) {
283
+ if (config.optional) {
284
+ next()
285
+ } else {
286
+ res.status(401).json({
287
+ error: error.message,
288
+ code: 'UNAUTHORIZED',
289
+ })
290
+ }
291
+ }
292
+ }
293
+ }
294
+
295
+ /**
296
+ * Refresh token utilities
297
+ */
298
+ export const refreshTokenUtils = {
299
+ /**
300
+ * Create access and refresh tokens
301
+ */
302
+ createTokenPair: (
303
+ user: User,
304
+ config: {
305
+ accessSecret: string
306
+ refreshSecret: string
307
+ accessExpiresIn?: string
308
+ refreshExpiresIn?: string
309
+ }
310
+ ) => {
311
+ const { accessSecret, refreshSecret, accessExpiresIn = '15m', refreshExpiresIn = '7d' } = config
312
+
313
+ const accessPayload: any = {
314
+ sub: user.id,
315
+ roles: user.roles,
316
+ type: 'access',
317
+ }
318
+
319
+ const refreshPayload: any = {
320
+ sub: user.id,
321
+ type: 'refresh',
322
+ }
323
+
324
+ // Create access token with proper options
325
+ const accessOptions: jwt.SignOptions = {}
326
+ if (accessExpiresIn) {
327
+ accessOptions.expiresIn = accessExpiresIn as any // Type cast to avoid TS issues
328
+ }
329
+
330
+ const accessToken = jwt.sign(accessPayload, accessSecret, accessOptions)
331
+
332
+ // Create refresh token with proper options
333
+ const refreshOptions: jwt.SignOptions = {}
334
+ if (refreshExpiresIn) {
335
+ refreshOptions.expiresIn = refreshExpiresIn as any // Type cast to avoid TS issues
336
+ }
337
+
338
+ const refreshToken = jwt.sign(refreshPayload, refreshSecret, refreshOptions)
339
+
340
+ return { accessToken, refreshToken }
341
+ },
342
+
343
+ /**
344
+ * Verify refresh token and create new access token
345
+ */
346
+ refreshAccessToken: async (
347
+ refreshToken: string,
348
+ config: {
349
+ accessSecret: string
350
+ refreshSecret: string
351
+ getUserById: (id: string) => Promise<User | null>
352
+ accessExpiresIn?: string
353
+ }
354
+ ) => {
355
+ const { accessSecret, refreshSecret, getUserById, accessExpiresIn = '15m' } = config
356
+
357
+ try {
358
+ // Verify refresh token
359
+ const payload = jwt.verify(refreshToken, refreshSecret) as any
360
+
361
+ if (payload.type !== 'refresh') {
362
+ throw new Error('Invalid token type')
363
+ }
364
+
365
+ // Get fresh user data
366
+ const user = await getUserById(payload.sub)
367
+ if (!user) {
368
+ throw new Error('User not found')
369
+ }
370
+
371
+ // Create new access token
372
+ const accessPayload: any = {
373
+ sub: user.id,
374
+ roles: user.roles,
375
+ type: 'access',
376
+ }
377
+
378
+ // Create access token with proper options
379
+ const accessOptions: jwt.SignOptions = {}
380
+ if (accessExpiresIn) {
381
+ accessOptions.expiresIn = accessExpiresIn as any // Type cast to avoid TS issues
382
+ }
383
+
384
+ const accessToken = jwt.sign(accessPayload, accessSecret, accessOptions)
385
+
386
+ return { accessToken, user }
387
+ } catch (error: any) {
388
+ if (error.name === 'TokenExpiredError') {
389
+ throw new Error('Refresh token expired')
390
+ }
391
+ throw error
392
+ }
393
+ },
394
+ }