@friggframework/core 2.0.0--canary.461.fa770c1.0 → 2.0.0--canary.461.41b7566.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.
@@ -6,6 +6,32 @@ const { logger } = require('./encryption/logger');
6
6
  const { Cryptor } = require('../encrypt/Cryptor');
7
7
  const config = require('./config');
8
8
 
9
+ /**
10
+ * Ensures DATABASE_URL is set for MongoDB connections
11
+ * Falls back to MONGO_URI if DATABASE_URL is not set
12
+ * Infrastructure layer concern - maps legacy MONGO_URI to Prisma's expected DATABASE_URL
13
+ *
14
+ * Note: This should only be called when DB_TYPE is 'mongodb'
15
+ */
16
+ function ensureMongoDbUrl() {
17
+ // If DATABASE_URL is already set, use it
18
+ if (process.env.DATABASE_URL && process.env.DATABASE_URL.trim()) {
19
+ return;
20
+ }
21
+
22
+ // Fallback to MONGO_URI for backwards compatibility with DocumentDB deployments
23
+ if (process.env.MONGO_URI && process.env.MONGO_URI.trim()) {
24
+ process.env.DATABASE_URL = process.env.MONGO_URI;
25
+ logger.debug('Using MONGO_URI as DATABASE_URL for MongoDB connection');
26
+ return;
27
+ }
28
+
29
+ // Neither is set - error
30
+ throw new Error(
31
+ 'DATABASE_URL or MONGO_URI environment variable must be set for MongoDB'
32
+ );
33
+ }
34
+
9
35
  function getEncryptionConfig() {
10
36
  const STAGE = process.env.STAGE || process.env.NODE_ENV || 'development';
11
37
  const shouldBypassEncryption = ['dev', 'test', 'local'].includes(STAGE);
@@ -99,6 +125,8 @@ const prismaClientSingleton = () => {
99
125
  };
100
126
 
101
127
  if (config.DB_TYPE === 'mongodb') {
128
+ // Ensure DATABASE_URL is set (fallback to MONGO_URI if needed)
129
+ ensureMongoDbUrl();
102
130
  PrismaClient = loadPrismaClient('mongodb');
103
131
  } else if (config.DB_TYPE === 'postgresql') {
104
132
  PrismaClient = loadPrismaClient('postgresql');
@@ -181,4 +209,5 @@ module.exports = {
181
209
  connectPrisma,
182
210
  disconnectPrisma,
183
211
  getEncryptionConfig,
212
+ ensureMongoDbUrl, // Exported for testing
184
213
  };
@@ -61,13 +61,13 @@ const getDatabaseStateUseCase = new GetDatabaseStateViaWorkerUseCase({
61
61
  * Matches pattern from health.js:72-88
62
62
  */
63
63
  const validateApiKey = (req, res, next) => {
64
- const apiKey = req.headers['x-api-key'];
64
+ const apiKey = req.headers['x-frigg-admin-api-key'];
65
65
 
66
66
  if (!apiKey || apiKey !== process.env.ADMIN_API_KEY) {
67
67
  console.error('Unauthorized access attempt to db-migrate endpoint');
68
68
  return res.status(401).json({
69
69
  status: 'error',
70
- message: 'Unauthorized',
70
+ message: 'Unauthorized - x-frigg-admin-api-key header required',
71
71
  });
72
72
  }
73
73
 
@@ -70,7 +70,7 @@ const checkIntegrationsHealthUseCase = new CheckIntegrationsHealthUseCase({
70
70
  });
71
71
 
72
72
  const validateApiKey = (req, res, next) => {
73
- const apiKey = req.headers['x-api-key'];
73
+ const apiKey = req.headers['x-frigg-health-api-key'];
74
74
 
75
75
  if (req.path === '/health') {
76
76
  return next();
@@ -80,7 +80,7 @@ const validateApiKey = (req, res, next) => {
80
80
  console.error('Unauthorized access attempt to health endpoint');
81
81
  return res.status(401).json({
82
82
  status: 'error',
83
- message: 'Unauthorized',
83
+ message: 'Unauthorized - x-frigg-health-api-key header required',
84
84
  });
85
85
  }
86
86
 
@@ -62,6 +62,9 @@ const {
62
62
  const {
63
63
  GetUserFromAdopterJwt,
64
64
  } = require('../user/use-cases/get-user-from-adopter-jwt');
65
+ const {
66
+ GetUserFromSharedSecret,
67
+ } = require('../user/use-cases/get-user-from-shared-secret');
65
68
  const {
66
69
  AuthenticateUser,
67
70
  } = require('../user/use-cases/authenticate-user');
@@ -92,10 +95,16 @@ function createIntegrationRouter() {
92
95
  userConfig,
93
96
  });
94
97
 
98
+ const getUserFromSharedSecret = new GetUserFromSharedSecret({
99
+ userRepository,
100
+ userConfig,
101
+ });
102
+
95
103
  const authenticateUser = new AuthenticateUser({
96
104
  getUserFromBearerToken,
97
105
  getUserFromXFriggHeaders,
98
106
  getUserFromAdopterJwt,
107
+ getUserFromSharedSecret,
99
108
  userConfig,
100
109
  });
101
110
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@friggframework/core",
3
3
  "prettier": "@friggframework/prettier-config",
4
- "version": "2.0.0--canary.461.fa770c1.0",
4
+ "version": "2.0.0--canary.461.41b7566.0",
5
5
  "dependencies": {
6
6
  "@aws-sdk/client-apigatewaymanagementapi": "^3.588.0",
7
7
  "@aws-sdk/client-kms": "^3.588.0",
@@ -38,9 +38,9 @@
38
38
  }
39
39
  },
40
40
  "devDependencies": {
41
- "@friggframework/eslint-config": "2.0.0--canary.461.fa770c1.0",
42
- "@friggframework/prettier-config": "2.0.0--canary.461.fa770c1.0",
43
- "@friggframework/test": "2.0.0--canary.461.fa770c1.0",
41
+ "@friggframework/eslint-config": "2.0.0--canary.461.41b7566.0",
42
+ "@friggframework/prettier-config": "2.0.0--canary.461.41b7566.0",
43
+ "@friggframework/test": "2.0.0--canary.461.41b7566.0",
44
44
  "@prisma/client": "^6.17.0",
45
45
  "@types/lodash": "4.17.15",
46
46
  "@typescript-eslint/eslint-plugin": "^8.0.0",
@@ -80,5 +80,5 @@
80
80
  "publishConfig": {
81
81
  "access": "public"
82
82
  },
83
- "gitHead": "fa770c14042eacc322cb204dd0886d913938ec11"
83
+ "gitHead": "41b7566abaa46939705d52e01904022e52cdd06a"
84
84
  }
@@ -2,8 +2,15 @@ const Boom = require('@hapi/boom');
2
2
 
3
3
  /**
4
4
  * Use case for authenticating a user using multiple authentication strategies.
5
- * Supports Frigg native tokens, x-frigg headers, and adopter JWT (when implemented).
6
- * Tries authentication methods in priority order based on userConfig.authModes.
5
+ *
6
+ * Supports three authentication modes in priority order:
7
+ * 1. Shared Secret (backend-to-backend with x-frigg-api-key + x-frigg headers)
8
+ * 2. Adopter JWT (custom JWT authentication)
9
+ * 3. Frigg Native Token (bearer token from /user/login)
10
+ *
11
+ * x-frigg-appUserId and x-frigg-appOrgId headers are automatically supported
12
+ * for user identification with any auth mode. When present with JWT or Frigg
13
+ * tokens, they are validated to match the authenticated user.
7
14
  *
8
15
  * @class AuthenticateUser
9
16
  */
@@ -14,17 +21,20 @@ class AuthenticateUser {
14
21
  * @param {import('./get-user-from-bearer-token').GetUserFromBearerToken} params.getUserFromBearerToken - Use case for bearer token auth.
15
22
  * @param {import('./get-user-from-x-frigg-headers').GetUserFromXFriggHeaders} params.getUserFromXFriggHeaders - Use case for x-frigg header auth.
16
23
  * @param {import('./get-user-from-adopter-jwt').GetUserFromAdopterJwt} params.getUserFromAdopterJwt - Use case for adopter JWT auth.
24
+ * @param {import('./get-user-from-shared-secret').GetUserFromSharedSecret} params.getUserFromSharedSecret - Use case for shared secret auth.
17
25
  * @param {Object} params.userConfig - The user config in the app definition.
18
26
  */
19
27
  constructor({
20
28
  getUserFromBearerToken,
21
29
  getUserFromXFriggHeaders,
22
30
  getUserFromAdopterJwt,
31
+ getUserFromSharedSecret,
23
32
  userConfig,
24
33
  }) {
25
34
  this.getUserFromBearerToken = getUserFromBearerToken;
26
35
  this.getUserFromXFriggHeaders = getUserFromXFriggHeaders;
27
36
  this.getUserFromAdopterJwt = getUserFromAdopterJwt;
37
+ this.getUserFromSharedSecret = getUserFromSharedSecret;
28
38
  this.userConfig = userConfig;
29
39
  }
30
40
 
@@ -34,17 +44,20 @@ class AuthenticateUser {
34
44
  * @param {Object} req - Express request object with headers.
35
45
  * @returns {Promise<import('../user').User>} The authenticated user object.
36
46
  * @throws {Boom} Unauthorized if no valid authentication provided.
47
+ * @throws {Boom} Forbidden if x-frigg headers don't match authenticated user.
37
48
  */
38
49
  async execute(req) {
39
50
  const authModes = this.userConfig.authModes || { friggToken: true };
51
+ const appUserId = req.headers['x-frigg-appuserid'];
52
+ const appOrgId = req.headers['x-frigg-apporgid'];
53
+ let user = null;
40
54
 
41
- // Priority 1: x-frigg headers (backend-to-backend)
42
- if (authModes.xFriggHeaders !== false) {
43
- const appUserId = req.headers['x-frigg-appuserid'];
44
- const appOrgId = req.headers['x-frigg-apporgid'];
45
-
46
- if (appUserId || appOrgId) {
47
- return await this.getUserFromXFriggHeaders.execute(
55
+ // Priority 1: Shared Secret (backend-to-backend with API key)
56
+ if (authModes.sharedSecret !== false) {
57
+ const apiKey = req.headers['x-frigg-api-key'];
58
+ if (apiKey) {
59
+ return await this.getUserFromSharedSecret.execute(
60
+ apiKey,
48
61
  appUserId,
49
62
  appOrgId
50
63
  );
@@ -59,19 +72,52 @@ class AuthenticateUser {
59
72
  const token = req.headers.authorization.split(' ')[1];
60
73
  // Detect JWT format (3 parts separated by dots)
61
74
  if (token && token.split('.').length === 3) {
62
- return await this.getUserFromAdopterJwt.execute(token);
75
+ user = await this.getUserFromAdopterJwt.execute(token);
76
+ // Validate x-frigg headers match JWT claims if present
77
+ if (appUserId || appOrgId) {
78
+ this.validateUserMatch(user, appUserId, appOrgId);
79
+ }
80
+ return user;
63
81
  }
64
82
  }
65
83
 
66
84
  // Priority 3: Frigg native token (default)
67
- if (authModes.friggToken !== false) {
68
- return await this.getUserFromBearerToken.execute(
85
+ if (authModes.friggToken !== false && req.headers.authorization) {
86
+ user = await this.getUserFromBearerToken.execute(
69
87
  req.headers.authorization
70
88
  );
89
+ // Validate x-frigg headers match token user if present
90
+ if (appUserId || appOrgId) {
91
+ this.validateUserMatch(user, appUserId, appOrgId);
92
+ }
93
+ return user;
71
94
  }
72
95
 
73
96
  throw Boom.unauthorized('No valid authentication provided');
74
97
  }
98
+
99
+ /**
100
+ * Validates that x-frigg headers match authenticated user if provided.
101
+ * This ensures that when both authentication (via token/JWT) and
102
+ * x-frigg headers are present, they refer to the same user.
103
+ *
104
+ * @param {import('../user').User} user - The authenticated user
105
+ * @param {string} [appUserId] - The x-frigg-appuserid header value
106
+ * @param {string} [appOrgId] - The x-frigg-apporgid header value
107
+ * @throws {Boom} 403 Forbidden if headers don't match user
108
+ */
109
+ validateUserMatch(user, appUserId, appOrgId) {
110
+ if (appUserId && user.getAppUserId() !== appUserId) {
111
+ throw Boom.forbidden(
112
+ 'x-frigg-appuserid header does not match authenticated user'
113
+ );
114
+ }
115
+ if (appOrgId && user.getAppOrgId() !== appOrgId) {
116
+ throw Boom.forbidden(
117
+ 'x-frigg-apporgid header does not match authenticated user'
118
+ );
119
+ }
120
+ }
75
121
  }
76
122
 
77
123
  module.exports = { AuthenticateUser };
@@ -0,0 +1,124 @@
1
+ const Boom = require('@hapi/boom');
2
+ const { User } = require('../user');
3
+
4
+ /**
5
+ * Use case for authenticating requests with shared secret API key.
6
+ * Used for backend-to-backend communication where the secret proves
7
+ * authenticity, and x-frigg headers identify the user/org.
8
+ *
9
+ * This separates authentication (proving legitimacy via API key) from
10
+ * authorization (identifying user/org via x-frigg headers).
11
+ *
12
+ * @class GetUserFromSharedSecret
13
+ */
14
+ class GetUserFromSharedSecret {
15
+ /**
16
+ * Creates a new GetUserFromSharedSecret instance.
17
+ * @param {Object} params - Configuration parameters.
18
+ * @param {import('../repositories/user-repository-interface').UserRepositoryInterface} params.userRepository - Repository for user data operations.
19
+ * @param {Object} params.userConfig - The user config in the app definition.
20
+ */
21
+ constructor({ userRepository, userConfig }) {
22
+ this.userRepository = userRepository;
23
+ this.userConfig = userConfig;
24
+ }
25
+
26
+ /**
27
+ * Validates shared secret and extracts user from x-frigg headers.
28
+ * @async
29
+ * @param {string} providedSecret - Secret from x-frigg-api-key header
30
+ * @param {string} [appUserId] - From x-frigg-appuserid header
31
+ * @param {string} [appOrgId] - From x-frigg-apporgid header
32
+ * @returns {Promise<import('../user').User>} The authenticated user object.
33
+ * @throws {Boom} 500 if FRIGG_API_KEY not configured
34
+ * @throws {Boom} 401 if provided secret doesn't match
35
+ * @throws {Boom} 400 if no user identifiers provided or if they refer to different users
36
+ */
37
+ async execute(providedSecret, appUserId, appOrgId) {
38
+ // Validate secret
39
+ const expectedSecret = process.env.FRIGG_API_KEY;
40
+ if (!expectedSecret) {
41
+ throw Boom.badImplementation(
42
+ 'FRIGG_API_KEY environment variable is not configured. ' +
43
+ 'Set FRIGG_API_KEY to enable shared secret authentication.'
44
+ );
45
+ }
46
+
47
+ if (!providedSecret || providedSecret !== expectedSecret) {
48
+ throw Boom.unauthorized('Invalid API key');
49
+ }
50
+
51
+ // Require at least one user identifier
52
+ if (!appUserId && !appOrgId) {
53
+ throw Boom.badRequest(
54
+ 'At least one of x-frigg-appuserid or x-frigg-apporgid headers is required with shared secret authentication'
55
+ );
56
+ }
57
+
58
+ // Find or create users (reuse logic from GetUserFromXFriggHeaders)
59
+ let individualUserData = null;
60
+ let organizationUserData = null;
61
+
62
+ if (appUserId && this.userConfig.individualUserRequired !== false) {
63
+ individualUserData =
64
+ await this.userRepository.findIndividualUserByAppUserId(
65
+ appUserId
66
+ );
67
+ }
68
+
69
+ if (appOrgId && this.userConfig.organizationUserRequired) {
70
+ organizationUserData =
71
+ await this.userRepository.findOrganizationUserByAppOrgId(
72
+ appOrgId
73
+ );
74
+ }
75
+
76
+ // VALIDATION: If both IDs provided and both users exist, verify they match
77
+ if (
78
+ appUserId &&
79
+ appOrgId &&
80
+ individualUserData &&
81
+ organizationUserData
82
+ ) {
83
+ const individualOrgId =
84
+ individualUserData.organizationUser?.toString();
85
+ const expectedOrgId = organizationUserData.id?.toString();
86
+
87
+ if (individualOrgId !== expectedOrgId) {
88
+ throw Boom.badRequest(
89
+ 'User ID mismatch: x-frigg-appuserid and x-frigg-apporgid refer to different users. ' +
90
+ 'Provide only one identifier or ensure they belong to the same user.'
91
+ );
92
+ }
93
+ }
94
+
95
+ // Auto-create user if not found
96
+ if (!individualUserData && !organizationUserData) {
97
+ if (appUserId) {
98
+ individualUserData =
99
+ await this.userRepository.createIndividualUser({
100
+ appUserId,
101
+ username: `api-user-${appUserId}`,
102
+ email: `${appUserId}@api.local`,
103
+ });
104
+ } else {
105
+ organizationUserData =
106
+ await this.userRepository.createOrganizationUser({
107
+ appOrgId,
108
+ });
109
+ }
110
+ }
111
+
112
+ return new User(
113
+ individualUserData,
114
+ organizationUserData,
115
+ this.userConfig.usePassword,
116
+ this.userConfig.primary,
117
+ this.userConfig.individualUserRequired,
118
+ this.userConfig.organizationUserRequired
119
+ );
120
+ }
121
+ }
122
+
123
+ module.exports = { GetUserFromSharedSecret };
124
+
package/user/user.js CHANGED
@@ -72,6 +72,22 @@ class User {
72
72
  getOrganizationUser() {
73
73
  return this.organizationUser;
74
74
  }
75
+
76
+ /**
77
+ * Gets the appUserId from the individual user if present.
78
+ * @returns {string|null} The app user ID or null
79
+ */
80
+ getAppUserId() {
81
+ return this.individualUser?.appUserId || null;
82
+ }
83
+
84
+ /**
85
+ * Gets the appOrgId from the organization user if present.
86
+ * @returns {string|null} The app organization ID or null
87
+ */
88
+ getAppOrgId() {
89
+ return this.organizationUser?.appOrgId || null;
90
+ }
75
91
  }
76
92
 
77
93
  module.exports = { User };