@simtlix/simfinity-js 2.3.4 → 2.4.1

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.
@@ -0,0 +1,274 @@
1
+ import { UnauthenticatedError, ForbiddenError } from './errors.js';
2
+
3
+ /**
4
+ * Resolves a value from an object using a dotted path string or function.
5
+ * @param {Object} obj - The object to resolve from
6
+ * @param {string|Function} pathOrFn - Dotted path (e.g., 'user.profile.id') or function to extract value
7
+ * @returns {*} The resolved value or undefined if not found
8
+ * @example
9
+ * resolvePath({ user: { id: '123' } }, 'user.id') // returns '123'
10
+ * resolvePath({ user: { id: '123' } }, (obj) => obj.user.id) // returns '123'
11
+ */
12
+ export const resolvePath = (obj, pathOrFn) => {
13
+ if (typeof pathOrFn === 'function') {
14
+ return pathOrFn(obj);
15
+ }
16
+ if (typeof pathOrFn === 'string') {
17
+ const parts = pathOrFn.split('.');
18
+ let value = obj;
19
+ for (const part of parts) {
20
+ if (value == null) return undefined;
21
+ value = value[part];
22
+ }
23
+ return value;
24
+ }
25
+ return undefined;
26
+ };
27
+
28
+ /**
29
+ * Rule that requires the user to be authenticated
30
+ * Checks for user existence at the specified path in context
31
+ * @param {string|Function} [userPath='user'] - Path to user in context (e.g., 'user', 'auth.user', 'session.user')
32
+ * @returns {Function} Rule function (parent, args, ctx, info) => boolean | throws
33
+ * @example
34
+ * requireAuth() // checks ctx.user
35
+ * requireAuth('auth.user') // checks ctx.auth.user
36
+ * requireAuth('session.currentUser') // checks ctx.session.currentUser
37
+ */
38
+ export const requireAuth = (userPath = 'user') => {
39
+ return (_parent, _args, ctx) => {
40
+ if (!ctx) {
41
+ throw new UnauthenticatedError('You must be logged in to access this resource');
42
+ }
43
+ const user = resolvePath(ctx, userPath);
44
+ if (!user) {
45
+ throw new UnauthenticatedError('You must be logged in to access this resource');
46
+ }
47
+ return true;
48
+ };
49
+ };
50
+
51
+ /**
52
+ * Rule that requires the user to have a specific role
53
+ * @param {string|string[]} role - Required role or array of roles (any match)
54
+ * @param {Object} [options] - Configuration options
55
+ * @param {string|Function} [options.userPath='user'] - Path to user in context
56
+ * @param {string|Function} [options.rolePath='role'] - Path to role field in user object
57
+ * @returns {Function} Rule function (parent, args, ctx, info) => boolean | throws
58
+ * @example
59
+ * requireRole('ADMIN')
60
+ * requireRole(['ADMIN', 'EDITOR'])
61
+ * requireRole('ADMIN', { userPath: 'auth.user', rolePath: 'roles.primary' })
62
+ */
63
+ export const requireRole = (role, options = {}) => {
64
+ const roles = Array.isArray(role) ? role : [role];
65
+ const { userPath = 'user', rolePath = 'role' } = options;
66
+
67
+ return (_parent, _args, ctx) => {
68
+ if (!ctx) {
69
+ throw new UnauthenticatedError('You must be logged in to access this resource');
70
+ }
71
+ const user = resolvePath(ctx, userPath);
72
+ if (!user) {
73
+ throw new UnauthenticatedError('You must be logged in to access this resource');
74
+ }
75
+
76
+ const userRole = resolvePath(user, rolePath);
77
+
78
+ if (!roles.includes(userRole)) {
79
+ throw new ForbiddenError(`Requires role: ${roles.join(' or ')}`);
80
+ }
81
+
82
+ return true;
83
+ };
84
+ };
85
+
86
+ /**
87
+ * Rule that requires the user to have a specific permission
88
+ * @param {string|string[]} permission - Required permission(s) (all must match if array)
89
+ * @param {Object} [options] - Configuration options
90
+ * @param {string|Function} [options.userPath='user'] - Path to user in context
91
+ * @param {string|Function} [options.permissionsPath='permissions'] - Path to permissions array in user object
92
+ * @returns {Function} Rule function (parent, args, ctx, info) => boolean | throws
93
+ * @example
94
+ * requirePermission('posts:read')
95
+ * requirePermission(['posts:read', 'posts:write'])
96
+ * requirePermission('posts:read', { userPath: 'auth.user', permissionsPath: 'grants' })
97
+ */
98
+ export const requirePermission = (permission, options = {}) => {
99
+ const requiredPermissions = Array.isArray(permission) ? permission : [permission];
100
+ const { userPath = 'user', permissionsPath = 'permissions' } = options;
101
+
102
+ return (_parent, _args, ctx) => {
103
+ if (!ctx) {
104
+ throw new UnauthenticatedError('You must be logged in to access this resource');
105
+ }
106
+ const user = resolvePath(ctx, userPath);
107
+ if (!user) {
108
+ throw new UnauthenticatedError('You must be logged in to access this resource');
109
+ }
110
+
111
+ const userPermissions = resolvePath(user, permissionsPath) || [];
112
+
113
+ // Check if user has wildcard permission
114
+ if (userPermissions.includes('*')) {
115
+ return true;
116
+ }
117
+
118
+ // All required permissions must be present
119
+ for (const perm of requiredPermissions) {
120
+ if (!userPermissions.includes(perm)) {
121
+ throw new ForbiddenError(`Missing permission: ${perm}`);
122
+ }
123
+ }
124
+
125
+ return true;
126
+ };
127
+ };
128
+
129
+ /**
130
+ * Composes multiple rules - all must pass (logical AND)
131
+ * @param {...Function} rules - Rule functions to compose
132
+ * @returns {Function} Composed rule function
133
+ */
134
+ export const composeRules = (...rules) => {
135
+ return async (parent, args, ctx, info) => {
136
+ for (const rule of rules) {
137
+ const result = await rule(parent, args, ctx, info);
138
+ // If rule returns false, deny access
139
+ if (result === false) {
140
+ return false;
141
+ }
142
+ // If rule throws, it will propagate
143
+ }
144
+ return true;
145
+ };
146
+ };
147
+
148
+ /**
149
+ * Creates a rule that allows access if ANY of the provided rules pass (logical OR)
150
+ * @param {...Function} rules - Rule functions to check
151
+ * @returns {Function} Combined rule function
152
+ */
153
+ export const anyRule = (...rules) => {
154
+ return async (parent, args, ctx, info) => {
155
+ let lastError = null;
156
+
157
+ for (const rule of rules) {
158
+ try {
159
+ const result = await rule(parent, args, ctx, info);
160
+ if (result !== false) {
161
+ return true; // At least one rule passed
162
+ }
163
+ } catch (error) {
164
+ lastError = error;
165
+ // Continue to next rule
166
+ }
167
+ }
168
+
169
+ // No rule passed - throw the last error or return false
170
+ if (lastError) {
171
+ throw lastError;
172
+ }
173
+ return false;
174
+ };
175
+ };
176
+
177
+ /**
178
+ * Creates a rule that checks if the authenticated user owns the resource
179
+ * @param {string|Function} [ownerField='userId'] - Path to owner ID in parent, or function to extract it
180
+ * @param {string|Function} [userIdField='id'] - Path to user ID in user object, or function to extract it
181
+ * @param {Object} [options] - Configuration options
182
+ * @param {string|Function} [options.userPath='user'] - Path to user in context
183
+ * @returns {Function} Rule function
184
+ * @example
185
+ * isOwner() // compares parent.userId with ctx.user.id
186
+ * isOwner('authorId') // compares parent.authorId with ctx.user.id
187
+ * isOwner('author.id', 'profile.id') // compares parent.author.id with ctx.user.profile.id
188
+ * isOwner('authorId', 'id', { userPath: 'auth.user' }) // uses ctx.auth.user instead of ctx.user
189
+ */
190
+ export const isOwner = (ownerField = 'userId', userIdField = 'id', options = {}) => {
191
+ const { userPath = 'user' } = options;
192
+
193
+ return (parent, _args, ctx) => {
194
+ if (!ctx) {
195
+ throw new UnauthenticatedError('You must be logged in to access this resource');
196
+ }
197
+ const user = resolvePath(ctx, userPath);
198
+ if (!user) {
199
+ throw new UnauthenticatedError('You must be logged in to access this resource');
200
+ }
201
+
202
+ // Get ownerId from parent (using path or function)
203
+ const ownerId = resolvePath(parent, ownerField);
204
+
205
+ // Get userId from user object (using path or function)
206
+ const userId = resolvePath(user, userIdField);
207
+
208
+ if (ownerId === undefined || userId === undefined) {
209
+ return false;
210
+ }
211
+
212
+ return String(ownerId) === String(userId);
213
+ };
214
+ };
215
+
216
+ /**
217
+ * Creates a custom rule from a predicate function
218
+ * @param {Function} predicate - Function (parent, args, ctx, info) => boolean | Promise<boolean>
219
+ * @param {string} errorMessage - Error message if rule fails
220
+ * @param {string} errorCode - Error code (FORBIDDEN or UNAUTHENTICATED)
221
+ * @returns {Function} Rule function
222
+ */
223
+ export const createRule = (predicate, errorMessage = 'Access denied', errorCode = 'FORBIDDEN') => {
224
+ return async (parent, args, ctx, info) => {
225
+ const result = await predicate(parent, args, ctx, info);
226
+
227
+ if (result === false) {
228
+ if (errorCode === 'UNAUTHENTICATED') {
229
+ throw new UnauthenticatedError(errorMessage);
230
+ }
231
+ throw new ForbiddenError(errorMessage);
232
+ }
233
+
234
+ return true;
235
+ };
236
+ };
237
+
238
+ /**
239
+ * Rule that always allows access (useful for public fields)
240
+ * @returns {Function} Rule function that always returns true
241
+ */
242
+ export const allow = () => {
243
+ return () => true;
244
+ };
245
+
246
+ /**
247
+ * Rule that always denies access
248
+ * @param {string} message - Optional denial message
249
+ * @returns {Function} Rule function that always throws ForbiddenError
250
+ */
251
+ export const deny = (message = 'Access denied') => {
252
+ return () => {
253
+ throw new ForbiddenError(message);
254
+ };
255
+ };
256
+
257
+ // Export all rules as an object for convenience
258
+ const rules = {
259
+ // Utility
260
+ resolvePath,
261
+ // Rule helpers
262
+ requireAuth,
263
+ requireRole,
264
+ requirePermission,
265
+ composeRules,
266
+ anyRule,
267
+ isOwner,
268
+ createRule,
269
+ allow,
270
+ deny,
271
+ };
272
+
273
+ export default rules;
274
+
package/src/index.js CHANGED
@@ -2144,6 +2144,7 @@ export { createValidatedScalar };
2144
2144
  export { default as validators } from './validators.js';
