@friggframework/core 2.0.0-next.122 → 2.0.0-next.124
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/handlers/app-definition-loader.js +9 -2
- package/integrations/integration-router.js +18 -22
- package/modules/use-cases/create-entity-from-credential.js +105 -0
- package/modules/use-cases/get-entity-options-by-id.js +7 -5
- package/modules/use-cases/get-module.js +29 -8
- package/modules/use-cases/refresh-entity-options.js +7 -5
- package/modules/use-cases/test-module-auth.js +7 -5
- package/modules/utils/find-module-definition.js +23 -0
- package/package.json +6 -5
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
const { findNearestBackendPackageJson } = require('@friggframework/core/utils');
|
|
2
2
|
const path = require('node:path');
|
|
3
3
|
const fs = require('fs-extra');
|
|
4
|
+
const {
|
|
5
|
+
applyAppDefinitionDefaults,
|
|
6
|
+
} = require('@friggframework/schemas/app-definition');
|
|
4
7
|
const { resolveTelemetryConfig } = require('../telemetry/telemetry-config');
|
|
5
8
|
const { registerDeniedKeys } = require('../logs/denied-keys');
|
|
6
9
|
const {
|
|
@@ -33,7 +36,9 @@ function registerCredentialLogKeys(appDefinition, integrations) {
|
|
|
33
36
|
* @function loadAppDefinition
|
|
34
37
|
* @description Searches for the nearest backend package.json, loads the corresponding index.js file,
|
|
35
38
|
* and extracts the application definition containing integrations and user configuration.
|
|
36
|
-
*
|
|
39
|
+
* The defaults declared in the app-definition schema are applied first (ADR-051), so
|
|
40
|
+
* `userConfig` always holds a complete user config, also when the app omits `user`.
|
|
41
|
+
* @returns {{integrations: Array<object>, userConfig: object, adminScripts: Array<object>, reports: Array<object>, admin: object, telemetry: object, logging: object | null}} An object containing the application definition.
|
|
37
42
|
* @throws {Error} Throws error if backend package.json cannot be found.
|
|
38
43
|
* @throws {Error} Throws error if index.js file cannot be found in the backend directory.
|
|
39
44
|
* @example
|
|
@@ -53,7 +58,9 @@ function loadAppDefinition() {
|
|
|
53
58
|
}
|
|
54
59
|
|
|
55
60
|
const backendJsFile = require(backendFilePath);
|
|
56
|
-
|
|
61
|
+
// The one place schema defaults are applied at runtime (ADR-051): a
|
|
62
|
+
// default in app-definition.schema.json is the value core sees.
|
|
63
|
+
const appDefinition = applyAppDefinitionDefaults(backendJsFile.Definition);
|
|
57
64
|
|
|
58
65
|
const {
|
|
59
66
|
integrations = [],
|
|
@@ -42,6 +42,9 @@ const {
|
|
|
42
42
|
} = require('../modules/use-cases/get-entity-options-by-type');
|
|
43
43
|
const { TestModuleAuth } = require('../modules/use-cases/test-module-auth');
|
|
44
44
|
const { GetModule } = require('../modules/use-cases/get-module');
|
|
45
|
+
const {
|
|
46
|
+
CreateEntityFromCredential,
|
|
47
|
+
} = require('../modules/use-cases/create-entity-from-credential');
|
|
45
48
|
const {
|
|
46
49
|
GetEntityOptionsById,
|
|
47
50
|
} = require('../modules/use-cases/get-entity-options-by-id');
|
|
@@ -198,6 +201,13 @@ function createIntegrationRouter() {
|
|
|
198
201
|
getModulesDefinitionFromIntegrationClasses(integrationClasses),
|
|
199
202
|
});
|
|
200
203
|
|
|
204
|
+
const createEntityFromCredential = new CreateEntityFromCredential({
|
|
205
|
+
processAuthorizationCallback,
|
|
206
|
+
credentialRepository,
|
|
207
|
+
moduleDefinitions:
|
|
208
|
+
getModulesDefinitionFromIntegrationClasses(integrationClasses),
|
|
209
|
+
});
|
|
210
|
+
|
|
201
211
|
const router = express();
|
|
202
212
|
|
|
203
213
|
setIntegrationRoutes(router, authenticateUser, {
|
|
@@ -218,6 +228,7 @@ function createIntegrationRouter() {
|
|
|
218
228
|
getEntityOptionsById,
|
|
219
229
|
refreshEntityOptions,
|
|
220
230
|
processAuthorizationCallback,
|
|
231
|
+
createEntityFromCredential,
|
|
221
232
|
});
|
|
222
233
|
return router;
|
|
223
234
|
}
|
|
@@ -502,6 +513,7 @@ function setEntityRoutes(router, authenticateUser, useCases) {
|
|
|
502
513
|
getEntityOptionsById,
|
|
503
514
|
refreshEntityOptions,
|
|
504
515
|
processAuthorizationCallback,
|
|
516
|
+
createEntityFromCredential,
|
|
505
517
|
} = useCases;
|
|
506
518
|
|
|
507
519
|
router.route('/api/authorize').get(
|
|
@@ -566,35 +578,19 @@ function setEntityRoutes(router, authenticateUser, useCases) {
|
|
|
566
578
|
router.route('/api/entity').post(
|
|
567
579
|
catchAsyncError(async (req, res) => {
|
|
568
580
|
const user = await authenticateUser.execute(req);
|
|
569
|
-
const userId = user.getId();
|
|
570
581
|
const params = checkRequiredParams(req.body, [
|
|
571
582
|
'entityType',
|
|
572
583
|
'data',
|
|
573
584
|
]);
|
|
574
585
|
checkRequiredParams(req.body.data, ['credential_id']);
|
|
575
586
|
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
if (!credential) {
|
|
583
|
-
throw Boom.badRequest('Invalid credential ID');
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
const module = await getModuleInstanceFromType.execute(
|
|
587
|
-
userId,
|
|
588
|
-
params.entityType
|
|
587
|
+
res.json(
|
|
588
|
+
await createEntityFromCredential.execute({
|
|
589
|
+
user,
|
|
590
|
+
entityType: params.entityType,
|
|
591
|
+
credentialId: params.data.credential_id,
|
|
592
|
+
})
|
|
589
593
|
);
|
|
590
|
-
const entityDetails = await module.getEntityDetails(
|
|
591
|
-
module.api,
|
|
592
|
-
null,
|
|
593
|
-
null,
|
|
594
|
-
userId
|
|
595
|
-
);
|
|
596
|
-
|
|
597
|
-
res.json(await module.findOrCreateEntity(entityDetails));
|
|
598
594
|
})
|
|
599
595
|
);
|
|
600
596
|
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
const Boom = require('@hapi/boom');
|
|
2
|
+
const { Module } = require('../module');
|
|
3
|
+
|
|
4
|
+
const sameId = (a, b) =>
|
|
5
|
+
a !== undefined &&
|
|
6
|
+
a !== null &&
|
|
7
|
+
b !== undefined &&
|
|
8
|
+
b !== null &&
|
|
9
|
+
String(a) === String(b);
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Creates (or finds) the entity for a credential the caller already holds:
|
|
13
|
+
* the v1 `POST /api/entity`.
|
|
14
|
+
*
|
|
15
|
+
* The module is built with the stored credential loaded, so the API can call
|
|
16
|
+
* the provider and a token refresh during `getEntityDetails` updates that
|
|
17
|
+
* credential. Persistence reuses `ProcessAuthorizationCallback.findOrCreateEntity`,
|
|
18
|
+
* the same find-or-create (and credential repointing) the authorization flow
|
|
19
|
+
* uses, so the two paths cannot drift.
|
|
20
|
+
*/
|
|
21
|
+
class CreateEntityFromCredential {
|
|
22
|
+
/**
|
|
23
|
+
* @param {Object} params
|
|
24
|
+
* @param {import('./process-authorization-callback').ProcessAuthorizationCallback} params.processAuthorizationCallback
|
|
25
|
+
* @param {Object} params.credentialRepository
|
|
26
|
+
* @param {Array<Object>} params.moduleDefinitions
|
|
27
|
+
* @param {Function} [params.createModule] - builds a Module; injectable for tests
|
|
28
|
+
*/
|
|
29
|
+
constructor({
|
|
30
|
+
processAuthorizationCallback,
|
|
31
|
+
credentialRepository,
|
|
32
|
+
moduleDefinitions,
|
|
33
|
+
createModule,
|
|
34
|
+
}) {
|
|
35
|
+
this.processAuthorizationCallback = processAuthorizationCallback;
|
|
36
|
+
this.credentialRepository = credentialRepository;
|
|
37
|
+
this.moduleDefinitions = moduleDefinitions || [];
|
|
38
|
+
this.createModule = createModule || ((params) => new Module(params));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {Object} params
|
|
43
|
+
* @param {Object} params.user - authenticated User
|
|
44
|
+
* @param {string} params.entityType
|
|
45
|
+
* @param {string} params.credentialId
|
|
46
|
+
* @returns {Promise<Object>} the persisted entity
|
|
47
|
+
*/
|
|
48
|
+
async execute({ user, entityType, credentialId }) {
|
|
49
|
+
const userId = user?.getId?.();
|
|
50
|
+
if (userId === undefined || userId === null) {
|
|
51
|
+
throw Boom.unauthorized('No authenticated user');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const definition = this.moduleDefinitions.find(
|
|
55
|
+
(def) => def && def.moduleName === entityType
|
|
56
|
+
);
|
|
57
|
+
if (!definition) {
|
|
58
|
+
throw Boom.badRequest(`Unknown entity type ${entityType}`, {
|
|
59
|
+
code: 'ENTITY_TYPE_NOT_FOUND',
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const credential =
|
|
64
|
+
await this.credentialRepository.findCredentialById(credentialId);
|
|
65
|
+
// A missing credential and someone else's get the same answer, so
|
|
66
|
+
// credential ids cannot be probed.
|
|
67
|
+
if (!credential || !sameId(credential.userId, userId)) {
|
|
68
|
+
throw Boom.badRequest('Invalid credential ID', {
|
|
69
|
+
code: 'INVALID_CREDENTIAL',
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// The repository returns the tokens flat; a Module reads them from
|
|
74
|
+
// `credential.data`.
|
|
75
|
+
const { id, userId: ownerId, externalId, authIsValid, ...data } =
|
|
76
|
+
credential;
|
|
77
|
+
const module = this.createModule({
|
|
78
|
+
userId,
|
|
79
|
+
definition,
|
|
80
|
+
entity: {
|
|
81
|
+
credential: { id, userId: ownerId, externalId, authIsValid, data },
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const entityDetails =
|
|
86
|
+
await definition.requiredAuthMethods.getEntityDetails(
|
|
87
|
+
module.api,
|
|
88
|
+
null,
|
|
89
|
+
null,
|
|
90
|
+
userId
|
|
91
|
+
);
|
|
92
|
+
Object.assign(
|
|
93
|
+
entityDetails.details,
|
|
94
|
+
module.apiParamsFromEntity(module.api)
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
return this.processAuthorizationCallback.findOrCreateEntity(
|
|
98
|
+
entityDetails,
|
|
99
|
+
definition.moduleName,
|
|
100
|
+
credential.id
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = { CreateEntityFromCredential };
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
const { Module } = require('../module');
|
|
2
|
+
const {
|
|
3
|
+
findModuleDefinitionForEntity,
|
|
4
|
+
} = require('../utils/find-module-definition');
|
|
2
5
|
|
|
3
6
|
class GetEntityOptionsById {
|
|
4
7
|
/**
|
|
@@ -45,11 +48,10 @@ class GetEntityOptionsById {
|
|
|
45
48
|
}
|
|
46
49
|
|
|
47
50
|
const entityType = entity.moduleName;
|
|
48
|
-
const moduleDefinition =
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
});
|
|
51
|
+
const moduleDefinition = findModuleDefinitionForEntity(
|
|
52
|
+
this.moduleDefinitions,
|
|
53
|
+
entity
|
|
54
|
+
);
|
|
53
55
|
|
|
54
56
|
if (!moduleDefinition) {
|
|
55
57
|
throw new Error(
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
const { Module } = require('../module');
|
|
2
|
+
const {
|
|
3
|
+
findModuleDefinitionForEntity,
|
|
4
|
+
} = require('../utils/find-module-definition');
|
|
2
5
|
|
|
3
6
|
class GetModule {
|
|
4
7
|
constructor({ moduleRepository, moduleDefinitions }) {
|
|
@@ -42,10 +45,10 @@ class GetModule {
|
|
|
42
45
|
}
|
|
43
46
|
|
|
44
47
|
const entityType = entity.moduleName;
|
|
45
|
-
const moduleDefinition =
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
const moduleDefinition = findModuleDefinitionForEntity(
|
|
49
|
+
this.moduleDefinitions,
|
|
50
|
+
entity
|
|
51
|
+
);
|
|
49
52
|
|
|
50
53
|
if (!moduleDefinition) {
|
|
51
54
|
throw new Error(
|
|
@@ -59,15 +62,33 @@ class GetModule {
|
|
|
59
62
|
definition: moduleDefinition,
|
|
60
63
|
});
|
|
61
64
|
|
|
62
|
-
//
|
|
65
|
+
// The credential itself (tokens, API keys) never leaves the server:
|
|
66
|
+
// the response names it and says whether it still works, as the v2
|
|
67
|
+
// entity DTO does.
|
|
68
|
+
const credential = module.credential;
|
|
69
|
+
const credentialId =
|
|
70
|
+
credential && typeof credential === 'object'
|
|
71
|
+
? credential.id
|
|
72
|
+
: credential;
|
|
73
|
+
const ownerId = module.entity.userId ?? module.entity.user;
|
|
63
74
|
return {
|
|
64
75
|
id: module.entity.id,
|
|
65
76
|
name: module.entity.name,
|
|
66
77
|
moduleName: module.entity.moduleName,
|
|
67
|
-
|
|
78
|
+
credentialId:
|
|
79
|
+
credentialId === undefined || credentialId === null
|
|
80
|
+
? null
|
|
81
|
+
: String(credentialId),
|
|
82
|
+
authIsValid:
|
|
83
|
+
typeof credential?.authIsValid === 'boolean'
|
|
84
|
+
? credential.authIsValid
|
|
85
|
+
: null,
|
|
68
86
|
externalId: module.entity.externalId,
|
|
69
|
-
userId:
|
|
70
|
-
|
|
87
|
+
userId:
|
|
88
|
+
ownerId === undefined || ownerId === null
|
|
89
|
+
? null
|
|
90
|
+
: String(ownerId),
|
|
91
|
+
};
|
|
71
92
|
}
|
|
72
93
|
}
|
|
73
94
|
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
const { Module } = require('../module');
|
|
2
|
+
const {
|
|
3
|
+
findModuleDefinitionForEntity,
|
|
4
|
+
} = require('../utils/find-module-definition');
|
|
2
5
|
|
|
3
6
|
class RefreshEntityOptions {
|
|
4
7
|
/**
|
|
@@ -46,11 +49,10 @@ class RefreshEntityOptions {
|
|
|
46
49
|
}
|
|
47
50
|
|
|
48
51
|
const entityType = entity.moduleName;
|
|
49
|
-
const moduleDefinition =
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
});
|
|
52
|
+
const moduleDefinition = findModuleDefinitionForEntity(
|
|
53
|
+
this.moduleDefinitions,
|
|
54
|
+
entity
|
|
55
|
+
);
|
|
54
56
|
|
|
55
57
|
if (!moduleDefinition) {
|
|
56
58
|
throw new Error(
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
const { Module } = require('../module');
|
|
2
|
+
const {
|
|
3
|
+
findModuleDefinitionForEntity,
|
|
4
|
+
} = require('../utils/find-module-definition');
|
|
2
5
|
|
|
3
6
|
class TestModuleAuth {
|
|
4
7
|
/**
|
|
@@ -45,11 +48,10 @@ class TestModuleAuth {
|
|
|
45
48
|
}
|
|
46
49
|
|
|
47
50
|
const entityType = entity.moduleName;
|
|
48
|
-
const moduleDefinition =
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
});
|
|
51
|
+
const moduleDefinition = findModuleDefinitionForEntity(
|
|
52
|
+
this.moduleDefinitions,
|
|
53
|
+
entity
|
|
54
|
+
);
|
|
53
55
|
|
|
54
56
|
if (!moduleDefinition) {
|
|
55
57
|
throw new Error(
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The API module definition a stored entity belongs to.
|
|
3
|
+
*
|
|
4
|
+
* Entities are persisted with `moduleName` set to the definition's
|
|
5
|
+
* `moduleName` (see ProcessAuthorizationCallback.findOrCreateEntity). A
|
|
6
|
+
* definition's legacy `modelName` is accepted as well, for entities written
|
|
7
|
+
* before the two were the same.
|
|
8
|
+
*
|
|
9
|
+
* @param {Array<Object>} moduleDefinitions
|
|
10
|
+
* @param {Object} entity - entity record with a `moduleName`
|
|
11
|
+
* @returns {Object|undefined}
|
|
12
|
+
*/
|
|
13
|
+
function findModuleDefinitionForEntity(moduleDefinitions, entity) {
|
|
14
|
+
const entityType = entity?.moduleName;
|
|
15
|
+
if (!entityType) return undefined;
|
|
16
|
+
return (moduleDefinitions || []).find(
|
|
17
|
+
(def) =>
|
|
18
|
+
def &&
|
|
19
|
+
(def.moduleName === entityType || def.modelName === entityType)
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = { findModuleDefinitionForEntity };
|
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-next.
|
|
4
|
+
"version": "2.0.0-next.124",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@aws-sdk/client-apigatewaymanagementapi": "^3.588.0",
|
|
7
7
|
"@aws-sdk/client-kms": "^3.588.0",
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"@aws-sdk/client-sqs": "^3.588.0",
|
|
12
12
|
"@aws-sdk/client-ssm": "^3.588.0",
|
|
13
13
|
"@aws-sdk/s3-request-presigner": "^3.588.0",
|
|
14
|
+
"@friggframework/schemas": "2.0.0-next.124",
|
|
14
15
|
"@hapi/boom": "^10.0.1",
|
|
15
16
|
"@opentelemetry/api": "^1.9.1",
|
|
16
17
|
"@opentelemetry/context-async-hooks": "^2.9.0",
|
|
@@ -48,9 +49,9 @@
|
|
|
48
49
|
}
|
|
49
50
|
},
|
|
50
51
|
"devDependencies": {
|
|
51
|
-
"@friggframework/eslint-config": "2.0.0-next.
|
|
52
|
-
"@friggframework/prettier-config": "2.0.0-next.
|
|
53
|
-
"@friggframework/test": "2.0.0-next.
|
|
52
|
+
"@friggframework/eslint-config": "2.0.0-next.124",
|
|
53
|
+
"@friggframework/prettier-config": "2.0.0-next.124",
|
|
54
|
+
"@friggframework/test": "2.0.0-next.124",
|
|
54
55
|
"@prisma/client": "^6.19.3",
|
|
55
56
|
"@types/lodash": "4.17.15",
|
|
56
57
|
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
@@ -91,5 +92,5 @@
|
|
|
91
92
|
"publishConfig": {
|
|
92
93
|
"access": "public"
|
|
93
94
|
},
|
|
94
|
-
"gitHead": "
|
|
95
|
+
"gitHead": "679a3f0f550136151563c34f9a8c7680b88b6984"
|
|
95
96
|
}
|