@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.
Files changed (267) hide show
  1. package/CLAUDE.md +693 -0
  2. package/README.md +959 -50
  3. package/application/commands/README.md +421 -0
  4. package/application/commands/credential-commands.js +224 -0
  5. package/application/commands/entity-commands.js +315 -0
  6. package/application/commands/integration-commands.js +179 -0
  7. package/application/commands/user-commands.js +213 -0
  8. package/application/index.js +69 -0
  9. package/core/CLAUDE.md +690 -0
  10. package/core/Worker.js +8 -21
  11. package/core/create-handler.js +2 -7
  12. package/credential/repositories/credential-repository-factory.js +47 -0
  13. package/credential/repositories/credential-repository-interface.js +98 -0
  14. package/credential/repositories/credential-repository-mongo.js +307 -0
  15. package/credential/repositories/credential-repository-postgres.js +313 -0
  16. package/credential/repositories/credential-repository.js +302 -0
  17. package/credential/use-cases/get-credential-for-user.js +21 -0
  18. package/credential/use-cases/update-authentication-status.js +15 -0
  19. package/database/MONGODB_TRANSACTION_FIX.md +198 -0
  20. package/database/adapters/lambda-invoker.js +97 -0
  21. package/database/config.js +154 -0
  22. package/database/encryption/README.md +684 -0
  23. package/database/encryption/encryption-schema-registry.js +141 -0
  24. package/database/encryption/field-encryption-service.js +226 -0
  25. package/database/encryption/logger.js +79 -0
  26. package/database/encryption/prisma-encryption-extension.js +222 -0
  27. package/database/index.js +25 -12
  28. package/database/models/WebsocketConnection.js +16 -10
  29. package/database/models/readme.md +1 -0
  30. package/database/prisma.js +222 -0
  31. package/database/repositories/health-check-repository-factory.js +43 -0
  32. package/database/repositories/health-check-repository-interface.js +87 -0
  33. package/database/repositories/health-check-repository-mongodb.js +91 -0
  34. package/database/repositories/health-check-repository-postgres.js +82 -0
  35. package/database/repositories/health-check-repository.js +108 -0
  36. package/database/repositories/migration-status-repository-s3.js +137 -0
  37. package/database/use-cases/check-database-health-use-case.js +29 -0
  38. package/database/use-cases/check-database-state-use-case.js +81 -0
  39. package/database/use-cases/check-encryption-health-use-case.js +83 -0
  40. package/database/use-cases/get-database-state-via-worker-use-case.js +61 -0
  41. package/database/use-cases/get-migration-status-use-case.js +93 -0
  42. package/database/use-cases/run-database-migration-use-case.js +137 -0
  43. package/database/use-cases/test-encryption-use-case.js +253 -0
  44. package/database/use-cases/trigger-database-migration-use-case.js +157 -0
  45. package/database/utils/mongodb-collection-utils.js +91 -0
  46. package/database/utils/mongodb-schema-init.js +106 -0
  47. package/database/utils/prisma-runner.js +400 -0
  48. package/database/utils/prisma-schema-parser.js +182 -0
  49. package/docs/PROCESS_MANAGEMENT_QUEUE_SPEC.md +517 -0
  50. package/encrypt/Cryptor.js +34 -168
  51. package/encrypt/index.js +1 -2
  52. package/encrypt/test-encrypt.js +0 -2
  53. package/generated/prisma-mongodb/client.d.ts +1 -0
  54. package/generated/prisma-mongodb/client.js +4 -0
  55. package/generated/prisma-mongodb/default.d.ts +1 -0
  56. package/generated/prisma-mongodb/default.js +4 -0
  57. package/generated/prisma-mongodb/edge.d.ts +1 -0
  58. package/generated/prisma-mongodb/edge.js +334 -0
  59. package/generated/prisma-mongodb/index-browser.js +316 -0
  60. package/generated/prisma-mongodb/index.d.ts +22898 -0
  61. package/generated/prisma-mongodb/index.js +359 -0
  62. package/generated/prisma-mongodb/package.json +183 -0
  63. package/generated/prisma-mongodb/query-engine-debian-openssl-3.0.x +0 -0
  64. package/generated/prisma-mongodb/query-engine-rhel-openssl-3.0.x +0 -0
  65. package/generated/prisma-mongodb/runtime/binary.d.ts +1 -0
  66. package/generated/prisma-mongodb/runtime/binary.js +289 -0
  67. package/generated/prisma-mongodb/runtime/edge-esm.js +34 -0
  68. package/generated/prisma-mongodb/runtime/edge.js +34 -0
  69. package/generated/prisma-mongodb/runtime/index-browser.d.ts +370 -0
  70. package/generated/prisma-mongodb/runtime/index-browser.js +16 -0
  71. package/generated/prisma-mongodb/runtime/library.d.ts +3982 -0
  72. package/generated/prisma-mongodb/runtime/react-native.js +83 -0
  73. package/generated/prisma-mongodb/runtime/wasm-compiler-edge.js +84 -0
  74. package/generated/prisma-mongodb/runtime/wasm-engine-edge.js +36 -0
  75. package/generated/prisma-mongodb/schema.prisma +362 -0
  76. package/generated/prisma-mongodb/wasm-edge-light-loader.mjs +4 -0
  77. package/generated/prisma-mongodb/wasm-worker-loader.mjs +4 -0
  78. package/generated/prisma-mongodb/wasm.d.ts +1 -0
  79. package/generated/prisma-mongodb/wasm.js +341 -0
  80. package/generated/prisma-postgresql/client.d.ts +1 -0
  81. package/generated/prisma-postgresql/client.js +4 -0
  82. package/generated/prisma-postgresql/default.d.ts +1 -0
  83. package/generated/prisma-postgresql/default.js +4 -0
  84. package/generated/prisma-postgresql/edge.d.ts +1 -0
  85. package/generated/prisma-postgresql/edge.js +356 -0
  86. package/generated/prisma-postgresql/index-browser.js +338 -0
  87. package/generated/prisma-postgresql/index.d.ts +25072 -0
  88. package/generated/prisma-postgresql/index.js +381 -0
  89. package/generated/prisma-postgresql/package.json +183 -0
  90. package/generated/prisma-postgresql/query-engine-debian-openssl-3.0.x +0 -0
  91. package/generated/prisma-postgresql/query-engine-rhel-openssl-3.0.x +0 -0
  92. package/generated/prisma-postgresql/query_engine_bg.js +2 -0
  93. package/generated/prisma-postgresql/query_engine_bg.wasm +0 -0
  94. package/generated/prisma-postgresql/runtime/binary.d.ts +1 -0
  95. package/generated/prisma-postgresql/runtime/binary.js +289 -0
  96. package/generated/prisma-postgresql/runtime/edge-esm.js +34 -0
  97. package/generated/prisma-postgresql/runtime/edge.js +34 -0
  98. package/generated/prisma-postgresql/runtime/index-browser.d.ts +370 -0
  99. package/generated/prisma-postgresql/runtime/index-browser.js +16 -0
  100. package/generated/prisma-postgresql/runtime/library.d.ts +3982 -0
  101. package/generated/prisma-postgresql/runtime/react-native.js +83 -0
  102. package/generated/prisma-postgresql/runtime/wasm-compiler-edge.js +84 -0
  103. package/generated/prisma-postgresql/runtime/wasm-engine-edge.js +36 -0
  104. package/generated/prisma-postgresql/schema.prisma +345 -0
  105. package/generated/prisma-postgresql/wasm-edge-light-loader.mjs +4 -0
  106. package/generated/prisma-postgresql/wasm-worker-loader.mjs +4 -0
  107. package/generated/prisma-postgresql/wasm.d.ts +1 -0
  108. package/generated/prisma-postgresql/wasm.js +363 -0
  109. package/handlers/WEBHOOKS.md +653 -0
  110. package/handlers/app-definition-loader.js +38 -0
  111. package/handlers/app-handler-helpers.js +56 -0
  112. package/handlers/backend-utils.js +180 -0
  113. package/handlers/database-migration-handler.js +227 -0
  114. package/handlers/integration-event-dispatcher.js +54 -0
  115. package/handlers/routers/HEALTHCHECK.md +342 -0
  116. package/handlers/routers/auth.js +15 -0
  117. package/handlers/routers/db-migration.handler.js +29 -0
  118. package/handlers/routers/db-migration.js +256 -0
  119. package/handlers/routers/health.js +519 -0
  120. package/handlers/routers/integration-defined-routers.js +45 -0
  121. package/handlers/routers/integration-webhook-routers.js +67 -0
  122. package/handlers/routers/user.js +63 -0
  123. package/handlers/routers/websocket.js +57 -0
  124. package/handlers/use-cases/check-external-apis-health-use-case.js +81 -0
  125. package/handlers/use-cases/check-integrations-health-use-case.js +44 -0
  126. package/handlers/workers/db-migration.js +352 -0
  127. package/handlers/workers/integration-defined-workers.js +27 -0
  128. package/index.js +77 -22
  129. package/integrations/WEBHOOK-QUICKSTART.md +151 -0
  130. package/integrations/index.js +12 -10
  131. package/integrations/integration-base.js +296 -54
  132. package/integrations/integration-router.js +381 -182
  133. package/integrations/options.js +1 -1
  134. package/integrations/repositories/integration-mapping-repository-factory.js +50 -0
  135. package/integrations/repositories/integration-mapping-repository-interface.js +106 -0
  136. package/integrations/repositories/integration-mapping-repository-mongo.js +161 -0
  137. package/integrations/repositories/integration-mapping-repository-postgres.js +227 -0
  138. package/integrations/repositories/integration-mapping-repository.js +156 -0
  139. package/integrations/repositories/integration-repository-factory.js +44 -0
  140. package/integrations/repositories/integration-repository-interface.js +127 -0
  141. package/integrations/repositories/integration-repository-mongo.js +303 -0
  142. package/integrations/repositories/integration-repository-postgres.js +352 -0
  143. package/integrations/repositories/process-repository-factory.js +46 -0
  144. package/integrations/repositories/process-repository-interface.js +90 -0
  145. package/integrations/repositories/process-repository-mongo.js +190 -0
  146. package/integrations/repositories/process-repository-postgres.js +217 -0
  147. package/integrations/tests/doubles/dummy-integration-class.js +83 -0
  148. package/integrations/tests/doubles/test-integration-repository.js +99 -0
  149. package/integrations/use-cases/create-integration.js +83 -0
  150. package/integrations/use-cases/create-process.js +128 -0
  151. package/integrations/use-cases/delete-integration-for-user.js +101 -0
  152. package/integrations/use-cases/find-integration-context-by-external-entity-id.js +72 -0
  153. package/integrations/use-cases/get-integration-for-user.js +78 -0
  154. package/integrations/use-cases/get-integration-instance-by-definition.js +67 -0
  155. package/integrations/use-cases/get-integration-instance.js +83 -0
  156. package/integrations/use-cases/get-integrations-for-user.js +87 -0
  157. package/integrations/use-cases/get-possible-integrations.js +27 -0
  158. package/integrations/use-cases/get-process.js +87 -0
  159. package/integrations/use-cases/index.js +19 -0
  160. package/integrations/use-cases/load-integration-context.js +71 -0
  161. package/integrations/use-cases/update-integration-messages.js +44 -0
  162. package/integrations/use-cases/update-integration-status.js +32 -0
  163. package/integrations/use-cases/update-integration.js +93 -0
  164. package/integrations/use-cases/update-process-metrics.js +201 -0
  165. package/integrations/use-cases/update-process-state.js +119 -0
  166. package/integrations/utils/map-integration-dto.js +36 -0
  167. package/jest-global-setup-noop.js +3 -0
  168. package/jest-global-teardown-noop.js +3 -0
  169. package/logs/logger.js +0 -4
  170. package/{module-plugin → modules}/entity.js +1 -1
  171. package/{module-plugin → modules}/index.js +0 -8
  172. package/modules/module-factory.js +56 -0
  173. package/modules/module.js +221 -0
  174. package/modules/repositories/module-repository-factory.js +33 -0
  175. package/modules/repositories/module-repository-interface.js +129 -0
  176. package/modules/repositories/module-repository-mongo.js +377 -0
  177. package/modules/repositories/module-repository-postgres.js +426 -0
  178. package/modules/repositories/module-repository.js +316 -0
  179. package/{module-plugin → modules}/requester/requester.js +1 -0
  180. package/{module-plugin → modules}/test/mock-api/api.js +8 -3
  181. package/{module-plugin → modules}/test/mock-api/definition.js +12 -8
  182. package/modules/tests/doubles/test-module-factory.js +16 -0
  183. package/modules/tests/doubles/test-module-repository.js +39 -0
  184. package/modules/use-cases/get-entities-for-user.js +32 -0
  185. package/modules/use-cases/get-entity-options-by-id.js +59 -0
  186. package/modules/use-cases/get-entity-options-by-type.js +34 -0
  187. package/modules/use-cases/get-module-instance-from-type.js +31 -0
  188. package/modules/use-cases/get-module.js +55 -0
  189. package/modules/use-cases/process-authorization-callback.js +122 -0
  190. package/modules/use-cases/refresh-entity-options.js +59 -0
  191. package/modules/use-cases/test-module-auth.js +55 -0
  192. package/modules/utils/map-module-dto.js +18 -0
  193. package/package.json +82 -50
  194. package/prisma-mongodb/schema.prisma +362 -0
  195. package/prisma-postgresql/migrations/20250930193005_init/migration.sql +315 -0
  196. package/prisma-postgresql/migrations/20251006135218_init/migration.sql +9 -0
  197. package/prisma-postgresql/migrations/20251010000000_remove_unused_entity_reference_map/migration.sql +3 -0
  198. package/prisma-postgresql/migrations/migration_lock.toml +3 -0
  199. package/prisma-postgresql/schema.prisma +345 -0
  200. package/queues/queuer-util.js +28 -15
  201. package/syncs/manager.js +468 -443
  202. package/syncs/repositories/sync-repository-factory.js +38 -0
  203. package/syncs/repositories/sync-repository-interface.js +109 -0
  204. package/syncs/repositories/sync-repository-mongo.js +239 -0
  205. package/syncs/repositories/sync-repository-postgres.js +319 -0
  206. package/syncs/sync.js +0 -1
  207. package/token/repositories/token-repository-factory.js +33 -0
  208. package/token/repositories/token-repository-interface.js +131 -0
  209. package/token/repositories/token-repository-mongo.js +212 -0
  210. package/token/repositories/token-repository-postgres.js +257 -0
  211. package/token/repositories/token-repository.js +219 -0
  212. package/types/core/index.d.ts +2 -2
  213. package/types/integrations/index.d.ts +2 -6
  214. package/types/module-plugin/index.d.ts +5 -59
  215. package/types/syncs/index.d.ts +0 -2
  216. package/user/repositories/user-repository-factory.js +46 -0
  217. package/user/repositories/user-repository-interface.js +198 -0
  218. package/user/repositories/user-repository-mongo.js +291 -0
  219. package/user/repositories/user-repository-postgres.js +350 -0
  220. package/user/tests/doubles/test-user-repository.js +72 -0
  221. package/user/use-cases/authenticate-user.js +127 -0
  222. package/user/use-cases/authenticate-with-shared-secret.js +48 -0
  223. package/user/use-cases/create-individual-user.js +61 -0
  224. package/user/use-cases/create-organization-user.js +47 -0
  225. package/user/use-cases/create-token-for-user-id.js +30 -0
  226. package/user/use-cases/get-user-from-adopter-jwt.js +149 -0
  227. package/user/use-cases/get-user-from-bearer-token.js +77 -0
  228. package/user/use-cases/get-user-from-x-frigg-headers.js +106 -0
  229. package/user/use-cases/login-user.js +122 -0
  230. package/user/user.js +93 -0
  231. package/utils/backend-path.js +38 -0
  232. package/utils/index.js +6 -0
  233. package/websocket/repositories/websocket-connection-repository-factory.js +37 -0
  234. package/websocket/repositories/websocket-connection-repository-interface.js +106 -0
  235. package/websocket/repositories/websocket-connection-repository-mongo.js +156 -0
  236. package/websocket/repositories/websocket-connection-repository-postgres.js +196 -0
  237. package/websocket/repositories/websocket-connection-repository.js +161 -0
  238. package/database/models/State.js +0 -9
  239. package/database/models/Token.js +0 -70
  240. package/database/mongo.js +0 -45
  241. package/encrypt/Cryptor.test.js +0 -32
  242. package/encrypt/encrypt.js +0 -132
  243. package/encrypt/encrypt.test.js +0 -1069
  244. package/errors/base-error.test.js +0 -32
  245. package/errors/fetch-error.test.js +0 -79
  246. package/errors/halt-error.test.js +0 -11
  247. package/errors/validation-errors.test.js +0 -120
  248. package/integrations/create-frigg-backend.js +0 -31
  249. package/integrations/integration-factory.js +0 -251
  250. package/integrations/integration-mapping.js +0 -43
  251. package/integrations/integration-model.js +0 -46
  252. package/integrations/integration-user.js +0 -144
  253. package/integrations/test/integration-base.test.js +0 -144
  254. package/lambda/TimeoutCatcher.test.js +0 -68
  255. package/logs/logger.test.js +0 -76
  256. package/module-plugin/auther.js +0 -393
  257. package/module-plugin/credential.js +0 -22
  258. package/module-plugin/entity-manager.js +0 -70
  259. package/module-plugin/manager.js +0 -169
  260. package/module-plugin/module-factory.js +0 -61
  261. package/module-plugin/requester/requester.test.js +0 -28
  262. package/module-plugin/test/auther.test.js +0 -97
  263. /package/{module-plugin → modules}/ModuleConstants.js +0 -0
  264. /package/{module-plugin → modules}/requester/api-key.js +0 -0
  265. /package/{module-plugin → modules}/requester/basic.js +0 -0
  266. /package/{module-plugin → modules}/requester/oauth-2.js +0 -0
  267. /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 };