2145
2145
  export { default as scalars } from './scalars.js';
2146
2146
  export { default as plugins } from './plugins.js';
2147
+ export { default as auth } from './auth/index.js';
2147
2148
 
2148
2149
  const createArgsForQuery = (argTypes) => {
2149
2150
  const argsObject = {};
package/src/plugins.js CHANGED
@@ -1,3 +1,7 @@
1
+ import { createAuthPlugin } from './auth/index.js';
2
+
3
+ export { createAuthPlugin } from './auth/index.js';
4
+
1
5
  /**
2
6
  * Apollo Server plugin to add count to GraphQL response extensions
3
7
  * @returns {Object} Apollo Server plugin
@@ -13,9 +17,9 @@ export const apolloCountPlugin = () => {
13
17
  count: contextValue.count,
14
18
  };
15
19
  }
16
- }
20
+ },
17
21
  };
18
- }
22
+ },
19
23
  };
20
24
  };
21
25
 
@@ -31,20 +35,19 @@ export const envelopCountPlugin = () => {
31
35
  if (args.contextValue?.count) {
32
36
  result.extensions = {
33
37
  ...result.extensions,
34
- count: args.contextValue.count
38
+ count: args.contextValue.count,
35
39
  };
36
40
  }
37
- }
41
+ },
38
42
  };
39
- }
43
+ },
40
44
  };
41
45
  };
42
46
 
43
- // Export all plugins as an object for convenience
44
47
  const plugins = {
48
+ createAuthPlugin,
45
49
  apolloCountPlugin,
46
50
  envelopCountPlugin,
47
51
  };
48
52
 
49
53
  export default plugins;
50
-