@friggframework/core 2.0.0-next.54 → 2.0.0-next.56
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/application/commands/credential-commands.js +1 -1
- package/core/create-handler.js +12 -0
- package/credential/repositories/credential-repository-documentdb.js +81 -77
- package/credential/repositories/credential-repository-mongo.js +16 -54
- package/credential/repositories/credential-repository-postgres.js +14 -41
- package/credential/use-cases/get-credential-for-user.js +7 -3
- package/database/encryption/README.md +126 -21
- package/database/encryption/encryption-schema-registry.js +83 -2
- package/errors/client-safe-error.js +26 -0
- package/errors/fetch-error.js +2 -1
- package/errors/index.js +2 -0
- package/integrations/integration-router.js +6 -6
- package/integrations/repositories/integration-mapping-repository-documentdb.js +178 -33
- package/integrations/repositories/integration-repository-documentdb.js +21 -0
- package/integrations/repositories/process-repository-documentdb.js +143 -41
- package/modules/requester/api-key.js +24 -8
- package/package.json +5 -5
- package/token/repositories/token-repository-documentdb.js +20 -8
- package/token/repositories/token-repository-mongo.js +10 -3
- package/token/repositories/token-repository-postgres.js +10 -3
- package/user/repositories/user-repository-documentdb.js +177 -37
- package/user/repositories/user-repository-mongo.js +3 -2
- package/user/repositories/user-repository-postgres.js +3 -2
- package/user/use-cases/login-user.js +1 -1
|
@@ -4,14 +4,18 @@ class GetCredentialForUser {
|
|
|
4
4
|
}
|
|
5
5
|
|
|
6
6
|
async execute(credentialId, userId) {
|
|
7
|
-
const credential = await this.credentialRepository.findCredentialById(
|
|
7
|
+
const credential = await this.credentialRepository.findCredentialById(
|
|
8
|
+
credentialId
|
|
9
|
+
);
|
|
8
10
|
|
|
9
11
|
if (!credential) {
|
|
10
12
|
throw new Error(`Credential with id ${credentialId} not found`);
|
|
11
13
|
}
|
|
12
14
|
|
|
13
|
-
if (credential.
|
|
14
|
-
throw new Error(
|
|
15
|
+
if (credential.userId.toString() !== userId.toString()) {
|
|
16
|
+
throw new Error(
|
|
17
|
+
`Credential ${credentialId} does not belong to user ${userId}`
|
|
18
|
+
);
|
|
15
19
|
}
|
|
16
20
|
|
|
17
21
|
return credential;
|
|
@@ -122,35 +122,140 @@ Or simply don't configure any encryption keys. In Production field level encrypt
|
|
|
122
122
|
|
|
123
123
|
## Encrypted Fields
|
|
124
124
|
|
|
125
|
-
|
|
125
|
+
Core and custom encrypted fields are defined in `encryption-schema-registry.js`. See that file for the current list of encrypted fields.
|
|
126
126
|
|
|
127
|
+
**Core fields include**:
|
|
128
|
+
- OAuth tokens: `access_token`, `refresh_token`, `id_token`
|
|
129
|
+
- API keys: `api_key`, `apiKey`, `API_KEY_VALUE`
|
|
130
|
+
- Basic auth: `password`
|
|
131
|
+
- OAuth client credentials: `client_secret`
|
|
132
|
+
|
|
133
|
+
**Note**: API modules should use `api_key` (snake_case) in their `apiPropertiesToPersist.credential` arrays for consistency with OAuth2Requester and BasicAuthRequester conventions.
|
|
134
|
+
|
|
135
|
+
### API Module Credential Naming Conventions
|
|
136
|
+
|
|
137
|
+
When creating API module definitions, use **snake_case** for credential property names to ensure automatic encryption:
|
|
138
|
+
|
|
139
|
+
**✅ Recommended (automatically encrypted):**
|
|
127
140
|
```javascript
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
'
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
|
|
141
|
+
// API Module Definition
|
|
142
|
+
const Definition = {
|
|
143
|
+
requiredAuthMethods: {
|
|
144
|
+
apiPropertiesToPersist: {
|
|
145
|
+
// For API key authentication
|
|
146
|
+
credential: ['api_key'], // ✅ Automatically encrypted
|
|
147
|
+
// or for OAuth authentication
|
|
148
|
+
credential: ['access_token', 'refresh_token'], // ✅ OAuth - encrypted
|
|
149
|
+
// or for Basic authentication
|
|
150
|
+
credential: ['username', 'password'], // ✅ Basic auth - encrypted
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
// API class (extends ApiKeyRequester)
|
|
156
|
+
class MyApi extends ApiKeyRequester {
|
|
157
|
+
constructor(params) {
|
|
158
|
+
super(params);
|
|
159
|
+
this.api_key = params.api_key; // ✅ snake_case convention
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
**❌ Avoid (requires manual encryption schema):**
|
|
165
|
+
```javascript
|
|
166
|
+
apiPropertiesToPersist: {
|
|
167
|
+
credential: ['customToken', 'proprietaryKey'] // ❌ Not in core schema
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
For custom credential fields not in the core schema, use the custom encryption schema feature (see below).
|
|
172
|
+
|
|
173
|
+
### Extending Encryption Schema
|
|
174
|
+
|
|
175
|
+
#### Option 1: Module-Level Encryption (API Module Developers)
|
|
176
|
+
|
|
177
|
+
**NEW**: API modules can now declare their encryption requirements directly in the module definition:
|
|
178
|
+
|
|
179
|
+
```javascript
|
|
180
|
+
// api-module-library/my-service/definition.js
|
|
181
|
+
const Definition = {
|
|
182
|
+
moduleName: 'myService',
|
|
183
|
+
API: MyServiceApi,
|
|
184
|
+
|
|
185
|
+
// Declare which credential fields need encryption
|
|
186
|
+
encryption: {
|
|
187
|
+
credentialFields: ['api_key', 'webhook_secret']
|
|
142
188
|
},
|
|
143
|
-
|
|
144
|
-
|
|
189
|
+
|
|
190
|
+
requiredAuthMethods: {
|
|
191
|
+
apiPropertiesToPersist: {
|
|
192
|
+
credential: ['api_key', 'webhook_secret'], // These will be auto-encrypted
|
|
193
|
+
entity: []
|
|
194
|
+
},
|
|
195
|
+
// ... other methods
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
**How it works**:
|
|
201
|
+
1. Module declares `encryption.credentialFields` array
|
|
202
|
+
2. Framework automatically adds `data.` prefix: `['api_key']` → `['data.api_key']`
|
|
203
|
+
3. Fields are merged with core encryption schema on app startup
|
|
204
|
+
4. All modules across all integrations are scanned and combined
|
|
205
|
+
|
|
206
|
+
**Benefits**:
|
|
207
|
+
- ✅ Module authors control their own security requirements
|
|
208
|
+
- ✅ No need to modify core framework or app configuration
|
|
209
|
+
- ✅ Automatic encryption for API key-based integrations
|
|
210
|
+
- ✅ Works seamlessly with `apiPropertiesToPersist`
|
|
211
|
+
|
|
212
|
+
**Example - API Key Module**:
|
|
213
|
+
```javascript
|
|
214
|
+
// API Module Definition
|
|
215
|
+
const Definition = {
|
|
216
|
+
moduleName: 'axiscare',
|
|
217
|
+
API: AxisCareApi,
|
|
218
|
+
encryption: {
|
|
219
|
+
credentialFields: ['api_key'] // Auto-encrypted as 'data.api_key'
|
|
145
220
|
},
|
|
221
|
+
requiredAuthMethods: {
|
|
222
|
+
apiPropertiesToPersist: {
|
|
223
|
+
credential: ['api_key'] // Will be encrypted automatically
|
|
224
|
+
}
|
|
225
|
+
}
|
|
146
226
|
};
|
|
227
|
+
|
|
228
|
+
// API Class (extends ApiKeyRequester)
|
|
229
|
+
class AxisCareApi extends ApiKeyRequester {
|
|
230
|
+
constructor(params) {
|
|
231
|
+
super(params);
|
|
232
|
+
this.api_key = params.api_key; // snake_case convention
|
|
233
|
+
}
|
|
234
|
+
}
|
|
147
235
|
```
|
|
148
236
|
|
|
149
|
-
|
|
237
|
+
**Example - Custom Authentication**:
|
|
238
|
+
```javascript
|
|
239
|
+
const Definition = {
|
|
240
|
+
moduleName: 'customService',
|
|
241
|
+
encryption: {
|
|
242
|
+
credentialFields: [
|
|
243
|
+
'signing_key',
|
|
244
|
+
'webhook_secret',
|
|
245
|
+
'data.custom_nested_field' // Can specify data. prefix explicitly
|
|
246
|
+
]
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
**Limitations**:
|
|
252
|
+
- Only supports Credential model fields (stored in `credential.data`)
|
|
253
|
+
- Cannot encrypt entity fields or custom models (use app-level schema for those)
|
|
254
|
+
- Applied globally once - module schemas loaded at app startup
|
|
150
255
|
|
|
151
|
-
####
|
|
256
|
+
#### Option 2: App-Level Custom Schema (Integration Developers)
|
|
152
257
|
|
|
153
|
-
Integration developers can extend encryption without modifying core framework files
|
|
258
|
+
Integration developers can extend encryption without modifying core framework files.
|
|
154
259
|
|
|
155
260
|
**In `backend/index.js`:**
|
|
156
261
|
|
|
@@ -235,7 +340,7 @@ await prisma.asanaTaskMapping.create({
|
|
|
235
340
|
FRIGG_DEBUG=1 npm run frigg:start
|
|
236
341
|
```
|
|
237
342
|
|
|
238
|
-
####
|
|
343
|
+
#### Option 3: Modifying Core Schema (Framework Developers)
|
|
239
344
|
|
|
240
345
|
Framework developers maintaining core models can modify `encryption-schema-registry.js`:
|
|
241
346
|
|
|
@@ -19,6 +19,11 @@ const CORE_ENCRYPTION_SCHEMA = {
|
|
|
19
19
|
'data.access_token',
|
|
20
20
|
'data.refresh_token',
|
|
21
21
|
'data.id_token',
|
|
22
|
+
'data.api_key',
|
|
23
|
+
'data.apiKey',
|
|
24
|
+
'data.API_KEY_VALUE',
|
|
25
|
+
'data.password',
|
|
26
|
+
'data.client_secret',
|
|
22
27
|
],
|
|
23
28
|
},
|
|
24
29
|
|
|
@@ -109,6 +114,74 @@ function registerCustomSchema(schema) {
|
|
|
109
114
|
);
|
|
110
115
|
}
|
|
111
116
|
|
|
117
|
+
/**
|
|
118
|
+
* Extracts credential field paths from module definitions
|
|
119
|
+
* @param {Array} moduleDefinitions - Array of module definition objects
|
|
120
|
+
* @returns {Array<string>} Array of field paths with data. prefix
|
|
121
|
+
*/
|
|
122
|
+
function extractCredentialFieldsFromModules(moduleDefinitions) {
|
|
123
|
+
const fields = [];
|
|
124
|
+
|
|
125
|
+
for (const moduleDef of moduleDefinitions) {
|
|
126
|
+
if (!moduleDef?.encryption?.credentialFields) {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const credentialFields = moduleDef.encryption.credentialFields;
|
|
131
|
+
if (!Array.isArray(credentialFields) || credentialFields.length === 0) {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
for (const field of credentialFields) {
|
|
136
|
+
const prefixedField = field.startsWith('data.') ? field : `data.${field}`;
|
|
137
|
+
fields.push(prefixedField);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return [...new Set(fields)];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Loads and registers encryption schemas from API module definitions.
|
|
146
|
+
* Each module can declare credentialFields to encrypt in its encryption config.
|
|
147
|
+
*
|
|
148
|
+
* @param {Array} integrations - Array of integration classes with modules
|
|
149
|
+
*/
|
|
150
|
+
function loadModuleEncryptionSchemas(integrations) {
|
|
151
|
+
if (!integrations) {
|
|
152
|
+
throw new Error('integrations parameter is required');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (!Array.isArray(integrations)) {
|
|
156
|
+
throw new Error('integrations must be an array');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (integrations.length === 0) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const { getModulesDefinitionFromIntegrationClasses } = require('../integrations/utils/map-integration-dto');
|
|
164
|
+
|
|
165
|
+
const moduleDefinitions = getModulesDefinitionFromIntegrationClasses(integrations);
|
|
166
|
+
const credentialFields = extractCredentialFieldsFromModules(moduleDefinitions);
|
|
167
|
+
|
|
168
|
+
if (credentialFields.length === 0) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const moduleSchema = {
|
|
173
|
+
Credential: {
|
|
174
|
+
fields: credentialFields
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
logger.info(
|
|
179
|
+
`Registering module-level encryption for ${credentialFields.length} credential fields`
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
registerCustomSchema(moduleSchema);
|
|
183
|
+
}
|
|
184
|
+
|
|
112
185
|
/**
|
|
113
186
|
* Loads and registers custom encryption schema from appDefinition.
|
|
114
187
|
* Gracefully handles cases where appDefinition is not available.
|
|
@@ -139,11 +212,17 @@ function loadCustomEncryptionSchema() {
|
|
|
139
212
|
return; // No app definition found
|
|
140
213
|
}
|
|
141
214
|
|
|
215
|
+
// Load app-level custom schema
|
|
142
216
|
const customSchema = appDefinition.encryption?.schema;
|
|
143
|
-
|
|
144
217
|
if (customSchema && Object.keys(customSchema).length > 0) {
|
|
145
218
|
registerCustomSchema(customSchema);
|
|
146
219
|
}
|
|
220
|
+
|
|
221
|
+
// Load module-level encryption schemas from integrations
|
|
222
|
+
const integrations = appDefinition.integrations;
|
|
223
|
+
if (integrations && Array.isArray(integrations)) {
|
|
224
|
+
loadModuleEncryptionSchemas(integrations);
|
|
225
|
+
}
|
|
147
226
|
} catch (error) {
|
|
148
227
|
// Silently ignore errors - custom schema is optional
|
|
149
228
|
// This handles cases like:
|
|
@@ -182,6 +261,8 @@ module.exports = {
|
|
|
182
261
|
getEncryptedModels,
|
|
183
262
|
registerCustomSchema,
|
|
184
263
|
loadCustomEncryptionSchema,
|
|
264
|
+
loadModuleEncryptionSchemas,
|
|
265
|
+
extractCredentialFieldsFromModules,
|
|
185
266
|
validateCustomSchema,
|
|
186
|
-
resetCustomSchema,
|
|
267
|
+
resetCustomSchema,
|
|
187
268
|
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const { BaseError } = require('./base-error');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ClientSafeError - An error that is safe to expose to end users
|
|
5
|
+
*
|
|
6
|
+
* Use this error class when the error message does not contain sensitive
|
|
7
|
+
* implementation details and can be safely shown to users.
|
|
8
|
+
*
|
|
9
|
+
* Examples:
|
|
10
|
+
* - "Invalid Token: Token is expired"
|
|
11
|
+
* - "User not found"
|
|
12
|
+
* - "Invalid credentials"
|
|
13
|
+
*
|
|
14
|
+
* @param {string} message - The user-safe error message
|
|
15
|
+
* @param {number} statusCode - HTTP status code (default: 400)
|
|
16
|
+
* @param {object} options - Additional error options (cause, etc.)
|
|
17
|
+
*/
|
|
18
|
+
class ClientSafeError extends BaseError {
|
|
19
|
+
constructor(message, statusCode = 400, options) {
|
|
20
|
+
super(message, options);
|
|
21
|
+
this.statusCode = statusCode;
|
|
22
|
+
this.isClientSafe = true;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module.exports = { ClientSafeError };
|
package/errors/fetch-error.js
CHANGED
package/errors/index.js
CHANGED
|
@@ -5,6 +5,7 @@ const {
|
|
|
5
5
|
RequiredPropertyError,
|
|
6
6
|
ParameterTypeError,
|
|
7
7
|
} = require('./validation-errors');
|
|
8
|
+
const { ClientSafeError } = require('./client-safe-error');
|
|
8
9
|
|
|
9
10
|
module.exports = {
|
|
10
11
|
BaseError,
|
|
@@ -12,4 +13,5 @@ module.exports = {
|
|
|
12
13
|
HaltError,
|
|
13
14
|
RequiredPropertyError,
|
|
14
15
|
ParameterTypeError,
|
|
16
|
+
ClientSafeError,
|
|
15
17
|
};
|
|
@@ -65,9 +65,7 @@ const {
|
|
|
65
65
|
const {
|
|
66
66
|
AuthenticateWithSharedSecret,
|
|
67
67
|
} = require('../user/use-cases/authenticate-with-shared-secret');
|
|
68
|
-
const {
|
|
69
|
-
AuthenticateUser,
|
|
70
|
-
} = require('../user/use-cases/authenticate-user');
|
|
68
|
+
const { AuthenticateUser } = require('../user/use-cases/authenticate-user');
|
|
71
69
|
const {
|
|
72
70
|
ProcessAuthorizationCallback,
|
|
73
71
|
} = require('../modules/use-cases/process-authorization-callback');
|
|
@@ -234,8 +232,10 @@ function checkRequiredParams(params, requiredKeys) {
|
|
|
234
232
|
|
|
235
233
|
if (missingKeys.length > 0) {
|
|
236
234
|
throw Boom.badRequest(
|
|
237
|
-
`Missing Parameter${
|
|
238
|
-
|
|
235
|
+
`Missing Parameter${
|
|
236
|
+
missingKeys.length === 1 ? '' : 's'
|
|
237
|
+
}: ${missingKeys.join(', ')} ${
|
|
238
|
+
missingKeys.length === 1 ? 'is' : 'are'
|
|
239
239
|
} required.`
|
|
240
240
|
);
|
|
241
241
|
}
|
|
@@ -584,7 +584,7 @@ function setEntityRoutes(router, authenticateUser, useCases) {
|
|
|
584
584
|
req.params.credentialId,
|
|
585
585
|
userId
|
|
586
586
|
);
|
|
587
|
-
if (credential.
|
|
587
|
+
if (credential.userId.toString() !== userId) {
|
|
588
588
|
throw Boom.forbidden('Credential does not belong to user');
|
|
589
589
|
}
|
|
590
590
|
|