@friggframework/core 2.0.0-next.41 → 2.0.0-next.43

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 (197) hide show
  1. package/CLAUDE.md +693 -0
  2. package/README.md +931 -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 +160 -0
  7. package/application/commands/integration-commands.test.js +123 -0
  8. package/application/commands/user-commands.js +213 -0
  9. package/application/index.js +69 -0
  10. package/core/CLAUDE.md +690 -0
  11. package/core/create-handler.js +0 -6
  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 +301 -0
  15. package/credential/repositories/credential-repository-postgres.js +307 -0
  16. package/credential/repositories/credential-repository.js +307 -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/config.js +117 -0
  20. package/database/encryption/README.md +683 -0
  21. package/database/encryption/encryption-integration.test.js +553 -0
  22. package/database/encryption/encryption-schema-registry.js +141 -0
  23. package/database/encryption/encryption-schema-registry.test.js +392 -0
  24. package/database/encryption/field-encryption-service.js +226 -0
  25. package/database/encryption/field-encryption-service.test.js +525 -0
  26. package/database/encryption/logger.js +79 -0
  27. package/database/encryption/mongo-decryption-fix-verification.test.js +348 -0
  28. package/database/encryption/postgres-decryption-fix-verification.test.js +371 -0
  29. package/database/encryption/postgres-relation-decryption.test.js +245 -0
  30. package/database/encryption/prisma-encryption-extension.js +222 -0
  31. package/database/encryption/prisma-encryption-extension.test.js +439 -0
  32. package/database/index.js +25 -12
  33. package/database/models/readme.md +1 -0
  34. package/database/prisma.js +162 -0
  35. package/database/repositories/health-check-repository-factory.js +38 -0
  36. package/database/repositories/health-check-repository-interface.js +86 -0
  37. package/database/repositories/health-check-repository-mongodb.js +72 -0
  38. package/database/repositories/health-check-repository-postgres.js +75 -0
  39. package/database/repositories/health-check-repository.js +108 -0
  40. package/database/use-cases/check-database-health-use-case.js +34 -0
  41. package/database/use-cases/check-encryption-health-use-case.js +82 -0
  42. package/database/use-cases/test-encryption-use-case.js +252 -0
  43. package/encrypt/Cryptor.js +20 -152
  44. package/encrypt/index.js +1 -2
  45. package/encrypt/test-encrypt.js +0 -2
  46. package/handlers/app-definition-loader.js +38 -0
  47. package/handlers/app-handler-helpers.js +0 -3
  48. package/handlers/auth-flow.integration.test.js +147 -0
  49. package/handlers/backend-utils.js +25 -45
  50. package/handlers/integration-event-dispatcher.js +54 -0
  51. package/handlers/integration-event-dispatcher.test.js +141 -0
  52. package/handlers/routers/HEALTHCHECK.md +103 -1
  53. package/handlers/routers/auth.js +3 -14
  54. package/handlers/routers/health.js +63 -424
  55. package/handlers/routers/health.test.js +7 -0
  56. package/handlers/routers/integration-defined-routers.js +8 -5
  57. package/handlers/routers/user.js +27 -5
  58. package/handlers/routers/websocket.js +5 -3
  59. package/handlers/use-cases/check-external-apis-health-use-case.js +81 -0
  60. package/handlers/use-cases/check-integrations-health-use-case.js +32 -0
  61. package/handlers/workers/integration-defined-workers.js +6 -3
  62. package/index.js +45 -22
  63. package/integrations/index.js +12 -10
  64. package/integrations/integration-base.js +224 -53
  65. package/integrations/integration-router.js +386 -178
  66. package/integrations/options.js +1 -1
  67. package/integrations/repositories/integration-mapping-repository-factory.js +50 -0
  68. package/integrations/repositories/integration-mapping-repository-interface.js +106 -0
  69. package/integrations/repositories/integration-mapping-repository-mongo.js +161 -0
  70. package/integrations/repositories/integration-mapping-repository-postgres.js +227 -0
  71. package/integrations/repositories/integration-mapping-repository.js +156 -0
  72. package/integrations/repositories/integration-repository-factory.js +44 -0
  73. package/integrations/repositories/integration-repository-interface.js +115 -0
  74. package/integrations/repositories/integration-repository-mongo.js +271 -0
  75. package/integrations/repositories/integration-repository-postgres.js +319 -0
  76. package/integrations/tests/doubles/dummy-integration-class.js +90 -0
  77. package/integrations/tests/doubles/test-integration-repository.js +99 -0
  78. package/integrations/tests/use-cases/create-integration.test.js +131 -0
  79. package/integrations/tests/use-cases/delete-integration-for-user.test.js +150 -0
  80. package/integrations/tests/use-cases/find-integration-context-by-external-entity-id.test.js +92 -0
  81. package/integrations/tests/use-cases/get-integration-for-user.test.js +150 -0
  82. package/integrations/tests/use-cases/get-integration-instance.test.js +176 -0
  83. package/integrations/tests/use-cases/get-integrations-for-user.test.js +176 -0
  84. package/integrations/tests/use-cases/get-possible-integrations.test.js +188 -0
  85. package/integrations/tests/use-cases/update-integration-messages.test.js +142 -0
  86. package/integrations/tests/use-cases/update-integration-status.test.js +103 -0
  87. package/integrations/tests/use-cases/update-integration.test.js +141 -0
  88. package/integrations/use-cases/create-integration.js +83 -0
  89. package/integrations/use-cases/delete-integration-for-user.js +73 -0
  90. package/integrations/use-cases/find-integration-context-by-external-entity-id.js +72 -0
  91. package/integrations/use-cases/get-integration-for-user.js +78 -0
  92. package/integrations/use-cases/get-integration-instance-by-definition.js +67 -0
  93. package/integrations/use-cases/get-integration-instance.js +83 -0
  94. package/integrations/use-cases/get-integrations-for-user.js +87 -0
  95. package/integrations/use-cases/get-possible-integrations.js +27 -0
  96. package/integrations/use-cases/index.js +11 -0
  97. package/integrations/use-cases/load-integration-context-full.test.js +329 -0
  98. package/integrations/use-cases/load-integration-context.js +71 -0
  99. package/integrations/use-cases/load-integration-context.test.js +114 -0
  100. package/integrations/use-cases/update-integration-messages.js +44 -0
  101. package/integrations/use-cases/update-integration-status.js +32 -0
  102. package/integrations/use-cases/update-integration.js +93 -0
  103. package/integrations/utils/map-integration-dto.js +36 -0
  104. package/jest-global-setup-noop.js +3 -0
  105. package/jest-global-teardown-noop.js +3 -0
  106. package/{module-plugin → modules}/entity.js +1 -0
  107. package/{module-plugin → modules}/index.js +0 -8
  108. package/modules/module-factory.js +56 -0
  109. package/modules/module-hydration.test.js +205 -0
  110. package/modules/module.js +221 -0
  111. package/modules/repositories/module-repository-factory.js +33 -0
  112. package/modules/repositories/module-repository-interface.js +129 -0
  113. package/modules/repositories/module-repository-mongo.js +386 -0
  114. package/modules/repositories/module-repository-postgres.js +437 -0
  115. package/modules/repositories/module-repository.js +327 -0
  116. package/{module-plugin → modules}/test/mock-api/api.js +8 -3
  117. package/{module-plugin → modules}/test/mock-api/definition.js +12 -8
  118. package/modules/tests/doubles/test-module-factory.js +16 -0
  119. package/modules/tests/doubles/test-module-repository.js +39 -0
  120. package/modules/use-cases/get-entities-for-user.js +32 -0
  121. package/modules/use-cases/get-entity-options-by-id.js +59 -0
  122. package/modules/use-cases/get-entity-options-by-type.js +34 -0
  123. package/modules/use-cases/get-module-instance-from-type.js +31 -0
  124. package/modules/use-cases/get-module.js +56 -0
  125. package/modules/use-cases/process-authorization-callback.js +122 -0
  126. package/modules/use-cases/refresh-entity-options.js +59 -0
  127. package/modules/use-cases/test-module-auth.js +55 -0
  128. package/modules/utils/map-module-dto.js +18 -0
  129. package/package.json +14 -6
  130. package/prisma-mongodb/schema.prisma +318 -0
  131. package/prisma-postgresql/migrations/20250930193005_init/migration.sql +315 -0
  132. package/prisma-postgresql/migrations/20251006135218_init/migration.sql +9 -0
  133. package/prisma-postgresql/migrations/20251010000000_remove_unused_entity_reference_map/migration.sql +3 -0
  134. package/prisma-postgresql/migrations/migration_lock.toml +3 -0
  135. package/prisma-postgresql/schema.prisma +300 -0
  136. package/syncs/manager.js +468 -443
  137. package/syncs/repositories/sync-repository-factory.js +38 -0
  138. package/syncs/repositories/sync-repository-interface.js +109 -0
  139. package/syncs/repositories/sync-repository-mongo.js +239 -0
  140. package/syncs/repositories/sync-repository-postgres.js +319 -0
  141. package/syncs/sync.js +0 -1
  142. package/token/repositories/token-repository-factory.js +33 -0
  143. package/token/repositories/token-repository-interface.js +131 -0
  144. package/token/repositories/token-repository-mongo.js +212 -0
  145. package/token/repositories/token-repository-postgres.js +257 -0
  146. package/token/repositories/token-repository.js +219 -0
  147. package/types/integrations/index.d.ts +2 -6
  148. package/types/module-plugin/index.d.ts +5 -57
  149. package/types/syncs/index.d.ts +0 -2
  150. package/user/repositories/user-repository-factory.js +46 -0
  151. package/user/repositories/user-repository-interface.js +198 -0
  152. package/user/repositories/user-repository-mongo.js +250 -0
  153. package/user/repositories/user-repository-postgres.js +311 -0
  154. package/user/tests/doubles/test-user-repository.js +72 -0
  155. package/user/tests/use-cases/create-individual-user.test.js +24 -0
  156. package/user/tests/use-cases/create-organization-user.test.js +28 -0
  157. package/user/tests/use-cases/create-token-for-user-id.test.js +19 -0
  158. package/user/tests/use-cases/get-user-from-bearer-token.test.js +64 -0
  159. package/user/tests/use-cases/login-user.test.js +140 -0
  160. package/user/use-cases/create-individual-user.js +61 -0
  161. package/user/use-cases/create-organization-user.js +47 -0
  162. package/user/use-cases/create-token-for-user-id.js +30 -0
  163. package/user/use-cases/get-user-from-bearer-token.js +77 -0
  164. package/user/use-cases/login-user.js +122 -0
  165. package/user/user.js +77 -0
  166. package/websocket/repositories/websocket-connection-repository-factory.js +37 -0
  167. package/websocket/repositories/websocket-connection-repository-interface.js +106 -0
  168. package/websocket/repositories/websocket-connection-repository-mongo.js +155 -0
  169. package/websocket/repositories/websocket-connection-repository-postgres.js +195 -0
  170. package/websocket/repositories/websocket-connection-repository.js +160 -0
  171. package/database/models/State.js +0 -9
  172. package/database/models/Token.js +0 -70
  173. package/database/mongo.js +0 -171
  174. package/encrypt/Cryptor.test.js +0 -32
  175. package/encrypt/encrypt.js +0 -104
  176. package/encrypt/encrypt.test.js +0 -1069
  177. package/handlers/routers/middleware/loadUser.js +0 -15
  178. package/handlers/routers/middleware/requireLoggedInUser.js +0 -12
  179. package/integrations/create-frigg-backend.js +0 -31
  180. package/integrations/integration-factory.js +0 -251
  181. package/integrations/integration-mapping.js +0 -43
  182. package/integrations/integration-model.js +0 -46
  183. package/integrations/integration-user.js +0 -144
  184. package/integrations/test/integration-base.test.js +0 -144
  185. package/module-plugin/auther.js +0 -393
  186. package/module-plugin/credential.js +0 -22
  187. package/module-plugin/entity-manager.js +0 -70
  188. package/module-plugin/manager.js +0 -169
  189. package/module-plugin/module-factory.js +0 -61
  190. package/module-plugin/test/auther.test.js +0 -97
  191. /package/{module-plugin → modules}/ModuleConstants.js +0 -0
  192. /package/{module-plugin → modules}/requester/api-key.js +0 -0
  193. /package/{module-plugin → modules}/requester/basic.js +0 -0
  194. /package/{module-plugin → modules}/requester/oauth-2.js +0 -0
  195. /package/{module-plugin → modules}/requester/requester.js +0 -0
  196. /package/{module-plugin → modules}/requester/requester.test.js +0 -0
  197. /package/{module-plugin → modules}/test/mock-api/mocks/hubspot.js +0 -0
