@stonecrop/casl-middleware 0.30.0 → 0.32.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 (38) hide show
  1. package/dist/casl-middleware.d.ts +15 -0
  2. package/dist/casl-middleware.js +3328 -2650
  3. package/dist/casl-middleware.js.map +1 -1
  4. package/dist/tsdoc-metadata.json +1 -1
  5. package/package.json +23 -22
  6. package/dist/casl-middleware.tsbuildinfo +0 -1
  7. package/dist/src/index.d.ts +0 -6
  8. package/dist/src/index.d.ts.map +0 -1
  9. package/dist/src/index.js +0 -3
  10. package/dist/src/middleware/ability.d.ts +0 -55
  11. package/dist/src/middleware/ability.d.ts.map +0 -1
  12. package/dist/src/middleware/ability.js +0 -139
  13. package/dist/src/middleware/graphql.d.ts +0 -11
  14. package/dist/src/middleware/graphql.d.ts.map +0 -1
  15. package/dist/src/middleware/graphql.js +0 -120
  16. package/dist/src/middleware/introspection.d.ts +0 -71
  17. package/dist/src/middleware/introspection.d.ts.map +0 -1
  18. package/dist/src/middleware/introspection.js +0 -169
  19. package/dist/src/middleware/jwt.d.ts +0 -114
  20. package/dist/src/middleware/jwt.d.ts.map +0 -1
  21. package/dist/src/middleware/jwt.js +0 -302
  22. package/dist/src/middleware/postgraphile.d.ts +0 -7
  23. package/dist/src/middleware/postgraphile.d.ts.map +0 -1
  24. package/dist/src/middleware/postgraphile.js +0 -79
  25. package/dist/src/middleware/yoga.d.ts +0 -15
  26. package/dist/src/middleware/yoga.d.ts.map +0 -1
  27. package/dist/src/middleware/yoga.js +0 -30
  28. package/dist/src/types/index.d.ts +0 -114
  29. package/dist/src/types/index.d.ts.map +0 -1
  30. package/dist/src/types/index.js +0 -0
  31. package/src/index.ts +0 -15
  32. package/src/middleware/ability.ts +0 -197
  33. package/src/middleware/graphql.ts +0 -157
  34. package/src/middleware/introspection.ts +0 -258
  35. package/src/middleware/jwt.ts +0 -405
  36. package/src/middleware/postgraphile.ts +0 -89
  37. package/src/middleware/yoga.ts +0 -36
  38. package/src/types/index.ts +0 -133
