@friggframework/core 2.0.0-next.9 → 2.0.0-next.90

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 (309) hide show
  1. package/CLAUDE.md +702 -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 +271 -0
  7. package/application/commands/scheduler-commands.js +263 -0
  8. package/application/commands/user-commands.js +283 -0
  9. package/application/index.js +73 -0
  10. package/assertions/index.js +0 -3
  11. package/core/CLAUDE.md +690 -0
  12. package/core/Worker.js +60 -24
  13. package/core/create-handler.js +84 -6
  14. package/credential/repositories/credential-repository-documentdb.js +304 -0
  15. package/credential/repositories/credential-repository-factory.js +54 -0
  16. package/credential/repositories/credential-repository-interface.js +98 -0
  17. package/credential/repositories/credential-repository-mongo.js +269 -0
  18. package/credential/repositories/credential-repository-postgres.js +287 -0
  19. package/credential/repositories/credential-repository.js +300 -0
  20. package/credential/use-cases/get-credential-for-user.js +25 -0
  21. package/credential/use-cases/update-authentication-status.js +15 -0
  22. package/database/MONGODB_TRANSACTION_FIX.md +198 -0
  23. package/database/adapters/lambda-invoker.js +97 -0
  24. package/database/config.js +154 -0
  25. package/database/documentdb-encryption-service.js +330 -0
  26. package/database/documentdb-utils.js +136 -0
  27. package/database/encryption/README.md +839 -0
  28. package/database/encryption/documentdb-encryption-service.md +3575 -0
  29. package/database/encryption/encryption-schema-registry.js +401 -0
  30. package/database/encryption/field-encryption-service.js +254 -0
  31. package/database/encryption/logger.js +79 -0
  32. package/database/encryption/prisma-encryption-extension.js +230 -0
  33. package/database/index.js +21 -21
  34. package/database/prisma.js +182 -0
  35. package/database/repositories/health-check-repository-documentdb.js +138 -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/migration-status-repository-s3.js +137 -0
  41. package/database/use-cases/check-database-health-use-case.js +29 -0
  42. package/database/use-cases/check-database-state-use-case.js +81 -0
  43. package/database/use-cases/check-encryption-health-use-case.js +83 -0
  44. package/database/use-cases/get-database-state-via-worker-use-case.js +61 -0
  45. package/database/use-cases/get-migration-status-use-case.js +93 -0
  46. package/database/use-cases/run-database-migration-use-case.js +139 -0
  47. package/database/use-cases/test-encryption-use-case.js +253 -0
  48. package/database/use-cases/trigger-database-migration-use-case.js +157 -0
  49. package/database/utils/mongodb-collection-utils.js +94 -0
  50. package/database/utils/mongodb-schema-init.js +108 -0
  51. package/database/utils/prisma-runner.js +477 -0
  52. package/database/utils/prisma-schema-parser.js +182 -0
  53. package/docs/PROCESS_MANAGEMENT_QUEUE_SPEC.md +517 -0
  54. package/encrypt/Cryptor.js +34 -168
  55. package/encrypt/index.js +1 -2
  56. package/errors/client-safe-error.js +26 -0
  57. package/errors/fetch-error.js +15 -7
  58. package/errors/index.js +2 -0
  59. package/generated/prisma-mongodb/client.d.ts +1 -0
  60. package/generated/prisma-mongodb/client.js +4 -0
  61. package/generated/prisma-mongodb/default.d.ts +1 -0
  62. package/generated/prisma-mongodb/default.js +4 -0
  63. package/generated/prisma-mongodb/edge.d.ts +1 -0
  64. package/generated/prisma-mongodb/edge.js +335 -0
  65. package/generated/prisma-mongodb/index-browser.js +317 -0
  66. package/generated/prisma-mongodb/index.d.ts +22955 -0
  67. package/generated/prisma-mongodb/index.js +360 -0
  68. package/generated/prisma-mongodb/libquery_engine-debian-openssl-3.0.x.so.node +0 -0
  69. package/generated/prisma-mongodb/libquery_engine-rhel-openssl-3.0.x.so.node +0 -0
  70. package/generated/prisma-mongodb/package.json +183 -0
  71. package/generated/prisma-mongodb/runtime/edge-esm.js +34 -0
  72. package/generated/prisma-mongodb/runtime/edge.js +34 -0
  73. package/generated/prisma-mongodb/runtime/index-browser.d.ts +370 -0
  74. package/generated/prisma-mongodb/runtime/index-browser.js +16 -0
  75. package/generated/prisma-mongodb/runtime/library.d.ts +3977 -0
  76. package/generated/prisma-mongodb/runtime/library.js +146 -0
  77. package/generated/prisma-mongodb/runtime/react-native.js +83 -0
  78. package/generated/prisma-mongodb/runtime/wasm-compiler-edge.js +84 -0
  79. package/generated/prisma-mongodb/runtime/wasm-engine-edge.js +36 -0
  80. package/generated/prisma-mongodb/schema.prisma +368 -0
  81. package/generated/prisma-mongodb/wasm-edge-light-loader.mjs +4 -0
  82. package/generated/prisma-mongodb/wasm-worker-loader.mjs +4 -0
  83. package/generated/prisma-mongodb/wasm.d.ts +1 -0
  84. package/generated/prisma-mongodb/wasm.js +342 -0
  85. package/generated/prisma-postgresql/client.d.ts +1 -0
  86. package/generated/prisma-postgresql/client.js +4 -0
  87. package/generated/prisma-postgresql/default.d.ts +1 -0
  88. package/generated/prisma-postgresql/default.js +4 -0
  89. package/generated/prisma-postgresql/edge.d.ts +1 -0
  90. package/generated/prisma-postgresql/edge.js +357 -0
  91. package/generated/prisma-postgresql/index-browser.js +339 -0
  92. package/generated/prisma-postgresql/index.d.ts +25135 -0
  93. package/generated/prisma-postgresql/index.js +382 -0
  94. package/generated/prisma-postgresql/libquery_engine-debian-openssl-3.0.x.so.node +0 -0
  95. package/generated/prisma-postgresql/libquery_engine-rhel-openssl-3.0.x.so.node +0 -0
  96. package/generated/prisma-postgresql/package.json +183 -0
  97. package/generated/prisma-postgresql/query_engine_bg.js +2 -0
  98. package/generated/prisma-postgresql/query_engine_bg.wasm +0 -0
  99. package/generated/prisma-postgresql/runtime/edge-esm.js +34 -0
  100. package/generated/prisma-postgresql/runtime/edge.js +34 -0
  101. package/generated/prisma-postgresql/runtime/index-browser.d.ts +370 -0
  102. package/generated/prisma-postgresql/runtime/index-browser.js +16 -0
  103. package/generated/prisma-postgresql/runtime/library.d.ts +3977 -0
  104. package/generated/prisma-postgresql/runtime/library.js +146 -0
  105. package/generated/prisma-postgresql/runtime/react-native.js +83 -0
  106. package/generated/prisma-postgresql/runtime/wasm-compiler-edge.js +84 -0
  107. package/generated/prisma-postgresql/runtime/wasm-engine-edge.js +36 -0
  108. package/generated/prisma-postgresql/schema.prisma +351 -0
  109. package/generated/prisma-postgresql/wasm-edge-light-loader.mjs +4 -0
  110. package/generated/prisma-postgresql/wasm-worker-loader.mjs +4 -0
  111. package/generated/prisma-postgresql/wasm.d.ts +1 -0
  112. package/generated/prisma-postgresql/wasm.js +364 -0
  113. package/handlers/WEBHOOKS.md +653 -0
  114. package/handlers/app-definition-loader.js +38 -0
  115. package/handlers/app-handler-helpers.js +57 -0
  116. package/handlers/backend-utils.js +297 -0
  117. package/handlers/database-migration-handler.js +227 -0
  118. package/handlers/integration-event-dispatcher.js +54 -0
  119. package/handlers/routers/HEALTHCHECK.md +342 -0
  120. package/handlers/routers/auth.js +15 -0
  121. package/handlers/routers/db-migration.handler.js +29 -0
  122. package/handlers/routers/db-migration.js +326 -0
  123. package/handlers/routers/health.js +518 -0
  124. package/handlers/routers/integration-defined-routers.js +117 -0
  125. package/handlers/routers/integration-webhook-routers.js +67 -0
  126. package/handlers/routers/user.js +63 -0
  127. package/handlers/routers/websocket.js +57 -0
  128. package/handlers/use-cases/check-external-apis-health-use-case.js +81 -0
  129. package/handlers/use-cases/check-integrations-health-use-case.js +44 -0
  130. package/handlers/workers/db-migration.js +352 -0
  131. package/handlers/workers/dlq-processor.js +63 -0
  132. package/handlers/workers/integration-defined-workers.js +30 -0
  133. package/index.js +82 -46
  134. package/infrastructure/scheduler/eventbridge-scheduler-adapter.js +184 -0
  135. package/infrastructure/scheduler/index.js +33 -0
  136. package/infrastructure/scheduler/mock-scheduler-adapter.js +143 -0
  137. package/infrastructure/scheduler/scheduler-service-factory.js +73 -0
  138. package/infrastructure/scheduler/scheduler-service-interface.js +47 -0
  139. package/integrations/EXTENSIONS.md +240 -0
  140. package/integrations/WEBHOOK-QUICKSTART.md +151 -0
  141. package/integrations/extension.js +254 -0
  142. package/integrations/index.js +20 -10
  143. package/integrations/integration-base.js +487 -55
  144. package/integrations/integration-router.js +396 -179
  145. package/integrations/options.js +1 -1
  146. package/integrations/repositories/integration-mapping-repository-documentdb.js +280 -0
  147. package/integrations/repositories/integration-mapping-repository-factory.js +57 -0
  148. package/integrations/repositories/integration-mapping-repository-interface.js +106 -0
  149. package/integrations/repositories/integration-mapping-repository-mongo.js +161 -0
  150. package/integrations/repositories/integration-mapping-repository-postgres.js +227 -0
  151. package/integrations/repositories/integration-mapping-repository.js +156 -0
  152. package/integrations/repositories/integration-repository-documentdb.js +219 -0
  153. package/integrations/repositories/integration-repository-factory.js +51 -0
  154. package/integrations/repositories/integration-repository-interface.js +144 -0
  155. package/integrations/repositories/integration-repository-mongo.js +330 -0
  156. package/integrations/repositories/integration-repository-postgres.js +385 -0
  157. package/integrations/repositories/process-repository-documentdb.js +311 -0
  158. package/integrations/repositories/process-repository-factory.js +53 -0
  159. package/integrations/repositories/process-repository-interface.js +136 -0
  160. package/integrations/repositories/process-repository-mongo.js +262 -0
  161. package/integrations/repositories/process-repository-postgres.js +380 -0
  162. package/integrations/repositories/process-update-ops-shared.js +112 -0
  163. package/integrations/tests/doubles/config-capturing-integration.js +81 -0
  164. package/integrations/tests/doubles/dummy-integration-class.js +105 -0
  165. package/integrations/tests/doubles/test-integration-repository.js +112 -0
  166. package/integrations/use-cases/create-integration.js +83 -0
  167. package/integrations/use-cases/create-process.js +128 -0
  168. package/integrations/use-cases/delete-integration-for-user.js +101 -0
  169. package/integrations/use-cases/find-integration-by-entity-external-id.js +74 -0
  170. package/integrations/use-cases/find-integration-context-by-external-entity-id.js +76 -0
  171. package/integrations/use-cases/get-integration-for-user.js +78 -0
  172. package/integrations/use-cases/get-integration-instance-by-definition.js +67 -0
  173. package/integrations/use-cases/get-integration-instance.js +83 -0
  174. package/integrations/use-cases/get-integrations-for-user.js +88 -0
  175. package/integrations/use-cases/get-possible-integrations.js +27 -0
  176. package/integrations/use-cases/get-process.js +87 -0
  177. package/integrations/use-cases/index.js +19 -0
  178. package/integrations/use-cases/list-integrations-by-entity-external-id.js +46 -0
  179. package/integrations/use-cases/load-integration-context.js +71 -0
  180. package/integrations/use-cases/update-integration-messages.js +44 -0
  181. package/integrations/use-cases/update-integration-status.js +32 -0
  182. package/integrations/use-cases/update-integration.js +92 -0
  183. package/integrations/use-cases/update-process-metrics.js +214 -0
  184. package/integrations/use-cases/update-process-state.js +158 -0
  185. package/integrations/utils/map-integration-dto.js +37 -0
  186. package/jest-global-setup-noop.js +3 -0
  187. package/jest-global-teardown-noop.js +3 -0
  188. package/logs/logger.js +0 -4
  189. package/{module-plugin → modules}/index.js +0 -10
  190. package/modules/module-factory.js +56 -0
  191. package/modules/module.js +274 -0
  192. package/modules/repositories/module-repository-documentdb.js +350 -0
  193. package/modules/repositories/module-repository-factory.js +40 -0
  194. package/modules/repositories/module-repository-interface.js +145 -0
  195. package/modules/repositories/module-repository-mongo.js +436 -0
  196. package/modules/repositories/module-repository-postgres.js +481 -0
  197. package/modules/repositories/module-repository.js +369 -0
  198. package/modules/requester/api-key.js +52 -0
  199. package/modules/requester/oauth-2.js +396 -0
  200. package/modules/requester/requester.js +280 -0
  201. package/{module-plugin → modules}/test/mock-api/api.js +8 -3
  202. package/{module-plugin → modules}/test/mock-api/definition.js +14 -10
  203. package/modules/tests/doubles/test-module-factory.js +16 -0
  204. package/modules/tests/doubles/test-module-repository.js +39 -0
  205. package/modules/use-cases/get-entities-for-user.js +32 -0
  206. package/modules/use-cases/get-entity-options-by-id.js +71 -0
  207. package/modules/use-cases/get-entity-options-by-type.js +34 -0
  208. package/modules/use-cases/get-module-instance-from-type.js +34 -0
  209. package/modules/use-cases/get-module.js +74 -0
  210. package/modules/use-cases/process-authorization-callback.js +243 -0
  211. package/modules/use-cases/refresh-entity-options.js +72 -0
  212. package/modules/use-cases/test-module-auth.js +72 -0
  213. package/modules/utils/map-module-dto.js +18 -0
  214. package/package.json +82 -50
  215. package/prisma-mongodb/schema.prisma +368 -0
  216. package/prisma-postgresql/migrations/20250930193005_init/migration.sql +315 -0
  217. package/prisma-postgresql/migrations/20251006135218_init/migration.sql +9 -0
  218. package/prisma-postgresql/migrations/20251010000000_remove_unused_entity_reference_map/migration.sql +3 -0
  219. package/prisma-postgresql/migrations/20251112195422_update_user_unique_constraints/migration.sql +25 -0
  220. package/prisma-postgresql/migrations/20260422120000_add_entity_data_column/migration.sql +10 -0
  221. package/prisma-postgresql/migrations/20260422120001_create_process_table/migration.sql +48 -0
  222. package/prisma-postgresql/migrations/migration_lock.toml +3 -0
  223. package/prisma-postgresql/schema.prisma +351 -0
  224. package/queues/queuer-util.js +103 -21
  225. package/syncs/manager.js +468 -443
  226. package/syncs/repositories/sync-repository-documentdb.js +240 -0
  227. package/syncs/repositories/sync-repository-factory.js +43 -0
  228. package/syncs/repositories/sync-repository-interface.js +109 -0
  229. package/syncs/repositories/sync-repository-mongo.js +239 -0
  230. package/syncs/repositories/sync-repository-postgres.js +319 -0
  231. package/syncs/sync.js +0 -1
  232. package/token/repositories/token-repository-documentdb.js +137 -0
  233. package/token/repositories/token-repository-factory.js +40 -0
  234. package/token/repositories/token-repository-interface.js +131 -0
  235. package/token/repositories/token-repository-mongo.js +219 -0
  236. package/token/repositories/token-repository-postgres.js +264 -0
  237. package/token/repositories/token-repository.js +219 -0
  238. package/types/associations/index.d.ts +0 -17
  239. package/types/core/index.d.ts +12 -4
  240. package/types/database/index.d.ts +10 -2
  241. package/types/encrypt/index.d.ts +5 -3
  242. package/types/integrations/index.d.ts +3 -8
  243. package/types/module-plugin/index.d.ts +17 -69
  244. package/types/syncs/index.d.ts +0 -17
  245. package/user/repositories/user-repository-documentdb.js +441 -0
  246. package/user/repositories/user-repository-factory.js +52 -0
  247. package/user/repositories/user-repository-interface.js +201 -0
  248. package/user/repositories/user-repository-mongo.js +308 -0
  249. package/user/repositories/user-repository-postgres.js +360 -0
  250. package/user/tests/doubles/test-user-repository.js +72 -0
  251. package/user/use-cases/authenticate-user.js +127 -0
  252. package/user/use-cases/authenticate-with-shared-secret.js +48 -0
  253. package/user/use-cases/create-individual-user.js +61 -0
  254. package/user/use-cases/create-organization-user.js +47 -0
  255. package/user/use-cases/create-token-for-user-id.js +30 -0
  256. package/user/use-cases/get-user-from-adopter-jwt.js +149 -0
  257. package/user/use-cases/get-user-from-bearer-token.js +77 -0
  258. package/user/use-cases/get-user-from-x-frigg-headers.js +132 -0
  259. package/user/use-cases/login-user.js +122 -0
  260. package/user/user.js +125 -0
  261. package/utils/backend-path.js +38 -0
  262. package/utils/index.js +6 -0
  263. package/websocket/repositories/websocket-connection-repository-documentdb.js +119 -0
  264. package/websocket/repositories/websocket-connection-repository-factory.js +44 -0
  265. package/websocket/repositories/websocket-connection-repository-interface.js +106 -0
  266. package/websocket/repositories/websocket-connection-repository-mongo.js +156 -0
  267. package/websocket/repositories/websocket-connection-repository-postgres.js +196 -0
  268. package/websocket/repositories/websocket-connection-repository.js +161 -0
  269. package/assertions/is-equal.js +0 -17
  270. package/associations/model.js +0 -54
  271. package/database/models/IndividualUser.js +0 -76
  272. package/database/models/OrganizationUser.js +0 -29
  273. package/database/models/State.js +0 -9
  274. package/database/models/Token.js +0 -70
  275. package/database/models/UserModel.js +0 -7
  276. package/database/models/WebsocketConnection.js +0 -49
  277. package/database/mongo.js +0 -45
  278. package/database/mongoose.js +0 -5
  279. package/encrypt/Cryptor.test.js +0 -32
  280. package/encrypt/encrypt.js +0 -132
  281. package/encrypt/encrypt.test.js +0 -1069
  282. package/encrypt/test-encrypt.js +0 -107
  283. package/errors/base-error.test.js +0 -32
  284. package/errors/fetch-error.test.js +0 -79
  285. package/errors/halt-error.test.js +0 -11
  286. package/errors/validation-errors.test.js +0 -120
  287. package/integrations/create-frigg-backend.js +0 -31
  288. package/integrations/integration-factory.js +0 -251
  289. package/integrations/integration-mapping.js +0 -43
  290. package/integrations/integration-model.js +0 -46
  291. package/integrations/integration-user.js +0 -144
  292. package/integrations/test/integration-base.test.js +0 -144
  293. package/lambda/TimeoutCatcher.test.js +0 -68
  294. package/logs/logger.test.js +0 -76
  295. package/module-plugin/auther.js +0 -393
  296. package/module-plugin/credential.js +0 -22
  297. package/module-plugin/entity-manager.js +0 -70
  298. package/module-plugin/entity.js +0 -46
  299. package/module-plugin/manager.js +0 -169
  300. package/module-plugin/module-factory.js +0 -61
  301. package/module-plugin/requester/api-key.js +0 -36
  302. package/module-plugin/requester/oauth-2.js +0 -219
  303. package/module-plugin/requester/requester.js +0 -165
  304. package/module-plugin/requester/requester.test.js +0 -28
  305. package/module-plugin/test/auther.test.js +0 -97
  306. package/syncs/model.js +0 -62
  307. /package/{module-plugin → modules}/ModuleConstants.js +0 -0
  308. /package/{module-plugin → modules}/requester/basic.js +0 -0
  309. /package/{module-plugin → modules}/test/mock-api/mocks/hubspot.js +0 -0
