@friggframework/core 2.0.0--canary.548.c8ae0ca.0 → 2.0.0--canary.545.c40eca4.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.
- package/CHANGELOG.md +66 -67
- package/CLAUDE.md +148 -124
- package/README.md +165 -130
- package/admin-scripts/index.js +52 -0
- package/admin-scripts/repositories/admin-api-key-repository-documentdb.js +21 -0
- package/admin-scripts/repositories/admin-api-key-repository-factory.js +51 -0
- package/admin-scripts/repositories/admin-api-key-repository-interface.js +121 -0
- package/admin-scripts/repositories/admin-api-key-repository-mongo.js +155 -0
- package/admin-scripts/repositories/admin-api-key-repository-postgres.js +189 -0
- package/admin-scripts/repositories/admin-process-repository-documentdb.js +21 -0
- package/admin-scripts/repositories/admin-process-repository-factory.js +49 -0
- package/admin-scripts/repositories/admin-process-repository-interface.js +150 -0
- package/admin-scripts/repositories/admin-process-repository-mongo.js +213 -0
- package/admin-scripts/repositories/admin-process-repository-postgres.js +251 -0
- package/admin-scripts/repositories/script-execution-repository-documentdb.js +21 -0
- package/admin-scripts/repositories/script-execution-repository-factory.js +51 -0
- package/admin-scripts/repositories/script-execution-repository-interface.js +193 -0
- package/admin-scripts/repositories/script-execution-repository-mongo.js +278 -0
- package/admin-scripts/repositories/script-execution-repository-postgres.js +320 -0
- package/admin-scripts/repositories/script-schedule-repository-documentdb.js +21 -0
- package/admin-scripts/repositories/script-schedule-repository-factory.js +49 -0
- package/admin-scripts/repositories/script-schedule-repository-interface.js +108 -0
- package/admin-scripts/repositories/script-schedule-repository-mongo.js +179 -0
- package/admin-scripts/repositories/script-schedule-repository-postgres.js +210 -0
- package/application/commands/admin-script-commands.js +348 -0
- package/application/commands/integration-commands.js +12 -6
- package/application/index.js +5 -3
- package/assertions/get.js +17 -7
- package/associations/association.js +59 -57
- package/core/CLAUDE.md +222 -156
- package/core/Worker.js +33 -12
- package/credential/credential-router.js +262 -0
- package/credential/repositories/credential-repository-documentdb.js +71 -1
- package/credential/repositories/credential-repository-factory.js +4 -6
- package/credential/repositories/credential-repository-interface.js +8 -2
- package/credential/repositories/credential-repository-mongo.js +45 -4
- package/credential/repositories/credential-repository-postgres.js +44 -3
- package/credential/repositories/credential-repository.js +3 -1
- package/credential/use-cases/delete-credential-for-user.js +50 -0
- package/credential/use-cases/get-credential-for-user.js +21 -3
- package/credential/use-cases/list-credentials-for-user.js +31 -0
- package/credential/use-cases/reauthorize-credential.js +103 -0
- package/credential/use-cases/update-authentication-status.js +5 -2
- package/database/MONGODB_TRANSACTION_FIX.md +72 -52
- package/database/config.js +13 -59
- package/database/documentdb-encryption-service.js +29 -13
- package/database/documentdb-utils.js +21 -12
- package/database/encryption/README.md +53 -36
- package/database/encryption/encryption-schema-registry.js +29 -23
- package/database/encryption/field-encryption-service.js +15 -4
- package/database/encryption/logger.js +3 -2
- package/database/encryption/prisma-encryption-extension.js +5 -8
- package/database/prisma.js +22 -14
- package/database/repositories/health-check-repository-factory.js +7 -5
- package/database/repositories/health-check-repository-interface.js +15 -5
- package/database/repositories/health-check-repository-postgres.js +1 -1
- package/database/use-cases/check-database-health-use-case.js +3 -2
- package/database/use-cases/check-database-state-use-case.js +8 -7
- package/database/use-cases/get-database-state-via-worker-use-case.js +15 -12
- package/database/use-cases/get-migration-status-use-case.js +4 -2
- package/database/use-cases/run-database-migration-use-case.js +41 -9
- package/database/use-cases/test-encryption-use-case.js +4 -5
- package/database/use-cases/trigger-database-migration-use-case.js +9 -8
- package/database/utils/mongodb-collection-utils.js +11 -3
- package/database/utils/mongodb-schema-init.js +10 -4
- package/database/utils/prisma-runner.js +144 -74
- package/database/utils/prisma-schema-parser.js +2 -2
- package/docs/PROCESS_MANAGEMENT_QUEUE_SPEC.md +135 -80
- package/encrypt/Cryptor.js +54 -49
- package/encrypt/aes-encryption-key-provider.js +82 -0
- package/encrypt/encryption-key-provider-interface.js +50 -0
- package/errors/fetch-error.js +1 -1
- package/generated/prisma-mongodb/edge.js +59 -4
- package/generated/prisma-mongodb/index-browser.js +49 -1
- package/generated/prisma-mongodb/index.d.ts +4023 -61
- package/generated/prisma-mongodb/index.js +59 -4
- package/generated/prisma-mongodb/package.json +1 -1
- package/generated/prisma-mongodb/schema.prisma +87 -0
- package/generated/prisma-mongodb/wasm.js +59 -4
- package/generated/prisma-postgresql/edge.js +58 -4
- package/generated/prisma-postgresql/index-browser.js +48 -1
- package/generated/prisma-postgresql/index.d.ts +4738 -54
- package/generated/prisma-postgresql/index.js +58 -4
- package/generated/prisma-postgresql/package.json +1 -1
- package/generated/prisma-postgresql/schema.prisma +85 -0
- package/generated/prisma-postgresql/wasm.js +58 -4
- package/handlers/WEBHOOKS.md +104 -81
- package/handlers/app-definition-loader.js +53 -10
- package/handlers/app-handler-helpers.js +14 -3
- package/handlers/backend-utils.js +0 -1
- package/handlers/database-migration-handler.js +64 -45
- package/handlers/middleware/admin-auth.js +53 -0
- package/handlers/routers/HEALTHCHECK.md +170 -135
- package/handlers/routers/admin.js +474 -0
- package/handlers/routers/auth.js +51 -9
- package/handlers/routers/db-migration.handler.js +2 -3
- package/handlers/routers/db-migration.js +102 -66
- package/handlers/routers/docs.js +180 -0
- package/handlers/routers/health.js +84 -283
- package/handlers/routers/integration-defined-routers.js +44 -33
- package/handlers/routers/integration-webhook-routers.js +73 -52
- package/handlers/routers/middleware/loadUser.js +39 -0
- package/handlers/routers/middleware/requireAdmin.js +43 -0
- package/handlers/routers/middleware/requireLoggedInUser.js +19 -0
- package/handlers/routers/user.js +30 -14
- package/handlers/routers/websocket.js +18 -4
- package/handlers/use-cases/check-integrations-health-use-case.js +13 -6
- package/handlers/workers/db-migration.js +74 -35
- package/handlers/workers/integration-defined-workers.js +31 -20
- package/index.js +50 -16
- package/infrastructure/scheduler/index.js +15 -11
- package/infrastructure/scheduler/netlify-scheduler-adapter.js +185 -0
- package/infrastructure/scheduler/scheduler-service-factory.js +38 -31
- package/integrations/WEBHOOK-QUICKSTART.md +21 -14
- package/integrations/index.js +4 -0
- package/integrations/integration-base.js +9 -6
- package/integrations/integration-router.js +1828 -157
- package/integrations/options.js +47 -20
- package/integrations/repositories/integration-mapping-repository-factory.js +4 -8
- package/integrations/repositories/integration-repository-documentdb.js +53 -24
- package/integrations/repositories/integration-repository-factory.js +6 -6
- package/integrations/repositories/integration-repository-interface.js +27 -9
- package/integrations/repositories/integration-repository-postgres.js +3 -3
- package/integrations/repositories/process-repository-factory.js +4 -7
- package/integrations/repositories/process-repository-interface.js +9 -5
- package/integrations/repositories/process-repository-mongo.js +7 -3
- package/integrations/repositories/process-repository-postgres.js +2 -2
- package/integrations/tests/doubles/config-capturing-integration.js +9 -8
- package/integrations/tests/doubles/dummy-integration-class.js +81 -8
- package/integrations/tests/doubles/test-integration-repository.js +51 -11
- package/integrations/use-cases/create-integration.js +36 -32
- package/integrations/use-cases/create-process.js +20 -9
- package/integrations/use-cases/delete-integration-for-user.js +5 -4
- package/integrations/use-cases/execute-proxy-request.js +564 -0
- package/integrations/use-cases/get-integration-for-user.js +25 -10
- package/integrations/use-cases/get-integration-instance-by-definition.js +13 -7
- package/integrations/use-cases/get-integrations-for-user.js +4 -1
- package/integrations/use-cases/get-possible-integrations.js +1 -1
- package/integrations/use-cases/get-process.js +5 -6
- package/integrations/use-cases/index.js +3 -1
- package/integrations/use-cases/load-integration-context.js +1 -5
- package/integrations/use-cases/update-process-state.js +10 -8
- package/integrations/utils/map-integration-dto.js +9 -3
- package/jest-setup.js +1 -1
- package/jest-teardown.js +1 -1
- package/lambda/TimeoutCatcher.js +32 -30
- package/lambda/index.js +1 -1
- package/logs/index.js +3 -3
- package/modules/domain/entities/AuthorizationSession.js +131 -0
- package/modules/domain/entities/index.js +10 -0
- package/modules/module.js +4 -9
- package/modules/repositories/authorization-session-repository-factory.js +53 -0
- package/modules/repositories/authorization-session-repository-interface.js +75 -0
- package/modules/repositories/authorization-session-repository-mongo.js +169 -0
- package/modules/repositories/authorization-session-repository-postgres.js +170 -0
- package/modules/repositories/module-repository-documentdb.js +89 -41
- package/modules/repositories/module-repository-factory.js +4 -6
- package/modules/repositories/module-repository-interface.js +13 -0
- package/modules/repositories/module-repository-mongo.js +40 -5
- package/modules/repositories/module-repository-postgres.js +42 -5
- package/modules/repositories/module-repository.js +28 -0
- package/modules/requester/api-key.js +0 -2
- package/modules/requester/basic.js +0 -1
- package/modules/requester/oauth-2.js +3 -14
- package/modules/test/mock-api/definition.js +15 -10
- package/modules/test/mock-api/mocks/hubspot.js +36 -36
- package/modules/tests/doubles/test-module-factory.js +10 -4
- package/modules/tests/doubles/test-module-repository.js +1 -1
- package/modules/use-cases/delete-module-entity.js +23 -0
- package/modules/use-cases/get-authorization-requirements.js +110 -0
- package/modules/use-cases/get-entities-for-user.js +1 -1
- package/modules/use-cases/get-entity-options-by-id.js +8 -6
- package/modules/use-cases/get-module-entity-by-id.js +16 -0
- package/modules/use-cases/get-module.js +12 -9
- package/modules/use-cases/process-authorization-callback.js +12 -6
- package/modules/use-cases/process-authorization-step.js +160 -0
- package/modules/use-cases/refresh-entity-options.js +8 -6
- package/modules/use-cases/start-authorization-session.js +86 -0
- package/modules/use-cases/test-module-auth.js +8 -6
- package/modules/use-cases/update-module-entity.js +27 -0
- package/modules/utils/map-module-dto.js +2 -2
- package/openapi/openapi-spec-generator.js +370 -0
- package/openapi/openapi-v1.yaml +1360 -0
- package/openapi/openapi-v2.yaml +1535 -0
- package/package.json +7 -9
- package/prisma-mongodb/schema.prisma +87 -0
- package/prisma-postgresql/migrations/20260222000000_add_admin_process_and_script_schedule/migration.sql +55 -0
- package/prisma-postgresql/schema.prisma +85 -0
- package/providers/index.js +13 -0
- package/providers/resolve-provider.js +93 -0
- package/queues/index.js +19 -0
- package/queues/providers/index.js +7 -0
- package/queues/providers/netlify-background-provider.js +76 -0
- package/queues/providers/qstash-queue-provider.js +122 -0
- package/queues/queue-client-interface.js +55 -0
- package/queues/queue-provider-factory.js +93 -0
- package/queues/queue-provider.js +46 -0
- package/queues/queuer-util.js +33 -25
- package/syncs/repositories/sync-repository-documentdb.js +39 -19
- package/syncs/repositories/sync-repository-factory.js +4 -4
- package/syncs/repositories/sync-repository-interface.js +6 -2
- package/syncs/repositories/sync-repository-postgres.js +19 -13
- package/syncs/sync.js +104 -100
- package/token/repositories/token-repository-factory.js +4 -6
- package/types/assertions/index.d.ts +72 -72
- package/types/core/index.d.ts +98 -53
- package/types/errors/index.d.ts +61 -54
- package/types/eslint-config/index.d.ts +40 -40
- package/types/lambda/index.d.ts +26 -26
- package/types/logs/index.d.ts +4 -4
- package/types/prettier-config/index.d.ts +5 -5
- package/types/test-environment/index.d.ts +14 -14
- package/types/tsconfig.json +95 -93
- package/user/repositories/user-repository-factory.js +4 -4
- package/user/tests/doubles/test-user-repository.js +5 -2
- package/user/use-cases/authenticate-user.js +2 -4
- package/user/use-cases/authenticate-with-shared-secret.js +1 -1
- package/user/use-cases/create-individual-user.js +8 -7
- package/user/use-cases/create-organization-user.js +1 -1
- package/user/use-cases/create-token-for-user-id.js +1 -1
- package/user/use-cases/delete-user.js +47 -0
- package/user/use-cases/get-user-from-adopter-jwt.js +0 -2
- package/user/use-cases/get-user-from-bearer-token.js +7 -3
- package/user/use-cases/get-user-from-x-frigg-headers.js +14 -11
- package/user/use-cases/login-user.js +8 -8
- package/user/user.js +18 -4
- package/utils/backend-path.js +2 -2
- package/utils/index.js +4 -1
- package/websocket/repositories/websocket-connection-repository-documentdb.js +46 -21
- package/websocket/repositories/websocket-connection-repository-factory.js +4 -8
- package/websocket/repositories/websocket-connection-repository-mongo.js +26 -16
- package/websocket/repositories/websocket-connection-repository-postgres.js +26 -15
- package/websocket/repositories/websocket-connection-repository.js +26 -20
- package/websocket/websocket-message-sender-interface.js +38 -0
- package/database/adapters/lambda-invoker.js +0 -97
- package/database/repositories/migration-status-repository-s3.js +0 -137
- package/infrastructure/scheduler/eventbridge-scheduler-adapter.js +0 -184
|
@@ -0,0 +1,564 @@
|
|
|
1
|
+
const Boom = require('@hapi/boom');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Use case for proxying HTTP requests through an entity's or credential's API connection
|
|
5
|
+
*
|
|
6
|
+
* This use case handles:
|
|
7
|
+
* - Entity and credential validation
|
|
8
|
+
* - Authentication state verification
|
|
9
|
+
* - HTTP method validation
|
|
10
|
+
* - Request forwarding to upstream API
|
|
11
|
+
* - Error mapping and response formatting
|
|
12
|
+
*
|
|
13
|
+
* @class ExecuteProxyRequest
|
|
14
|
+
*/
|
|
15
|
+
class ExecuteProxyRequest {
|
|
16
|
+
/**
|
|
17
|
+
* @param {Object} params - Configuration parameters
|
|
18
|
+
* @param {import('../../modules/repositories/module-repository-interface').ModuleRepositoryInterface} params.moduleRepository - Repository for entity data
|
|
19
|
+
* @param {import('../../credential/repositories/credential-repository-interface').CredentialRepositoryInterface} params.credentialRepository - Repository for credential data
|
|
20
|
+
* @param {import('../../modules/module-factory').ModuleFactory} params.moduleFactory - Factory for creating module instances
|
|
21
|
+
* @param {Array} params.moduleDefinitions - Array of module definitions
|
|
22
|
+
*/
|
|
23
|
+
constructor({
|
|
24
|
+
moduleRepository,
|
|
25
|
+
credentialRepository,
|
|
26
|
+
moduleFactory,
|
|
27
|
+
moduleDefinitions,
|
|
28
|
+
}) {
|
|
29
|
+
this.moduleRepository = moduleRepository;
|
|
30
|
+
this.credentialRepository = credentialRepository;
|
|
31
|
+
this.moduleFactory = moduleFactory;
|
|
32
|
+
this.moduleDefinitions = moduleDefinitions;
|
|
33
|
+
|
|
34
|
+
// Valid HTTP methods for proxy requests
|
|
35
|
+
this.VALID_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Execute proxy request through an entity
|
|
40
|
+
*
|
|
41
|
+
* @param {string} entityId - Entity ID to proxy through
|
|
42
|
+
* @param {string} userId - User ID making the request
|
|
43
|
+
* @param {Object} proxyRequest - Proxy request parameters
|
|
44
|
+
* @param {string} proxyRequest.method - HTTP method (GET, POST, PUT, PATCH, DELETE)
|
|
45
|
+
* @param {string} proxyRequest.path - API path to call
|
|
46
|
+
* @param {Object} [proxyRequest.query] - Query parameters
|
|
47
|
+
* @param {Object} [proxyRequest.headers] - Request headers
|
|
48
|
+
* @param {*} [proxyRequest.body] - Request body
|
|
49
|
+
* @returns {Promise<Object>} Proxy response with status, headers, and data
|
|
50
|
+
*/
|
|
51
|
+
async executeViaEntity(entityId, userId, proxyRequest) {
|
|
52
|
+
// Validate request
|
|
53
|
+
this._validateProxyRequest(proxyRequest);
|
|
54
|
+
|
|
55
|
+
// Load entity for user (validates ownership)
|
|
56
|
+
const entity = await this.moduleRepository.findByIdForUser(
|
|
57
|
+
entityId,
|
|
58
|
+
userId
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
if (!entity) {
|
|
62
|
+
throw Boom.notFound('Entity not found');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Load credential
|
|
66
|
+
const credential = await this._loadAndValidateCredential(
|
|
67
|
+
entity.credential
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
// Get module instance with API client
|
|
71
|
+
const moduleInstance = await this._getModuleInstance(entityId, userId);
|
|
72
|
+
|
|
73
|
+
// Execute proxy request
|
|
74
|
+
return await this._executeProxyRequest(
|
|
75
|
+
moduleInstance.api,
|
|
76
|
+
proxyRequest
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Execute proxy request through a credential directly
|
|
82
|
+
*
|
|
83
|
+
* @param {string} credentialId - Credential ID to proxy through
|
|
84
|
+
* @param {string} userId - User ID making the request
|
|
85
|
+
* @param {Object} proxyRequest - Proxy request parameters
|
|
86
|
+
* @returns {Promise<Object>} Proxy response with status, headers, and data
|
|
87
|
+
*/
|
|
88
|
+
async executeViaCredential(credentialId, userId, proxyRequest) {
|
|
89
|
+
// Validate request
|
|
90
|
+
this._validateProxyRequest(proxyRequest);
|
|
91
|
+
|
|
92
|
+
// Load credential for user (validates ownership)
|
|
93
|
+
const credential = await this.credentialRepository.findByIdForUser(
|
|
94
|
+
credentialId,
|
|
95
|
+
userId
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
if (!credential) {
|
|
99
|
+
throw Boom.notFound('Credential not found');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Validate credential is usable
|
|
103
|
+
this._validateCredentialAuth(credential);
|
|
104
|
+
|
|
105
|
+
// Get API instance for credential
|
|
106
|
+
const moduleInstance = await this._getModuleInstanceFromCredential(
|
|
107
|
+
credential,
|
|
108
|
+
userId
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
// Execute proxy request
|
|
112
|
+
return await this._executeProxyRequest(
|
|
113
|
+
moduleInstance.api,
|
|
114
|
+
proxyRequest
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Validate proxy request parameters
|
|
120
|
+
*
|
|
121
|
+
* @private
|
|
122
|
+
* @param {Object} proxyRequest - Request to validate
|
|
123
|
+
* @throws {Boom.badRequest} When validation fails
|
|
124
|
+
*/
|
|
125
|
+
_validateProxyRequest(proxyRequest) {
|
|
126
|
+
// Validate method
|
|
127
|
+
if (!proxyRequest.method) {
|
|
128
|
+
throw Boom.badRequest('Missing Parameter: method is required.');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (!this.VALID_METHODS.includes(proxyRequest.method)) {
|
|
132
|
+
throw Boom.badRequest(
|
|
133
|
+
`Invalid method. method must be one of: ${this.VALID_METHODS.join(
|
|
134
|
+
', '
|
|
135
|
+
)}`
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Validate path
|
|
140
|
+
if (!proxyRequest.path) {
|
|
141
|
+
throw Boom.badRequest('Missing Parameter: path is required.');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (
|
|
145
|
+
typeof proxyRequest.path !== 'string' ||
|
|
146
|
+
proxyRequest.path.trim() === ''
|
|
147
|
+
) {
|
|
148
|
+
throw Boom.badRequest('path must be a non-empty string');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (!proxyRequest.path.startsWith('/')) {
|
|
152
|
+
throw Boom.badRequest('path must start with /');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Validate query parameters (if provided)
|
|
156
|
+
if (proxyRequest.query !== undefined && proxyRequest.query !== null) {
|
|
157
|
+
if (
|
|
158
|
+
typeof proxyRequest.query !== 'object' ||
|
|
159
|
+
Array.isArray(proxyRequest.query)
|
|
160
|
+
) {
|
|
161
|
+
throw Boom.badRequest('query must be an object');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Validate each query parameter value type
|
|
165
|
+
for (const [key, value] of Object.entries(proxyRequest.query)) {
|
|
166
|
+
const valueType = typeof value;
|
|
167
|
+
const isValidType =
|
|
168
|
+
valueType === 'string' ||
|
|
169
|
+
valueType === 'number' ||
|
|
170
|
+
valueType === 'boolean' ||
|
|
171
|
+
Array.isArray(value);
|
|
172
|
+
|
|
173
|
+
if (!isValidType) {
|
|
174
|
+
throw Boom.badRequest(
|
|
175
|
+
`Invalid query parameter "${key}". Query parameters must be string, number, boolean, or array.`
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// If array, validate all items are strings
|
|
180
|
+
if (Array.isArray(value)) {
|
|
181
|
+
for (const item of value) {
|
|
182
|
+
if (typeof item !== 'string') {
|
|
183
|
+
throw Boom.badRequest(
|
|
184
|
+
`Invalid query parameter "${key}". Query array items must be strings.`
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Validate headers (if provided)
|
|
193
|
+
if (
|
|
194
|
+
proxyRequest.headers !== undefined &&
|
|
195
|
+
proxyRequest.headers !== null
|
|
196
|
+
) {
|
|
197
|
+
if (
|
|
198
|
+
typeof proxyRequest.headers !== 'object' ||
|
|
199
|
+
Array.isArray(proxyRequest.headers)
|
|
200
|
+
) {
|
|
201
|
+
throw Boom.badRequest('headers must be an object');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Validate each header value is a string
|
|
205
|
+
for (const [key, value] of Object.entries(proxyRequest.headers)) {
|
|
206
|
+
if (typeof value !== 'string') {
|
|
207
|
+
throw Boom.badRequest(
|
|
208
|
+
`Invalid header "${key}". Headers must be strings.`
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Load and validate credential
|
|
217
|
+
*
|
|
218
|
+
* @private
|
|
219
|
+
* @param {string} credentialId - Credential ID to load
|
|
220
|
+
* @returns {Promise<Object>} Credential object
|
|
221
|
+
* @throws {Boom} When credential is invalid
|
|
222
|
+
*/
|
|
223
|
+
async _loadAndValidateCredential(credentialId) {
|
|
224
|
+
if (!credentialId) {
|
|
225
|
+
throw Boom.badRequest('Entity has no credential associated');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const credential = await this.credentialRepository.findById(
|
|
229
|
+
credentialId
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
if (!credential) {
|
|
233
|
+
throw Boom.notFound('Credential not found');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
this._validateCredentialAuth(credential);
|
|
237
|
+
|
|
238
|
+
return credential;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Validate credential has valid authentication data
|
|
243
|
+
*
|
|
244
|
+
* @private
|
|
245
|
+
* @param {Object} credential - Credential to validate
|
|
246
|
+
* @throws {Boom.unauthorized} When credential is invalid
|
|
247
|
+
*/
|
|
248
|
+
_validateCredentialAuth(credential) {
|
|
249
|
+
// Check credential status
|
|
250
|
+
if (credential.status && credential.status !== 'AUTHORIZED') {
|
|
251
|
+
throw Boom.unauthorized(
|
|
252
|
+
'Credential is not authorized. Please reauthorize your connection.',
|
|
253
|
+
'INVALID_CREDENTIALS'
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Check credential has auth data
|
|
258
|
+
if (!credential.data || !credential.data.access_token) {
|
|
259
|
+
throw Boom.unauthorized(
|
|
260
|
+
'Credential is missing required authentication data',
|
|
261
|
+
'INVALID_CREDENTIALS'
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Get module instance with API client
|
|
268
|
+
*
|
|
269
|
+
* @private
|
|
270
|
+
* @param {string} entityId - Entity ID
|
|
271
|
+
* @param {string} userId - User ID
|
|
272
|
+
* @returns {Promise<Object>} Module instance with API client
|
|
273
|
+
*/
|
|
274
|
+
async _getModuleInstance(entityId, userId) {
|
|
275
|
+
try {
|
|
276
|
+
const moduleInstance = await this.moduleFactory.getModuleInstance(
|
|
277
|
+
entityId,
|
|
278
|
+
userId
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
if (!moduleInstance || !moduleInstance.api) {
|
|
282
|
+
throw Boom.internal(
|
|
283
|
+
'Failed to initialize API client for entity'
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return moduleInstance;
|
|
288
|
+
} catch (error) {
|
|
289
|
+
if (Boom.isBoom(error)) {
|
|
290
|
+
throw error;
|
|
291
|
+
}
|
|
292
|
+
throw Boom.internal('Failed to load module instance', error);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Get module instance from credential
|
|
298
|
+
*
|
|
299
|
+
* @private
|
|
300
|
+
* @param {Object} credential - Credential object
|
|
301
|
+
* @param {string} userId - User ID
|
|
302
|
+
* @returns {Promise<Object>} Module instance with API client
|
|
303
|
+
*/
|
|
304
|
+
async _getModuleInstanceFromCredential(credential, userId) {
|
|
305
|
+
try {
|
|
306
|
+
// Find module definition for this credential type
|
|
307
|
+
const moduleDefinition = this.moduleDefinitions.find(
|
|
308
|
+
(def) => def.moduleName === credential.type
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
if (!moduleDefinition) {
|
|
312
|
+
throw Boom.badRequest(
|
|
313
|
+
`Unknown credential type: ${credential.type}`
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Create API instance directly from credential
|
|
318
|
+
const ModuleDefinition = moduleDefinition.definition;
|
|
319
|
+
const api = new ModuleDefinition.Api(credential);
|
|
320
|
+
|
|
321
|
+
return { api };
|
|
322
|
+
} catch (error) {
|
|
323
|
+
if (Boom.isBoom(error)) {
|
|
324
|
+
throw error;
|
|
325
|
+
}
|
|
326
|
+
throw Boom.internal(
|
|
327
|
+
'Failed to initialize API client from credential',
|
|
328
|
+
error
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Sensitive headers that should be stripped from outgoing requests
|
|
335
|
+
* @private
|
|
336
|
+
*/
|
|
337
|
+
static SENSITIVE_REQUEST_HEADERS = ['authorization', 'cookie', 'x-api-key'];
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Sensitive headers that should be stripped from upstream responses
|
|
341
|
+
* @private
|
|
342
|
+
*/
|
|
343
|
+
static SENSITIVE_RESPONSE_HEADERS = [
|
|
344
|
+
'authorization',
|
|
345
|
+
'set-cookie',
|
|
346
|
+
'x-api-key',
|
|
347
|
+
];
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Strip sensitive headers from request headers
|
|
351
|
+
* @private
|
|
352
|
+
* @param {Object} headers - Request headers
|
|
353
|
+
* @returns {Object} Sanitized headers
|
|
354
|
+
*/
|
|
355
|
+
_sanitizeRequestHeaders(headers) {
|
|
356
|
+
if (!headers) return {};
|
|
357
|
+
const sanitized = { ...headers };
|
|
358
|
+
for (const key of Object.keys(sanitized)) {
|
|
359
|
+
if (
|
|
360
|
+
ExecuteProxyRequest.SENSITIVE_REQUEST_HEADERS.includes(
|
|
361
|
+
key.toLowerCase()
|
|
362
|
+
)
|
|
363
|
+
) {
|
|
364
|
+
delete sanitized[key];
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return sanitized;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Strip sensitive headers from response headers
|
|
372
|
+
* @private
|
|
373
|
+
* @param {Object} headers - Response headers
|
|
374
|
+
* @returns {Object} Sanitized headers
|
|
375
|
+
*/
|
|
376
|
+
_sanitizeResponseHeaders(headers) {
|
|
377
|
+
if (!headers) return {};
|
|
378
|
+
const sanitized = { ...headers };
|
|
379
|
+
for (const key of Object.keys(sanitized)) {
|
|
380
|
+
if (
|
|
381
|
+
ExecuteProxyRequest.SENSITIVE_RESPONSE_HEADERS.includes(
|
|
382
|
+
key.toLowerCase()
|
|
383
|
+
)
|
|
384
|
+
) {
|
|
385
|
+
delete sanitized[key];
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return sanitized;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Execute the actual proxy request through the API client
|
|
393
|
+
*
|
|
394
|
+
* @private
|
|
395
|
+
* @param {Object} apiClient - API client instance (Requester)
|
|
396
|
+
* @param {Object} proxyRequest - Proxy request parameters
|
|
397
|
+
* @returns {Promise<Object>} Formatted proxy response
|
|
398
|
+
*/
|
|
399
|
+
async _executeProxyRequest(apiClient, proxyRequest) {
|
|
400
|
+
try {
|
|
401
|
+
// Sanitize request headers (strip Authorization, etc.)
|
|
402
|
+
const sanitizedHeaders = this._sanitizeRequestHeaders(
|
|
403
|
+
proxyRequest.headers
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
// Make the upstream API request
|
|
407
|
+
const upstreamResponse = await apiClient.request({
|
|
408
|
+
method: proxyRequest.method,
|
|
409
|
+
url: proxyRequest.path,
|
|
410
|
+
query: proxyRequest.query,
|
|
411
|
+
headers: sanitizedHeaders,
|
|
412
|
+
body: proxyRequest.body,
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
// Return successful response with sanitized headers
|
|
416
|
+
return {
|
|
417
|
+
success: true,
|
|
418
|
+
status: upstreamResponse.status,
|
|
419
|
+
headers: this._sanitizeResponseHeaders(
|
|
420
|
+
upstreamResponse.headers
|
|
421
|
+
),
|
|
422
|
+
data: upstreamResponse.data,
|
|
423
|
+
};
|
|
424
|
+
} catch (error) {
|
|
425
|
+
// Map upstream errors to proxy error responses
|
|
426
|
+
return this._mapUpstreamError(error);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Map upstream API errors to standardized proxy error responses
|
|
432
|
+
*
|
|
433
|
+
* @private
|
|
434
|
+
* @param {Error} error - Upstream error
|
|
435
|
+
* @returns {Object} Formatted error response
|
|
436
|
+
* @throws {Boom} Rethrows as Boom error with appropriate status
|
|
437
|
+
*/
|
|
438
|
+
_mapUpstreamError(error) {
|
|
439
|
+
// Check if this is a timeout error
|
|
440
|
+
if (error.code === 'ETIMEDOUT' || error.type === 'request-timeout') {
|
|
441
|
+
throw Boom.gatewayTimeout('Request to upstream API timed out', {
|
|
442
|
+
code: 'TIMEOUT',
|
|
443
|
+
details: null,
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Check if this is a network error
|
|
448
|
+
if (
|
|
449
|
+
error.code === 'ENOTFOUND' ||
|
|
450
|
+
error.code === 'ECONNREFUSED' ||
|
|
451
|
+
error.type === 'system'
|
|
452
|
+
) {
|
|
453
|
+
throw Boom.badGateway('Failed to connect to upstream API', {
|
|
454
|
+
code: 'NETWORK_ERROR',
|
|
455
|
+
details: {
|
|
456
|
+
error: error.message,
|
|
457
|
+
},
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Check if we have an HTTP response from upstream
|
|
462
|
+
if (!error.response) {
|
|
463
|
+
// Unknown error without response
|
|
464
|
+
throw Boom.internal('Unexpected error calling upstream API', {
|
|
465
|
+
code: 'UNKNOWN_ERROR',
|
|
466
|
+
details: {
|
|
467
|
+
error: error.message,
|
|
468
|
+
},
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const { status, data } = error.response;
|
|
473
|
+
|
|
474
|
+
// Map by status code
|
|
475
|
+
switch (status) {
|
|
476
|
+
case 401: {
|
|
477
|
+
// Check if this is specifically a token expiration
|
|
478
|
+
const isExpired =
|
|
479
|
+
data?.error === 'token_expired' ||
|
|
480
|
+
data?.error === 'expired_token' ||
|
|
481
|
+
(data?.error_description &&
|
|
482
|
+
data.error_description
|
|
483
|
+
.toLowerCase()
|
|
484
|
+
.includes('expired'));
|
|
485
|
+
|
|
486
|
+
const code = isExpired ? 'EXPIRED_TOKEN' : 'INVALID_AUTH';
|
|
487
|
+
const message = isExpired
|
|
488
|
+
? 'Access token has expired'
|
|
489
|
+
: 'Authentication credentials are invalid or expired';
|
|
490
|
+
|
|
491
|
+
// Note: Boom.unauthorized(message, scheme, attributes) - second param is WWW-Authenticate scheme
|
|
492
|
+
// We pass null for scheme and set data manually
|
|
493
|
+
const boomError = Boom.unauthorized(message);
|
|
494
|
+
boomError.data = {
|
|
495
|
+
code,
|
|
496
|
+
details: data,
|
|
497
|
+
upstreamStatus: status,
|
|
498
|
+
};
|
|
499
|
+
throw boomError;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
case 403:
|
|
503
|
+
throw Boom.forbidden(
|
|
504
|
+
'Insufficient permissions for this operation',
|
|
505
|
+
{
|
|
506
|
+
code: 'PERMISSION_DENIED',
|
|
507
|
+
details: data,
|
|
508
|
+
upstreamStatus: status,
|
|
509
|
+
}
|
|
510
|
+
);
|
|
511
|
+
|
|
512
|
+
case 404:
|
|
513
|
+
throw Boom.notFound('Resource not found', {
|
|
514
|
+
code: 'NOT_FOUND',
|
|
515
|
+
details: data,
|
|
516
|
+
upstreamStatus: status,
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
case 429:
|
|
520
|
+
throw Boom.tooManyRequests('Rate limit exceeded for this API', {
|
|
521
|
+
code: 'RATE_LIMITED',
|
|
522
|
+
details: data,
|
|
523
|
+
upstreamStatus: status,
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
case 503:
|
|
527
|
+
throw Boom.serverUnavailable(
|
|
528
|
+
'Upstream service is unavailable',
|
|
529
|
+
{
|
|
530
|
+
code: 'SERVICE_UNAVAILABLE',
|
|
531
|
+
details: data,
|
|
532
|
+
upstreamStatus: status,
|
|
533
|
+
}
|
|
534
|
+
);
|
|
535
|
+
|
|
536
|
+
default: {
|
|
537
|
+
// For all other errors (400, 500, etc.)
|
|
538
|
+
const boomError =
|
|
539
|
+
status >= 500
|
|
540
|
+
? Boom.internal('Upstream API returned an error', {
|
|
541
|
+
code: 'UPSTREAM_ERROR',
|
|
542
|
+
details: data,
|
|
543
|
+
upstreamStatus: status,
|
|
544
|
+
})
|
|
545
|
+
: Boom.badRequest('Upstream API returned an error', {
|
|
546
|
+
code: 'UPSTREAM_ERROR',
|
|
547
|
+
details: data,
|
|
548
|
+
upstreamStatus: status,
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
// Override status to match upstream
|
|
552
|
+
boomError.output.statusCode = status;
|
|
553
|
+
// For 5xx errors, Boom.internal uses a generic message, so override it
|
|
554
|
+
if (status >= 500) {
|
|
555
|
+
boomError.output.payload.message =
|
|
556
|
+
'Upstream API returned an error';
|
|
557
|
+
}
|
|
558
|
+
throw boomError;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
module.exports = { ExecuteProxyRequest };
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// Removed Integration wrapper - using IntegrationBase directly
|
|
2
|
-
const {
|
|
2
|
+
const {
|
|
3
|
+
mapIntegrationClassToIntegrationDTO,
|
|
4
|
+
} = require('../utils/map-integration-dto');
|
|
3
5
|
const Boom = require('@hapi/boom');
|
|
4
6
|
|
|
5
7
|
/**
|
|
@@ -15,8 +17,12 @@ class GetIntegrationForUser {
|
|
|
15
17
|
* @param {import('../../modules/module-factory').ModuleFactory} params.moduleFactory - Service for module instantiation and management.
|
|
16
18
|
* @param {import('../../modules/module-repository-interface').ModuleRepositoryInterface} params.moduleRepository - Repository for module and entity data operations.
|
|
17
19
|
*/
|
|
18
|
-
constructor({
|
|
19
|
-
|
|
20
|
+
constructor({
|
|
21
|
+
integrationRepository,
|
|
22
|
+
integrationClasses,
|
|
23
|
+
moduleFactory,
|
|
24
|
+
moduleRepository,
|
|
25
|
+
}) {
|
|
20
26
|
/**
|
|
21
27
|
* @type {import('../integration-repository-interface').IntegrationRepositoryInterface}
|
|
22
28
|
*/
|
|
@@ -36,19 +42,28 @@ class GetIntegrationForUser {
|
|
|
36
42
|
* @throws {Boom.forbidden} When user does not have access to the integration.
|
|
37
43
|
*/
|
|
38
44
|
async execute(integrationId, userId) {
|
|
39
|
-
const integrationRecord =
|
|
40
|
-
|
|
45
|
+
const integrationRecord =
|
|
46
|
+
await this.integrationRepository.findIntegrationById(integrationId);
|
|
47
|
+
const entities = await this.moduleRepository.findEntitiesByIds(
|
|
48
|
+
integrationRecord.entitiesIds
|
|
49
|
+
);
|
|
41
50
|
|
|
42
51
|
if (!integrationRecord) {
|
|
43
|
-
throw Boom.notFound(
|
|
52
|
+
throw Boom.notFound(
|
|
53
|
+
`Integration with id of ${integrationId} does not exist`
|
|
54
|
+
);
|
|
44
55
|
}
|
|
45
56
|
|
|
46
57
|
if (integrationRecord.userId.toString() !== userId.toString()) {
|
|
47
|
-
throw Boom.forbidden(
|
|
58
|
+
throw Boom.forbidden(
|
|
59
|
+
'User does not have access to this integration'
|
|
60
|
+
);
|
|
48
61
|
}
|
|
49
62
|
|
|
50
63
|
const integrationClass = this.integrationClasses.find(
|
|
51
|
-
(integrationClass) =>
|
|
64
|
+
(integrationClass) =>
|
|
65
|
+
integrationClass.Definition.name ===
|
|
66
|
+
integrationRecord.config.type
|
|
52
67
|
);
|
|
53
68
|
|
|
54
69
|
const modules = [];
|
|
@@ -68,11 +83,11 @@ class GetIntegrationForUser {
|
|
|
68
83
|
status: integrationRecord.status,
|
|
69
84
|
version: integrationRecord.version,
|
|
70
85
|
messages: integrationRecord.messages,
|
|
71
|
-
modules
|
|
86
|
+
modules,
|
|
72
87
|
});
|
|
73
88
|
|
|
74
89
|
return mapIntegrationClassToIntegrationDTO(integrationInstance);
|
|
75
90
|
}
|
|
76
91
|
}
|
|
77
92
|
|
|
78
|
-
module.exports = { GetIntegrationForUser };
|
|
93
|
+
module.exports = { GetIntegrationForUser };
|
|
@@ -14,7 +14,6 @@ class GetIntegrationInstanceByDefinition {
|
|
|
14
14
|
* @param {import('../../modules/module-repository-interface').ModuleRepositoryInterface} params.moduleRepository - Repository for module and entity data operations.
|
|
15
15
|
*/
|
|
16
16
|
constructor({ integrationRepository, moduleFactory, moduleRepository }) {
|
|
17
|
-
|
|
18
17
|
/**
|
|
19
18
|
* @type {import('../integration-repository-interface').IntegrationRepositoryInterface}
|
|
20
19
|
*/
|
|
@@ -30,13 +29,20 @@ class GetIntegrationInstanceByDefinition {
|
|
|
30
29
|
* @throws {Boom.notFound} When integration with the specified definition does not exist.
|
|
31
30
|
*/
|
|
32
31
|
async execute(integrationClass) {
|
|
33
|
-
const integrationRecord =
|
|
32
|
+
const integrationRecord =
|
|
33
|
+
await this.integrationRepository.findIntegrationByName(
|
|
34
|
+
integrationClass.Definition.name
|
|
35
|
+
);
|
|
34
36
|
|
|
35
37
|
if (!integrationRecord) {
|
|
36
|
-
throw Boom.notFound(
|
|
38
|
+
throw Boom.notFound(
|
|
39
|
+
`Integration with name of ${integrationClass.Definition.name} does not exist`
|
|
40
|
+
);
|
|
37
41
|
}
|
|
38
42
|
|
|
39
|
-
const entities = await this.moduleRepository.findEntitiesByIds(
|
|
43
|
+
const entities = await this.moduleRepository.findEntitiesByIds(
|
|
44
|
+
integrationRecord.entitiesIds
|
|
45
|
+
);
|
|
40
46
|
|
|
41
47
|
const modules = [];
|
|
42
48
|
for (const entity of entities) {
|
|
@@ -55,13 +61,13 @@ class GetIntegrationInstanceByDefinition {
|
|
|
55
61
|
status: integrationRecord.status,
|
|
56
62
|
version: integrationRecord.version,
|
|
57
63
|
messages: integrationRecord.messages,
|
|
58
|
-
modules
|
|
64
|
+
modules,
|
|
59
65
|
});
|
|
60
66
|
|
|
61
67
|
await integrationInstance.initialize();
|
|
62
68
|
|
|
63
|
-
return integrationInstance
|
|
69
|
+
return integrationInstance;
|
|
64
70
|
}
|
|
65
71
|
}
|
|
66
72
|
|
|
67
|
-
module.exports = { GetIntegrationInstanceByDefinition };
|
|
73
|
+
module.exports = { GetIntegrationInstanceByDefinition };
|
|
@@ -71,7 +71,10 @@ class GetIntegrationsForUser {
|
|
|
71
71
|
config: integrationRecord.config,
|
|
72
72
|
status: integrationRecord.status,
|
|
73
73
|
version: integrationRecord.version,
|
|
74
|
-
messages: integrationRecord.messages || {
|
|
74
|
+
messages: integrationRecord.messages || {
|
|
75
|
+
errors: [],
|
|
76
|
+
warnings: [],
|
|
77
|
+
},
|
|
75
78
|
modules,
|
|
76
79
|
options: integrationClass.getOptionDetails(),
|
|
77
80
|
};
|