@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
package/core/Worker.js
CHANGED
|
@@ -1,19 +1,41 @@
|
|
|
1
|
-
const { SQSClient, GetQueueUrlCommand, SendMessageCommand } = require('@aws-sdk/client-sqs');
|
|
2
1
|
const _ = require('lodash');
|
|
3
2
|
const { RequiredPropertyError } = require('../errors');
|
|
4
3
|
const { get } = require('../assertions');
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Worker - Queue message producer/consumer base class.
|
|
7
|
+
*
|
|
8
|
+
* Subclass and override _run() to implement your worker logic.
|
|
9
|
+
* The queue transport (SQS, Netlify, etc.) is abstracted behind
|
|
10
|
+
* QueueClientInterface, injected via constructor options.
|
|
11
|
+
*
|
|
12
|
+
* BREAKING CHANGE (v3): A queueClient must be explicitly provided.
|
|
13
|
+
* For AWS/SQS, pass `new SqsQueueClient()` from @friggframework/provider-aws.
|
|
14
|
+
* See docs/architecture-decisions/010-decouple-aws-from-core.md for migration guide.
|
|
15
|
+
*/
|
|
8
16
|
class Worker {
|
|
17
|
+
constructor(options = {}) {
|
|
18
|
+
this._queueClient = options.queueClient || null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Get the queue client. Throws if none was injected.
|
|
23
|
+
* @returns {QueueClientInterface}
|
|
24
|
+
*/
|
|
25
|
+
_getQueueClient() {
|
|
26
|
+
if (!this._queueClient) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
'Worker requires a queueClient. Pass one via constructor options, e.g.:\n' +
|
|
29
|
+
' const { SqsQueueClient } = require("@friggframework/provider-aws");\n' +
|
|
30
|
+
' new MyWorker({ queueClient: new SqsQueueClient() })\n' +
|
|
31
|
+
'See docs/architecture-decisions/010-decouple-aws-from-core.md for migration guide.'
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
return this._queueClient;
|
|
35
|
+
}
|
|
36
|
+
|
|
9
37
|
async getQueueURL(params) {
|
|
10
|
-
|
|
11
|
-
// let params = {
|
|
12
|
-
// QueueName: process.env.QueueName
|
|
13
|
-
// };
|
|
14
|
-
const command = new GetQueueUrlCommand(params);
|
|
15
|
-
const data = await sqs.send(command);
|
|
16
|
-
return data.QueueUrl;
|
|
38
|
+
return this._getQueueClient().getQueueUrl(params);
|
|
17
39
|
}
|
|
18
40
|
|
|
19
41
|
async run(params, context = {}) {
|
|
@@ -47,8 +69,7 @@ class Worker {
|
|
|
47
69
|
}
|
|
48
70
|
|
|
49
71
|
async sendAsyncSQSMessage(params) {
|
|
50
|
-
const
|
|
51
|
-
const data = await sqs.send(command);
|
|
72
|
+
const data = await this._getQueueClient().sendMessage(params);
|
|
52
73
|
return data.MessageId;
|
|
53
74
|
}
|
|
54
75
|
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const Boom = require('@hapi/boom');
|
|
3
|
+
const catchAsyncError = require('express-async-handler');
|
|
4
|
+
const {
|
|
5
|
+
createCredentialRepository,
|
|
6
|
+
} = require('./repositories/credential-repository-factory');
|
|
7
|
+
const {
|
|
8
|
+
createModuleRepository,
|
|
9
|
+
} = require('../modules/repositories/module-repository-factory');
|
|
10
|
+
const {
|
|
11
|
+
ListCredentialsForUser,
|
|
12
|
+
} = require('./use-cases/list-credentials-for-user');
|
|
13
|
+
const { GetCredentialForUser } = require('./use-cases/get-credential-for-user');
|
|
14
|
+
const {
|
|
15
|
+
DeleteCredentialForUser,
|
|
16
|
+
} = require('./use-cases/delete-credential-for-user');
|
|
17
|
+
const { ReauthorizeCredential } = require('./use-cases/reauthorize-credential');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Boom Error Handler Middleware
|
|
21
|
+
* Handles Boom errors and converts them to appropriate HTTP responses
|
|
22
|
+
* @param {Error} err - Error object
|
|
23
|
+
* @param {Request} req - Express request
|
|
24
|
+
* @param {Response} res - Express response
|
|
25
|
+
* @param {Function} next - Express next function
|
|
26
|
+
*/
|
|
27
|
+
function boomErrorHandler(err, req, res, next) {
|
|
28
|
+
// Handle Boom errors
|
|
29
|
+
if (err.isBoom) {
|
|
30
|
+
return res.status(err.output.statusCode).json({
|
|
31
|
+
error: err.output.payload.message,
|
|
32
|
+
statusCode: err.output.statusCode,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Handle generic errors (500)
|
|
37
|
+
console.error('Unexpected error:', err);
|
|
38
|
+
return res.status(500).json({
|
|
39
|
+
error: 'Internal server error',
|
|
40
|
+
statusCode: 500,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Create Credential Router
|
|
46
|
+
* Factory function that creates an Express router with credential management endpoints
|
|
47
|
+
*
|
|
48
|
+
* Endpoints:
|
|
49
|
+
* - GET /api/credentials - List all credentials for authenticated user
|
|
50
|
+
* - GET /api/credentials/:id - Get a single credential
|
|
51
|
+
* - DELETE /api/credentials/:id - Delete a credential
|
|
52
|
+
* - POST /api/credentials/:id/reauthorize - Reauthorize a credential
|
|
53
|
+
*
|
|
54
|
+
* Security:
|
|
55
|
+
* - All endpoints require authentication (via authenticateUser middleware)
|
|
56
|
+
* - Credentials are filtered to only belong to the authenticated user
|
|
57
|
+
* - Sensitive data (tokens) are filtered from responses
|
|
58
|
+
*
|
|
59
|
+
* @returns {express.Router} Configured Express router
|
|
60
|
+
*/
|
|
61
|
+
function createCredentialRouter() {
|
|
62
|
+
const router = express.Router();
|
|
63
|
+
|
|
64
|
+
// Load configuration and create repositories
|
|
65
|
+
const credentialRepository = createCredentialRepository();
|
|
66
|
+
const moduleRepository = createModuleRepository();
|
|
67
|
+
|
|
68
|
+
// Create credential use cases
|
|
69
|
+
const listCredentialsForUser = new ListCredentialsForUser({
|
|
70
|
+
credentialRepository,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const getCredentialForUser = new GetCredentialForUser({
|
|
74
|
+
credentialRepository,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const deleteCredentialForUser = new DeleteCredentialForUser({
|
|
78
|
+
credentialRepository,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const reauthorizeCredential = new ReauthorizeCredential({
|
|
82
|
+
credentialRepository,
|
|
83
|
+
moduleRepository,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Filter sensitive data from credential objects
|
|
88
|
+
* Removes tokens and other sensitive fields from credentials before sending to client
|
|
89
|
+
*
|
|
90
|
+
* @param {Object|Array} credentials - Credential(s) to filter
|
|
91
|
+
* @returns {Object|Array} Filtered credential(s)
|
|
92
|
+
*/
|
|
93
|
+
function filterSensitiveData(credentials) {
|
|
94
|
+
const filter = (cred) => {
|
|
95
|
+
if (!cred) return cred;
|
|
96
|
+
|
|
97
|
+
// Create a copy without sensitive fields
|
|
98
|
+
const {
|
|
99
|
+
data,
|
|
100
|
+
access_token,
|
|
101
|
+
refresh_token,
|
|
102
|
+
id_token,
|
|
103
|
+
domain,
|
|
104
|
+
...safeCredential
|
|
105
|
+
} = cred;
|
|
106
|
+
|
|
107
|
+
// Ensure we have timestamps in ISO format
|
|
108
|
+
if (
|
|
109
|
+
safeCredential.createdAt &&
|
|
110
|
+
!(safeCredential.createdAt instanceof Date)
|
|
111
|
+
) {
|
|
112
|
+
safeCredential.createdAt = new Date(
|
|
113
|
+
safeCredential.createdAt
|
|
114
|
+
).toISOString();
|
|
115
|
+
}
|
|
116
|
+
if (
|
|
117
|
+
safeCredential.updatedAt &&
|
|
118
|
+
!(safeCredential.updatedAt instanceof Date)
|
|
119
|
+
) {
|
|
120
|
+
safeCredential.updatedAt = new Date(
|
|
121
|
+
safeCredential.updatedAt
|
|
122
|
+
).toISOString();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return safeCredential;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
return Array.isArray(credentials)
|
|
129
|
+
? credentials.map(filter)
|
|
130
|
+
: filter(credentials);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// GET /api/credentials - List all credentials for authenticated user
|
|
134
|
+
router.get(
|
|
135
|
+
'/',
|
|
136
|
+
catchAsyncError(async (req, res) => {
|
|
137
|
+
// Expect authentication middleware to have set req.user
|
|
138
|
+
if (!req.user) {
|
|
139
|
+
throw Boom.unauthorized('Authentication required');
|
|
140
|
+
}
|
|
141
|
+
const userId =
|
|
142
|
+
typeof req.user.getId === 'function'
|
|
143
|
+
? req.user.getId()
|
|
144
|
+
: req.user.id;
|
|
145
|
+
|
|
146
|
+
const credentials = await listCredentialsForUser.execute(userId);
|
|
147
|
+
|
|
148
|
+
// Filter out sensitive data before responding
|
|
149
|
+
const safeCredentials = filterSensitiveData(credentials);
|
|
150
|
+
|
|
151
|
+
res.json({ credentials: safeCredentials });
|
|
152
|
+
})
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
// GET /api/credentials/:id - Get a single credential
|
|
156
|
+
router.get(
|
|
157
|
+
'/:id',
|
|
158
|
+
catchAsyncError(async (req, res) => {
|
|
159
|
+
if (!req.user) {
|
|
160
|
+
throw Boom.unauthorized('Authentication required');
|
|
161
|
+
}
|
|
162
|
+
const userId =
|
|
163
|
+
typeof req.user.getId === 'function'
|
|
164
|
+
? req.user.getId()
|
|
165
|
+
: req.user.id;
|
|
166
|
+
const credentialId = req.params.id;
|
|
167
|
+
|
|
168
|
+
const credential = await getCredentialForUser.execute(
|
|
169
|
+
credentialId,
|
|
170
|
+
userId
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
// Filter out sensitive data before responding
|
|
174
|
+
const safeCredential = filterSensitiveData(credential);
|
|
175
|
+
|
|
176
|
+
res.json(safeCredential);
|
|
177
|
+
})
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
// DELETE /api/credentials/:id - Delete a credential
|
|
181
|
+
router.delete(
|
|
182
|
+
'/:id',
|
|
183
|
+
catchAsyncError(async (req, res) => {
|
|
184
|
+
if (!req.user) {
|
|
185
|
+
throw Boom.unauthorized('Authentication required');
|
|
186
|
+
}
|
|
187
|
+
const userId =
|
|
188
|
+
typeof req.user.getId === 'function'
|
|
189
|
+
? req.user.getId()
|
|
190
|
+
: req.user.id;
|
|
191
|
+
const credentialId = req.params.id;
|
|
192
|
+
|
|
193
|
+
const result = await deleteCredentialForUser.execute(
|
|
194
|
+
credentialId,
|
|
195
|
+
userId
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
// Check if deletion was successful
|
|
199
|
+
if (result.deletedCount === 0) {
|
|
200
|
+
throw Boom.internal('Failed to delete credential');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
res.json({
|
|
204
|
+
success: true,
|
|
205
|
+
message: `Credential ${credentialId} deleted successfully`,
|
|
206
|
+
});
|
|
207
|
+
})
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
// POST /api/credentials/:id/reauthorize - Reauthorize a credential
|
|
211
|
+
router.post(
|
|
212
|
+
'/:id/reauthorize',
|
|
213
|
+
catchAsyncError(async (req, res) => {
|
|
214
|
+
if (!req.user) {
|
|
215
|
+
throw Boom.unauthorized('Authentication required');
|
|
216
|
+
}
|
|
217
|
+
const userId =
|
|
218
|
+
typeof req.user.getId === 'function'
|
|
219
|
+
? req.user.getId()
|
|
220
|
+
: req.user.id;
|
|
221
|
+
const credentialId = req.params.id;
|
|
222
|
+
|
|
223
|
+
// Validate request body
|
|
224
|
+
if (!req.body.data) {
|
|
225
|
+
throw Boom.badRequest('data is required in request body');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Get step and sessionId from request
|
|
229
|
+
const step = parseInt(req.body.step || '1', 10);
|
|
230
|
+
const sessionId = req.body.sessionId;
|
|
231
|
+
|
|
232
|
+
// Validate step is a positive integer
|
|
233
|
+
if (step < 1 || !Number.isInteger(step)) {
|
|
234
|
+
throw Boom.badRequest('step must be a positive integer');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Validate sessionId is present for steps > 1
|
|
238
|
+
if (step > 1 && !sessionId) {
|
|
239
|
+
throw Boom.badRequest('sessionId is required for step > 1');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Execute the reauthorization
|
|
243
|
+
const result = await reauthorizeCredential.execute(
|
|
244
|
+
credentialId,
|
|
245
|
+
userId,
|
|
246
|
+
req.body.data,
|
|
247
|
+
step,
|
|
248
|
+
sessionId
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
res.json(result);
|
|
252
|
+
})
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
// Boom error handler middleware
|
|
256
|
+
// Must be added after all routes to catch errors from handlers
|
|
257
|
+
router.use(boomErrorHandler);
|
|
258
|
+
|
|
259
|
+
return router;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
module.exports = { createCredentialRouter, boomErrorHandler };
|
|
@@ -3,6 +3,7 @@ const {
|
|
|
3
3
|
toObjectId,
|
|
4
4
|
fromObjectId,
|
|
5
5
|
findOne,
|
|
6
|
+
findMany,
|
|
6
7
|
insertOne,
|
|
7
8
|
updateOne,
|
|
8
9
|
deleteOne,
|
|
@@ -106,7 +107,10 @@ class CredentialRepositoryDocumentDB extends CredentialRepositoryInterface {
|
|
|
106
107
|
const updateDocument = {
|
|
107
108
|
userId: existing.userId,
|
|
108
109
|
externalId: existing.externalId,
|
|
109
|
-
authIsValid:
|
|
110
|
+
authIsValid:
|
|
111
|
+
authIsValid !== undefined
|
|
112
|
+
? authIsValid
|
|
113
|
+
: existing.authIsValid,
|
|
110
114
|
data: mergedData,
|
|
111
115
|
updatedAt: now,
|
|
112
116
|
};
|
|
@@ -172,8 +176,50 @@ class CredentialRepositoryDocumentDB extends CredentialRepositoryInterface {
|
|
|
172
176
|
return this._mapCredential(decryptedCredential);
|
|
173
177
|
}
|
|
174
178
|
|
|
179
|
+
/**
|
|
180
|
+
* Find credential(s) by filter criteria
|
|
181
|
+
*
|
|
182
|
+
* When filter includes only userId, returns an array of all credentials for that user
|
|
183
|
+
* When filter includes credentialId or externalId, returns a single credential or null
|
|
184
|
+
*
|
|
185
|
+
* @param {Object} filter
|
|
186
|
+
* @param {string} [filter.userId] - User ID
|
|
187
|
+
* @param {string} [filter.externalId] - External ID
|
|
188
|
+
* @param {string} [filter.credentialId] - Credential ID
|
|
189
|
+
* @returns {Promise<Array|Object|null>} Credential array, single credential, or null
|
|
190
|
+
*/
|
|
175
191
|
async findCredential(filter) {
|
|
176
192
|
const query = this._buildFilter(filter);
|
|
193
|
+
|
|
194
|
+
// If filtering by userId only, return all credentials for that user
|
|
195
|
+
const hasOnlyUserId =
|
|
196
|
+
filter.userId &&
|
|
197
|
+
!filter.credentialId &&
|
|
198
|
+
!filter.externalId &&
|
|
199
|
+
!filter.id;
|
|
200
|
+
|
|
201
|
+
if (hasOnlyUserId) {
|
|
202
|
+
const credentials = await findMany(
|
|
203
|
+
this.prisma,
|
|
204
|
+
'Credential',
|
|
205
|
+
query
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
const decryptedCredentials = await Promise.all(
|
|
209
|
+
credentials.map(async (credential) => {
|
|
210
|
+
const decrypted =
|
|
211
|
+
await this.encryptionService.decryptFields(
|
|
212
|
+
'Credential',
|
|
213
|
+
credential
|
|
214
|
+
);
|
|
215
|
+
return this._mapCredentialWithMetadata(decrypted);
|
|
216
|
+
})
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
return decryptedCredentials;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Otherwise, find single credential
|
|
177
223
|
const credential = await findOne(this.prisma, 'Credential', query);
|
|
178
224
|
if (!credential) return null;
|
|
179
225
|
|
|
@@ -299,6 +345,30 @@ class CredentialRepositoryDocumentDB extends CredentialRepositoryInterface {
|
|
|
299
345
|
...data,
|
|
300
346
|
};
|
|
301
347
|
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Map credential document with metadata (for list views)
|
|
351
|
+
* Includes timestamps and additional fields needed by API
|
|
352
|
+
* @private
|
|
353
|
+
*/
|
|
354
|
+
_mapCredentialWithMetadata(doc) {
|
|
355
|
+
const data = doc?.data || {};
|
|
356
|
+
const id = fromObjectId(doc?._id);
|
|
357
|
+
const userId = doc?.userId;
|
|
358
|
+
return {
|
|
359
|
+
id,
|
|
360
|
+
type: doc?.type,
|
|
361
|
+
userId,
|
|
362
|
+
externalId: doc?.externalId ?? null,
|
|
363
|
+
authIsValid: doc?.authIsValid ?? null,
|
|
364
|
+
entityCount: doc?.entityCount,
|
|
365
|
+
createdAt: doc?.createdAt,
|
|
366
|
+
updatedAt: doc?.updatedAt,
|
|
367
|
+
access_token: data.access_token,
|
|
368
|
+
refresh_token: data.refresh_token,
|
|
369
|
+
...data,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
302
372
|
}
|
|
303
373
|
|
|
304
374
|
module.exports = { CredentialRepositoryDocumentDB };
|
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
const { CredentialRepositoryMongo } = require('./credential-repository-mongo');
|
|
2
1
|
const {
|
|
3
2
|
CredentialRepositoryPostgres,
|
|
4
3
|
} = require('./credential-repository-postgres');
|
|
5
|
-
const {
|
|
6
|
-
CredentialRepositoryDocumentDB,
|
|
7
|
-
} = require('./credential-repository-documentdb');
|
|
8
4
|
const config = require('../../database/config');
|
|
9
5
|
|
|
10
6
|
/**
|
|
@@ -30,12 +26,14 @@ function createCredentialRepository() {
|
|
|
30
26
|
|
|
31
27
|
switch (dbType) {
|
|
32
28
|
case 'mongodb':
|
|
29
|
+
const { CredentialRepositoryMongo } = require('./credential-repository-mongo');
|
|
33
30
|
return new CredentialRepositoryMongo();
|
|
34
31
|
|
|
35
32
|
case 'postgresql':
|
|
36
33
|
return new CredentialRepositoryPostgres();
|
|
37
34
|
|
|
38
35
|
case 'documentdb':
|
|
36
|
+
const { CredentialRepositoryDocumentDB } = require('./credential-repository-documentdb');
|
|
39
37
|
return new CredentialRepositoryDocumentDB();
|
|
40
38
|
|
|
41
39
|
default:
|
|
@@ -48,7 +46,7 @@ function createCredentialRepository() {
|
|
|
48
46
|
module.exports = {
|
|
49
47
|
createCredentialRepository,
|
|
50
48
|
// Export adapters for direct testing
|
|
51
|
-
CredentialRepositoryMongo,
|
|
49
|
+
get CredentialRepositoryMongo() { return require('./credential-repository-mongo').CredentialRepositoryMongo; },
|
|
52
50
|
CredentialRepositoryPostgres,
|
|
53
|
-
CredentialRepositoryDocumentDB,
|
|
51
|
+
get CredentialRepositoryDocumentDB() { return require('./credential-repository-documentdb').CredentialRepositoryDocumentDB; },
|
|
54
52
|
};
|
|
@@ -68,10 +68,16 @@ class CredentialRepositoryInterface {
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
/**
|
|
71
|
-
* Find
|
|
71
|
+
* Find credential(s) by filter criteria
|
|
72
|
+
*
|
|
73
|
+
* When filter includes only userId, returns an array of all credentials for that user
|
|
74
|
+
* When filter includes credentialId or externalId, returns a single credential or null
|
|
72
75
|
*
|
|
73
76
|
* @param {Object} filter - Filter criteria
|
|
74
|
-
* @
|
|
77
|
+
* @param {string} [filter.userId] - User ID
|
|
78
|
+
* @param {string} [filter.externalId] - External ID
|
|
79
|
+
* @param {string} [filter.credentialId] - Credential ID
|
|
80
|
+
* @returns {Promise<Array|Object|null>} Credential array, single credential, or null
|
|
75
81
|
* @abstract
|
|
76
82
|
*/
|
|
77
83
|
async findCredential(filter) {
|
|
@@ -121,7 +121,10 @@ class CredentialRepositoryMongo extends CredentialRepositoryInterface {
|
|
|
121
121
|
data: {
|
|
122
122
|
userId: existing.userId,
|
|
123
123
|
externalId: existing.externalId,
|
|
124
|
-
authIsValid:
|
|
124
|
+
authIsValid:
|
|
125
|
+
authIsValid !== undefined
|
|
126
|
+
? authIsValid
|
|
127
|
+
: existing.authIsValid,
|
|
125
128
|
data: mergedData,
|
|
126
129
|
},
|
|
127
130
|
});
|
|
@@ -154,18 +157,52 @@ class CredentialRepositoryMongo extends CredentialRepositoryInterface {
|
|
|
154
157
|
}
|
|
155
158
|
|
|
156
159
|
/**
|
|
157
|
-
* Find
|
|
158
|
-
* Replaces: Credential.findOne(query)
|
|
160
|
+
* Find credential(s) by filter criteria
|
|
161
|
+
* Replaces: Credential.find(query) or Credential.findOne(query)
|
|
162
|
+
*
|
|
163
|
+
* When filter includes only userId, returns an array of all credentials for that user
|
|
164
|
+
* When filter includes credentialId or externalId, returns a single credential or null
|
|
159
165
|
*
|
|
160
166
|
* @param {Object} filter
|
|
161
167
|
* @param {string} [filter.userId] - User ID
|
|
162
168
|
* @param {string} [filter.externalId] - External ID
|
|
163
169
|
* @param {string} [filter.credentialId] - Credential ID
|
|
164
|
-
* @returns {Promise<Object|null>} Credential
|
|
170
|
+
* @returns {Promise<Array|Object|null>} Credential array, single credential, or null
|
|
165
171
|
*/
|
|
166
172
|
async findCredential(filter) {
|
|
167
173
|
const where = this._convertFilterToWhere(filter);
|
|
168
174
|
|
|
175
|
+
// If filtering by userId only, return all credentials for that user
|
|
176
|
+
const hasOnlyUserId =
|
|
177
|
+
filter.userId &&
|
|
178
|
+
!filter.credentialId &&
|
|
179
|
+
!filter.externalId &&
|
|
180
|
+
!filter.id;
|
|
181
|
+
|
|
182
|
+
if (hasOnlyUserId) {
|
|
183
|
+
const credentials = await this.prisma.credential.findMany({
|
|
184
|
+
where,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
return credentials.map((credential) => {
|
|
188
|
+
const data = credential.data || {};
|
|
189
|
+
return {
|
|
190
|
+
id: credential.id,
|
|
191
|
+
type: credential.type,
|
|
192
|
+
userId: credential.userId,
|
|
193
|
+
externalId: credential.externalId,
|
|
194
|
+
authIsValid: credential.authIsValid,
|
|
195
|
+
entityCount: credential.entityCount,
|
|
196
|
+
createdAt: credential.createdAt,
|
|
197
|
+
updatedAt: credential.updatedAt,
|
|
198
|
+
access_token: data.access_token,
|
|
199
|
+
refresh_token: data.refresh_token,
|
|
200
|
+
...data,
|
|
201
|
+
};
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Otherwise, find single credential
|
|
169
206
|
const credential = await this.prisma.credential.findFirst({
|
|
170
207
|
where,
|
|
171
208
|
});
|
|
@@ -178,9 +215,13 @@ class CredentialRepositoryMongo extends CredentialRepositoryInterface {
|
|
|
178
215
|
|
|
179
216
|
return {
|
|
180
217
|
id: credential.id,
|
|
218
|
+
type: credential.type,
|
|
181
219
|
userId: credential.userId,
|
|
182
220
|
externalId: credential.externalId,
|
|
183
221
|
authIsValid: credential.authIsValid,
|
|
222
|
+
entityCount: credential.entityCount,
|
|
223
|
+
createdAt: credential.createdAt,
|
|
224
|
+
updatedAt: credential.updatedAt,
|
|
184
225
|
access_token: data.access_token,
|
|
185
226
|
refresh_token: data.refresh_token,
|
|
186
227
|
...data,
|
|
@@ -138,7 +138,10 @@ class CredentialRepositoryPostgres extends CredentialRepositoryInterface {
|
|
|
138
138
|
data: {
|
|
139
139
|
userId: this._convertId(existing.userId),
|
|
140
140
|
externalId: existing.externalId,
|
|
141
|
-
authIsValid:
|
|
141
|
+
authIsValid:
|
|
142
|
+
authIsValid !== undefined
|
|
143
|
+
? authIsValid
|
|
144
|
+
: existing.authIsValid,
|
|
142
145
|
data: mergedData,
|
|
143
146
|
},
|
|
144
147
|
});
|
|
@@ -172,17 +175,51 @@ class CredentialRepositoryPostgres extends CredentialRepositoryInterface {
|
|
|
172
175
|
}
|
|
173
176
|
|
|
174
177
|
/**
|
|
175
|
-
* Find
|
|
178
|
+
* Find credential(s) by filter criteria
|
|
179
|
+
*
|
|
180
|
+
* When filter includes only userId, returns an array of all credentials for that user
|
|
181
|
+
* When filter includes credentialId or externalId, returns a single credential or null
|
|
176
182
|
*
|
|
177
183
|
* @param {Object} filter
|
|
178
184
|
* @param {string} [filter.userId] - User ID (string from application layer)
|
|
179
185
|
* @param {string} [filter.externalId] - External ID
|
|
180
186
|
* @param {string} [filter.credentialId] - Credential ID (string from application layer)
|
|
181
|
-
* @returns {Promise<Object|null>} Credential
|
|
187
|
+
* @returns {Promise<Array|Object|null>} Credential array, single credential with string IDs, or null
|
|
182
188
|
*/
|
|
183
189
|
async findCredential(filter) {
|
|
184
190
|
const where = this._convertFilterToWhere(filter);
|
|
185
191
|
|
|
192
|
+
// If filtering by userId only, return all credentials for that user
|
|
193
|
+
const hasOnlyUserId =
|
|
194
|
+
filter.userId &&
|
|
195
|
+
!filter.credentialId &&
|
|
196
|
+
!filter.externalId &&
|
|
197
|
+
!filter.id;
|
|
198
|
+
|
|
199
|
+
if (hasOnlyUserId) {
|
|
200
|
+
const credentials = await this.prisma.credential.findMany({
|
|
201
|
+
where,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
return credentials.map((credential) => {
|
|
205
|
+
const data = credential.data || {};
|
|
206
|
+
return {
|
|
207
|
+
id: credential.id.toString(),
|
|
208
|
+
type: credential.type,
|
|
209
|
+
userId: credential.userId?.toString(),
|
|
210
|
+
externalId: credential.externalId,
|
|
211
|
+
authIsValid: credential.authIsValid,
|
|
212
|
+
entityCount: credential.entityCount,
|
|
213
|
+
createdAt: credential.createdAt,
|
|
214
|
+
updatedAt: credential.updatedAt,
|
|
215
|
+
access_token: data.access_token,
|
|
216
|
+
refresh_token: data.refresh_token,
|
|
217
|
+
...data,
|
|
218
|
+
};
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Otherwise, find single credential
|
|
186
223
|
const credential = await this.prisma.credential.findFirst({
|
|
187
224
|
where,
|
|
188
225
|
});
|
|
@@ -195,9 +232,13 @@ class CredentialRepositoryPostgres extends CredentialRepositoryInterface {
|
|
|
195
232
|
|
|
196
233
|
return {
|
|
197
234
|
id: credential.id.toString(),
|
|
235
|
+
type: credential.type,
|
|
198
236
|
userId: credential.userId?.toString(),
|
|
199
237
|
externalId: credential.externalId,
|
|
200
238
|
authIsValid: credential.authIsValid,
|
|
239
|
+
entityCount: credential.entityCount,
|
|
240
|
+
createdAt: credential.createdAt,
|
|
241
|
+
updatedAt: credential.updatedAt,
|
|
201
242
|
access_token: data.access_token,
|
|
202
243
|
refresh_token: data.refresh_token,
|
|
203
244
|
...data,
|
|
@@ -243,7 +243,9 @@ class CredentialRepository extends CredentialRepositoryInterface {
|
|
|
243
243
|
externalId:
|
|
244
244
|
externalId !== undefined ? externalId : existing.externalId,
|
|
245
245
|
authIsValid:
|
|
246
|
-
authIsValid !== undefined
|
|
246
|
+
authIsValid !== undefined
|
|
247
|
+
? authIsValid
|
|
248
|
+
: existing.authIsValid,
|
|
247
249
|
data: mergedData,
|
|
248
250
|
},
|
|
249
251
|
});
|