@friggframework/core 2.0.0--canary.548.c8ae0ca.0 → 2.0.0--canary.545.c40eca4.0

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 (237) hide show
  1. package/CHANGELOG.md +66 -67
  2. package/CLAUDE.md +148 -124
  3. package/README.md +165 -130
  4. package/admin-scripts/index.js +52 -0
  5. package/admin-scripts/repositories/admin-api-key-repository-documentdb.js +21 -0
  6. package/admin-scripts/repositories/admin-api-key-repository-factory.js +51 -0
  7. package/admin-scripts/repositories/admin-api-key-repository-interface.js +121 -0
  8. package/admin-scripts/repositories/admin-api-key-repository-mongo.js +155 -0
  9. package/admin-scripts/repositories/admin-api-key-repository-postgres.js +189 -0
  10. package/admin-scripts/repositories/admin-process-repository-documentdb.js +21 -0
  11. package/admin-scripts/repositories/admin-process-repository-factory.js +49 -0
  12. package/admin-scripts/repositories/admin-process-repository-interface.js +150 -0
  13. package/admin-scripts/repositories/admin-process-repository-mongo.js +213 -0
  14. package/admin-scripts/repositories/admin-process-repository-postgres.js +251 -0
  15. package/admin-scripts/repositories/script-execution-repository-documentdb.js +21 -0
  16. package/admin-scripts/repositories/script-execution-repository-factory.js +51 -0
  17. package/admin-scripts/repositories/script-execution-repository-interface.js +193 -0
  18. package/admin-scripts/repositories/script-execution-repository-mongo.js +278 -0
  19. package/admin-scripts/repositories/script-execution-repository-postgres.js +320 -0
  20. package/admin-scripts/repositories/script-schedule-repository-documentdb.js +21 -0
  21. package/admin-scripts/repositories/script-schedule-repository-factory.js +49 -0
  22. package/admin-scripts/repositories/script-schedule-repository-interface.js +108 -0
  23. package/admin-scripts/repositories/script-schedule-repository-mongo.js +179 -0
  24. package/admin-scripts/repositories/script-schedule-repository-postgres.js +210 -0
  25. package/application/commands/admin-script-commands.js +348 -0
  26. package/application/commands/integration-commands.js +12 -6
  27. package/application/index.js +5 -3
  28. package/assertions/get.js +17 -7
  29. package/associations/association.js +59 -57
  30. package/core/CLAUDE.md +222 -156
  31. package/core/Worker.js +33 -12
  32. package/credential/credential-router.js +262 -0
  33. package/credential/repositories/credential-repository-documentdb.js +71 -1
  34. package/credential/repositories/credential-repository-factory.js +4 -6
  35. package/credential/repositories/credential-repository-interface.js +8 -2
  36. package/credential/repositories/credential-repository-mongo.js +45 -4
  37. package/credential/repositories/credential-repository-postgres.js +44 -3
  38. package/credential/repositories/credential-repository.js +3 -1
  39. package/credential/use-cases/delete-credential-for-user.js +50 -0
  40. package/credential/use-cases/get-credential-for-user.js +21 -3
  41. package/credential/use-cases/list-credentials-for-user.js +31 -0
  42. package/credential/use-cases/reauthorize-credential.js +103 -0
  43. package/credential/use-cases/update-authentication-status.js +5 -2
  44. package/database/MONGODB_TRANSACTION_FIX.md +72 -52
  45. package/database/config.js +13 -59
  46. package/database/documentdb-encryption-service.js +29 -13
  47. package/database/documentdb-utils.js +21 -12
  48. package/database/encryption/README.md +53 -36
  49. package/database/encryption/encryption-schema-registry.js +29 -23
  50. package/database/encryption/field-encryption-service.js +15 -4
  51. package/database/encryption/logger.js +3 -2
  52. package/database/encryption/prisma-encryption-extension.js +5 -8
  53. package/database/prisma.js +22 -14
  54. package/database/repositories/health-check-repository-factory.js +7 -5
  55. package/database/repositories/health-check-repository-interface.js +15 -5
  56. package/database/repositories/health-check-repository-postgres.js +1 -1
  57. package/database/use-cases/check-database-health-use-case.js +3 -2
  58. package/database/use-cases/check-database-state-use-case.js +8 -7
  59. package/database/use-cases/get-database-state-via-worker-use-case.js +15 -12
  60. package/database/use-cases/get-migration-status-use-case.js +4 -2
  61. package/database/use-cases/run-database-migration-use-case.js +41 -9
  62. package/database/use-cases/test-encryption-use-case.js +4 -5
  63. package/database/use-cases/trigger-database-migration-use-case.js +9 -8
  64. package/database/utils/mongodb-collection-utils.js +11 -3
  65. package/database/utils/mongodb-schema-init.js +10 -4
  66. package/database/utils/prisma-runner.js +144 -74
  67. package/database/utils/prisma-schema-parser.js +2 -2
  68. package/docs/PROCESS_MANAGEMENT_QUEUE_SPEC.md +135 -80
  69. package/encrypt/Cryptor.js +54 -49
  70. package/encrypt/aes-encryption-key-provider.js +82 -0
  71. package/encrypt/encryption-key-provider-interface.js +50 -0
  72. package/errors/fetch-error.js +1 -1
  73. package/generated/prisma-mongodb/edge.js +59 -4
  74. package/generated/prisma-mongodb/index-browser.js +49 -1
  75. package/generated/prisma-mongodb/index.d.ts +4023 -61
  76. package/generated/prisma-mongodb/index.js +59 -4
  77. package/generated/prisma-mongodb/package.json +1 -1
  78. package/generated/prisma-mongodb/schema.prisma +87 -0
  79. package/generated/prisma-mongodb/wasm.js +59 -4
  80. package/generated/prisma-postgresql/edge.js +58 -4
  81. package/generated/prisma-postgresql/index-browser.js +48 -1
  82. package/generated/prisma-postgresql/index.d.ts +4738 -54
  83. package/generated/prisma-postgresql/index.js +58 -4
  84. package/generated/prisma-postgresql/package.json +1 -1
  85. package/generated/prisma-postgresql/schema.prisma +85 -0
  86. package/generated/prisma-postgresql/wasm.js +58 -4
  87. package/handlers/WEBHOOKS.md +104 -81
  88. package/handlers/app-definition-loader.js +53 -10
  89. package/handlers/app-handler-helpers.js +14 -3
  90. package/handlers/backend-utils.js +0 -1
  91. package/handlers/database-migration-handler.js +64 -45
  92. package/handlers/middleware/admin-auth.js +53 -0
  93. package/handlers/routers/HEALTHCHECK.md +170 -135
  94. package/handlers/routers/admin.js +474 -0
  95. package/handlers/routers/auth.js +51 -9
  96. package/handlers/routers/db-migration.handler.js +2 -3
  97. package/handlers/routers/db-migration.js +102 -66
  98. package/handlers/routers/docs.js +180 -0
  99. package/handlers/routers/health.js +84 -283
  100. package/handlers/routers/integration-defined-routers.js +44 -33
  101. package/handlers/routers/integration-webhook-routers.js +73 -52
  102. package/handlers/routers/middleware/loadUser.js +39 -0
  103. package/handlers/routers/middleware/requireAdmin.js +43 -0
  104. package/handlers/routers/middleware/requireLoggedInUser.js +19 -0
  105. package/handlers/routers/user.js +30 -14
  106. package/handlers/routers/websocket.js +18 -4
  107. package/handlers/use-cases/check-integrations-health-use-case.js +13 -6
  108. package/handlers/workers/db-migration.js +74 -35
  109. package/handlers/workers/integration-defined-workers.js +31 -20
  110. package/index.js +50 -16
  111. package/infrastructure/scheduler/index.js +15 -11
  112. package/infrastructure/scheduler/netlify-scheduler-adapter.js +185 -0
  113. package/infrastructure/scheduler/scheduler-service-factory.js +38 -31
  114. package/integrations/WEBHOOK-QUICKSTART.md +21 -14
  115. package/integrations/index.js +4 -0
  116. package/integrations/integration-base.js +9 -6
  117. package/integrations/integration-router.js +1828 -157
  118. package/integrations/options.js +47 -20
  119. package/integrations/repositories/integration-mapping-repository-factory.js +4 -8
  120. package/integrations/repositories/integration-repository-documentdb.js +53 -24
  121. package/integrations/repositories/integration-repository-factory.js +6 -6
  122. package/integrations/repositories/integration-repository-interface.js +27 -9
  123. package/integrations/repositories/integration-repository-postgres.js +3 -3
  124. package/integrations/repositories/process-repository-factory.js +4 -7
  125. package/integrations/repositories/process-repository-interface.js +9 -5
  126. package/integrations/repositories/process-repository-mongo.js +7 -3
  127. package/integrations/repositories/process-repository-postgres.js +2 -2
  128. package/integrations/tests/doubles/config-capturing-integration.js +9 -8
  129. package/integrations/tests/doubles/dummy-integration-class.js +81 -8
  130. package/integrations/tests/doubles/test-integration-repository.js +51 -11
  131. package/integrations/use-cases/create-integration.js +36 -32
  132. package/integrations/use-cases/create-process.js +20 -9
  133. package/integrations/use-cases/delete-integration-for-user.js +5 -4
  134. package/integrations/use-cases/execute-proxy-request.js +564 -0
  135. package/integrations/use-cases/get-integration-for-user.js +25 -10
  136. package/integrations/use-cases/get-integration-instance-by-definition.js +13 -7
  137. package/integrations/use-cases/get-integrations-for-user.js +4 -1
  138. package/integrations/use-cases/get-possible-integrations.js +1 -1
  139. package/integrations/use-cases/get-process.js +5 -6
  140. package/integrations/use-cases/index.js +3 -1
  141. package/integrations/use-cases/load-integration-context.js +1 -5
  142. package/integrations/use-cases/update-process-state.js +10 -8
  143. package/integrations/utils/map-integration-dto.js +9 -3
  144. package/jest-setup.js +1 -1
  145. package/jest-teardown.js +1 -1
  146. package/lambda/TimeoutCatcher.js +32 -30
  147. package/lambda/index.js +1 -1
  148. package/logs/index.js +3 -3
  149. package/modules/domain/entities/AuthorizationSession.js +131 -0
  150. package/modules/domain/entities/index.js +10 -0
  151. package/modules/module.js +4 -9
  152. package/modules/repositories/authorization-session-repository-factory.js +53 -0
  153. package/modules/repositories/authorization-session-repository-interface.js +75 -0
  154. package/modules/repositories/authorization-session-repository-mongo.js +169 -0
  155. package/modules/repositories/authorization-session-repository-postgres.js +170 -0
  156. package/modules/repositories/module-repository-documentdb.js +89 -41
  157. package/modules/repositories/module-repository-factory.js +4 -6
  158. package/modules/repositories/module-repository-interface.js +13 -0
  159. package/modules/repositories/module-repository-mongo.js +40 -5
  160. package/modules/repositories/module-repository-postgres.js +42 -5
  161. package/modules/repositories/module-repository.js +28 -0
  162. package/modules/requester/api-key.js +0 -2
  163. package/modules/requester/basic.js +0 -1
  164. package/modules/requester/oauth-2.js +3 -14
  165. package/modules/test/mock-api/definition.js +15 -10
  166. package/modules/test/mock-api/mocks/hubspot.js +36 -36
  167. package/modules/tests/doubles/test-module-factory.js +10 -4
  168. package/modules/tests/doubles/test-module-repository.js +1 -1
  169. package/modules/use-cases/delete-module-entity.js +23 -0
  170. package/modules/use-cases/get-authorization-requirements.js +110 -0
  171. package/modules/use-cases/get-entities-for-user.js +1 -1
  172. package/modules/use-cases/get-entity-options-by-id.js +8 -6
  173. package/modules/use-cases/get-module-entity-by-id.js +16 -0
  174. package/modules/use-cases/get-module.js +12 -9
  175. package/modules/use-cases/process-authorization-callback.js +12 -6
  176. package/modules/use-cases/process-authorization-step.js +160 -0
  177. package/modules/use-cases/refresh-entity-options.js +8 -6
  178. package/modules/use-cases/start-authorization-session.js +86 -0
  179. package/modules/use-cases/test-module-auth.js +8 -6
  180. package/modules/use-cases/update-module-entity.js +27 -0
  181. package/modules/utils/map-module-dto.js +2 -2
  182. package/openapi/openapi-spec-generator.js +370 -0
  183. package/openapi/openapi-v1.yaml +1360 -0
  184. package/openapi/openapi-v2.yaml +1535 -0
  185. package/package.json +7 -9
  186. package/prisma-mongodb/schema.prisma +87 -0
  187. package/prisma-postgresql/migrations/20260222000000_add_admin_process_and_script_schedule/migration.sql +55 -0
  188. package/prisma-postgresql/schema.prisma +85 -0
  189. package/providers/index.js +13 -0
  190. package/providers/resolve-provider.js +93 -0
  191. package/queues/index.js +19 -0
  192. package/queues/providers/index.js +7 -0
  193. package/queues/providers/netlify-background-provider.js +76 -0
  194. package/queues/providers/qstash-queue-provider.js +122 -0
  195. package/queues/queue-client-interface.js +55 -0
  196. package/queues/queue-provider-factory.js +93 -0
  197. package/queues/queue-provider.js +46 -0
  198. package/queues/queuer-util.js +33 -25
  199. package/syncs/repositories/sync-repository-documentdb.js +39 -19
  200. package/syncs/repositories/sync-repository-factory.js +4 -4
  201. package/syncs/repositories/sync-repository-interface.js +6 -2
  202. package/syncs/repositories/sync-repository-postgres.js +19 -13
  203. package/syncs/sync.js +104 -100
  204. package/token/repositories/token-repository-factory.js +4 -6
  205. package/types/assertions/index.d.ts +72 -72
  206. package/types/core/index.d.ts +98 -53
  207. package/types/errors/index.d.ts +61 -54
  208. package/types/eslint-config/index.d.ts +40 -40
  209. package/types/lambda/index.d.ts +26 -26
  210. package/types/logs/index.d.ts +4 -4
  211. package/types/prettier-config/index.d.ts +5 -5
  212. package/types/test-environment/index.d.ts +14 -14
  213. package/types/tsconfig.json +95 -93
  214. package/user/repositories/user-repository-factory.js +4 -4
  215. package/user/tests/doubles/test-user-repository.js +5 -2
  216. package/user/use-cases/authenticate-user.js +2 -4
  217. package/user/use-cases/authenticate-with-shared-secret.js +1 -1
  218. package/user/use-cases/create-individual-user.js +8 -7
  219. package/user/use-cases/create-organization-user.js +1 -1
  220. package/user/use-cases/create-token-for-user-id.js +1 -1
  221. package/user/use-cases/delete-user.js +47 -0
  222. package/user/use-cases/get-user-from-adopter-jwt.js +0 -2
  223. package/user/use-cases/get-user-from-bearer-token.js +7 -3
  224. package/user/use-cases/get-user-from-x-frigg-headers.js +14 -11
  225. package/user/use-cases/login-user.js +8 -8
  226. package/user/user.js +18 -4
  227. package/utils/backend-path.js +2 -2
  228. package/utils/index.js +4 -1
  229. package/websocket/repositories/websocket-connection-repository-documentdb.js +46 -21
  230. package/websocket/repositories/websocket-connection-repository-factory.js +4 -8
  231. package/websocket/repositories/websocket-connection-repository-mongo.js +26 -16
  232. package/websocket/repositories/websocket-connection-repository-postgres.js +26 -15
  233. package/websocket/repositories/websocket-connection-repository.js +26 -20
  234. package/websocket/websocket-message-sender-interface.js +38 -0
  235. package/database/adapters/lambda-invoker.js +0 -97
  236. package/database/repositories/migration-status-repository-s3.js +0 -137
  237. package/infrastructure/scheduler/eventbridge-scheduler-adapter.js +0 -184