@@ -1,258 +0,0 @@
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
- }
@@ -1,405 +0,0 @@
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 decoded = jwt.verify(token, secretOrPublicKey, verifyOptions)
126
- if (typeof decoded === 'string' || decoded == null) {
127
- throw new Error('Invalid JWT payload: expected object')
128
- }
129
- const payload: JWTPayload = decoded
130
-
131
- // Extract user from payload
132
- const user = extractUser(payload)
133
-
134
- if (user) {
135
- context.user = user
136
- // Store the raw payload for potential use
137
- context.jwtPayload = payload
138
- }
139
-
140
- // Continue to next middleware
141
- return next()
142
- } catch (error: any) {
143
- if (optional) {
144
- // Log error in development
145
- if (process.env.NODE_ENV === 'development') {
146
- console.warn('JWT verification failed (optional):', error.message)
147
- }
148
- // Continue without user if optional
149
- return next()
150
- }
151
-
152
- // Re-throw with more specific error messages
153
- if (error.name === 'TokenExpiredError') {
154
- throw new Error('Token has expired', { cause: error })
155
- } else if (error.name === 'JsonWebTokenError') {
156
- throw new Error('Invalid token', { cause: error })
157
- } else if (error.name === 'NotBeforeError') {
158
- throw new Error('Token not active yet', { cause: error })
159
- }
160
-
161
- throw error
162
- }
163
- }
164
- }
165
-
166
- /**
167
- * Create a JWT token with user data
168
- */
169
- export const createJWT = (
170
- user: User,
171
- config: {
172
- secret: string
173
- expiresIn?: jwt.SignOptions['expiresIn']
174
- issuer?: string
175
- audience?: string
176
- additionalClaims?: Record<string, any>
177
- }
178
- ): string => {
179
- const { secret, expiresIn = '1h', issuer, audience, additionalClaims = {} } = config
180
-
181
- const payload: JWTPayload = {
182
- sub: user.id,
183
- roles: user.roles || [],
184
- ...additionalClaims,
185
- }
186
-
187
- const signOptions: jwt.SignOptions = {}
188
-
189
- // Add optional fields only if they exist
190
- if (issuer !== undefined) signOptions.issuer = issuer
191
- if (audience !== undefined) signOptions.audience = audience
192
- if (expiresIn !== undefined) signOptions.expiresIn = expiresIn
193
-
194
- return jwt.sign(payload, secret, signOptions)
195
- }
196
-
197
- /**
198
- * Integration with CASL ability builder
199
- */
200
- export const createJWTAbilityBuilder = (_config: JWTConfig = {}) => {
201
- return async (user?: User) => {
202
- // If user has direct permissions in JWT, use those
203
- const jwtPermissions: unknown = user?.permissions
204
-
205
- if (jwtPermissions && Array.isArray(jwtPermissions)) {
206
- // Build ability from JWT permissions
207
- const { can, cannot, build } = new AbilityBuilder<PureAbility>(PureAbility)
208
-
209
- jwtPermissions.forEach((permission: any) => {
210
- if (permission.inverted) {
211
- cannot(permission.action, permission.subject, permission.conditions)
212
- } else {
213
- can(permission.action, permission.subject, permission.conditions)
214
- }
215
- })
216
-
217
- return build()
218
- }
219
-
220
- // Fall back to role-based abilities
221
- return defaultAbilityBuilder(user)
222
- }
223
- }
224
-
225
- /**
226
- * Postgraphile-specific JWT plugin
227
- */
228
- export const createPostgraphileJWTPlugin = (config: JWTConfig) => {
229
- return {
230
- name: 'JWTAuthPlugin',
231
- version: '1.0.0',
232
-
233
- // Hook into Postgraphile's context building
234
- grafast: {
235
- hooks: {
236
- async context(ctx: any, _build: any) {
237
- const middleware = createJWTMiddleware(config)
238
-
239
- // Create a simple context object that the middleware can work with
240
- const context = {
241
- req: ctx.req,
242
- headers: ctx.req?.headers,
243
- user: undefined,
244
- jwtPayload: undefined,
245
- }
246
-
247
- // Run the JWT middleware
248
- await middleware(context, async () => {})
249
-
250
- // Add user to Postgraphile context
251
- if (context.user) {
252
- return { ...ctx, user: context.user, jwtPayload: context.jwtPayload }
253
- }
254
-
255
- return ctx
256
- },
257
- },
258
- },
259
- }
260
- }
261
-
262
- /**
263
- * Express/Koa middleware for REST endpoints
264
- */
265
- export const createHTTPJWTMiddleware = (config: JWTConfig) => {
266
- const jwtMiddleware = createJWTMiddleware(config)
267
-
268
- // Express middleware
269
- return async (req: any, res: any, next: any) => {
270
- const context = {
271
- req: {
272
- headers: {
273
- get: (name: string) => req.headers[name],
274
- },
275
- },
276
- headers: req.headers,
277
- user: undefined,
278
- jwtPayload: undefined,
279
- }
280
-
281
- try {
282
- await jwtMiddleware(context, async () => {})
283
- req.user = context.user
284
- req.jwtPayload = context.jwtPayload
285
- next()
286
- } catch (error: any) {
287
- if (config.optional) {
288
- next()
289
- } else {
290
- res.status(401).json({
291
- error: error.message,
292
- code: 'UNAUTHORIZED',
293
- })
294
- }
295
- }
296
- }
297
- }
298
-
299
- /**
300
- * Refresh token utilities
301
- */
302
- export const refreshTokenUtils = {
303
- /**
304
- * Create access and refresh tokens
305
- */
306
- createTokenPair: (
307
- user: User,
308
- config: {
309
- accessSecret: string
310
- refreshSecret: string
311
- accessExpiresIn?: jwt.SignOptions['expiresIn']
312
- refreshExpiresIn?: jwt.SignOptions['expiresIn']
313
- }
314
- ) => {
315
- const { accessSecret, refreshSecret, accessExpiresIn = '15m', refreshExpiresIn = '7d' } = config
316
-
317
- const accessPayload: any = {
318
- sub: user.id,
319
- roles: user.roles,
320
- type: 'access',
321
- }
322
-
323
- const refreshPayload: any = {
324
- sub: user.id,
325
- type: 'refresh',
326
- }
327
-
328
- // Create access token with proper options
329
- const accessOptions: jwt.SignOptions = {}
330
- if (accessExpiresIn) {
331
- accessOptions.expiresIn = accessExpiresIn
332
- }
333
-
334
- const accessToken = jwt.sign(accessPayload, accessSecret, accessOptions)
335
-
336
- // Create refresh token with proper options
337
- const refreshOptions: jwt.SignOptions = {}
338
- if (refreshExpiresIn) {
339
- refreshOptions.expiresIn = refreshExpiresIn
340
- }
341
-
342
- const refreshToken = jwt.sign(refreshPayload, refreshSecret, refreshOptions)
343
-
344
- return { accessToken, refreshToken }
345
- },
346
-
347
- /**
348
- * Verify refresh token and create new access token
349
- */
350
- refreshAccessToken: async (
351
- refreshToken: string,
352
- config: {
353
- accessSecret: string
354
- refreshSecret: string
355
- getUserById: (id: string) => Promise<User | null>
356
- accessExpiresIn?: jwt.SignOptions['expiresIn']
357
- }
358
- ) => {
359
- const { accessSecret, refreshSecret, getUserById, accessExpiresIn = '15m' } = config
360
-
361
- try {
362
- // Verify refresh token
363
- const decoded = jwt.verify(refreshToken, refreshSecret)
364
- if (typeof decoded === 'string' || decoded == null) {
365
- throw new Error('Invalid refresh token payload: expected object')
366
- }
367
- const payload = decoded as jwt.JwtPayload & { type?: string; sub?: string }
368
-
369
- if (payload.type !== 'refresh') {
370
- throw new Error('Invalid token type')
371
- }
372
-
373
- // Get fresh user data
374
- if (!payload.sub) {
375
- throw new Error('Invalid refresh token payload: missing subject')
376
- }
377
- const user = await getUserById(payload.sub)
378
- if (!user) {
379
- throw new Error('User not found')
380
- }
381
-
382
- // Create new access token
383
- const accessPayload: any = {
384
- sub: user.id,
385
- roles: user.roles,
386
- type: 'access',
387
- }
388
-
389
- // Create access token with proper options
390
- const accessOptions: jwt.SignOptions = {}
391
- if (accessExpiresIn) {
392
- accessOptions.expiresIn = accessExpiresIn
393
- }
394
-
395
- const accessToken = jwt.sign(accessPayload, accessSecret, accessOptions)
396
-
397
- return { accessToken, user }
398
- } catch (error: any) {
399
- if (error.name === 'TokenExpiredError') {
400
- throw new Error('Refresh token expired', { cause: error })
401
- }
402
- throw error
403
- }
404
- },
405
- }