@friggframework/core 2.0.0-next.6 → 2.0.0-next.61
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/CLAUDE.md +694 -0
- package/README.md +959 -50
- package/application/commands/README.md +451 -0
- package/application/commands/credential-commands.js +245 -0
- package/application/commands/entity-commands.js +336 -0
- package/application/commands/integration-commands.js +210 -0
- package/application/commands/user-commands.js +283 -0
- package/application/index.js +69 -0
- package/core/CLAUDE.md +690 -0
- package/core/Worker.js +8 -21
- package/core/create-handler.js +14 -7
- package/credential/repositories/credential-repository-documentdb.js +304 -0
- package/credential/repositories/credential-repository-factory.js +54 -0
- package/credential/repositories/credential-repository-interface.js +98 -0
- package/credential/repositories/credential-repository-mongo.js +269 -0
- package/credential/repositories/credential-repository-postgres.js +291 -0
- package/credential/repositories/credential-repository.js +302 -0
- package/credential/use-cases/get-credential-for-user.js +25 -0
- package/credential/use-cases/update-authentication-status.js +15 -0
- package/database/MONGODB_TRANSACTION_FIX.md +198 -0
- package/database/adapters/lambda-invoker.js +97 -0
- package/database/config.js +154 -0
- package/database/documentdb-encryption-service.js +330 -0
- package/database/documentdb-utils.js +136 -0
- package/database/encryption/README.md +839 -0
- package/database/encryption/documentdb-encryption-service.md +3575 -0
- package/database/encryption/encryption-schema-registry.js +268 -0
- package/database/encryption/field-encryption-service.js +226 -0
- package/database/encryption/logger.js +79 -0
- package/database/encryption/prisma-encryption-extension.js +222 -0
- package/database/index.js +61 -21
- package/database/models/WebsocketConnection.js +16 -10
- package/database/models/readme.md +1 -0
- package/database/prisma.js +182 -0
- package/database/repositories/health-check-repository-documentdb.js +134 -0
- package/database/repositories/health-check-repository-factory.js +48 -0
- package/database/repositories/health-check-repository-interface.js +82 -0
- package/database/repositories/health-check-repository-mongodb.js +89 -0
- package/database/repositories/health-check-repository-postgres.js +82 -0
- package/database/repositories/health-check-repository.js +108 -0
- package/database/repositories/migration-status-repository-s3.js +137 -0
- package/database/use-cases/check-database-health-use-case.js +29 -0
- package/database/use-cases/check-database-state-use-case.js +81 -0
- package/database/use-cases/check-encryption-health-use-case.js +83 -0
- package/database/use-cases/get-database-state-via-worker-use-case.js +61 -0
- package/database/use-cases/get-migration-status-use-case.js +93 -0
- package/database/use-cases/run-database-migration-use-case.js +139 -0
- package/database/use-cases/test-encryption-use-case.js +253 -0
- package/database/use-cases/trigger-database-migration-use-case.js +157 -0
- package/database/utils/mongodb-collection-utils.js +91 -0
- package/database/utils/mongodb-schema-init.js +106 -0
- package/database/utils/prisma-runner.js +477 -0
- package/database/utils/prisma-schema-parser.js +182 -0
- package/docs/PROCESS_MANAGEMENT_QUEUE_SPEC.md +517 -0
- package/encrypt/Cryptor.js +34 -168
- package/encrypt/index.js +1 -2
- package/encrypt/test-encrypt.js +0 -2
- package/errors/client-safe-error.js +26 -0
- package/errors/fetch-error.js +2 -1
- package/errors/index.js +2 -0
- package/generated/prisma-mongodb/client.d.ts +1 -0
- package/generated/prisma-mongodb/client.js +4 -0
- package/generated/prisma-mongodb/default.d.ts +1 -0
- package/generated/prisma-mongodb/default.js +4 -0
- package/generated/prisma-mongodb/edge.d.ts +1 -0
- package/generated/prisma-mongodb/edge.js +334 -0
- package/generated/prisma-mongodb/index-browser.js +316 -0
- package/generated/prisma-mongodb/index.d.ts +22903 -0
- package/generated/prisma-mongodb/index.js +359 -0
- package/generated/prisma-mongodb/package.json +183 -0
- package/generated/prisma-mongodb/query-engine-debian-openssl-3.0.x +0 -0
- package/generated/prisma-mongodb/query-engine-rhel-openssl-3.0.x +0 -0
- package/generated/prisma-mongodb/runtime/binary.d.ts +1 -0
- package/generated/prisma-mongodb/runtime/binary.js +289 -0
- package/generated/prisma-mongodb/runtime/edge-esm.js +34 -0
- package/generated/prisma-mongodb/runtime/edge.js +34 -0
- package/generated/prisma-mongodb/runtime/index-browser.d.ts +370 -0
- package/generated/prisma-mongodb/runtime/index-browser.js +16 -0
- package/generated/prisma-mongodb/runtime/library.d.ts +3977 -0
- package/generated/prisma-mongodb/runtime/react-native.js +83 -0
- package/generated/prisma-mongodb/runtime/wasm-compiler-edge.js +84 -0
- package/generated/prisma-mongodb/runtime/wasm-engine-edge.js +36 -0
- package/generated/prisma-mongodb/schema.prisma +360 -0
- package/generated/prisma-mongodb/wasm-edge-light-loader.mjs +4 -0
- package/generated/prisma-mongodb/wasm-worker-loader.mjs +4 -0
- package/generated/prisma-mongodb/wasm.d.ts +1 -0
- package/generated/prisma-mongodb/wasm.js +341 -0
- package/generated/prisma-postgresql/client.d.ts +1 -0
- package/generated/prisma-postgresql/client.js +4 -0
- package/generated/prisma-postgresql/default.d.ts +1 -0
- package/generated/prisma-postgresql/default.js +4 -0
- package/generated/prisma-postgresql/edge.d.ts +1 -0
- package/generated/prisma-postgresql/edge.js +356 -0
- package/generated/prisma-postgresql/index-browser.js +338 -0
- package/generated/prisma-postgresql/index.d.ts +25077 -0
- package/generated/prisma-postgresql/index.js +381 -0
- package/generated/prisma-postgresql/package.json +183 -0
- package/generated/prisma-postgresql/query-engine-debian-openssl-3.0.x +0 -0
- package/generated/prisma-postgresql/query-engine-rhel-openssl-3.0.x +0 -0
- package/generated/prisma-postgresql/query_engine_bg.js +2 -0
- package/generated/prisma-postgresql/query_engine_bg.wasm +0 -0
- package/generated/prisma-postgresql/runtime/binary.d.ts +1 -0
- package/generated/prisma-postgresql/runtime/binary.js +289 -0
- package/generated/prisma-postgresql/runtime/edge-esm.js +34 -0
- package/generated/prisma-postgresql/runtime/edge.js +34 -0
- package/generated/prisma-postgresql/runtime/index-browser.d.ts +370 -0
- package/generated/prisma-postgresql/runtime/index-browser.js +16 -0
- package/generated/prisma-postgresql/runtime/library.d.ts +3977 -0
- package/generated/prisma-postgresql/runtime/react-native.js +83 -0
- package/generated/prisma-postgresql/runtime/wasm-compiler-edge.js +84 -0
- package/generated/prisma-postgresql/runtime/wasm-engine-edge.js +36 -0
- package/generated/prisma-postgresql/schema.prisma +343 -0
- package/generated/prisma-postgresql/wasm-edge-light-loader.mjs +4 -0
- package/generated/prisma-postgresql/wasm-worker-loader.mjs +4 -0
- package/generated/prisma-postgresql/wasm.d.ts +1 -0
- package/generated/prisma-postgresql/wasm.js +363 -0
- package/handlers/WEBHOOKS.md +653 -0
- package/handlers/app-definition-loader.js +38 -0
- package/handlers/app-handler-helpers.js +56 -0
- package/handlers/backend-utils.js +186 -0
- package/handlers/database-migration-handler.js +227 -0
- package/handlers/integration-event-dispatcher.js +54 -0
- package/handlers/routers/HEALTHCHECK.md +342 -0
- package/handlers/routers/auth.js +15 -0
- package/handlers/routers/db-migration.handler.js +29 -0
- package/handlers/routers/db-migration.js +326 -0
- package/handlers/routers/health.js +516 -0
- package/handlers/routers/integration-defined-routers.js +45 -0
- package/handlers/routers/integration-webhook-routers.js +67 -0
- package/handlers/routers/user.js +63 -0
- package/handlers/routers/websocket.js +57 -0
- package/handlers/use-cases/check-external-apis-health-use-case.js +81 -0
- package/handlers/use-cases/check-integrations-health-use-case.js +44 -0
- package/handlers/workers/db-migration.js +352 -0
- package/handlers/workers/integration-defined-workers.js +27 -0
- package/index.js +77 -22
- package/integrations/WEBHOOK-QUICKSTART.md +151 -0
- package/integrations/index.js +12 -10
- package/integrations/integration-base.js +326 -55
- package/integrations/integration-router.js +374 -179
- package/integrations/options.js +1 -1
- package/integrations/repositories/integration-mapping-repository-documentdb.js +280 -0
- package/integrations/repositories/integration-mapping-repository-factory.js +57 -0
- package/integrations/repositories/integration-mapping-repository-interface.js +106 -0
- package/integrations/repositories/integration-mapping-repository-mongo.js +161 -0
- package/integrations/repositories/integration-mapping-repository-postgres.js +227 -0
- package/integrations/repositories/integration-mapping-repository.js +156 -0
- package/integrations/repositories/integration-repository-documentdb.js +210 -0
- package/integrations/repositories/integration-repository-factory.js +51 -0
- package/integrations/repositories/integration-repository-interface.js +127 -0
- package/integrations/repositories/integration-repository-mongo.js +303 -0
- package/integrations/repositories/integration-repository-postgres.js +352 -0
- package/integrations/repositories/process-repository-documentdb.js +243 -0
- package/integrations/repositories/process-repository-factory.js +53 -0
- package/integrations/repositories/process-repository-interface.js +90 -0
- package/integrations/repositories/process-repository-mongo.js +190 -0
- package/integrations/repositories/process-repository-postgres.js +217 -0
- package/integrations/tests/doubles/config-capturing-integration.js +81 -0
- package/integrations/tests/doubles/dummy-integration-class.js +105 -0
- package/integrations/tests/doubles/test-integration-repository.js +99 -0
- package/integrations/use-cases/create-integration.js +83 -0
- package/integrations/use-cases/create-process.js +128 -0
- package/integrations/use-cases/delete-integration-for-user.js +101 -0
- package/integrations/use-cases/find-integration-context-by-external-entity-id.js +72 -0
- package/integrations/use-cases/get-integration-for-user.js +78 -0
- package/integrations/use-cases/get-integration-instance-by-definition.js +67 -0
- package/integrations/use-cases/get-integration-instance.js +83 -0
- package/integrations/use-cases/get-integrations-for-user.js +88 -0
- package/integrations/use-cases/get-possible-integrations.js +27 -0
- package/integrations/use-cases/get-process.js +87 -0
- package/integrations/use-cases/index.js +19 -0
- package/integrations/use-cases/load-integration-context.js +71 -0
- package/integrations/use-cases/update-integration-messages.js +44 -0
- package/integrations/use-cases/update-integration-status.js +32 -0
- package/integrations/use-cases/update-integration.js +92 -0
- package/integrations/use-cases/update-process-metrics.js +201 -0
- package/integrations/use-cases/update-process-state.js +119 -0
- package/integrations/utils/map-integration-dto.js +37 -0
- package/jest-global-setup-noop.js +3 -0
- package/jest-global-teardown-noop.js +3 -0
- package/logs/logger.js +0 -4
- package/{module-plugin → modules}/entity.js +1 -1
- package/{module-plugin → modules}/index.js +0 -8
- package/modules/module-factory.js +56 -0
- package/modules/module.js +221 -0
- package/modules/repositories/module-repository-documentdb.js +307 -0
- package/modules/repositories/module-repository-factory.js +40 -0
- package/modules/repositories/module-repository-interface.js +129 -0
- package/modules/repositories/module-repository-mongo.js +377 -0
- package/modules/repositories/module-repository-postgres.js +426 -0
- package/modules/repositories/module-repository.js +316 -0
- package/modules/requester/api-key.js +52 -0
- package/{module-plugin → modules}/requester/requester.js +1 -0
- package/{module-plugin → modules}/test/mock-api/api.js +8 -3
- package/{module-plugin → modules}/test/mock-api/definition.js +12 -8
- package/modules/tests/doubles/test-module-factory.js +16 -0
- package/modules/tests/doubles/test-module-repository.js +39 -0
- package/modules/use-cases/get-entities-for-user.js +32 -0
- package/modules/use-cases/get-entity-options-by-id.js +71 -0
- package/modules/use-cases/get-entity-options-by-type.js +34 -0
- package/modules/use-cases/get-module-instance-from-type.js +31 -0
- package/modules/use-cases/get-module.js +74 -0
- package/modules/use-cases/process-authorization-callback.js +133 -0
- package/modules/use-cases/refresh-entity-options.js +72 -0
- package/modules/use-cases/test-module-auth.js +72 -0
- package/modules/utils/map-module-dto.js +18 -0
- package/package.json +82 -50
- package/prisma-mongodb/schema.prisma +360 -0
- package/prisma-postgresql/migrations/20250930193005_init/migration.sql +315 -0
- package/prisma-postgresql/migrations/20251006135218_init/migration.sql +9 -0
- package/prisma-postgresql/migrations/20251010000000_remove_unused_entity_reference_map/migration.sql +3 -0
- package/prisma-postgresql/migrations/20251112195422_update_user_unique_constraints/migration.sql +25 -0
- package/prisma-postgresql/migrations/migration_lock.toml +3 -0
- package/prisma-postgresql/schema.prisma +343 -0
- package/queues/queuer-util.js +27 -22
- package/syncs/manager.js +468 -443
- package/syncs/repositories/sync-repository-documentdb.js +240 -0
- package/syncs/repositories/sync-repository-factory.js +43 -0
- package/syncs/repositories/sync-repository-interface.js +109 -0
- package/syncs/repositories/sync-repository-mongo.js +239 -0
- package/syncs/repositories/sync-repository-postgres.js +319 -0
- package/syncs/sync.js +0 -1
- package/token/repositories/token-repository-documentdb.js +137 -0
- package/token/repositories/token-repository-factory.js +40 -0
- package/token/repositories/token-repository-interface.js +131 -0
- package/token/repositories/token-repository-mongo.js +219 -0
- package/token/repositories/token-repository-postgres.js +264 -0
- package/token/repositories/token-repository.js +219 -0
- package/types/core/index.d.ts +2 -2
- package/types/integrations/index.d.ts +2 -6
- package/types/module-plugin/index.d.ts +5 -59
- package/types/syncs/index.d.ts +0 -2
- package/user/repositories/user-repository-documentdb.js +441 -0
- package/user/repositories/user-repository-factory.js +52 -0
- package/user/repositories/user-repository-interface.js +201 -0
- package/user/repositories/user-repository-mongo.js +308 -0
- package/user/repositories/user-repository-postgres.js +360 -0
- package/user/tests/doubles/test-user-repository.js +72 -0
- package/user/use-cases/authenticate-user.js +127 -0
- package/user/use-cases/authenticate-with-shared-secret.js +48 -0
- package/user/use-cases/create-individual-user.js +61 -0
- package/user/use-cases/create-organization-user.js +47 -0
- package/user/use-cases/create-token-for-user-id.js +30 -0
- package/user/use-cases/get-user-from-adopter-jwt.js +149 -0
- package/user/use-cases/get-user-from-bearer-token.js +77 -0
- package/user/use-cases/get-user-from-x-frigg-headers.js +132 -0
- package/user/use-cases/login-user.js +122 -0
- package/user/user.js +125 -0
- package/utils/backend-path.js +38 -0
- package/utils/index.js +6 -0
- package/websocket/repositories/websocket-connection-repository-documentdb.js +119 -0
- package/websocket/repositories/websocket-connection-repository-factory.js +44 -0
- package/websocket/repositories/websocket-connection-repository-interface.js +106 -0
- package/websocket/repositories/websocket-connection-repository-mongo.js +156 -0
- package/websocket/repositories/websocket-connection-repository-postgres.js +196 -0
- package/websocket/repositories/websocket-connection-repository.js +161 -0
- package/database/models/State.js +0 -9
- package/database/models/Token.js +0 -70
- package/database/mongo.js +0 -45
- package/encrypt/Cryptor.test.js +0 -32
- package/encrypt/encrypt.js +0 -132
- package/encrypt/encrypt.test.js +0 -1069
- package/errors/base-error.test.js +0 -32
- package/errors/fetch-error.test.js +0 -79
- package/errors/halt-error.test.js +0 -11
- package/errors/validation-errors.test.js +0 -120
- package/integrations/create-frigg-backend.js +0 -31
- package/integrations/integration-factory.js +0 -251
- package/integrations/integration-mapping.js +0 -43
- package/integrations/integration-model.js +0 -46
- package/integrations/integration-user.js +0 -144
- package/integrations/test/integration-base.test.js +0 -144
- package/lambda/TimeoutCatcher.test.js +0 -68
- package/logs/logger.test.js +0 -76
- package/module-plugin/auther.js +0 -393
- package/module-plugin/credential.js +0 -22
- package/module-plugin/entity-manager.js +0 -70
- package/module-plugin/manager.js +0 -169
- package/module-plugin/module-factory.js +0 -61
- package/module-plugin/requester/api-key.js +0 -36
- package/module-plugin/requester/requester.test.js +0 -28
- package/module-plugin/test/auther.test.js +0 -97
- /package/{module-plugin → modules}/ModuleConstants.js +0 -0
- /package/{module-plugin → modules}/requester/basic.js +0 -0
- /package/{module-plugin → modules}/requester/oauth-2.js +0 -0
- /package/{module-plugin → modules}/test/mock-api/mocks/hubspot.js +0 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Encryption Schema Registry
|
|
3
|
+
*
|
|
4
|
+
* Centralized registry defining which fields require encryption for each Prisma model.
|
|
5
|
+
* Database-agnostic, works identically for MongoDB and PostgreSQL.
|
|
6
|
+
* Extensible by integration developers via appDefinition.
|
|
7
|
+
*
|
|
8
|
+
* Field path format: 'fieldName' or 'parent.child.field' for nested JSON.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { logger } = require('./logger');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Core encryption schema (immutable - cannot be overridden by custom schemas)
|
|
15
|
+
*/
|
|
16
|
+
const CORE_ENCRYPTION_SCHEMA = {
|
|
17
|
+
Credential: {
|
|
18
|
+
fields: [
|
|
19
|
+
'data.access_token',
|
|
20
|
+
'data.refresh_token',
|
|
21
|
+
'data.id_token',
|
|
22
|
+
'data.api_key',
|
|
23
|
+
'data.apiKey',
|
|
24
|
+
'data.API_KEY_VALUE',
|
|
25
|
+
'data.password',
|
|
26
|
+
'data.client_secret',
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
IntegrationMapping: {
|
|
31
|
+
fields: ['mapping'],
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
User: {
|
|
35
|
+
fields: ['hashword'],
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
Token: {
|
|
39
|
+
fields: ['token'],
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
let customSchema = {};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Validates a custom encryption schema
|
|
47
|
+
* @returns {{valid: boolean, errors: string[]}}
|
|
48
|
+
*/
|
|
49
|
+
function validateCustomSchema(schema) {
|
|
50
|
+
const errors = [];
|
|
51
|
+
|
|
52
|
+
if (!schema || typeof schema !== 'object') {
|
|
53
|
+
errors.push('Custom schema must be an object');
|
|
54
|
+
return { valid: false, errors };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
for (const [modelName, config] of Object.entries(schema)) {
|
|
58
|
+
if (typeof modelName !== 'string' || !modelName) {
|
|
59
|
+
errors.push(`Invalid model name: ${modelName}`);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!config || typeof config !== 'object') {
|
|
64
|
+
errors.push(`Model "${modelName}" must have a config object`);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (!Array.isArray(config.fields)) {
|
|
69
|
+
errors.push(`Model "${modelName}" must have a "fields" array`);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
for (const fieldPath of config.fields) {
|
|
74
|
+
if (typeof fieldPath !== 'string' || !fieldPath) {
|
|
75
|
+
errors.push(`Model "${modelName}" has invalid field path: ${fieldPath}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Check if trying to override core fields
|
|
79
|
+
const coreFields = CORE_ENCRYPTION_SCHEMA[modelName]?.fields || [];
|
|
80
|
+
if (coreFields.includes(fieldPath)) {
|
|
81
|
+
errors.push(
|
|
82
|
+
`Cannot override core encrypted field "${fieldPath}" in model "${modelName}"`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
valid: errors.length === 0,
|
|
90
|
+
errors,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Registers a custom encryption schema from integration developer.
|
|
96
|
+
* Merges with core schema, prevents overriding core fields.
|
|
97
|
+
* @throws {Error} If schema validation fails
|
|
98
|
+
*/
|
|
99
|
+
function registerCustomSchema(schema) {
|
|
100
|
+
if (!schema || Object.keys(schema).length === 0) {
|
|
101
|
+
return; // Nothing to register
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const validation = validateCustomSchema(schema);
|
|
105
|
+
if (!validation.valid) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
`Invalid custom encryption schema:\n- ${validation.errors.join('\n- ')}`
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
customSchema = { ...schema };
|
|
112
|
+
logger.info(
|
|
113
|
+
`Registered custom encryption schema for models: ${Object.keys(customSchema).join(', ')}`
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Extracts credential field paths from module definitions
|
|
119
|
+
* @param {Array} moduleDefinitions - Array of module definition objects
|
|
120
|
+
* @returns {Array<string>} Array of field paths with data. prefix
|
|
121
|
+
*/
|
|
122
|
+
function extractCredentialFieldsFromModules(moduleDefinitions) {
|
|
123
|
+
const fields = [];
|
|
124
|
+
|
|
125
|
+
for (const moduleDef of moduleDefinitions) {
|
|
126
|
+
if (!moduleDef?.encryption?.credentialFields) {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const credentialFields = moduleDef.encryption.credentialFields;
|
|
131
|
+
if (!Array.isArray(credentialFields) || credentialFields.length === 0) {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
for (const field of credentialFields) {
|
|
136
|
+
const prefixedField = field.startsWith('data.') ? field : `data.${field}`;
|
|
137
|
+
fields.push(prefixedField);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return [...new Set(fields)];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Loads and registers encryption schemas from API module definitions.
|
|
146
|
+
* Each module can declare credentialFields to encrypt in its encryption config.
|
|
147
|
+
*
|
|
148
|
+
* @param {Array} integrations - Array of integration classes with modules
|
|
149
|
+
*/
|
|
150
|
+
function loadModuleEncryptionSchemas(integrations) {
|
|
151
|
+
if (!integrations) {
|
|
152
|
+
throw new Error('integrations parameter is required');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (!Array.isArray(integrations)) {
|
|
156
|
+
throw new Error('integrations must be an array');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (integrations.length === 0) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const { getModulesDefinitionFromIntegrationClasses } = require('../integrations/utils/map-integration-dto');
|
|
164
|
+
|
|
165
|
+
const moduleDefinitions = getModulesDefinitionFromIntegrationClasses(integrations);
|
|
166
|
+
const credentialFields = extractCredentialFieldsFromModules(moduleDefinitions);
|
|
167
|
+
|
|
168
|
+
if (credentialFields.length === 0) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const moduleSchema = {
|
|
173
|
+
Credential: {
|
|
174
|
+
fields: credentialFields
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
logger.info(
|
|
179
|
+
`Registering module-level encryption for ${credentialFields.length} credential fields`
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
registerCustomSchema(moduleSchema);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Loads and registers custom encryption schema from appDefinition.
|
|
187
|
+
* Gracefully handles cases where appDefinition is not available.
|
|
188
|
+
*
|
|
189
|
+
* This ensures that custom encryption schemas defined in the backend's index.js
|
|
190
|
+
* are registered before any repositories attempt to encrypt data.
|
|
191
|
+
*
|
|
192
|
+
* Used by both Prisma (MongoDB/PostgreSQL) and DocumentDB encryption services.
|
|
193
|
+
*/
|
|
194
|
+
function loadCustomEncryptionSchema() {
|
|
195
|
+
try {
|
|
196
|
+
// Lazy require to avoid circular dependency issues
|
|
197
|
+
const path = require('node:path');
|
|
198
|
+
const { findNearestBackendPackageJson } = require('../../utils');
|
|
199
|
+
|
|
200
|
+
const backendPackagePath = findNearestBackendPackageJson();
|
|
201
|
+
if (!backendPackagePath) {
|
|
202
|
+
return; // No backend found, skip custom schema
|
|
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;
|
|
210
|
+
|
|
211
|
+
if (!appDefinition) {
|
|
212
|
+
return; // No app definition found
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Load app-level custom schema
|
|
216
|
+
const customSchema = appDefinition.encryption?.schema;
|
|
217
|
+
if (customSchema && Object.keys(customSchema).length > 0) {
|
|
218
|
+
registerCustomSchema(customSchema);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Load module-level encryption schemas from integrations
|
|
222
|
+
const integrations = appDefinition.integrations;
|
|
223
|
+
if (integrations && Array.isArray(integrations)) {
|
|
224
|
+
loadModuleEncryptionSchemas(integrations);
|
|
225
|
+
}
|
|
226
|
+
} catch (error) {
|
|
227
|
+
// Silently ignore errors - custom schema is optional
|
|
228
|
+
// This handles cases like:
|
|
229
|
+
// - Backend package.json not found (tests, standalone usage)
|
|
230
|
+
// - No appDefinition defined
|
|
231
|
+
// - No custom encryption schema specified
|
|
232
|
+
logger.debug('Could not load custom encryption schema:', error.message);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function getEncryptedFields(modelName) {
|
|
237
|
+
const coreFields = CORE_ENCRYPTION_SCHEMA[modelName]?.fields || [];
|
|
238
|
+
const customFields = customSchema[modelName]?.fields || [];
|
|
239
|
+
const allFields = [...coreFields, ...customFields];
|
|
240
|
+
return [...new Set(allFields)];
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function hasEncryptedFields(modelName) {
|
|
244
|
+
return getEncryptedFields(modelName).length > 0;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function getEncryptedModels() {
|
|
248
|
+
const coreModels = Object.keys(CORE_ENCRYPTION_SCHEMA);
|
|
249
|
+
const customModels = Object.keys(customSchema);
|
|
250
|
+
return [...new Set([...coreModels, ...customModels])];
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function resetCustomSchema() {
|
|
254
|
+
customSchema = {};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
module.exports = {
|
|
258
|
+
CORE_ENCRYPTION_SCHEMA,
|
|
259
|
+
getEncryptedFields,
|
|
260
|
+
hasEncryptedFields,
|
|
261
|
+
getEncryptedModels,
|
|
262
|
+
registerCustomSchema,
|
|
263
|
+
loadCustomEncryptionSchema,
|
|
264
|
+
loadModuleEncryptionSchemas,
|
|
265
|
+
extractCredentialFieldsFromModules,
|
|
266
|
+
validateCustomSchema,
|
|
267
|
+
resetCustomSchema,
|
|
268
|
+
};
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Field Encryption Service
|
|
3
|
+
*
|
|
4
|
+
* Infrastructure layer service that orchestrates field-level encryption/decryption.
|
|
5
|
+
* Handles nested JSON paths (e.g., 'data.access_token') and bulk operations.
|
|
6
|
+
*/
|
|
7
|
+
class FieldEncryptionService {
|
|
8
|
+
constructor({ cryptor, schema }) {
|
|
9
|
+
if (!cryptor) {
|
|
10
|
+
throw new Error('Cryptor instance required');
|
|
11
|
+
}
|
|
12
|
+
if (!schema || typeof schema.getEncryptedFields !== 'function') {
|
|
13
|
+
throw new Error('Schema with getEncryptedFields method required');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
this.cryptor = cryptor;
|
|
17
|
+
this.schema = schema;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async encryptFields(modelName, document) {
|
|
21
|
+
if (!document || typeof document !== 'object') {
|
|
22
|
+
return document;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const fields = this.schema.getEncryptedFields(modelName);
|
|
26
|
+
if (fields.length === 0) {
|
|
27
|
+
return document;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const encrypted = this._deepClone(document);
|
|
31
|
+
|
|
32
|
+
// Parallelize encryption of multiple fields
|
|
33
|
+
const encryptionPromises = fields.map(async (fieldPath) => {
|
|
34
|
+
const value = this._getNestedValue(encrypted, fieldPath);
|
|
35
|
+
|
|
36
|
+
if (this._shouldEncrypt(value)) {
|
|
37
|
+
const serializedValue = this._serializeForEncryption(value);
|
|
38
|
+
const encryptedValue = await this.cryptor.encrypt(serializedValue);
|
|
39
|
+
return { fieldPath, encryptedValue };
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const results = await Promise.all(encryptionPromises);
|
|
45
|
+
|
|
46
|
+
// Apply encrypted values
|
|
47
|
+
for (const result of results) {
|
|
48
|
+
if (result) {
|
|
49
|
+
this._setNestedValue(encrypted, result.fieldPath, result.encryptedValue);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return encrypted;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async decryptFields(modelName, document) {
|
|
57
|
+
if (!document || typeof document !== 'object') {
|
|
58
|
+
return document;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const fields = this.schema.getEncryptedFields(modelName);
|
|
62
|
+
if (fields.length === 0) {
|
|
63
|
+
return document;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const decrypted = this._deepClone(document);
|
|
67
|
+
|
|
68
|
+
// Parallelize decryption of multiple fields
|
|
69
|
+
const decryptionPromises = fields.map(async (fieldPath) => {
|
|
70
|
+
const value = this._getNestedValue(decrypted, fieldPath);
|
|
71
|
+
|
|
72
|
+
if (this._isEncrypted(value)) {
|
|
73
|
+
const decryptedValue = await this.cryptor.decrypt(value);
|
|
74
|
+
const deserializedValue = this._deserializeAfterDecryption(decryptedValue);
|
|
75
|
+
return { fieldPath, decryptedValue: deserializedValue };
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const results = await Promise.all(decryptionPromises);
|
|
81
|
+
|
|
82
|
+
// Apply decrypted values
|
|
83
|
+
for (const result of results) {
|
|
84
|
+
if (result) {
|
|
85
|
+
this._setNestedValue(decrypted, result.fieldPath, result.decryptedValue);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return decrypted;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async encryptFieldsInBulk(modelName, documents) {
|
|
93
|
+
if (!Array.isArray(documents)) {
|
|
94
|
+
return documents;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return Promise.all(
|
|
98
|
+
documents.map((doc) => this.encryptFields(modelName, doc))
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async decryptFieldsInBulk(modelName, documents) {
|
|
103
|
+
if (!Array.isArray(documents)) {
|
|
104
|
+
return documents;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return Promise.all(
|
|
108
|
+
documents.map((doc) => this.decryptFields(modelName, doc))
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
_shouldEncrypt(value) {
|
|
113
|
+
return (
|
|
114
|
+
value !== null &&
|
|
115
|
+
value !== undefined &&
|
|
116
|
+
value !== '' &&
|
|
117
|
+
!this._isEncrypted(value)
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
_isEncrypted(value) {
|
|
122
|
+
if (typeof value !== 'string') {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const parts = value.split(':');
|
|
127
|
+
return parts.length >= 4;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
_getNestedValue(obj, path) {
|
|
131
|
+
if (!obj || !path) {
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return path.split('.').reduce((current, key) => {
|
|
136
|
+
return current?.[key];
|
|
137
|
+
}, obj);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
_setNestedValue(obj, path, value) {
|
|
141
|
+
if (!obj || !path) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const keys = path.split('.');
|
|
146
|
+
const lastKey = keys.pop();
|
|
147
|
+
|
|
148
|
+
const target = keys.reduce((current, key) => {
|
|
149
|
+
if (!current[key] || typeof current[key] !== 'object') {
|
|
150
|
+
current[key] = {};
|
|
151
|
+
}
|
|
152
|
+
return current[key];
|
|
153
|
+
}, obj);
|
|
154
|
+
|
|
155
|
+
target[lastKey] = value;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
_deepClone(obj) {
|
|
159
|
+
// Use structuredClone (Node.js 17+) for better performance
|
|
160
|
+
// Falls back to custom implementation for older Node versions
|
|
161
|
+
if (typeof structuredClone !== 'undefined') {
|
|
162
|
+
try {
|
|
163
|
+
return structuredClone(obj);
|
|
164
|
+
} catch {
|
|
165
|
+
// Fall through to custom implementation
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Custom fallback for older environments
|
|
170
|
+
if (obj === null || typeof obj !== 'object') {
|
|
171
|
+
return obj;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (obj instanceof Date) {
|
|
175
|
+
return new Date(obj.getTime());
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (Array.isArray(obj)) {
|
|
179
|
+
return obj.map((item) => this._deepClone(item));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const cloned = {};
|
|
183
|
+
for (const key in obj) {
|
|
184
|
+
if (obj.hasOwnProperty(key)) {
|
|
185
|
+
cloned[key] = this._deepClone(obj[key]);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return cloned;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Serialize a value for encryption
|
|
194
|
+
* Objects/arrays are JSON stringified, primitives are converted to strings
|
|
195
|
+
* @private
|
|
196
|
+
*/
|
|
197
|
+
_serializeForEncryption(value) {
|
|
198
|
+
if (typeof value === 'object' && value !== null) {
|
|
199
|
+
// JSON.stringify for objects and arrays
|
|
200
|
+
return JSON.stringify(value);
|
|
201
|
+
}
|
|
202
|
+
// For primitives (string, number, boolean), convert to string
|
|
203
|
+
return String(value);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Deserialize a value after decryption
|
|
208
|
+
* Attempts to parse as JSON, returns string if parsing fails
|
|
209
|
+
* @private
|
|
210
|
+
*/
|
|
211
|
+
_deserializeAfterDecryption(value) {
|
|
212
|
+
if (typeof value !== 'string') {
|
|
213
|
+
return value;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Try to parse as JSON
|
|
217
|
+
try {
|
|
218
|
+
return JSON.parse(value);
|
|
219
|
+
} catch {
|
|
220
|
+
// Not valid JSON, return as-is (likely was a plain string field)
|
|
221
|
+
return value;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
module.exports = { FieldEncryptionService };
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Encryption Logger
|
|
3
|
+
*
|
|
4
|
+
* Centralized logging for encryption operations.
|
|
5
|
+
* Prevents sensitive data leakage in production logs.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const LOG_LEVELS = {
|
|
9
|
+
DEBUG: 0,
|
|
10
|
+
INFO: 1,
|
|
11
|
+
WARN: 2,
|
|
12
|
+
ERROR: 3,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
class EncryptionLogger {
|
|
16
|
+
constructor() {
|
|
17
|
+
this.minLevel = this._getMinLevel();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
_getMinLevel() {
|
|
21
|
+
const level = process.env.FRIGG_LOG_LEVEL || 'INFO';
|
|
22
|
+
return LOG_LEVELS[level.toUpperCase()] ?? LOG_LEVELS.INFO;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
_shouldLog(level) {
|
|
26
|
+
return LOG_LEVELS[level] >= this.minLevel;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
_sanitize(message) {
|
|
30
|
+
// Remove potential key material or encrypted data from logs
|
|
31
|
+
if (typeof message === 'string') {
|
|
32
|
+
// Truncate long base64 strings that might be keys or encrypted data
|
|
33
|
+
return message.replace(/([A-Za-z0-9+/=]{50,})/g, (match) =>
|
|
34
|
+
`${match.substring(0, 10)}...[${match.length} chars]`
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
return message;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
debug(message, ...args) {
|
|
41
|
+
if (this._shouldLog('DEBUG')) {
|
|
42
|
+
console.log(`[Frigg Debug]`, this._sanitize(message), ...args);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
info(message, ...args) {
|
|
47
|
+
if (this._shouldLog('INFO')) {
|
|
48
|
+
console.log(`[Frigg]`, this._sanitize(message), ...args);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
warn(message, ...args) {
|
|
53
|
+
if (this._shouldLog('WARN')) {
|
|
54
|
+
console.warn(`[Frigg]`, this._sanitize(message), ...args);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
error(message, error) {
|
|
59
|
+
if (this._shouldLog('ERROR')) {
|
|
60
|
+
const sanitizedMessage = this._sanitize(message);
|
|
61
|
+
|
|
62
|
+
// In production, don't log stack traces with sensitive paths
|
|
63
|
+
const isProduction = process.env.STAGE === 'production';
|
|
64
|
+
|
|
65
|
+
if (error && !isProduction) {
|
|
66
|
+
console.error(`[Frigg]`, sanitizedMessage, error);
|
|
67
|
+
} else if (error) {
|
|
68
|
+
console.error(`[Frigg]`, sanitizedMessage, error.message);
|
|
69
|
+
} else {
|
|
70
|
+
console.error(`[Frigg]`, sanitizedMessage);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Singleton instance
|
|
77
|
+
const logger = new EncryptionLogger();
|
|
78
|
+
|
|
79
|
+
module.exports = { logger };
|