@@ -29,43 +29,58 @@ const {
29
29
  } = require('../use-cases/check-integrations-health-use-case');
30
30
 
31
31
  const router = Router();
32
- const healthCheckRepository = createHealthCheckRepository({ prismaClient: prisma });
33
32
 
34
- // Load integrations and create factories just like auth router does
35
- // This verifies the system can properly load integrations
36
- let moduleFactory, integrationClasses;
37
- try {
38
- const appDef = loadAppDefinition();
39
- integrationClasses = appDef.integrations || [];
33
+ // Lazy-initialized use cases (deferred until first request)
34
+ let _checkDatabaseHealthUseCase,
35
+ _checkEncryptionHealthUseCase,
36
+ _checkExternalApisHealthUseCase,
37
+ _checkIntegrationsHealthUseCase;
40
38
 
41
- const moduleRepository = createModuleRepository();
42
- const moduleDefinitions = getModulesDefinitionFromIntegrationClasses(integrationClasses);
39
+ function ensureInitialized() {
40
+ if (!_checkDatabaseHealthUseCase) {
41
+ const healthCheckRepository = createHealthCheckRepository({
42
+ prismaClient: prisma,
43
+ });
43
44
 
44
- moduleFactory = new ModuleFactory({
45
- moduleRepository,
46
- moduleDefinitions,
47
- });
48
- } catch (error) {
49
- console.error('Failed to load integrations for health check:', error.message);
50
- // Factories will be undefined, health check will report unhealthy
51
- moduleFactory = undefined;
52
- integrationClasses = [];
53
- }
45
+ let moduleFactory, integrationClasses;
46
+ try {
47
+ const appDef = loadAppDefinition();
48
+ integrationClasses = appDef.integrations || [];
54
49
 
55
- const testEncryptionUseCase = new TestEncryptionUseCase({
56
- healthCheckRepository,
57
- });
58
- const checkDatabaseHealthUseCase = new CheckDatabaseHealthUseCase({
59
- healthCheckRepository,
60
- });
61
- const checkEncryptionHealthUseCase = new CheckEncryptionHealthUseCase({
62
- testEncryptionUseCase,
63
- });
64
- const checkExternalApisHealthUseCase = new CheckExternalApisHealthUseCase();
65
- const checkIntegrationsHealthUseCase = new CheckIntegrationsHealthUseCase({
66
- moduleFactory,
67
- integrationClasses,
68
- });
50
+ const moduleRepository = createModuleRepository();
51
+ const moduleDefinitions =
52
+ getModulesDefinitionFromIntegrationClasses(integrationClasses);
53
+
54
+ moduleFactory = new ModuleFactory({
55
+ moduleRepository,
56
+ moduleDefinitions,
57
+ });
58
+ } catch (error) {
59
+ console.error(
60
+ 'Failed to load integrations for health check:',
61
+ error.message
62
+ );
63
+ moduleFactory = undefined;
64
+ integrationClasses = [];
65
+ }
66
+
67
+ const testEncryptionUseCase = new TestEncryptionUseCase({
68
+ healthCheckRepository,
69
+ });
70
+ _checkDatabaseHealthUseCase = new CheckDatabaseHealthUseCase({
71
+ healthCheckRepository,
72
+ });
73
+ _checkEncryptionHealthUseCase = new CheckEncryptionHealthUseCase({
74
+ testEncryptionUseCase,
75
+ });
76
+ _checkExternalApisHealthUseCase =
77
+ new CheckExternalApisHealthUseCase();
78
+ _checkIntegrationsHealthUseCase = new CheckIntegrationsHealthUseCase({
79
+ moduleFactory,
80
+ integrationClasses,
81
+ });
82
+ }
83
+ }
69
84
 
