@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.
package/README.md CHANGED
@@ -20,6 +20,13 @@ A powerful Node.js framework that automatically generates GraphQL schemas from y
20
20
  - [Adding Middlewares](#adding-middlewares)
21
21
  - [Middleware Parameters](#middleware-parameters)
22
22
  - [Common Use Cases](#common-use-cases)
23
+ - [Authorization](#-authorization)
24
+ - [Quick Start](#quick-start-1)
25
+ - [Permission Schema](#permission-schema)
26
+ - [Rule Helpers](#rule-helpers)
27
+ - [Policy Expressions (JSON AST)](#policy-expressions-json-ast)
28
+ - [Integration with GraphQL Yoga / Envelop](#integration-with-graphql-yoga--envelop)
29
+ - [Legacy: Integration with graphql-middleware](#legacy-integration-with-graphql-middleware)
23
30
  - [Relationships](#-relationships)
24
31
  - [Defining Relationships](#defining-relationships)
25
32
  - [Auto-Generated Resolve Methods](#auto-generated-resolve-methods)
@@ -69,6 +76,7 @@ A powerful Node.js framework that automatically generates GraphQL schemas from y
69
76
  - **Lifecycle Hooks**: Controller methods for granular control over operations
70
77
  - **Custom Validation**: Field-level and type-level custom validations
71
78
  - **Relationship Management**: Support for embedded and referenced relationships
79
+ - **Authorization**: Production-grade GraphQL authorization with RBAC/ABAC, function-based rules, declarative policy expressions, and native Envelop/Yoga plugin support
72
80
 
73
81
  ## 📦 Installation
74
82
 
@@ -622,6 +630,392 @@ simfinity.use((params, next) => {
622
630
  5. **Performance consideration**: Middlewares run on every operation, keep them lightweight
623
631
  6. **Use context wisely**: Store request-specific data in the GraphQL context object
624
632
 
633
+ ## 🔐 Authorization
634
+
635
+ Simfinity.js provides production-grade centralized GraphQL authorization supporting RBAC/ABAC, function-based rules, declarative policy expressions (JSON AST), wildcard permissions, and configurable default policies. It ships as a native Envelop plugin for GraphQL Yoga (recommended) and also supports the legacy graphql-middleware approach.
636
+
637
+ ### Quick Start
638
+
639
+ ```javascript
640
+ const { auth } = require('@simtlix/simfinity-js');
641
+ const { createYoga } = require('graphql-yoga');
642
+
643
+ const { createAuthPlugin, requireAuth, requireRole } = auth;
644
+
645
+ // Define your permission schema
646
+ const permissions = {
647
+ Query: {
648
+ users: requireAuth(),
649
+ adminDashboard: requireRole('ADMIN'),
650
+ },
651
+ Mutation: {
652
+ publishPost: requireRole('EDITOR'),
653
+ },
654
+ User: {
655
+ '*': requireAuth(), // Wildcard: all fields require auth
656
+ email: requireRole('ADMIN'), // Override: email requires ADMIN role
657
+ },
658
+ Post: {
659
+ '*': requireAuth(),
660
+ content: async (post, _args, ctx) => {
661
+ // Custom logic: allow if published OR if author
662
+ if (post.published) return true;
663
+ if (post.authorId === ctx.user?.id) return true;
664
+ return false;
665
+ },
666
+ },
667
+ };
668
+
669
+ // Create the Envelop auth plugin and pass it to your server
670
+ const authPlugin = createAuthPlugin(permissions, { defaultPolicy: 'DENY' });
671
+ const yoga = createYoga({ schema, plugins: [authPlugin] });
672
+ ```
673
+
674
+ ### Permission Schema
675
+
676
+ The permission schema defines authorization rules per type and field:
677
+
678
+ ```javascript
679
+ const permissions = {
680
+ // Operation types (Query, Mutation, Subscription)
681
+ Query: {
682
+ fieldName: ruleOrRules,
683
+ },
684
+
685
+ // Object types
686
+ TypeName: {
687
+ '*': wildcardRule, // Applies to all fields unless overridden
688
+ fieldName: specificRule, // Overrides wildcard for this field
689
+ },
690
+ };
691
+ ```
692
+
693
+ **Resolution Order:**
694
+ 1. Check exact field rule: `permissions[TypeName][fieldName]`
695
+ 2. Fallback to wildcard: `permissions[TypeName]['*']`
696
+ 3. Apply default policy (ALLOW or DENY)
697
+
698
+ **Rule Types:**
699
+ - **Function**: `(parent, args, ctx, info) => boolean | void | Promise<boolean | void>`
700
+ - **Array of functions**: All rules must pass (AND logic)
701
+ - **Policy expression**: JSON AST object (see below)
702
+
703
+ **Rule Semantics:**
704
+ - `return true` or `return void` → allow
705
+ - `return false` → deny
706
+ - `throw Error` → deny with error
707
+
708
+ ### Rule Helpers
709
+
710
+ Simfinity.js provides reusable rule builders:
711
+
712
+ ```javascript
713
+ const { auth } = require('@simtlix/simfinity-js');
714
+
715
+ const {
716
+ resolvePath, // Utility to resolve dotted paths in objects
717
+ requireAuth, // Requires ctx.user to exist
718
+ requireRole, // Requires specific role(s)
719
+ requirePermission, // Requires specific permission(s)
720
+ composeRules, // Combine rules (AND logic)
721
+ anyRule, // Combine rules (OR logic)
722
+ isOwner, // Check resource ownership
723
+ allow, // Always allow
724
+ deny, // Always deny
725
+ createRule, // Create custom rule
726
+ } = auth;
727
+ ```
728
+
729
+ #### requireAuth(userPath?)
730
+
731
+ Requires the user to be authenticated. Supports custom user paths in context:
732
+
733
+ ```javascript
734
+ const permissions = {
735
+ Query: {
736
+ // Default: checks ctx.user
737
+ me: requireAuth(),
738
+
739
+ // Custom path: checks ctx.auth.currentUser
740
+ profile: requireAuth('auth.currentUser'),
741
+
742
+ // Deep path: checks ctx.session.data.user
743
+ settings: requireAuth('session.data.user'),
744
+ },
745
+ };
746
+ ```
747
+
748
+ #### requireRole(role, options?)
749
+
750
+ Requires the user to have a specific role. Supports custom paths:
751
+
752
+ ```javascript
753
+ const permissions = {
754
+ Query: {
755
+ // Default: checks ctx.user.role
756
+ adminDashboard: requireRole('ADMIN'),
757
+ modTools: requireRole(['ADMIN', 'MODERATOR']), // Any of these roles
758
+
759
+ // Custom paths: checks ctx.auth.user.profile.role
760
+ superAdmin: requireRole('SUPER_ADMIN', {
761
+ userPath: 'auth.user',
762
+ rolePath: 'profile.role',
763
+ }),
764
+ },
765
+ };
766
+ ```
767
+
768
+ #### requirePermission(permission, options?)
769
+
770
+ Requires the user to have specific permission(s). Supports custom paths:
771
+
772
+ ```javascript
773
+ const permissions = {
774
+ Mutation: {
775
+ // Default: checks ctx.user.permissions
776
+ deletePost: requirePermission('posts:delete'),
777
+ manageUsers: requirePermission(['users:read', 'users:write']), // All required
778
+
779
+ // Custom paths: checks ctx.session.user.access.grants
780
+ admin: requirePermission('admin:all', {
781
+ userPath: 'session.user',
782
+ permissionsPath: 'access.grants',
783
+ }),
784
+ },
785
+ };
786
+ ```
787
+
788
+ #### composeRules(...rules)
789
+
790
+ Combines multiple rules with AND logic (all must pass):
791
+
792
+ ```javascript
793
+ const permissions = {
794
+ Mutation: {
795
+ updatePost: composeRules(
796
+ requireAuth(),
797
+ requireRole('EDITOR'),
798
+ async (post, args, ctx) => post.authorId === ctx.user.id,
799
+ ),
800
+ },
801
+ };
802
+ ```
803
+
804
+ #### anyRule(...rules)
805
+
806
+ Combines multiple rules with OR logic (any must pass):
807
+
808
+ ```javascript
809
+ const permissions = {
810
+ Post: {
811
+ content: anyRule(
812
+ requireRole('ADMIN'),
813
+ async (post, args, ctx) => post.authorId === ctx.user.id,
814
+ ),
815
+ },
816
+ };
817
+ ```
818
+
819
+ #### isOwner(ownerField, userIdField)
820
+
821
+ Checks if the authenticated user owns the resource:
822
+
823
+ ```javascript
824
+ const permissions = {
825
+ Post: {
826
+ '*': composeRules(
827
+ requireAuth(),
828
+ isOwner('authorId', 'id'), // Compares post.authorId with ctx.user.id
829
+ ),
830
+ },
831
+ };
832
+ ```
833
+
834
+ ### Policy Expressions (JSON AST)
835
+
836
+ For declarative rules, use JSON AST policy expressions:
837
+
838
+ ```javascript
839
+ const permissions = {
840
+ Post: {
841
+ content: {
842
+ anyOf: [
843
+ { eq: [{ ref: 'parent.published' }, true] },
844
+ { eq: [{ ref: 'parent.authorId' }, { ref: 'ctx.user.id' }] },
845
+ ],
846
+ },
847
+ },
848
+ };
849
+ ```
850
+
851
+ **Supported Operators:**
852
+
853
+ | Operator | Description | Example |
854
+ |----------|-------------|---------|
855
+ | `eq` | Equals | `{ eq: [{ ref: 'parent.status' }, 'active'] }` |
856
+ | `in` | Value in array | `{ in: [{ ref: 'ctx.user.role' }, ['ADMIN', 'MOD']] }` |
857
+ | `allOf` | All must be true (AND) | `{ allOf: [expr1, expr2] }` |
858
+ | `anyOf` | Any must be true (OR) | `{ anyOf: [expr1, expr2] }` |
859
+ | `not` | Negation | `{ not: { eq: [{ ref: 'parent.deleted' }, true] } }` |
860
+
861
+ **References:**
862
+
863
+ Use `{ ref: 'path' }` to reference values:
864
+ - `parent.*` - Parent resolver result (the object being resolved)
865
+ - `args.*` - GraphQL arguments
866
+ - `ctx.*` - GraphQL context
867
+
868
+ **Security:**
869
+ - Only `parent`, `args`, and `ctx` roots are allowed
870
+ - Unknown operators fail closed (deny)
871
+ - No `eval()` or `Function()` - pure object traversal
872
+
873
+ ### Integration with GraphQL Yoga / Envelop
874
+
875
+ The recommended way to use the auth system is via the Envelop plugin, which works natively with GraphQL Yoga and any Envelop-based server. The plugin wraps resolvers in-place without rebuilding the schema, avoiding compatibility issues.
876
+
877
+ ```javascript
878
+ const { createYoga } = require('graphql-yoga');
879
+ const { createServer } = require('http');
880
+ const simfinity = require('@simtlix/simfinity-js');
881
+
882
+ const { auth } = simfinity;
883
+ const { createAuthPlugin, requireAuth, requireRole, requirePermission } = auth;
884
+
885
+ // Define your types and connect them
886
+ simfinity.connect(null, UserType, 'user', 'users');
887
+ simfinity.connect(null, PostType, 'post', 'posts');
888
+
889
+ // Create base schema
890
+ const schema = simfinity.createSchema();
891
+
892
+ // Define permissions
893
+ const permissions = {
894
+ Query: {
895
+ users: requireAuth(),
896
+ user: requireAuth(),
897
+ posts: requireAuth(),
898
+ post: requireAuth(),
899
+ },
900
+ Mutation: {
901
+ adduser: requireRole('ADMIN'),
902
+ updateuser: requireRole('ADMIN'),
903
+ deleteuser: requireRole('ADMIN'),
904
+ addpost: requireAuth(),
905
+ updatepost: composeRules(requireAuth(), isOwner('authorId')),
906
+ deletepost: requireRole('ADMIN'),
907
+ },
908
+ User: {
909
+ '*': requireAuth(),
910
+ email: requireRole('ADMIN'),
911
+ password: deny('Password field is not accessible'),
912
+ },
913
+ Post: {
914
+ '*': requireAuth(),
915
+ content: {
916
+ anyOf: [
917
+ { eq: [{ ref: 'parent.published' }, true] },
918
+ { eq: [{ ref: 'parent.authorId' }, { ref: 'ctx.user.id' }] },
919
+ ],
920
+ },
921
+ },
922
+ };
923
+
924
+ // Create auth plugin
925
+ const authPlugin = createAuthPlugin(permissions, {
926
+ defaultPolicy: 'DENY',
927
+ debug: false,
928
+ });
929
+
930
+ // Setup Yoga with the auth plugin
931
+ const yoga = createYoga({
932
+ schema,
933
+ plugins: [authPlugin],
934
+ context: (req) => ({
935
+ user: req.user, // Set by your authentication layer
936
+ }),
937
+ });
938
+
939
+ const server = createServer(yoga);
940
+ server.listen(4000);
941
+ ```
942
+
943
+ ### Legacy: Integration with graphql-middleware
944
+
945
+ > **Deprecated:** `applyMiddleware` from `graphql-middleware` rebuilds the schema via `mapSchema`,
946
+ > which can cause `"Schema must contain uniquely named types"` errors with Simfinity schemas.
947
+ > Use `createAuthPlugin` with GraphQL Yoga / Envelop instead.
948
+
949
+ ```javascript
950
+ const { applyMiddleware } = require('graphql-middleware');
951
+ const simfinity = require('@simtlix/simfinity-js');
952
+
953
+ const { auth } = simfinity;
954
+ const { createAuthMiddleware, requireAuth, requireRole } = auth;
955
+
956
+ const baseSchema = simfinity.createSchema();
957
+
958
+ const authMiddleware = createAuthMiddleware(permissions, {
959
+ defaultPolicy: 'DENY',
960
+ });
961
+
962
+ const schema = applyMiddleware(baseSchema, authMiddleware);
963
+ ```
964
+
965
+ ### Plugin / Middleware Options
966
+
967
+ ```javascript
968
+ const plugin = createAuthPlugin(permissions, {
969
+ defaultPolicy: 'DENY', // 'ALLOW' or 'DENY' (default: 'DENY')
970
+ debug: false, // Enable debug logging
971
+ });
972
+ ```
973
+
974
+ | Option | Type | Default | Description |
975
+ |--------|------|---------|-------------|
976
+ | `defaultPolicy` | `'ALLOW' \| 'DENY'` | `'DENY'` | Policy when no rule matches |
977
+ | `debug` | `boolean` | `false` | Log authorization decisions |
978
+
979
+ ### Error Handling
980
+
981
+ The auth middleware uses Simfinity error classes:
982
+
983
+ ```javascript
984
+ const { auth } = require('@simtlix/simfinity-js');
985
+
986
+ const { UnauthenticatedError, ForbiddenError } = auth;
987
+
988
+ // UnauthenticatedError: code 'UNAUTHENTICATED', status 401
989
+ // ForbiddenError: code 'FORBIDDEN', status 403
990
+ ```
991
+
992
+ Custom error handling in rules:
993
+
994
+ ```javascript
995
+ const permissions = {
996
+ Mutation: {
997
+ deleteAccount: async (parent, args, ctx) => {
998
+ if (!ctx.user) {
999
+ throw new auth.UnauthenticatedError('Please log in');
1000
+ }
1001
+ if (ctx.user.role !== 'ADMIN' && ctx.user.id !== args.id) {
1002
+ throw new auth.ForbiddenError('Cannot delete other users');
1003
+ }
1004
+ return true;
1005
+ },
1006
+ },
1007
+ };
1008
+ ```
1009
+
1010
+ ### Best Practices
1011
+
1012
+ 1. **Default to DENY**: Use `defaultPolicy: 'DENY'` for security
1013
+ 2. **Use wildcards wisely**: `'*'` rules provide baseline security per type
1014
+ 3. **Prefer helper rules**: Use `requireAuth()`, `requireRole()` over custom functions
1015
+ 4. **Fail closed**: Custom rules should deny on unexpected conditions
1016
+ 5. **Keep rules simple**: Complex logic belongs in controllers, not auth rules
1017
+ 6. **Test thoroughly**: Auth rules are critical - test all scenarios
1018
+
625
1019
  ## 🔗 Relationships
626
1020
 
627
1021
  ### Defining Relationships
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simtlix/simfinity-js",
3
- "version": "2.3.4",
3
+ "version": "2.4.1",
4
4
  "description": "",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -35,6 +35,7 @@
35
35
  },
36
36
  "optionalDependencies": {
37
37
  "graphql": "^16.11.0",
38
+ "graphql-middleware": "^6.1.35",
38
39
  "mongoose": "^8.16.2"
39
40
  }
40
41
  }
@@ -0,0 +1,44 @@
1
+ import SimfinityError from '../errors/simfinity.error.js';
2
+
3
+ /**
4
+ * Authentication error - thrown when user is not authenticated
5
+ * Uses code: UNAUTHENTICATED, status: 401
6
+ */
7
+ export class UnauthenticatedError extends SimfinityError {
8
+ constructor(message = 'Authentication required') {
9
+ super(message, 'UNAUTHENTICATED', 401);
10
+ this.name = 'UnauthenticatedError';
11
+ }
12
+ }
13
+
14
+ /**
15
+ * Authorization error - thrown when user lacks permission
16
+ * Uses code: FORBIDDEN, status: 403
17
+ */
18
+ export class ForbiddenError extends SimfinityError {
19
+ constructor(message = 'Access denied') {
20
+ super(message, 'FORBIDDEN', 403);
21
+ this.name = 'ForbiddenError';
22
+ }
23
+ }
24
+
25
+ /**
26
+ * Generic auth error factory for custom auth failures
27
+ * @param {string} message - Error message
28
+ * @param {string} code - Error code (UNAUTHENTICATED or FORBIDDEN)
29
+ * @returns {SimfinityError}
30
+ */
31
+ export const createAuthError = (message, code = 'FORBIDDEN') => {
32
+ const status = code === 'UNAUTHENTICATED' ? 401 : 403;
33
+ return new SimfinityError(message, code, status);
34
+ };
35
+
36
+ // Export all errors as an object for convenience
37
+ const errors = {
38
+ UnauthenticatedError,
39
+ ForbiddenError,
40
+ createAuthError,
41
+ };
42
+
43
+ export default errors;
44
+