@@ -0,0 +1,371 @@
1
+ /**
2
+ * Verification Test: Repository Fix for PostgreSQL Decryption Bug
3
+ *
4
+ * This test verifies that the fix in ModuleRepositoryPostgres successfully
5
+ * decrypts credentials when fetching entities (after removing `include`).
6
+ *
7
+ * Expected Behavior After Fix:
8
+ * - All repository methods should return decrypted credentials
9
+ * - No encrypted tokens should leak through to the application layer
10
+ */
11
+
12
+ // Set up test environment for PostgreSQL with encryption
13
+ process.env.DB_TYPE = 'postgresql';
14
+ process.env.DATABASE_URL = process.env.DATABASE_URL || 'postgresql://postgres:postgres@localhost:5432/frigg?schema=public';
15
+ process.env.STAGE = 'integration-test';
16
+ process.env.AES_KEY_ID = 'test-key-id';
17
+ process.env.AES_KEY = 'test-aes-key-32-characters-long!';
18
+
19
+ // Mock config to return postgresql
20
+ jest.mock('../config', () => ({
21
+ DB_TYPE: 'postgresql',
22
+ getDatabaseType: jest.fn(() => 'postgresql'),
23
+ PRISMA_LOG_LEVEL: 'error,warn',
24
+ PRISMA_QUERY_LOGGING: false,
25
+ }));
26
+
27
+ const { prisma, connectPrisma, disconnectPrisma } = require('../prisma');
28
+ const { ModuleRepositoryPostgres } = require('../../modules/repositories/module-repository-postgres');
29
+
30
+ describe('Repository Fix Verification - PostgreSQL Decryption', () => {
31
+ let repository;
32
+ let testCredentialId;
33
+ let testEntityId;
34
+ let testUserId;
35
+ const TEST_TOKEN = 'my-secret-access-token-12345';
36
+ const TEST_REFRESH_TOKEN = 'my-secret-refresh-token-67890';
37
+ const TEST_DOMAIN = 'example-test.com';
38
+
39
+ beforeAll(async () => {
40
+ await connectPrisma();
41
+ repository = new ModuleRepositoryPostgres();
42
+ });
43
+
44
+ afterAll(async () => {
45
+ // Cleanup test data
46
+ if (testEntityId) {
47
+ await prisma.entity.deleteMany({
48
+ where: { id: parseInt(testEntityId, 10) }
49
+ }).catch(() => {});
50
+ }
51
+ if (testCredentialId) {
52
+ await prisma.credential.deleteMany({
53
+ where: { id: testCredentialId }
54
+ }).catch(() => {});
55
+ }
56
+ if (testUserId) {
57
+ await prisma.user.deleteMany({
58
+ where: { id: testUserId }
59
+ }).catch(() => {});
60
+ }
61
+
62
+ await disconnectPrisma();
63
+ });
64
+
65
+ afterEach(async () => {
66
+ // Clean up after each test
67
+ if (testEntityId) {
68
+ await prisma.entity.deleteMany({
69
+ where: { id: parseInt(testEntityId, 10) }
70
+ }).catch(() => {});
71
+ testEntityId = null;
72
+ }
73
+ if (testCredentialId) {
74
+ await prisma.credential.deleteMany({
75
+ where: { id: testCredentialId }
76
+ }).catch(() => {});
77
+ testCredentialId = null;
78
+ }
79
+ if (testUserId) {
80
+ await prisma.user.deleteMany({
81
+ where: { id: testUserId }
82
+ }).catch(() => {});
83
+ testUserId = null;
84
+ }
85
+ });
86
+
87
+ test('āœ… FIX VERIFICATION: findEntityById returns decrypted credential', async () => {
88
+ // Setup: Create user, credential, and entity
89
+ const user = await prisma.user.create({
90
+ data: {
91
+ type: 'INDIVIDUAL',
92
+ hashword: 'test-hash'
93
+ }
94
+ });
95
+ testUserId = user.id;
96
+
97
+ const credential = await prisma.credential.create({
98
+ data: {
99
+ userId: testUserId,
100
+ externalId: 'test-cred-findEntityById',
101
+ data: {
102
+ access_token: TEST_TOKEN,
103
+ refresh_token: TEST_REFRESH_TOKEN,
104
+ domain: TEST_DOMAIN,
105
+ },
106
+ },
107
+ });
108
+ testCredentialId = credential.id;
109
+
110
+ const entity = await prisma.entity.create({
111
+ data: {
112
+ userId: testUserId,
113
+ credentialId: testCredentialId,
114
+ moduleName: 'test-module',
115
+ externalId: 'test-entity-findById',
116
+ },
117
+ });
118
+ testEntityId = entity.id.toString();
119
+
120
+ // Test: Fetch via repository
121
+ const result = await repository.findEntityById(testEntityId);
122
+
123
+ // Verify: Credential is decrypted
124
+ expect(result).toBeDefined();
125
+ expect(result.credential).toBeDefined();
126
+ expect(result.credential.data.access_token).toBe(TEST_TOKEN);
127
+ expect(result.credential.data.refresh_token).toBe(TEST_REFRESH_TOKEN);
128
+ expect(result.credential.data.domain).toBe(TEST_DOMAIN);
129
+
130
+ // Verify: No encrypted format (shouldn't contain ':' pattern)
131
+ expect(result.credential.data.access_token).not.toContain(':');
132
+
133
+ console.log('āœ… findEntityById: Credential successfully decrypted!');
134
+ });
135
+
136
+ test('āœ… FIX VERIFICATION: findEntitiesByUserId returns decrypted credentials', async () => {
137
+ // Setup
138
+ const user = await prisma.user.create({
139
+ data: {
140
+ type: 'INDIVIDUAL',
141
+ hashword: 'test-hash'
142
+ }
143
+ });
144
+ testUserId = user.id;
145
+
146
+ const credential = await prisma.credential.create({
147
+ data: {
148
+ userId: testUserId,
149
+ externalId: 'test-cred-findByUserId',
150
+ data: {
151
+ access_token: TEST_TOKEN,
152
+ domain: TEST_DOMAIN,
153
+ },
154
+ },
155
+ });
156
+ testCredentialId = credential.id;
157
+
158
+ const entity = await prisma.entity.create({
159
+ data: {
160
+ userId: testUserId,
161
+ credentialId: testCredentialId,
162
+ moduleName: 'test-module',
163
+ externalId: 'test-entity-findByUserId',
164
+ },
165
+ });
166
+ testEntityId = entity.id.toString();
167
+
168
+ // Test
169
+ const results = await repository.findEntitiesByUserId(testUserId.toString());
170
+
171
+ // Verify
172
+ expect(results).toBeDefined();
173
+ expect(results.length).toBeGreaterThan(0);
174
+ const firstEntity = results[0];
175
+ expect(firstEntity.credential).toBeDefined();
176
+ expect(firstEntity.credential.data.access_token).toBe(TEST_TOKEN);
177
+ expect(firstEntity.credential.data.access_token).not.toContain(':');
178
+
179
+ console.log('āœ… findEntitiesByUserId: Credentials successfully decrypted!');
180
+ });
181
+
182
+ test('āœ… FIX VERIFICATION: findEntitiesByIds returns decrypted credentials', async () => {
183
+ // Setup
184
+ const user = await prisma.user.create({
185
+ data: {
186
+ type: 'INDIVIDUAL',
187
+ hashword: 'test-hash'
188
+ }
189
+ });
190
+ testUserId = user.id;
191
+
192
+ const credential = await prisma.credential.create({
193
+ data: {
194
+ userId: testUserId,
195
+ externalId: 'test-cred-findByIds',
196
+ data: {
197
+ access_token: TEST_TOKEN,
198
+ domain: TEST_DOMAIN,
199
+ },
200
+ },
201
+ });
202
+ testCredentialId = credential.id;
203
+
204
+ const entity = await prisma.entity.create({
205
+ data: {
206
+ userId: testUserId,
207
+ credentialId: testCredentialId,
208
+ moduleName: 'test-module',
209
+ externalId: 'test-entity-findByIds',
210
+ },
211
+ });
212
+ testEntityId = entity.id.toString();
213
+
214
+ // Test
215
+ const results = await repository.findEntitiesByIds([testEntityId]);
216
+
217
+ // Verify
218
+ expect(results).toBeDefined();
219
+ expect(results.length).toBe(1);
220
+ expect(results[0].credential).toBeDefined();
221
+ expect(results[0].credential.data.access_token).toBe(TEST_TOKEN);
222
+ expect(results[0].credential.data.access_token).not.toContain(':');
223
+
224
+ console.log('āœ… findEntitiesByIds: Credentials successfully decrypted!');
225
+ });
226
+
227
+ test('āœ… FIX VERIFICATION: createEntity returns decrypted credential', async () => {
228
+ // Setup
229
+ const user = await prisma.user.create({
230
+ data: {
231
+ type: 'INDIVIDUAL',
232
+ hashword: 'test-hash'
233
+ }
234
+ });
235
+ testUserId = user.id;
236
+
237
+ const credential = await prisma.credential.create({
238
+ data: {
239
+ userId: testUserId,
240
+ externalId: 'test-cred-create',
241
+ data: {
242
+ access_token: TEST_TOKEN,
243
+ domain: TEST_DOMAIN,
244
+ },
245
+ },
246
+ });
247
+ testCredentialId = credential.id;
248
+
249
+ // Test: Create entity via repository
250
+ const entity = await repository.createEntity({
251
+ userId: testUserId.toString(),
252
+ credentialId: testCredentialId.toString(),
253
+ moduleName: 'test-module',
254
+ externalId: 'test-entity-create',
255
+ });
256
+
257
+ testEntityId = entity.id;
258
+
259
+ // Verify
260
+ expect(entity).toBeDefined();
261
+ expect(entity.credential).toBeDefined();
262
+ expect(entity.credential.data.access_token).toBe(TEST_TOKEN);
263
+ expect(entity.credential.data.access_token).not.toContain(':');
264
+
265
+ console.log('āœ… createEntity: Credential successfully decrypted!');
266
+ });
267
+
268
+ test('āœ… FIX VERIFICATION: updateEntity returns decrypted credential', async () => {
269
+ // Setup
270
+ const user = await prisma.user.create({
271
+ data: {
272
+ type: 'INDIVIDUAL',
273
+ hashword: 'test-hash'
274
+ }
275
+ });
276
+ testUserId = user.id;
277
+
278
+ const credential = await prisma.credential.create({
279
+ data: {
280
+ userId: testUserId,
281
+ externalId: 'test-cred-update',
282
+ data: {
283
+ access_token: TEST_TOKEN,
284
+ domain: TEST_DOMAIN,
285
+ },
286
+ },
287
+ });
288
+ testCredentialId = credential.id;
289
+
290
+ const entity = await prisma.entity.create({
291
+ data: {
292
+ userId: testUserId,
293
+ credentialId: testCredentialId,
294
+ moduleName: 'test-module',
295
+ externalId: 'test-entity-update',
296
+ },
297
+ });
298
+ testEntityId = entity.id.toString();
299
+
300
+ // Test: Update entity via repository
301
+ const updated = await repository.updateEntity(testEntityId, {
302
+ name: 'Updated Name',
303
+ });
304
+
305
+ // Verify
306
+ expect(updated).toBeDefined();
307
+ expect(updated.name).toBe('Updated Name');
308
+ expect(updated.credential).toBeDefined();
309
+ expect(updated.credential.data.access_token).toBe(TEST_TOKEN);
310
+ expect(updated.credential.data.access_token).not.toContain(':');
311
+
312
+ console.log('āœ… updateEntity: Credential successfully decrypted!');
313
+ });
314
+
315
+ test('šŸ“Š COMPARISON: Verify tokens are encrypted in database but decrypted in repository', async () => {
316
+ // Setup
317
+ const user = await prisma.user.create({
318
+ data: {
319
+ type: 'INDIVIDUAL',
320
+ hashword: 'test-hash'
321
+ }
322
+ });
323
+ testUserId = user.id;
324
+
325
+ const credential = await prisma.credential.create({
326
+ data: {
327
+ userId: testUserId,
328
+ externalId: 'test-cred-comparison',
329
+ data: {
330
+ access_token: TEST_TOKEN,
331
+ domain: TEST_DOMAIN,
332
+ },
333
+ },
334
+ });
335
+ testCredentialId = credential.id;
336
+
337
+ const entity = await prisma.entity.create({
338
+ data: {
339
+ userId: testUserId,
340
+ credentialId: testCredentialId,
341
+ moduleName: 'test-module',
342
+ externalId: 'test-entity-comparison',
343
+ },
344
+ });
345
+ testEntityId = entity.id.toString();
346
+
347
+ // 1. Check raw database (should be encrypted)
348
+ const rawCred = await prisma.$queryRaw`
349
+ SELECT data FROM "Credential" WHERE id = ${testCredentialId}
350
+ `;
351
+ const rawToken = rawCred[0].data.access_token;
352
+
353
+ // 2. Check via repository (should be decrypted)
354
+ const repoEntity = await repository.findEntityById(testEntityId);
355
+ const repoToken = repoEntity.credential.data.access_token;
356
+
357
+ console.log('\nšŸ“Š COMPARISON RESULTS:');
358
+ console.log('Raw DB token (encrypted):', rawToken.substring(0, 50) + '...');
359
+ console.log('Repository token (decrypted):', repoToken);
360
+
361
+ // Verify database has encrypted version
362
+ expect(rawToken).toContain(':');
363
+ expect(rawToken.split(':')).toHaveLength(4);
364
+
365
+ // Verify repository returns decrypted version
366
+ expect(repoToken).toBe(TEST_TOKEN);
367
+ expect(repoToken).not.toContain(':');
368
+
369
+ console.log('āœ… Database stores encrypted, repository returns decrypted - FIX WORKS!');
370
+ });
371
+ });
@@ -0,0 +1,245 @@
1
+ /**
2
+ * PostgreSQL Relation Decryption Bug Test
3
+ *
4
+ * This test proves that credentials fetched via Prisma `include` relations
5
+ * are NOT being decrypted by the encryption extension, while credentials
6
+ * fetched directly ARE being decrypted.
7
+ *
8
+ * Expected Behavior:
9
+ * - Direct credential fetch: SHOULD decrypt āœ…
10
+ * - Credential via Entity include: SHOULD decrypt but DOESN'T āŒ
11
+ * - Raw database query: SHOULD be encrypted āœ…
12
+ */
13
+
14
+ // Set up test environment for PostgreSQL with encryption
15
+ process.env.DB_TYPE = 'postgresql';
16
+ process.env.DATABASE_URL = process.env.DATABASE_URL || 'postgresql://postgres:postgres@localhost:5432/frigg?schema=public';
17
+ process.env.STAGE = 'integration-test';
18
+ process.env.AES_KEY_ID = 'test-key-id';
19
+ process.env.AES_KEY = 'test-aes-key-32-characters-long!';
20
+
21
+ // Mock config to return postgresql
22
+ jest.mock('../config', () => ({
23
+ DB_TYPE: 'postgresql',
24
+ getDatabaseType: jest.fn(() => 'postgresql'),
25
+ PRISMA_LOG_LEVEL: 'error,warn',
26
+ PRISMA_QUERY_LOGGING: false,
27
+ }));
28
+
29
+ const { prisma, connectPrisma, disconnectPrisma } = require('../prisma');
30
+
31
+ describe('PostgreSQL Relation Decryption Bug', () => {
32
+ let testCredentialId;
33
+ let testEntityId;
34
+ const TEST_TOKEN = 'secret-token-should-be-encrypted';
35
+ const TEST_EXTERNAL_ID = 'test-relation-bug-credential';
36
+
37
+ beforeAll(async () => {
38
+ await connectPrisma();
39
+ });
40
+
41
+ afterAll(async () => {
42
+ // Cleanup test data
43
+ if (testEntityId) {
44
+ await prisma.entity.deleteMany({
45
+ where: { id: testEntityId }
46
+ }).catch(() => {});
47
+ }
48
+ if (testCredentialId) {
49
+ await prisma.credential.deleteMany({
50
+ where: { id: testCredentialId }
51
+ }).catch(() => {});
52
+ }
53
+
54
+ await disconnectPrisma();
55
+ });
56
+
57
+ afterEach(async () => {
58
+ // Clean up after each test
59
+ if (testEntityId) {
60
+ await prisma.entity.deleteMany({
61
+ where: { id: testEntityId }
62
+ }).catch(() => {});
63
+ testEntityId = null;
64
+ }
65
+ if (testCredentialId) {
66
+ await prisma.credential.deleteMany({
67
+ where: { id: testCredentialId }
68
+ }).catch(() => {});
69
+ testCredentialId = null;
70
+ }
71
+ });
72
+
73
+ test('PROOF 1: Direct credential fetch DOES decrypt (extension works)', async () => {
74
+ // 1. Create credential with sensitive data
75
+ const created = await prisma.credential.create({
76
+ data: {
77
+ externalId: TEST_EXTERNAL_ID,
78
+ data: {
79
+ access_token: TEST_TOKEN,
80
+ domain: 'example.com',
81
+ },
82
+ },
83
+ });
84
+
85
+ testCredentialId = created.id;
86
+
87
+ // Verify creation returns decrypted data
88
+ expect(created.data.access_token).toBe(TEST_TOKEN);
89
+
90
+ // 2. Fetch directly via Credential model (simulating direct query)
91
+ const directFetch = await prisma.credential.findUnique({
92
+ where: { id: testCredentialId },
93
+ });
94
+
95
+ // āœ… EXPECT: Should be decrypted by extension
96
+ expect(directFetch).toBeDefined();
97
+ expect(directFetch.data.access_token).toBe(TEST_TOKEN);
98
+
99
+ // Should NOT contain colon pattern (not encrypted format)
100
+ expect(directFetch.data.access_token).not.toContain(':');
101
+ });
102
+
103
+ test('BUG PROOF: Credential via Entity include DOES NOT decrypt', async () => {
104
+ // 1. Create credential first
105
+ const credential = await prisma.credential.create({
106
+ data: {
107
+ externalId: TEST_EXTERNAL_ID,
108
+ data: {
109
+ access_token: TEST_TOKEN,
110
+ domain: 'example.com',
111
+ },
112
+ },
113
+ });
114
+
115
+ testCredentialId = credential.id;
116
+
117
+ // 2. Create entity that references the credential
118
+ const entity = await prisma.entity.create({
119
+ data: {
120
+ moduleName: 'test-module',
121
+ externalId: 'test-entity-for-bug-proof',
122
+ credentialId: testCredentialId,
123
+ },
124
+ });
125
+
126
+ testEntityId = entity.id;
127
+
128
+ // 3. Fetch entity with credential included (like ModuleRepository does)
129
+ const entityWithCredential = await prisma.entity.findUnique({
130
+ where: { id: testEntityId },
131
+ include: { credential: true },
132
+ });
133
+
134
+ // āŒ BUG: Credential data is STILL ENCRYPTED when fetched via include
135
+ expect(entityWithCredential).toBeDefined();
136
+ expect(entityWithCredential.credential).toBeDefined();
137
+
138
+ console.log('\nšŸ” DEBUG: Credential data from include:', entityWithCredential.credential.data);
139
+ console.log('šŸ” DEBUG: access_token value:', entityWithCredential.credential.data.access_token);
140
+
141
+ // The bug: Token should be decrypted but it's still in encrypted format
142
+ const tokenValue = entityWithCredential.credential.data.access_token;
143
+ const hasColonPattern = tokenValue.includes(':');
144
+ const isEncryptedFormat = tokenValue.split(':').length === 4;
145
+
146
+ if (hasColonPattern && isEncryptedFormat) {
147
+ console.log('āŒ BUG CONFIRMED: Token is still encrypted!');
148
+ console.log(` Expected: "${TEST_TOKEN}"`);
149
+ console.log(` Got: "${tokenValue}"`);
150
+ }
151
+
152
+ // This assertion SHOULD fail if the bug exists
153
+ // Comment it out initially to see the actual behavior
154
+ // expect(tokenValue).toBe(TEST_TOKEN);
155
+
156
+ // Instead, let's prove the bug by showing it's encrypted
157
+ expect(tokenValue).toContain(':'); // Still has encrypted format
158
+ expect(tokenValue).not.toBe(TEST_TOKEN); // Not the plain text
159
+ });
160
+
161
+ test('PROOF 2: Raw database has encrypted data (encryption works at storage)', async () => {
162
+ // 1. Create credential
163
+ const created = await prisma.credential.create({
164
+ data: {
165
+ externalId: TEST_EXTERNAL_ID,
166
+ data: {
167
+ access_token: TEST_TOKEN,
168
+ domain: 'example.com',
169
+ },
170
+ },
171
+ });
172
+
173
+ testCredentialId = created.id;
174
+
175
+ // 2. Query raw database to see actual stored value
176
+ const raw = await prisma.$queryRaw`
177
+ SELECT data FROM "Credential" WHERE id = ${testCredentialId}
178
+ `;
179
+
180
+ expect(raw).toBeDefined();
181
+ expect(raw.length).toBe(1);
182
+
183
+ const rawToken = raw[0].data.access_token;
184
+ console.log('\nšŸ” DEBUG: Raw database token:', rawToken);
185
+
186
+ // āœ… VERIFY: Database stores encrypted data
187
+ expect(rawToken).toContain(':'); // Has encrypted format
188
+
189
+ const parts = rawToken.split(':');
190
+ expect(parts.length).toBe(4); // keyId:iv:ciphertext:encryptedKey
191
+
192
+ console.log('āœ… CONFIRMED: Data is encrypted at rest in database');
193
+ });
194
+
195
+ test('COMPARISON: Direct fetch vs Include fetch behavior', async () => {
196
+ // Create credential and entity
197
+ const credential = await prisma.credential.create({
198
+ data: {
199
+ externalId: TEST_EXTERNAL_ID,
200
+ data: {
201
+ access_token: TEST_TOKEN,
202
+ refresh_token: 'refresh-token-test',
203
+ domain: 'comparison.com',
204
+ },
205
+ },
206
+ });
207
+
208
+ testCredentialId = credential.id;
209
+
210
+ const entity = await prisma.entity.create({
211
+ data: {
212
+ moduleName: 'comparison-module',
213
+ externalId: 'comparison-entity',
214
+ credentialId: testCredentialId,
215
+ },
216
+ });
217
+
218
+ testEntityId = entity.id;
219
+
220
+ // Fetch 1: Direct credential query
221
+ const directCredential = await prisma.credential.findUnique({
222
+ where: { id: testCredentialId },
223
+ });
224
+
225
+ // Fetch 2: Credential via entity include
226
+ const entityWithCredential = await prisma.entity.findUnique({
227
+ where: { id: testEntityId },
228
+ include: { credential: true },
229
+ });
230
+
231
+ console.log('\nšŸ“Š COMPARISON RESULTS:');
232
+ console.log('Direct fetch access_token:', directCredential.data.access_token);
233
+ console.log('Include fetch access_token:', entityWithCredential.credential.data.access_token);
234
+
235
+ const directIsDecrypted = directCredential.data.access_token === TEST_TOKEN;
236
+ const includeIsDecrypted = entityWithCredential.credential.data.access_token === TEST_TOKEN;
237
+
238
+ console.log(`\nDirect fetch decrypted: ${directIsDecrypted ? 'āœ… YES' : 'āŒ NO'}`);
239
+ console.log(`Include fetch decrypted: ${includeIsDecrypted ? 'āœ… YES' : 'āŒ NO'}`);
240
+
241
+ // Prove they're different
242
+ expect(directIsDecrypted).toBe(true);
243
+ expect(includeIsDecrypted).toBe(false); // BUG: This should be true but it's false
244
+ });
245
+ });