@friggframework/core 2.0.0-next.41 ā 2.0.0-next.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +693 -0
- package/README.md +931 -50
- package/application/commands/README.md +421 -0
- package/application/commands/credential-commands.js +224 -0
- package/application/commands/entity-commands.js +315 -0
- package/application/commands/integration-commands.js +160 -0
- package/application/commands/integration-commands.test.js +123 -0
- package/application/commands/user-commands.js +213 -0
- package/application/index.js +69 -0
- package/core/CLAUDE.md +690 -0
- package/core/create-handler.js +0 -6
- package/credential/repositories/credential-repository-factory.js +47 -0
- package/credential/repositories/credential-repository-interface.js +98 -0
- package/credential/repositories/credential-repository-mongo.js +301 -0
- package/credential/repositories/credential-repository-postgres.js +307 -0
- package/credential/repositories/credential-repository.js +307 -0
- package/credential/use-cases/get-credential-for-user.js +21 -0
- package/credential/use-cases/update-authentication-status.js +15 -0
- package/database/config.js +117 -0
- package/database/encryption/README.md +683 -0
- package/database/encryption/encryption-integration.test.js +553 -0
- package/database/encryption/encryption-schema-registry.js +141 -0
- package/database/encryption/encryption-schema-registry.test.js +392 -0
- package/database/encryption/field-encryption-service.js +226 -0
- package/database/encryption/field-encryption-service.test.js +525 -0
- package/database/encryption/logger.js +79 -0
- package/database/encryption/mongo-decryption-fix-verification.test.js +348 -0
- package/database/encryption/postgres-decryption-fix-verification.test.js +371 -0
- package/database/encryption/postgres-relation-decryption.test.js +245 -0
- package/database/encryption/prisma-encryption-extension.js +222 -0
- package/database/encryption/prisma-encryption-extension.test.js +439 -0
- package/database/index.js +25 -12
- package/database/models/readme.md +1 -0
- package/database/prisma.js +162 -0
- package/database/repositories/health-check-repository-factory.js +38 -0
- package/database/repositories/health-check-repository-interface.js +86 -0
- package/database/repositories/health-check-repository-mongodb.js +72 -0
- package/database/repositories/health-check-repository-postgres.js +75 -0
- package/database/repositories/health-check-repository.js +108 -0
- package/database/use-cases/check-database-health-use-case.js +34 -0
- package/database/use-cases/check-encryption-health-use-case.js +82 -0
- package/database/use-cases/test-encryption-use-case.js +252 -0
- package/encrypt/Cryptor.js +20 -152
- package/encrypt/index.js +1 -2
- package/encrypt/test-encrypt.js +0 -2
- package/handlers/app-definition-loader.js +38 -0
- package/handlers/app-handler-helpers.js +0 -3
- package/handlers/auth-flow.integration.test.js +147 -0
- package/handlers/backend-utils.js +25 -45
- package/handlers/integration-event-dispatcher.js +54 -0
- package/handlers/integration-event-dispatcher.test.js +141 -0
- package/handlers/routers/HEALTHCHECK.md +103 -1
- package/handlers/routers/auth.js +3 -14
- package/handlers/routers/health.js +63 -424
- package/handlers/routers/health.test.js +7 -0
- package/handlers/routers/integration-defined-routers.js +8 -5
- package/handlers/routers/user.js +25 -5
- package/handlers/routers/websocket.js +5 -3
- package/handlers/use-cases/check-external-apis-health-use-case.js +81 -0
- package/handlers/use-cases/check-integrations-health-use-case.js +32 -0
- package/handlers/workers/integration-defined-workers.js +6 -3
- package/index.js +45 -22
- package/integrations/index.js +12 -10
- package/integrations/integration-base.js +224 -53
- package/integrations/integration-router.js +386 -178
- package/integrations/options.js +1 -1
- package/integrations/repositories/integration-mapping-repository-factory.js +50 -0
- package/integrations/repositories/integration-mapping-repository-interface.js +106 -0
- package/integrations/repositories/integration-mapping-repository-mongo.js +161 -0
- package/integrations/repositories/integration-mapping-repository-postgres.js +227 -0
- package/integrations/repositories/integration-mapping-repository.js +156 -0
- package/integrations/repositories/integration-repository-factory.js +44 -0
- package/integrations/repositories/integration-repository-interface.js +115 -0
- package/integrations/repositories/integration-repository-mongo.js +271 -0
- package/integrations/repositories/integration-repository-postgres.js +319 -0
- package/integrations/tests/doubles/dummy-integration-class.js +90 -0
- package/integrations/tests/doubles/test-integration-repository.js +99 -0
- package/integrations/tests/use-cases/create-integration.test.js +131 -0
- package/integrations/tests/use-cases/delete-integration-for-user.test.js +150 -0
- package/integrations/tests/use-cases/find-integration-context-by-external-entity-id.test.js +92 -0
- package/integrations/tests/use-cases/get-integration-for-user.test.js +150 -0
- package/integrations/tests/use-cases/get-integration-instance.test.js +176 -0
- package/integrations/tests/use-cases/get-integrations-for-user.test.js +176 -0
- package/integrations/tests/use-cases/get-possible-integrations.test.js +188 -0
- package/integrations/tests/use-cases/update-integration-messages.test.js +142 -0
- package/integrations/tests/use-cases/update-integration-status.test.js +103 -0
- package/integrations/tests/use-cases/update-integration.test.js +141 -0
- package/integrations/use-cases/create-integration.js +83 -0
- package/integrations/use-cases/delete-integration-for-user.js +73 -0
- package/integrations/use-cases/find-integration-context-by-external-entity-id.js +72 -0
- package/integrations/use-cases/get-integration-for-user.js +78 -0
- package/integrations/use-cases/get-integration-instance-by-definition.js +67 -0
- package/integrations/use-cases/get-integration-instance.js +83 -0
- package/integrations/use-cases/get-integrations-for-user.js +87 -0
- package/integrations/use-cases/get-possible-integrations.js +27 -0
- package/integrations/use-cases/index.js +11 -0
- package/integrations/use-cases/load-integration-context-full.test.js +329 -0
- package/integrations/use-cases/load-integration-context.js +71 -0
- package/integrations/use-cases/load-integration-context.test.js +114 -0
- package/integrations/use-cases/update-integration-messages.js +44 -0
- package/integrations/use-cases/update-integration-status.js +32 -0
- package/integrations/use-cases/update-integration.js +93 -0
- package/integrations/utils/map-integration-dto.js +36 -0
- package/jest-global-setup-noop.js +3 -0
- package/jest-global-teardown-noop.js +3 -0
- package/{module-plugin ā modules}/entity.js +1 -0
- package/{module-plugin ā modules}/index.js +0 -8
- package/modules/module-factory.js +56 -0
- package/modules/module-hydration.test.js +205 -0
- package/modules/module.js +221 -0
- package/modules/repositories/module-repository-factory.js +33 -0
- package/modules/repositories/module-repository-interface.js +129 -0
- package/modules/repositories/module-repository-mongo.js +386 -0
- package/modules/repositories/module-repository-postgres.js +437 -0
- package/modules/repositories/module-repository.js +327 -0
- package/{module-plugin ā modules}/test/mock-api/api.js +8 -3
- package/{module-plugin ā modules}/test/mock-api/definition.js +12 -8
- package/modules/tests/doubles/test-module-factory.js +16 -0
- package/modules/tests/doubles/test-module-repository.js +39 -0
- package/modules/use-cases/get-entities-for-user.js +32 -0
- package/modules/use-cases/get-entity-options-by-id.js +59 -0
- package/modules/use-cases/get-entity-options-by-type.js +34 -0
- package/modules/use-cases/get-module-instance-from-type.js +31 -0
- package/modules/use-cases/get-module.js +56 -0
- package/modules/use-cases/process-authorization-callback.js +121 -0
- package/modules/use-cases/refresh-entity-options.js +59 -0
- package/modules/use-cases/test-module-auth.js +55 -0
- package/modules/utils/map-module-dto.js +18 -0
- package/package.json +14 -6
- package/prisma-mongodb/schema.prisma +321 -0
- package/prisma-postgresql/migrations/20250930193005_init/migration.sql +315 -0
- package/prisma-postgresql/migrations/20251006135218_init/migration.sql +9 -0
- package/prisma-postgresql/migrations/migration_lock.toml +3 -0
- package/prisma-postgresql/schema.prisma +303 -0
- package/syncs/manager.js +468 -443
- package/syncs/repositories/sync-repository-factory.js +38 -0
- package/syncs/repositories/sync-repository-interface.js +109 -0
- package/syncs/repositories/sync-repository-mongo.js +239 -0
- package/syncs/repositories/sync-repository-postgres.js +319 -0
- package/syncs/sync.js +0 -1
- package/token/repositories/token-repository-factory.js +33 -0
- package/token/repositories/token-repository-interface.js +131 -0
- package/token/repositories/token-repository-mongo.js +212 -0
- package/token/repositories/token-repository-postgres.js +257 -0
- package/token/repositories/token-repository.js +219 -0
- package/types/integrations/index.d.ts +2 -6
- package/types/module-plugin/index.d.ts +5 -57
- package/types/syncs/index.d.ts +0 -2
- package/user/repositories/user-repository-factory.js +46 -0
- package/user/repositories/user-repository-interface.js +198 -0
- package/user/repositories/user-repository-mongo.js +250 -0
- package/user/repositories/user-repository-postgres.js +311 -0
- package/user/tests/doubles/test-user-repository.js +72 -0
- package/user/tests/use-cases/create-individual-user.test.js +24 -0
- package/user/tests/use-cases/create-organization-user.test.js +28 -0
- package/user/tests/use-cases/create-token-for-user-id.test.js +19 -0
- package/user/tests/use-cases/get-user-from-bearer-token.test.js +64 -0
- package/user/tests/use-cases/login-user.test.js +140 -0
- package/user/use-cases/create-individual-user.js +61 -0
- package/user/use-cases/create-organization-user.js +47 -0
- package/user/use-cases/create-token-for-user-id.js +30 -0
- package/user/use-cases/get-user-from-bearer-token.js +77 -0
- package/user/use-cases/login-user.js +122 -0
- package/user/user.js +77 -0
- package/websocket/repositories/websocket-connection-repository-factory.js +37 -0
- package/websocket/repositories/websocket-connection-repository-interface.js +106 -0
- package/websocket/repositories/websocket-connection-repository-mongo.js +155 -0
- package/websocket/repositories/websocket-connection-repository-postgres.js +195 -0
- package/websocket/repositories/websocket-connection-repository.js +160 -0
- package/database/models/State.js +0 -9
- package/database/models/Token.js +0 -70
- package/database/mongo.js +0 -171
- package/encrypt/Cryptor.test.js +0 -32
- package/encrypt/encrypt.js +0 -104
- package/encrypt/encrypt.test.js +0 -1069
- package/handlers/routers/middleware/loadUser.js +0 -15
- package/handlers/routers/middleware/requireLoggedInUser.js +0 -12
- package/integrations/create-frigg-backend.js +0 -31
- package/integrations/integration-factory.js +0 -251
- package/integrations/integration-mapping.js +0 -43
- package/integrations/integration-model.js +0 -46
- package/integrations/integration-user.js +0 -144
- package/integrations/test/integration-base.test.js +0 -144
- package/module-plugin/auther.js +0 -393
- package/module-plugin/credential.js +0 -22
- package/module-plugin/entity-manager.js +0 -70
- package/module-plugin/manager.js +0 -169
- package/module-plugin/module-factory.js +0 -61
- package/module-plugin/test/auther.test.js +0 -97
- /package/{module-plugin ā modules}/ModuleConstants.js +0 -0
- /package/{module-plugin ā modules}/requester/api-key.js +0 -0
- /package/{module-plugin ā modules}/requester/basic.js +0 -0
- /package/{module-plugin ā modules}/requester/oauth-2.js +0 -0
- /package/{module-plugin ā modules}/requester/requester.js +0 -0
- /package/{module-plugin ā modules}/requester/requester.test.js +0 -0
- /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
|
+
});
|