@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
|
@@ -3,8 +3,10 @@ const { ObjectId } = require('bson');
|
|
|
3
3
|
function toObjectId(value) {
|
|
4
4
|
if (value === null || value === undefined || value === '') return undefined;
|
|
5
5
|
if (value instanceof ObjectId) return value;
|
|
6
|
-
if (typeof value === 'object' && value.$oid)
|
|
7
|
-
|
|
6
|
+
if (typeof value === 'object' && value.$oid)
|
|
7
|
+
return new ObjectId(value.$oid);
|
|
8
|
+
if (typeof value === 'string')
|
|
9
|
+
return ObjectId.isValid(value) ? new ObjectId(value) : undefined;
|
|
8
10
|
return undefined;
|
|
9
11
|
}
|
|
10
12
|
|
|
@@ -15,7 +17,8 @@ function toObjectIdArray(values) {
|
|
|
15
17
|
|
|
16
18
|
function fromObjectId(value) {
|
|
17
19
|
if (value instanceof ObjectId) return value.toHexString();
|
|
18
|
-
if (typeof value === 'object' && value !== null && value.$oid)
|
|
20
|
+
if (typeof value === 'object' && value !== null && value.$oid)
|
|
21
|
+
return value.$oid;
|
|
19
22
|
if (typeof value === 'string') return value;
|
|
20
23
|
return value === undefined || value === null ? value : String(value);
|
|
21
24
|
}
|
|
@@ -30,7 +33,10 @@ async function findMany(client, collection, filter = {}, options = {}) {
|
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
async function findOne(client, collection, filter = {}, options = {}) {
|
|
33
|
-
const docs = await findMany(client, collection, filter, {
|
|
36
|
+
const docs = await findMany(client, collection, filter, {
|
|
37
|
+
...options,
|
|
38
|
+
limit: 1,
|
|
39
|
+
});
|
|
34
40
|
return docs[0] || null;
|
|
35
41
|
}
|
|
36
42
|
|
|
@@ -47,7 +53,9 @@ async function insertOne(client, collection, document) {
|
|
|
47
53
|
// Validate insert succeeded
|
|
48
54
|
if (result.ok !== 1) {
|
|
49
55
|
throw new Error(
|
|
50
|
-
`Insert command failed for collection '${collection}': ${JSON.stringify(
|
|
56
|
+
`Insert command failed for collection '${collection}': ${JSON.stringify(
|
|
57
|
+
result
|
|
58
|
+
)}`
|
|
51
59
|
);
|
|
52
60
|
}
|
|
53
61
|
|
|
@@ -67,7 +75,7 @@ async function insertOne(client, collection, document) {
|
|
|
67
75
|
if (result.n !== 1) {
|
|
68
76
|
throw new Error(
|
|
69
77
|
`Expected to insert 1 document into '${collection}', but inserted ${result.n}. ` +
|
|
70
|
-
|
|
78
|
+
`Result: ${JSON.stringify(result)}`
|
|
71
79
|
);
|
|
72
80
|
}
|
|
73
81
|
|
|
@@ -75,11 +83,13 @@ async function insertOne(client, collection, document) {
|
|
|
75
83
|
}
|
|
76
84
|
|
|
77
85
|
async function updateOne(client, collection, filter, update, options = {}) {
|
|
78
|
-
const updates = [
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
86
|
+
const updates = [
|
|
87
|
+
{
|
|
88
|
+
q: filter,
|
|
89
|
+
u: update,
|
|
90
|
+
upsert: Boolean(options.upsert),
|
|
91
|
+
},
|
|
92
|
+
];
|
|
83
93
|
if (options.arrayFilters) updates[0].arrayFilters = options.arrayFilters;
|
|
84
94
|
const result = await client.$runCommandRaw({
|
|
85
95
|
update: collection,
|
|
@@ -133,4 +143,3 @@ module.exports = {
|
|
|
133
143
|
deleteMany,
|
|
134
144
|
aggregate,
|
|
135
145
|
};
|
|
136
|
-
|
|
@@ -125,10 +125,11 @@ Or simply don't configure any encryption keys. In Production field level encrypt
|
|
|
125
125
|
Core and custom encrypted fields are defined in `encryption-schema-registry.js`. See that file for the current list of encrypted fields.
|
|
126
126
|
|
|
127
127
|
**Core fields include**:
|
|
128
|
-
|
|
129
|
-
-
|
|
130
|
-
-
|
|
131
|
-
-
|
|
128
|
+
|
|
129
|
+
- OAuth tokens: `access_token`, `refresh_token`, `id_token`
|
|
130
|
+
- API keys: `api_key`, `apiKey`, `API_KEY_VALUE`
|
|
131
|
+
- Basic auth: `password`
|
|
132
|
+
- OAuth client credentials: `client_secret`
|
|
132
133
|
|
|
133
134
|
**Note**: API modules should use `api_key` (snake_case) in their `apiPropertiesToPersist.credential` arrays for consistency with OAuth2Requester and BasicAuthRequester conventions.
|
|
134
135
|
|
|
@@ -137,34 +138,36 @@ Core and custom encrypted fields are defined in `encryption-schema-registry.js`.
|
|
|
137
138
|
When creating API module definitions, use **snake_case** for credential property names to ensure automatic encryption:
|
|
138
139
|
|
|
139
140
|
**✅ Recommended (automatically encrypted):**
|
|
141
|
+
|
|
140
142
|
```javascript
|
|
141
143
|
// API Module Definition
|
|
142
144
|
const Definition = {
|
|
143
145
|
requiredAuthMethods: {
|
|
144
146
|
apiPropertiesToPersist: {
|
|
145
147
|
// For API key authentication
|
|
146
|
-
credential: ['api_key'],
|
|
148
|
+
credential: ['api_key'], // ✅ Automatically encrypted
|
|
147
149
|
// or for OAuth authentication
|
|
148
|
-
credential: ['access_token', 'refresh_token'],
|
|
150
|
+
credential: ['access_token', 'refresh_token'], // ✅ OAuth - encrypted
|
|
149
151
|
// or for Basic authentication
|
|
150
|
-
credential: ['username', 'password'],
|
|
151
|
-
}
|
|
152
|
-
}
|
|
152
|
+
credential: ['username', 'password'], // ✅ Basic auth - encrypted
|
|
153
|
+
},
|
|
154
|
+
},
|
|
153
155
|
};
|
|
154
156
|
|
|
155
157
|
// API class (extends ApiKeyRequester)
|
|
156
158
|
class MyApi extends ApiKeyRequester {
|
|
157
159
|
constructor(params) {
|
|
158
160
|
super(params);
|
|
159
|
-
this.api_key = params.api_key;
|
|
161
|
+
this.api_key = params.api_key; // ✅ snake_case convention
|
|
160
162
|
}
|
|
161
163
|
}
|
|
162
164
|
```
|
|
163
165
|
|
|
164
166
|
**❌ Avoid (requires manual encryption schema):**
|
|
167
|
+
|
|
165
168
|
```javascript
|
|
166
169
|
apiPropertiesToPersist: {
|
|
167
|
-
credential: ['customToken', 'proprietaryKey']
|
|
170
|
+
credential: ['customToken', 'proprietaryKey']; // ❌ Not in core schema
|
|
168
171
|
}
|
|
169
172
|
```
|
|
170
173
|
|
|
@@ -184,57 +187,61 @@ const Definition = {
|
|
|
184
187
|
|
|
185
188
|
// Declare which credential fields need encryption
|
|
186
189
|
encryption: {
|
|
187
|
-
credentialFields: ['api_key', 'webhook_secret']
|
|
190
|
+
credentialFields: ['api_key', 'webhook_secret'],
|
|
188
191
|
},
|
|
189
192
|
|
|
190
193
|
requiredAuthMethods: {
|
|
191
194
|
apiPropertiesToPersist: {
|
|
192
|
-
credential: ['api_key', 'webhook_secret'],
|
|
193
|
-
entity: []
|
|
195
|
+
credential: ['api_key', 'webhook_secret'], // These will be auto-encrypted
|
|
196
|
+
entity: [],
|
|
194
197
|
},
|
|
195
198
|
// ... other methods
|
|
196
|
-
}
|
|
199
|
+
},
|
|
197
200
|
};
|
|
198
201
|
```
|
|
199
202
|
|
|
200
203
|
**How it works**:
|
|
204
|
+
|
|
201
205
|
1. Module declares `encryption.credentialFields` array
|
|
202
206
|
2. Framework automatically adds `data.` prefix: `['api_key']` → `['data.api_key']`
|
|
203
207
|
3. Fields are merged with core encryption schema on app startup
|
|
204
208
|
4. All modules across all integrations are scanned and combined
|
|
205
209
|
|
|
206
210
|
**Benefits**:
|
|
207
|
-
|
|
208
|
-
-
|
|
209
|
-
-
|
|
210
|
-
-
|
|
211
|
+
|
|
212
|
+
- ✅ Module authors control their own security requirements
|
|
213
|
+
- ✅ No need to modify core framework or app configuration
|
|
214
|
+
- ✅ Automatic encryption for API key-based integrations
|
|
215
|
+
- ✅ Works seamlessly with `apiPropertiesToPersist`
|
|
211
216
|
|
|
212
217
|
**Example - API Key Module**:
|
|
218
|
+
|
|
213
219
|
```javascript
|
|
214
220
|
// API Module Definition
|
|
215
221
|
const Definition = {
|
|
216
222
|
moduleName: 'axiscare',
|
|
217
223
|
API: AxisCareApi,
|
|
218
224
|
encryption: {
|
|
219
|
-
credentialFields: ['api_key']
|
|
225
|
+
credentialFields: ['api_key'], // Auto-encrypted as 'data.api_key'
|
|
220
226
|
},
|
|
221
227
|
requiredAuthMethods: {
|
|
222
228
|
apiPropertiesToPersist: {
|
|
223
|
-
credential: ['api_key']
|
|
224
|
-
}
|
|
225
|
-
}
|
|
229
|
+
credential: ['api_key'], // Will be encrypted automatically
|
|
230
|
+
},
|
|
231
|
+
},
|
|
226
232
|
};
|
|
227
233
|
|
|
228
234
|
// API Class (extends ApiKeyRequester)
|
|
229
235
|
class AxisCareApi extends ApiKeyRequester {
|
|
230
236
|
constructor(params) {
|
|
231
237
|
super(params);
|
|
232
|
-
this.api_key = params.api_key;
|
|
238
|
+
this.api_key = params.api_key; // snake_case convention
|
|
233
239
|
}
|
|
234
240
|
}
|
|
235
241
|
```
|
|
236
242
|
|
|
237
243
|
**Example - Custom Authentication**:
|
|
244
|
+
|
|
238
245
|
```javascript
|
|
239
246
|
const Definition = {
|
|
240
247
|
moduleName: 'customService',
|
|
@@ -242,16 +249,17 @@ const Definition = {
|
|
|
242
249
|
credentialFields: [
|
|
243
250
|
'signing_key',
|
|
244
251
|
'webhook_secret',
|
|
245
|
-
'data.custom_nested_field'
|
|
246
|
-
]
|
|
247
|
-
}
|
|
252
|
+
'data.custom_nested_field', // Can specify data. prefix explicitly
|
|
253
|
+
],
|
|
254
|
+
},
|
|
248
255
|
};
|
|
249
256
|
```
|
|
250
257
|
|
|
251
258
|
**Limitations**:
|
|
252
|
-
|
|
253
|
-
-
|
|
254
|
-
-
|
|
259
|
+
|
|
260
|
+
- Only supports Credential model fields (stored in `credential.data`)
|
|
261
|
+
- Cannot encrypt entity fields or custom models (use app-level schema for those)
|
|
262
|
+
- Applied globally once - module schemas loaded at app startup
|
|
255
263
|
|
|
256
264
|
#### Option 2: App-Level Custom Schema (Integration Developers)
|
|
257
265
|
|
|
@@ -529,7 +537,9 @@ For DocumentDB repositories, use `DocumentDBEncryptionService` to manually encry
|
|
|
529
537
|
#### Usage Example
|
|
530
538
|
|
|
531
539
|
```javascript
|
|
532
|
-
const {
|
|
540
|
+
const {
|
|
541
|
+
DocumentDBEncryptionService,
|
|
542
|
+
} = require('../documentdb-encryption-service');
|
|
533
543
|
const { insertOne, findOne } = require('../documentdb-utils');
|
|
534
544
|
|
|
535
545
|
class MyRepositoryDocumentDB {
|
|
@@ -539,12 +549,18 @@ class MyRepositoryDocumentDB {
|
|
|
539
549
|
|
|
540
550
|
async create(data) {
|
|
541
551
|
// Encrypt before write
|
|
542
|
-
const encrypted = await this.encryptionService.encryptFields(
|
|
552
|
+
const encrypted = await this.encryptionService.encryptFields(
|
|
553
|
+
'ModelName',
|
|
554
|
+
data
|
|
555
|
+
);
|
|
543
556
|
const id = await insertOne(this.prisma, 'CollectionName', encrypted);
|
|
544
557
|
|
|
545
558
|
// Decrypt after read
|
|
546
559
|
const doc = await findOne(this.prisma, 'CollectionName', { _id: id });
|
|
547
|
-
const decrypted = await this.encryptionService.decryptFields(
|
|
560
|
+
const decrypted = await this.encryptionService.decryptFields(
|
|
561
|
+
'ModelName',
|
|
562
|
+
doc
|
|
563
|
+
);
|
|
548
564
|
|
|
549
565
|
return decrypted;
|
|
550
566
|
}
|
|
@@ -554,9 +570,10 @@ class MyRepositoryDocumentDB {
|
|
|
554
570
|
#### Configuration
|
|
555
571
|
|
|
556
572
|
Uses the same environment variables and Cryptor as the Prisma Extension:
|
|
557
|
-
|
|
558
|
-
-
|
|
559
|
-
-
|
|
573
|
+
|
|
574
|
+
- `STAGE`: Bypasses encryption for dev/test/local
|
|
575
|
+
- `KMS_KEY_ARN`: AWS KMS encryption (production)
|
|
576
|
+
- `AES_KEY_ID` + `AES_KEY`: AES encryption (fallback)
|
|
560
577
|
|
|
561
578
|
## Usage Examples
|
|
562
579
|
|
|
@@ -72,7 +72,9 @@ function validateCustomSchema(schema) {
|
|
|
72
72
|
|
|
73
73
|
for (const fieldPath of config.fields) {
|
|
74
74
|
if (typeof fieldPath !== 'string' || !fieldPath) {
|
|
75
|
-
errors.push(
|
|
75
|
+
errors.push(
|
|
76
|
+
`Model "${modelName}" has invalid field path: ${fieldPath}`
|
|
77
|
+
);
|
|
76
78
|
}
|
|
77
79
|
|
|
78
80
|
// Check if trying to override core fields
|
|
@@ -104,13 +106,17 @@ function registerCustomSchema(schema) {
|
|
|
104
106
|
const validation = validateCustomSchema(schema);
|
|
105
107
|
if (!validation.valid) {
|
|
106
108
|
throw new Error(
|
|
107
|
-
`Invalid custom encryption schema:\n- ${validation.errors.join(
|
|
109
|
+
`Invalid custom encryption schema:\n- ${validation.errors.join(
|
|
110
|
+
'\n- '
|
|
111
|
+
)}`
|
|
108
112
|
);
|
|
109
113
|
}
|
|
110
114
|
|
|
111
115
|
customSchema = { ...schema };
|
|
112
116
|
logger.info(
|
|
113
|
-
`Registered custom encryption schema for models: ${Object.keys(
|
|
117
|
+
`Registered custom encryption schema for models: ${Object.keys(
|
|
118
|
+
customSchema
|
|
119
|
+
).join(', ')}`
|
|
114
120
|
);
|
|
115
121
|
}
|
|
116
122
|
|
|
@@ -133,7 +139,9 @@ function extractCredentialFieldsFromModules(moduleDefinitions) {
|
|
|
133
139
|
}
|
|
134
140
|
|
|
135
141
|
for (const field of credentialFields) {
|
|
136
|
-
const prefixedField = field.startsWith('data.')
|
|
142
|
+
const prefixedField = field.startsWith('data.')
|
|
143
|
+
? field
|
|
144
|
+
: `data.${field}`;
|
|
137
145
|
fields.push(prefixedField);
|
|
138
146
|
}
|
|
139
147
|
}
|
|
@@ -160,10 +168,14 @@ function loadModuleEncryptionSchemas(integrations) {
|
|
|
160
168
|
return;
|
|
161
169
|
}
|
|
162
170
|
|
|
163
|
-
const {
|
|
171
|
+
const {
|
|
172
|
+
getModulesDefinitionFromIntegrationClasses,
|
|
173
|
+
} = require('../../integrations/utils/map-integration-dto');
|
|
164
174
|
|
|
165
|
-
const moduleDefinitions =
|
|
166
|
-
|
|
175
|
+
const moduleDefinitions =
|
|
176
|
+
getModulesDefinitionFromIntegrationClasses(integrations);
|
|
177
|
+
const credentialFields =
|
|
178
|
+
extractCredentialFieldsFromModules(moduleDefinitions);
|
|
167
179
|
|
|
168
180
|
if (credentialFields.length === 0) {
|
|
169
181
|
return;
|
|
@@ -171,8 +183,8 @@ function loadModuleEncryptionSchemas(integrations) {
|
|
|
171
183
|
|
|
172
184
|
const moduleSchema = {
|
|
173
185
|
Credential: {
|
|
174
|
-
fields: credentialFields
|
|
175
|
-
}
|
|
186
|
+
fields: credentialFields,
|
|
187
|
+
},
|
|
176
188
|
};
|
|
177
189
|
|
|
178
190
|
logger.info(
|
|
@@ -193,20 +205,14 @@ function loadModuleEncryptionSchemas(integrations) {
|
|
|
193
205
|
*/
|
|
194
206
|
function loadCustomEncryptionSchema() {
|
|
195
207
|
try {
|
|
196
|
-
//
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
const backendDir = path.dirname(backendPackagePath);
|
|
206
|
-
const backendIndexPath = path.join(backendDir, 'index.js');
|
|
207
|
-
|
|
208
|
-
const backendModule = require(backendIndexPath);
|
|
209
|
-
const appDefinition = backendModule?.Definition;
|
|
208
|
+
// Uses loadAppDefinition() which respects setAppDefinition() cache.
|
|
209
|
+
// This is critical for platforms like Netlify where process.cwd()-based
|
|
210
|
+
// discovery fails at runtime (process.cwd() is /var/task, not the project root).
|
|
211
|
+
const {
|
|
212
|
+
loadAppDefinition,
|
|
213
|
+
} = require('../../handlers/app-definition-loader');
|
|
214
|
+
|
|
215
|
+
const { appDefinition } = loadAppDefinition();
|
|
210
216
|
|
|
211
217
|
if (!appDefinition) {
|
|
212
218
|
return; // No app definition found
|
|
@@ -35,7 +35,9 @@ class FieldEncryptionService {
|
|
|
35
35
|
|
|
36
36
|
if (this._shouldEncrypt(value)) {
|
|
37
37
|
const serializedValue = this._serializeForEncryption(value);
|
|
38
|
-
const encryptedValue = await this.cryptor.encrypt(
|
|
38
|
+
const encryptedValue = await this.cryptor.encrypt(
|
|
39
|
+
serializedValue
|
|
40
|
+
);
|
|
39
41
|
return { fieldPath, encryptedValue };
|
|
40
42
|
}
|
|
41
43
|
return null;
|
|
@@ -46,7 +48,11 @@ class FieldEncryptionService {
|
|
|
46
48
|
// Apply encrypted values
|
|
47
49
|
for (const result of results) {
|
|
48
50
|
if (result) {
|
|
49
|
-
this._setNestedValue(
|
|
51
|
+
this._setNestedValue(
|
|
52
|
+
encrypted,
|
|
53
|
+
result.fieldPath,
|
|
54
|
+
result.encryptedValue
|
|
55
|
+
);
|
|
50
56
|
}
|
|
51
57
|
}
|
|
52
58
|
|
|
@@ -71,7 +77,8 @@ class FieldEncryptionService {
|
|
|
71
77
|
|
|
72
78
|
if (this._isEncrypted(value)) {
|
|
73
79
|
const decryptedValue = await this.cryptor.decrypt(value);
|
|
74
|
-
const deserializedValue =
|
|
80
|
+
const deserializedValue =
|
|
81
|
+
this._deserializeAfterDecryption(decryptedValue);
|
|
75
82
|
return { fieldPath, decryptedValue: deserializedValue };
|
|
76
83
|
}
|
|
77
84
|
return null;
|
|
@@ -82,7 +89,11 @@ class FieldEncryptionService {
|
|
|
82
89
|
// Apply decrypted values
|
|
83
90
|
for (const result of results) {
|
|
84
91
|
if (result) {
|
|
85
|
-
this._setNestedValue(
|
|
92
|
+
this._setNestedValue(
|
|
93
|
+
decrypted,
|
|
94
|
+
result.fieldPath,
|
|
95
|
+
result.decryptedValue
|
|
96
|
+
);
|
|
86
97
|
}
|
|
87
98
|
}
|
|
88
99
|
|
|
@@ -30,8 +30,9 @@ class EncryptionLogger {
|
|
|
30
30
|
// Remove potential key material or encrypted data from logs
|
|
31
31
|
if (typeof message === 'string') {
|
|
32
32
|
// Truncate long base64 strings that might be keys or encrypted data
|
|
33
|
-
return message.replace(
|
|
34
|
-
|
|
33
|
+
return message.replace(
|
|
34
|
+
/([A-Za-z0-9+/=]{50,})/g,
|
|
35
|
+
(match) => `${match.substring(0, 10)}...[${match.length} chars]`
|
|
35
36
|
);
|
|
36
37
|
}
|
|
37
38
|
return message;
|
|
@@ -12,9 +12,7 @@ function createEncryptionExtension({ cryptor, enabled = true }) {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
if (!cryptor) {
|
|
15
|
-
throw new Error(
|
|
16
|
-
'Cryptor instance required for encryption extension'
|
|
17
|
-
);
|
|
15
|
+
throw new Error('Cryptor instance required for encryption extension');
|
|
18
16
|
}
|
|
19
17
|
|
|
20
18
|
const encryptionService = new FieldEncryptionService({
|
|
@@ -48,11 +46,10 @@ function createEncryptionExtension({ cryptor, enabled = true }) {
|
|
|
48
46
|
|
|
49
47
|
async createMany({ model, args, query }) {
|
|
50
48
|
if (args.data && Array.isArray(args.data)) {
|
|
51
|
-
args.data =
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
);
|
|
49
|
+
args.data = await encryptionService.encryptFieldsInBulk(
|
|
50
|
+
model,
|
|
51
|
+
args.data
|
|
52
|
+
);
|
|
56
53
|
} else if (args.data) {
|
|
57
54
|
args.data = await encryptionService.encryptFields(
|
|
58
55
|
model,
|
package/database/prisma.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
const {
|
|
2
2
|
createEncryptionExtension,
|
|
3
3
|
} = require('./encryption/prisma-encryption-extension');
|
|
4
|
-
const {
|
|
4
|
+
const {
|
|
5
|
+
loadCustomEncryptionSchema,
|
|
6
|
+
} = require('./encryption/encryption-schema-registry');
|
|
5
7
|
const { logger } = require('./encryption/logger');
|
|
6
8
|
const { Cryptor } = require('../encrypt/Cryptor');
|
|
7
9
|
const config = require('./config');
|
|
@@ -10,7 +12,7 @@ const config = require('./config');
|
|
|
10
12
|
* Ensures DATABASE_URL is set for MongoDB connections
|
|
11
13
|
* Falls back to MONGO_URI if DATABASE_URL is not set
|
|
12
14
|
* Infrastructure layer concern - maps legacy MONGO_URI to Prisma's expected DATABASE_URL
|
|
13
|
-
*
|
|
15
|
+
*
|
|
14
16
|
* Note: This should only be called when DB_TYPE is 'mongodb' or 'documentdb'
|
|
15
17
|
*/
|
|
16
18
|
function ensureMongoDbUrl() {
|
|
@@ -22,7 +24,9 @@ function ensureMongoDbUrl() {
|
|
|
22
24
|
// Fallback to MONGO_URI for backwards compatibility with DocumentDB deployments
|
|
23
25
|
if (process.env.MONGO_URI && process.env.MONGO_URI.trim()) {
|
|
24
26
|
process.env.DATABASE_URL = process.env.MONGO_URI;
|
|
25
|
-
logger.debug(
|
|
27
|
+
logger.debug(
|
|
28
|
+
'Using MONGO_URI as DATABASE_URL for Mongo-compatible connection'
|
|
29
|
+
);
|
|
26
30
|
return;
|
|
27
31
|
}
|
|
28
32
|
|
|
@@ -48,7 +52,7 @@ function getEncryptionConfig() {
|
|
|
48
52
|
if (!hasKMS && !hasAES) {
|
|
49
53
|
logger.warn(
|
|
50
54
|
'No encryption keys configured (KMS_KEY_ARN or AES_KEY_ID). ' +
|
|
51
|
-
|
|
55
|
+
'Field-level encryption disabled. Set STAGE=production and configure keys to enable.'
|
|
52
56
|
);
|
|
53
57
|
return { enabled: false };
|
|
54
58
|
}
|
|
@@ -80,7 +84,9 @@ const prismaClientSingleton = () => {
|
|
|
80
84
|
}
|
|
81
85
|
|
|
82
86
|
throw new Error(
|
|
83
|
-
`Cannot find Prisma client for ${dbType}. Tried paths: ${paths.join(
|
|
87
|
+
`Cannot find Prisma client for ${dbType}. Tried paths: ${paths.join(
|
|
88
|
+
', '
|
|
89
|
+
)}`
|
|
84
90
|
);
|
|
85
91
|
};
|
|
86
92
|
|
|
@@ -125,10 +131,7 @@ const prismaClientSingleton = () => {
|
|
|
125
131
|
`Field-level encryption enabled using ${encryptionConfig.method.toUpperCase()}`
|
|
126
132
|
);
|
|
127
133
|
} catch (error) {
|
|
128
|
-
logger.error(
|
|
129
|
-
'Failed to initialize encryption extension:',
|
|
130
|
-
error
|
|
131
|
-
);
|
|
134
|
+
logger.error('Failed to initialize encryption extension:', error);
|
|
132
135
|
logger.warn('Continuing without encryption...');
|
|
133
136
|
}
|
|
134
137
|
} else {
|
|
@@ -149,11 +152,14 @@ function getPrismaClient() {
|
|
|
149
152
|
}
|
|
150
153
|
|
|
151
154
|
// Export a getter for lazy initialization
|
|
152
|
-
const prisma = new Proxy(
|
|
153
|
-
|
|
154
|
-
|
|
155
|
+
const prisma = new Proxy(
|
|
156
|
+
{},
|
|
157
|
+
{
|
|
158
|
+
get(target, prop) {
|
|
159
|
+
return getPrismaClient()[prop];
|
|
160
|
+
},
|
|
155
161
|
}
|
|
156
|
-
|
|
162
|
+
);
|
|
157
163
|
|
|
158
164
|
async function disconnectPrisma() {
|
|
159
165
|
await getPrismaClient().$disconnect();
|
|
@@ -166,7 +172,9 @@ async function connectPrisma() {
|
|
|
166
172
|
// Only run for MongoDB/DocumentDB (not PostgreSQL)
|
|
167
173
|
// This prevents "Cannot create namespace in multi-document transaction" errors
|
|
168
174
|
if (config.DB_TYPE === 'mongodb' || config.DB_TYPE === 'documentdb') {
|
|
169
|
-
const {
|
|
175
|
+
const {
|
|
176
|
+
initializeMongoDBSchema,
|
|
177
|
+
} = require('./utils/mongodb-schema-init');
|
|
170
178
|
await initializeMongoDBSchema();
|
|
171
179
|
}
|
|
172
180
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
const {
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
const {
|
|
2
|
+
HealthCheckRepositoryPostgreSQL,
|
|
3
|
+
} = require('./health-check-repository-postgres');
|
|
4
4
|
const config = require('../config');
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -25,12 +25,14 @@ function createHealthCheckRepository({ prismaClient } = {}) {
|
|
|
25
25
|
|
|
26
26
|
switch (dbType) {
|
|
27
27
|
case 'mongodb':
|
|
28
|
+
const { HealthCheckRepositoryMongoDB } = require('./health-check-repository-mongodb');
|
|
28
29
|
return new HealthCheckRepositoryMongoDB({ prismaClient });
|
|
29
30
|
|
|
30
31
|
case 'postgresql':
|
|
31
32
|
return new HealthCheckRepositoryPostgreSQL({ prismaClient });
|
|
32
33
|
|
|
33
34
|
case 'documentdb':
|
|
35
|
+
const { HealthCheckRepositoryDocumentDB } = require('./health-check-repository-documentdb');
|
|
34
36
|
return new HealthCheckRepositoryDocumentDB({ prismaClient });
|
|
35
37
|
|
|
36
38
|
default:
|
|
@@ -42,7 +44,7 @@ function createHealthCheckRepository({ prismaClient } = {}) {
|
|
|
42
44
|
|
|
43
45
|
module.exports = {
|
|
44
46
|
createHealthCheckRepository,
|
|
45
|
-
HealthCheckRepositoryMongoDB,
|
|
47
|
+
get HealthCheckRepositoryMongoDB() { return require('./health-check-repository-mongodb').HealthCheckRepositoryMongoDB; },
|
|
46
48
|
HealthCheckRepositoryPostgreSQL,
|
|
47
|
-
HealthCheckRepositoryDocumentDB,
|
|
49
|
+
get HealthCheckRepositoryDocumentDB() { return require('./health-check-repository-documentdb').HealthCheckRepositoryDocumentDB; },
|
|
48
50
|
};
|
|
@@ -19,7 +19,9 @@ class HealthCheckRepositoryInterface {
|
|
|
19
19
|
* @abstract
|
|
20
20
|
*/
|
|
21
21
|
async getDatabaseConnectionState() {
|
|
22
|
-
throw new Error(
|
|
22
|
+
throw new Error(
|
|
23
|
+
'Method getDatabaseConnectionState must be implemented by subclass'
|
|
24
|
+
);
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
/**
|
|
@@ -42,7 +44,9 @@ class HealthCheckRepositoryInterface {
|
|
|
42
44
|
* @abstract
|
|
43
45
|
*/
|
|
44
46
|
async createCredential(credentialData) {
|
|
45
|
-
throw new Error(
|
|
47
|
+
throw new Error(
|
|
48
|
+
'Method createCredential must be implemented by subclass'
|
|
49
|
+
);
|
|
46
50
|
}
|
|
47
51
|
|
|
48
52
|
/**
|
|
@@ -53,7 +57,9 @@ class HealthCheckRepositoryInterface {
|
|
|
53
57
|
* @abstract
|
|
54
58
|
*/
|
|
55
59
|
async findCredentialById(id) {
|
|
56
|
-
throw new Error(
|
|
60
|
+
throw new Error(
|
|
61
|
+
'Method findCredentialById must be implemented by subclass'
|
|
62
|
+
);
|
|
57
63
|
}
|
|
58
64
|
|
|
59
65
|
/**
|
|
@@ -64,7 +70,9 @@ class HealthCheckRepositoryInterface {
|
|
|
64
70
|
* @abstract
|
|
65
71
|
*/
|
|
66
72
|
async getRawCredentialById(id) {
|
|
67
|
-
throw new Error(
|
|
73
|
+
throw new Error(
|
|
74
|
+
'Method getRawCredentialById must be implemented by subclass'
|
|
75
|
+
);
|
|
68
76
|
}
|
|
69
77
|
|
|
70
78
|
/**
|
|
@@ -75,7 +83,9 @@ class HealthCheckRepositoryInterface {
|
|
|
75
83
|
* @abstract
|
|
76
84
|
*/
|
|
77
85
|
async deleteCredential(id) {
|
|
78
|
-
throw new Error(
|
|
86
|
+
throw new Error(
|
|
87
|
+
'Method deleteCredential must be implemented by subclass'
|
|
88
|
+
);
|
|
79
89
|
}
|
|
80
90
|
}
|
|
81
91
|
|
|
@@ -11,7 +11,8 @@ class CheckDatabaseHealthUseCase {
|
|
|
11
11
|
* @returns {Promise<{status: string, state: string, responseTime?: number}>}
|
|
12
12
|
*/
|
|
13
13
|
async execute() {
|
|
14
|
-
const { stateName, isConnected } =
|
|
14
|
+
const { stateName, isConnected } =
|
|
15
|
+
await this.repository.getDatabaseConnectionState();
|
|
15
16
|
|
|
16
17
|
const result = {
|
|
17
18
|
status: isConnected ? 'healthy' : 'unhealthy',
|
|
@@ -26,4 +27,4 @@ class CheckDatabaseHealthUseCase {
|
|
|
26
27
|
}
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
module.exports = { CheckDatabaseHealthUseCase };
|
|
30
|
+
module.exports = { CheckDatabaseHealthUseCase };
|