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