70
85
  const validateApiKey = (req, res, next) => {
71
86
  const apiKey = req.headers['x-frigg-health-api-key'];
@@ -87,256 +102,32 @@ const validateApiKey = (req, res, next) => {
87
102
 
88
103
  router.use(validateApiKey);
89
104
 
90
- // Helper to detect VPC configuration
91
- const detectVpcConfiguration = async () => {
92
- const results = {
93
- isInVpc: false,
94
- hasInternetAccess: false,
95
- canResolvePublicDns: false,
96
- canConnectToAws: false,
97
- vpcEndpoints: [],
98
- };
105
+ // Provider-specific health checks are loaded via resolveProvider()
106
+ // so core doesn't hardcode any provider package names.
107
+ const { resolveProvider } = require('../../providers/resolve-provider');
99
108
 
109
+ function getResolvedProvider() {
100
110
  try {
101
- // Check if we're in a VPC by looking for VPC-specific environment
102
- // Lambda in VPC has specific network interface configuration
103
- const dns = require('dns').promises;
104
-
105
- // Test 1: Can we resolve public DNS? (indicates DNS configuration)
106
- try {
107
- await Promise.race([
108
- dns.resolve4('www.google.com'),
109
- new Promise((_, reject) =>
110
- setTimeout(() => reject(new Error('timeout')), 2000)
111
- ),
112
- ]);
113
- results.canResolvePublicDns = true;
114
- } catch (e) {
115
- console.log('Public DNS resolution failed:', e.message);
116
- }
117
-
118
- // Test 2: Can we reach internet? (indicates NAT gateway)
119
- try {
120
- const https = require('https');
121
- await new Promise((resolve, reject) => {
122
- const req = https.get(
123
- 'https://www.google.com',
124
- { timeout: 2000 },
125
- (res) => {
126
- res.destroy();
127
- resolve(true);
128
- }
129
- );
130
- req.on('error', reject);
131
- req.on('timeout', () => {
132
- req.destroy();
133
- reject(new Error('timeout'));
134
- });
135
- });
136
- results.hasInternetAccess = true;
137
- } catch (e) {
138
- console.log('Internet connectivity test failed:', e.message);
139
- }
140
-
141
- // Test 3: Check for VPC endpoints by trying to resolve internal AWS endpoints
142
- const region = process.env.AWS_REGION; // Lambda always provides this
143
- const vpcEndpointDomains = [
144
- `com.amazonaws.${region}.kms`,
145
- `com.amazonaws.vpce.${region}`,
146
- `kms.${region}.amazonaws.com`,
147
- ];
148
-
149
- for (const domain of vpcEndpointDomains) {
150
- try {
151
- const addresses = await Promise.race([
152
- dns.resolve4(domain).catch(() => dns.resolve6(domain)),
153
- new Promise((_, reject) =>
154
- setTimeout(() => reject(new Error('timeout')), 1000)
155
- ),
156
- ]);
157
- if (addresses && addresses.length > 0) {
158
- // Check if it's a private IP (VPC endpoint indicator)
159
- const isPrivateIp = addresses.some(
160
- (ip) =>
161
- ip.startsWith('10.') ||
162
- ip.startsWith('172.') ||
163
- ip.startsWith('192.168.')
164
- );
165
- if (isPrivateIp) {
166
- results.vpcEndpoints.push(domain);
167
- }
168
- }
169
- } catch (e) {
170
- // Expected for non-existent endpoints
171
- }
172
- }
173
-
174
- // Check if Lambda is in VPC using VPC_ENABLED env var set by infrastructure
175
- results.isInVpc = process.env.VPC_ENABLED === 'true' ||
176
- (!results.hasInternetAccess && results.canResolvePublicDns) ||
177
- results.vpcEndpoints.length > 0;
178
-
179
- results.canConnectToAws =
180
- results.hasInternetAccess || results.vpcEndpoints.length > 0;
181
- } catch (error) {
182
- console.error('VPC detection error:', error.message);
111
+ return resolveProvider();
112
+ } catch {
113
+ return null;
183
114
  }
115
+ }
184
116
 
185
- return results;
186
- };
187
-
188
- // KMS decrypt capability check
189
117
  const checkKmsDecryptCapability = async () => {
190
- const start = Date.now();
191
- const { KMS_KEY_ARN } = process.env;
192
- if (!KMS_KEY_ARN) {
193
- return {
194
- status: 'skipped',
195
- reason: 'KMS_KEY_ARN not configured',
196
- };
118
+ const provider = getResolvedProvider();
119
+ if (!provider?.checkKmsDecryptCapability) {
120
+ return { status: 'skipped', reason: 'Provider does not support KMS health checks' };
197
121
  }
122
+ return provider.checkKmsDecryptCapability();
123
+ };
198
124
 
199
- // Log environment for debugging
200
- console.log('KMS Check Debug:', {
201
- hasKmsKeyArn: !!KMS_KEY_ARN,
202
- kmsKeyArnPrefix: KMS_KEY_ARN?.substring(0, 30),
203
- awsRegion: process.env.AWS_REGION,
204
- hasDiscoveryKey: !!process.env.AWS_DISCOVERY_KMS_KEY_ID,
205
- });
206
-
207
- // First, detect VPC configuration
208
- const vpcConfig = await detectVpcConfiguration();
209
- console.log('VPC Configuration:', vpcConfig);
210
-
211
- // Test DNS resolution for KMS endpoint
212
- try {
213
- const dns = require('dns').promises;
214
- const region = process.env.AWS_REGION; // Lambda always provides this
215
- const kmsEndpoint = `kms.${region}.amazonaws.com`;
216
- console.log('Testing DNS resolution for:', kmsEndpoint);
217
-
218
- // Wrap DNS resolution in a timeout
219
- const dnsPromise = dns.resolve4(kmsEndpoint);
220
- const timeoutPromise = new Promise((_, reject) =>
221
- setTimeout(() => reject(new Error('DNS resolution timeout')), 3000)
222
- );
223
-
224
- const addresses = await Promise.race([dnsPromise, timeoutPromise]);
225
- console.log('KMS endpoint resolved to:', addresses);
226
-
227
- // Check if resolved to private IP (VPC endpoint)
228
- const isVpcEndpoint = addresses.some(
229
- (ip) =>
230
- ip.startsWith('10.') ||
231
- ip.startsWith('172.') ||
232
- ip.startsWith('192.168.')
233
- );
234
-
235
- if (isVpcEndpoint) {
236
- console.log(
237
- 'KMS VPC Endpoint detected - using private connectivity'
238
- );
239
- }
240
-
241
- // Test TCP connectivity to KMS (port 443)
242
- const net = require('net');
243
- const testConnection = () =>
244
- new Promise((resolve) => {
245
- const socket = new net.Socket();
246
- const connectionTimeout = setTimeout(() => {
247
- socket.destroy();
248
- resolve({ connected: false, error: 'Connection timeout' });
249
- }, 3000);
250
-
251
- socket.on('connect', () => {
252
- clearTimeout(connectionTimeout);
253
- socket.destroy();
254
- resolve({ connected: true });
255
- });
256
-
257
- socket.on('error', (err) => {
258
- clearTimeout(connectionTimeout);
259
- resolve({ connected: false, error: err.message });
260
- });
261
-
262
- // Try connecting to first resolved address on HTTPS port
263
- socket.connect(443, addresses[0]);
264
- });
265
-
266
- const connResult = await testConnection();
267
- console.log('TCP connectivity test:', connResult);
268
-
269
- if (!connResult.connected) {
270
- return {
271
- status: 'unhealthy',
272
- error: `Cannot connect to KMS endpoint: ${connResult.error}`,
273
- dnsResolved: true,
274
- tcpConnection: false,
275
- vpcConfig,
276
- latencyMs: Date.now() - start,
277
- };
278
- }
279
- } catch (dnsError) {
280
- console.error('DNS resolution failed:', dnsError.message);
281
- return {
282
- status: 'unhealthy',
283
- error: `Cannot resolve KMS endpoint: ${dnsError.message}`,
284
- dnsResolved: false,
285
- vpcConfig,
286
- latencyMs: Date.now() - start,
287
- };
288
- }
289
-
290
- try {
291
- // Use AWS SDK v3 for consistency with the rest of the codebase
292
- // eslint-disable-next-line global-require
293
- const {
294
- KMSClient,
295
- GenerateDataKeyCommand,
296
- DecryptCommand,
297
- } = require('@aws-sdk/client-kms');
298
-
299
- // Lambda always provides AWS_REGION
300
- const region = process.env.AWS_REGION;
301
-
302
- const kms = new KMSClient({
303
- region,
304
- requestHandler: {
305
- connectionTimeout: 10000, // 10 second connection timeout
306
- requestTimeout: 25000, // 25 second timeout for slow VPC connections
307
- },
308
- maxAttempts: 1, // No retries on health checks
309
- });
310
-
311
- // Generate a data key (without plaintext logging) then immediately decrypt ciphertext to ensure decrypt perms.
312
- const dataKeyResp = await kms.send(
313
- new GenerateDataKeyCommand({
314
- KeyId: KMS_KEY_ARN,
315
- KeySpec: 'AES_256',
316
- })
317
- );
318
- const decryptResp = await kms.send(
319
- new DecryptCommand({ CiphertextBlob: dataKeyResp.CiphertextBlob })
320
- );
321
-
322
- const success = Boolean(
323
- dataKeyResp.CiphertextBlob && decryptResp.Plaintext
324
- );
325
-
326
- return {
327
- status: success ? 'healthy' : 'unhealthy',
328
- kmsKeyArnSuffix: KMS_KEY_ARN.slice(-12),
329
- vpcConfig,
330
- latencyMs: Date.now() - start,
331
- };
332
- } catch (error) {
333
- return {
334
- status: 'unhealthy',
335
- error: error.message,
336
- vpcConfig,
337
- latencyMs: Date.now() - start,
338
- };
125
+ const detectVpcConfiguration = async () => {
126
+ const provider = getResolvedProvider();
127
+ if (!provider?.detectVpcConfiguration) {
128
+ return { status: 'skipped', reason: 'Provider does not support VPC detection' };
339
129
  }
130
+ return provider.detectVpcConfiguration();
340
131
  };
341
132
 
342
133
  router.get('/health', async (_req, res) => {
@@ -350,6 +141,7 @@ router.get('/health', async (_req, res) => {
350
141
  });
351
142
 
352
143
  router.get('/health/detailed', async (_req, res) => {
144
+ ensureInitialized();
353
145
  console.log('Starting detailed health check');
354
146
  const startTime = Date.now();
355
147
 
@@ -415,7 +207,7 @@ router.get('/health/detailed', async (_req, res) => {
415
207
  }
416
208
 
417
209
  try {
418
- response.checks.database = await checkDatabaseHealthUseCase.execute();
210
+ response.checks.database = await _checkDatabaseHealthUseCase.execute();
419
211
  if (response.checks.database.status === 'unhealthy') {
420
212
  response.status = 'unhealthy';
421
213
  }
@@ -430,7 +222,8 @@ router.get('/health/detailed', async (_req, res) => {
430
222
  }
431
223
 
432
224
  try {
433
- response.checks.encryption = await checkEncryptionHealthUseCase.execute();
225
+ response.checks.encryption =
226
+ await _checkEncryptionHealthUseCase.execute();
434
227
  if (response.checks.encryption.status === 'unhealthy') {
435
228
  response.status = 'unhealthy';
436
229
  }
@@ -445,12 +238,16 @@ router.get('/health/detailed', async (_req, res) => {
445
238
  }
446
239
 
447
240
  try {
448
- const { apiStatuses, allReachable } = await checkExternalApisHealthUseCase.execute();
241
+ const { apiStatuses, allReachable } =
242
+ await _checkExternalApisHealthUseCase.execute();
449
243
  response.checks.externalApis = apiStatuses;
450
244
  if (!allReachable) {
451
245
  response.status = 'unhealthy';
452
246
  }
453
- console.log('External APIs check completed:', response.checks.externalApis);
247
+ console.log(
248
+ 'External APIs check completed:',
249
+ response.checks.externalApis
250
+ );
454
251
  } catch (error) {
455
252
  response.checks.externalApis = {
456
253
  status: 'unhealthy',
@@ -461,8 +258,11 @@ router.get('/health/detailed', async (_req, res) => {
461
258
  }
462
259
 
463
260
  try {
464
- response.checks.integrations = checkIntegrationsHealthUseCase.execute();
465
- console.log('Integrations check completed:', response.checks.integrations);
261
+ response.checks.integrations = _checkIntegrationsHealthUseCase.execute();
262
+ console.log(
263
+ 'Integrations check completed:',
264
+ response.checks.integrations
265
+ );
466
266
  } catch (error) {
467
267
  response.checks.integrations = {
468
268
  status: 'unhealthy',
@@ -493,10 +293,11 @@ router.get('/health/live', (_req, res) => {
493
293
  });
494
294
 
495
295
  router.get('/health/ready', async (_req, res) => {
496
- const dbHealth = await checkDatabaseHealthUseCase.execute();
296
+ ensureInitialized();
297
+ const dbHealth = await _checkDatabaseHealthUseCase.execute();
497
298
  const isDbReady = dbHealth.status === 'healthy';
498
299
 
499
- const integrationsHealth = checkIntegrationsHealthUseCase.execute();
300
+ const integrationsHealth = _checkIntegrationsHealthUseCase.execute();
500
301
  const areModulesReady = integrationsHealth.modules.count > 0;
501
302
 
502
303
  const isReady = isDbReady && areModulesReady;
@@ -1,45 +1,56 @@
1
1
  const { createAppHandler } = require('./../app-handler-helpers');
2
- const {
3
- loadAppDefinition,
4
- } = require('../app-definition-loader');
2
+ const { loadAppDefinition } = require('../app-definition-loader');
5
3
  const { Router } = require('express');
6
4
  const { loadRouterFromObject } = require('../backend-utils');
7
5
 
8
- const handlers = {};
9
- const { integrations: integrationClasses } = loadAppDefinition();
6
+ let _handlers;
10
7
 
11
- //todo: this should be in a use case class
12
- for (const IntegrationClass of integrationClasses) {
13
- const router = Router();
14
- const basePath = `/api/${IntegrationClass.Definition.name}-integration`;
8
+ function ensureHandlers() {
9
+ if (!_handlers) {
10
+ _handlers = {};
11
+ const { integrations: integrationClasses } = loadAppDefinition();
15
12
 
16
- console.log(`\n│ Configuring routes for ${IntegrationClass.Definition.name} Integration:`);
13
+ //todo: this should be in a use case class
14
+ for (const IntegrationClass of integrationClasses) {
15
+ const router = Router();
16
+ const basePath = `/api/${IntegrationClass.Definition.name}-integration`;
17
17
 
18
- for (const routeDef of IntegrationClass.Definition.routes) {
19
- if (typeof routeDef === 'function') {
20
- router.use(basePath, routeDef(IntegrationClass));
21
- console.log(`│ ANY ${basePath}/* (function handler)`);
22
- } else if (typeof routeDef === 'object') {
23
- router.use(
24
- basePath,
25
- loadRouterFromObject(IntegrationClass, routeDef)
18
+ console.log(
19
+ `\n│ Configuring routes for ${IntegrationClass.Definition.name} Integration:`
26
20
  );
27
- const method = (routeDef.method || 'ANY').toUpperCase();
28
- const fullPath = `${basePath}${routeDef.path}`;
29
- console.log(`│ ${method} ${fullPath}`);
30
- } else if (routeDef instanceof express.Router) {
31
- router.use(basePath, routeDef);
32
- console.log(`│ ANY ${basePath}/* (express router)`);
21
+
22
+ for (const routeDef of IntegrationClass.Definition.routes) {
23
+ if (typeof routeDef === 'function') {
24
+ router.use(basePath, routeDef(IntegrationClass));
25
+ console.log(`│ ANY ${basePath}/* (function handler)`);
26
+ } else if (typeof routeDef === 'object') {
27
+ router.use(
28
+ basePath,
29
+ loadRouterFromObject(IntegrationClass, routeDef)
30
+ );
31
+ const method = (routeDef.method || 'ANY').toUpperCase();
32
+ const fullPath = `${basePath}${routeDef.path}`;
33
+ console.log(`│ ${method} ${fullPath}`);
34
+ } else if (routeDef instanceof express.Router) {
35
+ router.use(basePath, routeDef);
36
+ console.log(`│ ANY ${basePath}/* (express router)`);
37
+ }
38
+ }
39
+ console.log('│');
40
+
41
+ _handlers[`${IntegrationClass.Definition.name}`] = {
42
+ handler: createAppHandler(
43
+ `HTTP Event: ${IntegrationClass.Definition.name}`,
44
+ router
45
+ ),
46
+ };
33
47
  }
34
48
  }
35
- console.log('│');
36
-
37
- handlers[`${IntegrationClass.Definition.name}`] = {
38
- handler: createAppHandler(
39
- `HTTP Event: ${IntegrationClass.Definition.name}`,
40
- router
41
- ),
42
- };
49
+ return _handlers;
43
50
  }
44
51
 
45
- module.exports = { handlers };
52
+ module.exports = {
53
+ get handlers() {
54
+ return ensureHandlers();
55
+ },
56
+ };
@@ -1,67 +1,88 @@
1
1
  const { createAppHandler } = require('./../app-handler-helpers');
2
2
  const { loadAppDefinition } = require('../app-definition-loader');
3
3
  const { Router } = require('express');
4
- const { IntegrationEventDispatcher } = require('../integration-event-dispatcher');
4
+ const {
5
+ IntegrationEventDispatcher,
6
+ } = require('../integration-event-dispatcher');
5
7
 
6
- const handlers = {};
7
- const { integrations: integrationClasses } = loadAppDefinition();
8
+ let _handlers;
8
9
 
9
- for (const IntegrationClass of integrationClasses) {
10
- const webhookConfig = IntegrationClass.Definition.webhooks;
10
+ function ensureHandlers() {
11
+ if (!_handlers) {
12
+ _handlers = {};
13
+ const { integrations: integrationClasses } = loadAppDefinition();
11
14
 
12
- // Skip if webhooks not enabled
13
- if (!webhookConfig || (typeof webhookConfig === 'object' && !webhookConfig.enabled)) {
14
- continue;
15
- }
15
+ for (const IntegrationClass of integrationClasses) {
16
+ const webhookConfig = IntegrationClass.Definition.webhooks;
17
+
18
+ // Skip if webhooks not enabled
19
+ if (
20
+ !webhookConfig ||
21
+ (typeof webhookConfig === 'object' && !webhookConfig.enabled)
22
+ ) {
23
+ continue;
24
+ }
16
25
 
17
- const router = Router();
18
- const basePath = `/api/${IntegrationClass.Definition.name}-integration/webhooks`;
26
+ const router = Router();
27
+ const basePath = `/api/${IntegrationClass.Definition.name}-integration/webhooks`;
19
28
 
20
- console.log(`\n│ Configuring webhook routes for ${IntegrationClass.Definition.name}:`);
29
+ console.log(
30
+ `\n│ Configuring webhook routes for ${IntegrationClass.Definition.name}:`
31
+ );
21
32
 
22
- // General webhook route (no integration ID)
23
- router.post(basePath, async (req, res, next) => {
24
- try {
25
- const integrationInstance = new IntegrationClass();
26
- const dispatcher = new IntegrationEventDispatcher(integrationInstance);
27
- await dispatcher.dispatchHttp({
28
- event: 'WEBHOOK_RECEIVED',
29
- req,
30
- res,
31
- next,
33
+ // General webhook route (no integration ID)
34
+ router.post(basePath, async (req, res, next) => {
35
+ try {
36
+ const integrationInstance = new IntegrationClass();
37
+ const dispatcher = new IntegrationEventDispatcher(
38
+ integrationInstance
39
+ );
40
+ await dispatcher.dispatchHttp({
41
+ event: 'WEBHOOK_RECEIVED',
42
+ req,
43
+ res,
44
+ next,
45
+ });
46
+ } catch (error) {
47
+ next(error);
48
+ }
32
49
  });
33
- } catch (error) {
34
- next(error);
35
- }
36
- });
37
- console.log(`│ POST ${basePath}`);
50
+ console.log(`│ POST ${basePath}`);
38
51
 
39
- // Integration-specific webhook route (with integration ID)
40
- router.post(`${basePath}/:integrationId`, async (req, res, next) => {
41
- try {
42
- const integrationInstance = new IntegrationClass();
43
- const dispatcher = new IntegrationEventDispatcher(integrationInstance);
44
- await dispatcher.dispatchHttp({
45
- event: 'WEBHOOK_RECEIVED',
46
- req,
47
- res,
48
- next,
52
+ // Integration-specific webhook route (with integration ID)
53
+ router.post(`${basePath}/:integrationId`, async (req, res, next) => {
54
+ try {
55
+ const integrationInstance = new IntegrationClass();
56
+ const dispatcher = new IntegrationEventDispatcher(
57
+ integrationInstance
58
+ );
59
+ await dispatcher.dispatchHttp({
60
+ event: 'WEBHOOK_RECEIVED',
61
+ req,
62
+ res,
63
+ next,
64
+ });
65
+ } catch (error) {
66
+ next(error);
67
+ }
49
68
  });
50
- } catch (error) {
51
- next(error);
52
- }
53
- });
54
- console.log(`│ POST ${basePath}/:integrationId`);
55
- console.log('│');
69
+ console.log(`│ POST ${basePath}/:integrationId`);
70
+ console.log('│');
56
71
 
57
- handlers[`${IntegrationClass.Definition.name}Webhook`] = {
58
- handler: createAppHandler(
59
- `HTTP Event: ${IntegrationClass.Definition.name} Webhook`,
60
- router,
61
- false // shouldUseDatabase = false
62
- ),
63
- };
72
+ _handlers[`${IntegrationClass.Definition.name}Webhook`] = {
73
+ handler: createAppHandler(
74
+ `HTTP Event: ${IntegrationClass.Definition.name} Webhook`,
75
+ router,
76
+ false // shouldUseDatabase = false
77
+ ),
78
+ };
79
+ }
80
+ }
81
+ return _handlers;
64
82
  }
65
83
 
66
- module.exports = { handlers };
67
-
84
+ module.exports = {
85
+ get handlers() {
86
+ return ensureHandlers();
87
+ },
88
+ };