@friggframework/core 2.0.0-next.5 → 2.0.0-next.50
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 +693 -0
- package/README.md +959 -50
- package/application/commands/README.md +421 -0
- package/application/commands/credential-commands.js +224 -0
- package/application/commands/entity-commands.js +315 -0
- package/application/commands/integration-commands.js +179 -0
- package/application/commands/user-commands.js +213 -0
- package/application/index.js +69 -0
- package/core/CLAUDE.md +690 -0
- package/core/Worker.js +8 -21
- package/core/create-handler.js +2 -7
- package/credential/repositories/credential-repository-factory.js +47 -0
- package/credential/repositories/credential-repository-interface.js +98 -0
- package/credential/repositories/credential-repository-mongo.js +307 -0
- package/credential/repositories/credential-repository-postgres.js +313 -0
- package/credential/repositories/credential-repository.js +302 -0
- package/credential/use-cases/get-credential-for-user.js +21 -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/encryption/README.md +684 -0
- package/database/encryption/encryption-schema-registry.js +141 -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 +25 -12
- package/database/models/WebsocketConnection.js +16 -10
- package/database/models/readme.md +1 -0
- package/database/prisma.js +222 -0
- package/database/repositories/health-check-repository-factory.js +43 -0
- package/database/repositories/health-check-repository-interface.js +87 -0
- package/database/repositories/health-check-repository-mongodb.js +91 -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 +137 -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 +400 -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/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 +22898 -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 +3982 -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 +362 -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 +25072 -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 +3982 -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 +345 -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 +180 -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 +256 -0
- package/handlers/routers/health.js +519 -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 +296 -54
- package/integrations/integration-router.js +381 -182
- package/integrations/options.js +1 -1
- package/integrations/repositories/integration-mapping-repository-factory.js +50 -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-factory.js +44 -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-factory.js +46 -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/dummy-integration-class.js +83 -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 +87 -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 +93 -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 +36 -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-factory.js +33 -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/{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 +59 -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 +55 -0
- package/modules/use-cases/process-authorization-callback.js +122 -0
- package/modules/use-cases/refresh-entity-options.js +59 -0
- package/modules/use-cases/test-module-auth.js +55 -0
- package/modules/utils/map-module-dto.js +18 -0
- package/package.json +82 -50
- package/prisma-mongodb/schema.prisma +362 -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/migration_lock.toml +3 -0
- package/prisma-postgresql/schema.prisma +345 -0
- package/queues/queuer-util.js +28 -15
- package/syncs/manager.js +468 -443
- package/syncs/repositories/sync-repository-factory.js +38 -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-factory.js +33 -0
- package/token/repositories/token-repository-interface.js +131 -0
- package/token/repositories/token-repository-mongo.js +212 -0
- package/token/repositories/token-repository-postgres.js +257 -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-factory.js +46 -0
- package/user/repositories/user-repository-interface.js +198 -0
- package/user/repositories/user-repository-mongo.js +291 -0
- package/user/repositories/user-repository-postgres.js +350 -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 +106 -0
- package/user/use-cases/login-user.js +122 -0
- package/user/user.js +93 -0
- package/utils/backend-path.js +38 -0
- package/utils/index.js +6 -0
- package/websocket/repositories/websocket-connection-repository-factory.js +37 -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/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/api-key.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,141 @@
|
|
|
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
|
+
],
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
IntegrationMapping: {
|
|
26
|
+
fields: ['mapping'],
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
User: {
|
|
30
|
+
fields: ['hashword'],
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
Token: {
|
|
34
|
+
fields: ['token'],
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
let customSchema = {};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Validates a custom encryption schema
|
|
42
|
+
* @returns {{valid: boolean, errors: string[]}}
|
|
43
|
+
*/
|
|
44
|
+
function validateCustomSchema(schema) {
|
|
45
|
+
const errors = [];
|
|
46
|
+
|
|
47
|
+
if (!schema || typeof schema !== 'object') {
|
|
48
|
+
errors.push('Custom schema must be an object');
|
|
49
|
+
return { valid: false, errors };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
for (const [modelName, config] of Object.entries(schema)) {
|
|
53
|
+
if (typeof modelName !== 'string' || !modelName) {
|
|
54
|
+
errors.push(`Invalid model name: ${modelName}`);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!config || typeof config !== 'object') {
|
|
59
|
+
errors.push(`Model "${modelName}" must have a config object`);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!Array.isArray(config.fields)) {
|
|
64
|
+
errors.push(`Model "${modelName}" must have a "fields" array`);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
for (const fieldPath of config.fields) {
|
|
69
|
+
if (typeof fieldPath !== 'string' || !fieldPath) {
|
|
70
|
+
errors.push(`Model "${modelName}" has invalid field path: ${fieldPath}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Check if trying to override core fields
|
|
74
|
+
const coreFields = CORE_ENCRYPTION_SCHEMA[modelName]?.fields || [];
|
|
75
|
+
if (coreFields.includes(fieldPath)) {
|
|
76
|
+
errors.push(
|
|
77
|
+
`Cannot override core encrypted field "${fieldPath}" in model "${modelName}"`
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
valid: errors.length === 0,
|
|
85
|
+
errors,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Registers a custom encryption schema from integration developer.
|
|
91
|
+
* Merges with core schema, prevents overriding core fields.
|
|
92
|
+
* @throws {Error} If schema validation fails
|
|
93
|
+
*/
|
|
94
|
+
function registerCustomSchema(schema) {
|
|
95
|
+
if (!schema || Object.keys(schema).length === 0) {
|
|
96
|
+
return; // Nothing to register
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const validation = validateCustomSchema(schema);
|
|
100
|
+
if (!validation.valid) {
|
|
101
|
+
throw new Error(
|
|
102
|
+
`Invalid custom encryption schema:\n- ${validation.errors.join('\n- ')}`
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
customSchema = { ...schema };
|
|
107
|
+
logger.info(
|
|
108
|
+
`Registered custom encryption schema for models: ${Object.keys(customSchema).join(', ')}`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function getEncryptedFields(modelName) {
|
|
113
|
+
const coreFields = CORE_ENCRYPTION_SCHEMA[modelName]?.fields || [];
|
|
114
|
+
const customFields = customSchema[modelName]?.fields || [];
|
|
115
|
+
const allFields = [...coreFields, ...customFields];
|
|
116
|
+
return [...new Set(allFields)];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function hasEncryptedFields(modelName) {
|
|
120
|
+
return getEncryptedFields(modelName).length > 0;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function getEncryptedModels() {
|
|
124
|
+
const coreModels = Object.keys(CORE_ENCRYPTION_SCHEMA);
|
|
125
|
+
const customModels = Object.keys(customSchema);
|
|
126
|
+
return [...new Set([...coreModels, ...customModels])];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function resetCustomSchema() {
|
|
130
|
+
customSchema = {};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
module.exports = {
|
|
134
|
+
CORE_ENCRYPTION_SCHEMA,
|
|
135
|
+
getEncryptedFields,
|
|
136
|
+
hasEncryptedFields,
|
|
137
|
+
getEncryptedModels,
|
|
138
|
+
registerCustomSchema,
|
|
139
|
+
validateCustomSchema,
|
|
140
|
+
resetCustomSchema, // For testing only
|
|
141
|
+
};
|
|
@@ -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 };
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prisma Client Extension for transparent field-level encryption.
|
|
3
|
+
* Intercepts Prisma queries to encrypt on write and decrypt on read.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { getEncryptedFields } = require('./encryption-schema-registry');
|
|
7
|
+
const { FieldEncryptionService } = require('./field-encryption-service');
|
|
8
|
+
|
|
9
|
+
function createEncryptionExtension({ cryptor, enabled = true }) {
|
|
10
|
+
if (!enabled) {
|
|
11
|
+
return (client) => client;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (!cryptor) {
|
|
15
|
+
throw new Error(
|
|
16
|
+
'Cryptor instance required for encryption extension'
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const encryptionService = new FieldEncryptionService({
|
|
21
|
+
cryptor,
|
|
22
|
+
schema: { getEncryptedFields },
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
name: 'frigg-field-encryption',
|
|
27
|
+
query: {
|
|
28
|
+
$allModels: {
|
|
29
|
+
async create({ model, args, query }) {
|
|
30
|
+
if (args.data) {
|
|
31
|
+
args.data = await encryptionService.encryptFields(
|
|
32
|
+
model,
|
|
33
|
+
args.data
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const result = await query(args);
|
|
38
|
+
|
|
39
|
+
if (result) {
|
|
40
|
+
return await encryptionService.decryptFields(
|
|
41
|
+
model,
|
|
42
|
+
result
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return result;
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
async createMany({ model, args, query }) {
|
|
50
|
+
if (args.data && Array.isArray(args.data)) {
|
|
51
|
+
args.data =
|
|
52
|
+
await encryptionService.encryptFieldsInBulk(
|
|
53
|
+
model,
|
|
54
|
+
args.data
|
|
55
|
+
);
|
|
56
|
+
} else if (args.data) {
|
|
57
|
+
args.data = await encryptionService.encryptFields(
|
|
58
|
+
model,
|
|
59
|
+
args.data
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return await query(args);
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
async update({ model, args, query }) {
|
|
67
|
+
if (args.data) {
|
|
68
|
+
args.data = await encryptionService.encryptFields(
|
|
69
|
+
model,
|
|
70
|
+
args.data
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const result = await query(args);
|
|
75
|
+
|
|
76
|
+
if (result) {
|
|
77
|
+
return await encryptionService.decryptFields(
|
|
78
|
+
model,
|
|
79
|
+
result
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return result;
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
async updateMany({ model, args, query }) {
|
|
87
|
+
if (args.data) {
|
|
88
|
+
args.data = await encryptionService.encryptFields(
|
|
89
|
+
model,
|
|
90
|
+
args.data
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return await query(args);
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
async upsert({ model, args, query }) {
|
|
98
|
+
if (args.create) {
|
|
99
|
+
args.create = await encryptionService.encryptFields(
|
|
100
|
+
model,
|
|
101
|
+
args.create
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (args.update) {
|
|
106
|
+
args.update = await encryptionService.encryptFields(
|
|
107
|
+
model,
|
|
108
|
+
args.update
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const result = await query(args);
|
|
113
|
+
|
|
114
|
+
if (result) {
|
|
115
|
+
return await encryptionService.decryptFields(
|
|
116
|
+
model,
|
|
117
|
+
result
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return result;
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
async findUnique({ model, args, query }) {
|
|
125
|
+
const result = await query(args);
|
|
126
|
+
|
|
127
|
+
if (result) {
|
|
128
|
+
return await encryptionService.decryptFields(
|
|
129
|
+
model,
|
|
130
|
+
result
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return result;
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
async findFirst({ model, args, query }) {
|
|
138
|
+
const result = await query(args);
|
|
139
|
+
|
|
140
|
+
if (result) {
|
|
141
|
+
return await encryptionService.decryptFields(
|
|
142
|
+
model,
|
|
143
|
+
result
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return result;
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
async findMany({ model, args, query }) {
|
|
151
|
+
const results = await query(args);
|
|
152
|
+
|
|
153
|
+
if (results && Array.isArray(results)) {
|
|
154
|
+
return await encryptionService.decryptFieldsInBulk(
|
|
155
|
+
model,
|
|
156
|
+
results
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return results;
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
async delete({ model, args, query }) {
|
|
164
|
+
const result = await query(args);
|
|
165
|
+
|
|
166
|
+
if (result) {
|
|
167
|
+
return await encryptionService.decryptFields(
|
|
168
|
+
model,
|
|
169
|
+
result
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return result;
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
async deleteMany({ model, args, query }) {
|
|
177
|
+
return await query(args);
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
async count({ model, args, query }) {
|
|
181
|
+
return await query(args);
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
async aggregate({ model, args, query }) {
|
|
185
|
+
return await query(args);
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
async groupBy({ model, args, query }) {
|
|
189
|
+
return await query(args);
|
|
190
|
+
},
|
|
191
|
+
|
|
192
|
+
async findFirstOrThrow({ model, args, query }) {
|
|
193
|
+
const result = await query(args);
|
|
194
|
+
|
|
195
|
+
if (result) {
|
|
196
|
+
return await encryptionService.decryptFields(
|
|
197
|
+
model,
|
|
198
|
+
result
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return result;
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
async findUniqueOrThrow({ model, args, query }) {
|
|
206
|
+
const result = await query(args);
|
|
207
|
+
|
|
208
|
+
if (result) {
|
|
209
|
+
return await encryptionService.decryptFields(
|
|
210
|
+
model,
|
|
211
|
+
result
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return result;
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
module.exports = { createEncryptionExtension };
|