@@ -0,0 +1,401 @@
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
+ * Per-model write-side opt-out: fields registered here are NOT encrypted on
47
+ * write, but ARE still decrypted on read so legacy encrypted rows continue to
48
+ * deserialize. Lets apps migrate a model from encrypted to plain JSON without
49
+ * a data migration — touched rows naturally rewrite as plain on the next save,
50
+ * untouched rows stay encrypted-but-readable forever.
51
+ *
52
+ * Shape: `{ ModelName: ['field.path', ...] }`
53
+ */
54
+ let encryptionOptOut = {};
55
+
56
+ /**
57
+ * Validates a custom encryption schema
58
+ * @returns {{valid: boolean, errors: string[]}}
59
+ */
60
+ function validateCustomSchema(schema) {
61
+ const errors = [];
62
+
63
+ if (!schema || typeof schema !== 'object') {
64
+ errors.push('Custom schema must be an object');
65
+ return { valid: false, errors };
66
+ }
67
+
68
+ for (const [modelName, config] of Object.entries(schema)) {
69
+ if (typeof modelName !== 'string' || !modelName) {
70
+ errors.push(`Invalid model name: ${modelName}`);
71
+ continue;
72
+ }
73
+
74
+ if (!config || typeof config !== 'object') {
75
+ errors.push(`Model "${modelName}" must have a config object`);
76
+ continue;
77
+ }
78
+
79
+ if (!Array.isArray(config.fields)) {
80
+ errors.push(`Model "${modelName}" must have a "fields" array`);
81
+ continue;
82
+ }
83
+
84
+ for (const fieldPath of config.fields) {
85
+ if (typeof fieldPath !== 'string' || !fieldPath) {
86
+ errors.push(`Model "${modelName}" has invalid field path: ${fieldPath}`);
87
+ }
88
+
89
+ // Check if trying to override core fields
90
+ const coreFields = CORE_ENCRYPTION_SCHEMA[modelName]?.fields || [];
91
+ if (coreFields.includes(fieldPath)) {
92
+ errors.push(
93
+ `Cannot override core encrypted field "${fieldPath}" in model "${modelName}"`
94
+ );
95
+ }
96
+ }
97
+ }
98
+
99
+ return {
100
+ valid: errors.length === 0,
101
+ errors,
102
+ };
103
+ }
104
+
105
+ /**
106
+ * Registers a custom encryption schema from integration developer.
107
+ * Merges with core schema, prevents overriding core fields.
108
+ * @throws {Error} If schema validation fails
109
+ */
110
+ function registerCustomSchema(schema) {
111
+ if (!schema || Object.keys(schema).length === 0) {
112
+ return; // Nothing to register
113
+ }
114
+
115
+ const validation = validateCustomSchema(schema);
116
+ if (!validation.valid) {
117
+ throw new Error(
118
+ `Invalid custom encryption schema:\n- ${validation.errors.join('\n- ')}`
119
+ );
120
+ }
121
+
122
+ customSchema = { ...schema };
123
+ logger.info(
124
+ `Registered custom encryption schema for models: ${Object.keys(customSchema).join(', ')}`
125
+ );
126
+ }
127
+
128
+ /**
129
+ * Extracts credential field paths from module definitions
130
+ * @param {Array} moduleDefinitions - Array of module definition objects
131
+ * @returns {Array<string>} Array of field paths with data. prefix
132
+ */
133
+ function extractCredentialFieldsFromModules(moduleDefinitions) {
134
+ const fields = [];
135
+
136
+ for (const moduleDef of moduleDefinitions) {
137
+ if (!moduleDef?.encryption?.credentialFields) {
138
+ continue;
139
+ }
140
+
141
+ const credentialFields = moduleDef.encryption.credentialFields;
142
+ if (!Array.isArray(credentialFields) || credentialFields.length === 0) {
143
+ continue;
144
+ }
145
+
146
+ for (const field of credentialFields) {
147
+ const prefixedField = field.startsWith('data.') ? field : `data.${field}`;
148
+ fields.push(prefixedField);
149
+ }
150
+ }
151
+
152
+ return [...new Set(fields)];
153
+ }
154
+
155
+ /**
156
+ * Loads and registers encryption schemas from API module definitions.
157
+ * Each module can declare credentialFields to encrypt in its encryption config.
158
+ *
159
+ * @param {Array} integrations - Array of integration classes with modules
160
+ */
161
+ function loadModuleEncryptionSchemas(integrations) {
162
+ if (!integrations) {
163
+ throw new Error('integrations parameter is required');
164
+ }
165
+
166
+ if (!Array.isArray(integrations)) {
167
+ throw new Error('integrations must be an array');
168
+ }
169
+
170
+ if (integrations.length === 0) {
171
+ return;
172
+ }
173
+
174
+ const { getModulesDefinitionFromIntegrationClasses } = require('../integrations/utils/map-integration-dto');
175
+
176
+ const moduleDefinitions = getModulesDefinitionFromIntegrationClasses(integrations);
177
+ const credentialFields = extractCredentialFieldsFromModules(moduleDefinitions);
178
+
179
+ if (credentialFields.length === 0) {
180
+ return;
181
+ }
182
+
183
+ const moduleSchema = {
184
+ Credential: {
185
+ fields: credentialFields
186
+ }
187
+ };
188
+
189
+ logger.info(
190
+ `Registering module-level encryption for ${credentialFields.length} credential fields`
191
+ );
192
+
193
+ registerCustomSchema(moduleSchema);
194
+ }
195
+
196
+ /**
197
+ * Loads and registers custom encryption schema from appDefinition.
198
+ * Gracefully handles cases where appDefinition is not available.
199
+ *
200
+ * This ensures that custom encryption schemas defined in the backend's index.js
201
+ * are registered before any repositories attempt to encrypt data.
202
+ *
203
+ * Used by both Prisma (MongoDB/PostgreSQL) and DocumentDB encryption services.
204
+ */
205
+ function loadCustomEncryptionSchema() {
206
+ try {
207
+ // Lazy require to avoid circular dependency issues
208
+ const path = require('node:path');
209
+ const { findNearestBackendPackageJson } = require('../../utils');
210
+
211
+ const backendPackagePath = findNearestBackendPackageJson();
212
+ if (!backendPackagePath) {
213
+ return; // No backend found, skip custom schema
214
+ }
215
+
216
+ const backendDir = path.dirname(backendPackagePath);
217
+ const backendIndexPath = path.join(backendDir, 'index.js');
218
+
219
+ const backendModule = require(backendIndexPath);
220
+ const appDefinition = backendModule?.Definition;
221
+
222
+ if (!appDefinition) {
223
+ return; // No app definition found
224
+ }
225
+
226
+ // Load app-level custom schema
227
+ const customSchema = appDefinition.encryption?.schema;
228
+ if (customSchema && Object.keys(customSchema).length > 0) {
229
+ registerCustomSchema(customSchema);
230
+ }
231
+
232
+ // Load app-level encryption opt-out — apps can declare fields they
233
+ // don't want encrypted on write (decryption on read still works,
234
+ // so legacy data remains readable).
235
+ const disable = appDefinition.encryption?.disable;
236
+ if (disable && Object.keys(disable).length > 0) {
237
+ registerEncryptionOptOut(disable);
238
+ }
239
+
240
+ // Load module-level encryption schemas from integrations
241
+ const integrations = appDefinition.integrations;
242
+ if (integrations && Array.isArray(integrations)) {
243
+ loadModuleEncryptionSchemas(integrations);
244
+ }
245
+ } catch (error) {
246
+ // Silently ignore errors - custom schema is optional
247
+ // This handles cases like:
248
+ // - Backend package.json not found (tests, standalone usage)
249
+ // - No appDefinition defined
250
+ // - No custom encryption schema specified
251
+ logger.debug('Could not load custom encryption schema:', error.message);
252
+ }
253
+ }
254
+
255
+ function getEncryptedFields(modelName) {
256
+ const coreFields = CORE_ENCRYPTION_SCHEMA[modelName]?.fields || [];
257
+ const customFields = customSchema[modelName]?.fields || [];
258
+ const allFields = [...coreFields, ...customFields];
259
+ return [...new Set(allFields)];
260
+ }
261
+
262
+ /**
263
+ * Validates an encryption opt-out config.
264
+ *
265
+ * Unlike custom schema validation, opt-out IS allowed to target paths that
266
+ * already live in CORE_ENCRYPTION_SCHEMA — that's the entire point.
267
+ *
268
+ * @param {Object} optOut - Map of `{ ModelName: ['field.path', ...] }`
269
+ * @returns {{valid: boolean, errors: string[]}}
270
+ */
271
+ function validateOptOut(optOut) {
272
+ const errors = [];
273
+
274
+ if (!optOut || typeof optOut !== 'object') {
275
+ errors.push('Encryption opt-out must be an object');
276
+ return { valid: false, errors };
277
+ }
278
+
279
+ for (const [modelName, fields] of Object.entries(optOut)) {
280
+ if (typeof modelName !== 'string' || !modelName) {
281
+ errors.push(`Invalid model name in opt-out: ${modelName}`);
282
+ continue;
283
+ }
284
+
285
+ if (!Array.isArray(fields)) {
286
+ errors.push(
287
+ `Model "${modelName}" opt-out must be an array of field paths`
288
+ );
289
+ continue;
290
+ }
291
+
292
+ for (const fieldPath of fields) {
293
+ if (typeof fieldPath !== 'string' || !fieldPath) {
294
+ errors.push(
295
+ `Model "${modelName}" has invalid opt-out field path: ${fieldPath}`
296
+ );
297
+ }
298
+ }
299
+ }
300
+
301
+ return { valid: errors.length === 0, errors };
302
+ }
303
+
304
+ /**
305
+ * Registers an encryption opt-out config. Listed fields will be skipped during
306
+ * encryption on write while still being eligible for decryption on read (so
307
+ * legacy encrypted rows still deserialize correctly).
308
+ *
309
+ * Intended call site: `appDefinition.encryption.disable` via
310
+ * `loadCustomEncryptionSchema`.
311
+ *
312
+ * @param {Object} optOut - Map of `{ ModelName: ['field.path', ...] }`
313
+ * @throws {Error} If opt-out validation fails
314
+ */
315
+ function registerEncryptionOptOut(optOut) {
316
+ if (!optOut || Object.keys(optOut).length === 0) {
317
+ return;
318
+ }
319
+
320
+ const validation = validateOptOut(optOut);
321
+ if (!validation.valid) {
322
+ throw new Error(
323
+ `Invalid encryption opt-out:\n- ${validation.errors.join('\n- ')}`
324
+ );
325
+ }
326
+
327
+ encryptionOptOut = { ...optOut };
328
+ logger.info(
329
+ `Registered encryption opt-out for models: ${Object.keys(
330
+ encryptionOptOut
331
+ ).join(', ')}`
332
+ );
333
+ }
334
+
335
+ /**
336
+ * Returns the field paths that should be encrypted when writing the given
337
+ * model. This is `getEncryptedFields` minus any paths the app has opted out
338
+ * of via `registerEncryptionOptOut`.
339
+ *
340
+ * Use this in the encrypt-on-write path of the FieldEncryptionService.
341
+ */
342
+ function getFieldsToEncryptOnWrite(modelName) {
343
+ const allFields = getEncryptedFields(modelName);
344
+ const optedOut = new Set(encryptionOptOut[modelName] || []);
345
+ if (optedOut.size === 0) return allFields;
346
+ return allFields.filter((path) => !optedOut.has(path));
347
+ }
348
+
349
+ /**
350
+ * Returns the field paths that should be checked for decryption when reading
351
+ * the given model. Always includes opted-out paths so legacy encrypted rows
352
+ * remain readable after an app opts a field out.
353
+ *
354
+ * `FieldEncryptionService._isEncrypted` already short-circuits for plain JSON
355
+ * values, so listing more fields than necessary here is harmless.
356
+ *
357
+ * Use this in the decrypt-on-read path of the FieldEncryptionService.
358
+ */
359
+ function getFieldsToDecryptOnRead(modelName) {
360
+ return getEncryptedFields(modelName);
361
+ }
362
+
363
+ /**
364
+ * Clears any registered encryption opt-outs. Test-helper; not intended for
365
+ * runtime use.
366
+ */
367
+ function resetEncryptionOptOut() {
368
+ encryptionOptOut = {};
369
+ }
370
+
371
+ function hasEncryptedFields(modelName) {
372
+ return getEncryptedFields(modelName).length > 0;
373
+ }
374
+
375
+ function getEncryptedModels() {
376
+ const coreModels = Object.keys(CORE_ENCRYPTION_SCHEMA);
377
+ const customModels = Object.keys(customSchema);
378
+ return [...new Set([...coreModels, ...customModels])];
379
+ }
380
+
381
+ function resetCustomSchema() {
382
+ customSchema = {};
383
+ }
384
+
385
+ module.exports = {
386
+ CORE_ENCRYPTION_SCHEMA,
387
+ getEncryptedFields,
388
+ getFieldsToEncryptOnWrite,
389
+ getFieldsToDecryptOnRead,
390
+ hasEncryptedFields,
391
+ getEncryptedModels,
392
+ registerCustomSchema,
393
+ registerEncryptionOptOut,
394
+ loadCustomEncryptionSchema,
395
+ loadModuleEncryptionSchemas,
396
+ extractCredentialFieldsFromModules,
397
+ validateCustomSchema,
398
+ validateOptOut,
399
+ resetCustomSchema,
400
+ resetEncryptionOptOut,
401
+ };
@@ -0,0 +1,254 @@
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
+ /**
21
+ * Resolve the field paths to encrypt on write. Prefers
22
+ * `schema.getFieldsToEncryptOnWrite` (which respects app opt-outs); falls
23
+ * back to `schema.getEncryptedFields` for backwards compatibility with
24
+ * older schema adapters.
25
+ * @private
26
+ */
27
+ _getWriteFields(modelName) {
28
+ if (typeof this.schema.getFieldsToEncryptOnWrite === 'function') {
29
+ return this.schema.getFieldsToEncryptOnWrite(modelName);
30
+ }
31
+ return this.schema.getEncryptedFields(modelName);
32
+ }
33
+
34
+ /**
35
+ * Resolve the field paths to attempt decryption on read. Prefers
36
+ * `schema.getFieldsToDecryptOnRead` (which IGNORES opt-outs so legacy
37
+ * encrypted rows still deserialize); falls back to
38
+ * `schema.getEncryptedFields` for backwards compatibility.
39
+ * @private
40
+ */
41
+ _getReadFields(modelName) {
42
+ if (typeof this.schema.getFieldsToDecryptOnRead === 'function') {
43
+ return this.schema.getFieldsToDecryptOnRead(modelName);
44
+ }
45
+ return this.schema.getEncryptedFields(modelName);
46
+ }
47
+
48
+ async encryptFields(modelName, document) {
49
+ if (!document || typeof document !== 'object') {
50
+ return document;
51
+ }
52
+
53
+ const fields = this._getWriteFields(modelName);
54
+ if (fields.length === 0) {
55
+ return document;
56
+ }
57
+
58
+ const encrypted = this._deepClone(document);
59
+
60
+ // Parallelize encryption of multiple fields
61
+ const encryptionPromises = fields.map(async (fieldPath) => {
62
+ const value = this._getNestedValue(encrypted, fieldPath);
63
+
64
+ if (this._shouldEncrypt(value)) {
65
+ const serializedValue = this._serializeForEncryption(value);
66
+ const encryptedValue = await this.cryptor.encrypt(serializedValue);
67
+ return { fieldPath, encryptedValue };
68
+ }
69
+ return null;
70
+ });
71
+
72
+ const results = await Promise.all(encryptionPromises);
73
+
74
+ // Apply encrypted values
75
+ for (const result of results) {
76
+ if (result) {
77
+ this._setNestedValue(encrypted, result.fieldPath, result.encryptedValue);
78
+ }
79
+ }
80
+
81
+ return encrypted;
82
+ }
83
+
84
+ async decryptFields(modelName, document) {
85
+ if (!document || typeof document !== 'object') {
86
+ return document;
87
+ }
88
+
89
+ const fields = this._getReadFields(modelName);
90
+ if (fields.length === 0) {
91
+ return document;
92
+ }
93
+
94
+ const decrypted = this._deepClone(document);
95
+
96
+ // Parallelize decryption of multiple fields
97
+ const decryptionPromises = fields.map(async (fieldPath) => {
98
+ const value = this._getNestedValue(decrypted, fieldPath);
99
+
100
+ if (this._isEncrypted(value)) {
101
+ const decryptedValue = await this.cryptor.decrypt(value);
102
+ const deserializedValue = this._deserializeAfterDecryption(decryptedValue);
103
+ return { fieldPath, decryptedValue: deserializedValue };
104
+ }
105
+ return null;
106
+ });
107
+
108
+ const results = await Promise.all(decryptionPromises);
109
+
110
+ // Apply decrypted values
111
+ for (const result of results) {
112
+ if (result) {
113
+ this._setNestedValue(decrypted, result.fieldPath, result.decryptedValue);
114
+ }
115
+ }
116
+
117
+ return decrypted;
118
+ }
119
+
120
+ async encryptFieldsInBulk(modelName, documents) {
121
+ if (!Array.isArray(documents)) {
122
+ return documents;
123
+ }
124
+
125
+ return Promise.all(
126
+ documents.map((doc) => this.encryptFields(modelName, doc))
127
+ );
128
+ }
129
+
130
+ async decryptFieldsInBulk(modelName, documents) {
131
+ if (!Array.isArray(documents)) {
132
+ return documents;
133
+ }
134
+
135
+ return Promise.all(
136
+ documents.map((doc) => this.decryptFields(modelName, doc))
137
+ );
138
+ }
139
+
140
+ _shouldEncrypt(value) {
141
+ return (
142
+ value !== null &&
143
+ value !== undefined &&
144
+ value !== '' &&
145
+ !this._isEncrypted(value)
146
+ );
147
+ }
148
+
149
+ _isEncrypted(value) {
150
+ if (typeof value !== 'string') {
151
+ return false;
152
+ }
153
+
154
+ const parts = value.split(':');
155
+ return parts.length >= 4;
156
+ }
157
+
158
+ _getNestedValue(obj, path) {
159
+ if (!obj || !path) {
160
+ return undefined;
161
+ }
162
+
163
+ return path.split('.').reduce((current, key) => {
164
+ return current?.[key];
165
+ }, obj);
166
+ }
167
+
168
+ _setNestedValue(obj, path, value) {
169
+ if (!obj || !path) {
170
+ return;
171
+ }
172
+
173
+ const keys = path.split('.');
174
+ const lastKey = keys.pop();
175
+
176
+ const target = keys.reduce((current, key) => {
177
+ if (!current[key] || typeof current[key] !== 'object') {
178
+ current[key] = {};
179
+ }
180
+ return current[key];
181
+ }, obj);
182
+
183
+ target[lastKey] = value;
184
+ }
185
+
186
+ _deepClone(obj) {
187
+ // Use structuredClone (Node.js 17+) for better performance
188
+ // Falls back to custom implementation for older Node versions
189
+ if (typeof structuredClone !== 'undefined') {
190
+ try {
191
+ return structuredClone(obj);
192
+ } catch {
193
+ // Fall through to custom implementation
194
+ }
195
+ }
196
+
197
+ // Custom fallback for older environments
198
+ if (obj === null || typeof obj !== 'object') {
199
+ return obj;
200
+ }
201
+
202
+ if (obj instanceof Date) {
203
+ return new Date(obj.getTime());
204
+ }
205
+
206
+ if (Array.isArray(obj)) {
207
+ return obj.map((item) => this._deepClone(item));
208
+ }
209
+
210
+ const cloned = {};
211
+ for (const key in obj) {
212
+ if (obj.hasOwnProperty(key)) {
213
+ cloned[key] = this._deepClone(obj[key]);
214
+ }
215
+ }
216
+
217
+ return cloned;
218
+ }
219
+
220
+ /**
221
+ * Serialize a value for encryption
222
+ * Objects/arrays are JSON stringified, primitives are converted to strings
223
+ * @private
224
+ */
225
+ _serializeForEncryption(value) {
226
+ if (typeof value === 'object' && value !== null) {
227
+ // JSON.stringify for objects and arrays
228
+ return JSON.stringify(value);
229
+ }
230
+ // For primitives (string, number, boolean), convert to string
231
+ return String(value);
232
+ }
233
+
234
+ /**
235
+ * Deserialize a value after decryption
236
+ * Attempts to parse as JSON, returns string if parsing fails
237
+ * @private
238
+ */
239
+ _deserializeAfterDecryption(value) {
240
+ if (typeof value !== 'string') {
241
+ return value;
242
+ }
243
+
244
+ // Try to parse as JSON
245
+ try {
246
+ return JSON.parse(value);
247
+ } catch {
248
+ // Not valid JSON, return as-is (likely was a plain string field)
249
+ return value;
250
+ }
251
+ }
252
+ }
253
+
254
+ module.exports = { FieldEncryptionService };