insforge 1.3.0 → 1.4.8

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 (269) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/auth/package.json +5 -3
  3. package/auth/src/lib/broadcastService.ts +115 -117
  4. package/auth/src/lib/insforge.ts +8 -0
  5. package/auth/src/main.tsx +2 -4
  6. package/auth/src/pages/SignInPage.tsx +60 -60
  7. package/auth/src/pages/SignUpPage.tsx +60 -60
  8. package/auth/src/pages/VerifyEmailPage.tsx +18 -0
  9. package/auth/tsconfig.json +2 -1
  10. package/backend/package.json +10 -6
  11. package/backend/src/api/middlewares/rate-limiters.ts +127 -127
  12. package/backend/src/api/routes/ai/index.routes.ts +475 -468
  13. package/backend/src/api/routes/auth/index.routes.ts +85 -32
  14. package/backend/src/api/routes/auth/oauth.routes.ts +11 -6
  15. package/backend/src/api/routes/database/index.routes.ts +2 -0
  16. package/backend/src/api/routes/database/records.routes.ts +39 -175
  17. package/backend/src/api/routes/database/rpc.routes.ts +69 -0
  18. package/backend/src/api/routes/deployments/index.routes.ts +192 -0
  19. package/backend/src/api/routes/docs/index.routes.ts +3 -2
  20. package/backend/src/api/routes/email/index.routes.ts +35 -35
  21. package/backend/src/api/routes/functions/index.routes.ts +3 -3
  22. package/backend/src/api/routes/metadata/index.routes.ts +26 -0
  23. package/backend/src/api/routes/webhooks/index.routes.ts +109 -0
  24. package/backend/src/infra/database/database.manager.ts +0 -10
  25. package/backend/src/infra/database/migrations/018_schema-rework.sql +441 -0
  26. package/backend/src/infra/database/migrations/019_create-deployments-table.sql +36 -0
  27. package/backend/src/infra/database/migrations/020_add-audio-modality.sql +11 -0
  28. package/backend/src/infra/database/migrations/bootstrap/bootstrap-migrations.js +103 -0
  29. package/backend/src/infra/security/token.manager.ts +1 -4
  30. package/backend/src/providers/ai/openrouter.provider.ts +12 -3
  31. package/backend/src/providers/database/base.provider.ts +39 -0
  32. package/backend/src/providers/database/cloud.provider.ts +159 -0
  33. package/backend/src/providers/deployments/vercel.provider.ts +516 -0
  34. package/backend/src/server.ts +19 -7
  35. package/backend/src/services/ai/ai-config.service.ts +6 -6
  36. package/backend/src/services/ai/ai-model.service.ts +60 -60
  37. package/backend/src/services/ai/ai-usage.service.ts +7 -7
  38. package/backend/src/services/ai/chat-completion.service.ts +415 -220
  39. package/backend/src/services/ai/helpers.ts +64 -64
  40. package/backend/src/services/ai/index.ts +13 -13
  41. package/backend/src/services/auth/auth-config.service.ts +4 -4
  42. package/backend/src/services/auth/auth-otp.service.ts +6 -6
  43. package/backend/src/services/auth/auth.service.ts +134 -74
  44. package/backend/src/services/auth/index.ts +4 -4
  45. package/backend/src/services/auth/oauth-config.service.ts +12 -12
  46. package/backend/src/services/database/database-advance.service.ts +19 -55
  47. package/backend/src/services/database/database-table.service.ts +38 -85
  48. package/backend/src/services/database/postgrest-proxy.service.ts +165 -0
  49. package/backend/src/services/deployments/deployment.service.ts +693 -0
  50. package/backend/src/services/functions/function.service.ts +61 -41
  51. package/backend/src/services/logs/audit.service.ts +10 -10
  52. package/backend/src/services/secrets/secret.service.ts +101 -27
  53. package/backend/src/services/storage/storage.service.ts +30 -30
  54. package/backend/src/services/usage/usage.service.ts +6 -6
  55. package/backend/src/types/ai.ts +8 -0
  56. package/backend/src/types/auth.ts +5 -1
  57. package/backend/src/types/database.ts +2 -0
  58. package/backend/src/types/deployments.ts +33 -0
  59. package/backend/src/types/storage.ts +1 -1
  60. package/backend/src/types/webhooks.ts +45 -0
  61. package/backend/src/utils/cookies.ts +34 -35
  62. package/backend/src/utils/environment.ts +0 -14
  63. package/backend/src/utils/s3-config-loader.ts +64 -64
  64. package/backend/src/utils/seed.ts +334 -301
  65. package/backend/src/utils/sql-parser.ts +126 -0
  66. package/backend/src/utils/utils.ts +114 -114
  67. package/backend/src/utils/validations.ts +10 -10
  68. package/backend/tests/local/test-rpc.sh +141 -0
  69. package/backend/tests/local/test-secrets.sh +1 -1
  70. package/backend/tests/manual/test-ai-model-plugins.sh +258 -0
  71. package/backend/tests/manual/test-rawsql-modes.sh +24 -24
  72. package/backend/tests/unit/database-advance.test.ts +326 -0
  73. package/backend/tests/unit/helpers.test.ts +2 -2
  74. package/claude-plugin/skills/insforge-schema-patterns/SKILL.md +13 -10
  75. package/docker-compose.prod.yml +1 -1
  76. package/docker-compose.yml +1 -1
  77. package/docs/agent-docs/deployment.md +79 -0
  78. package/docs/changelog.mdx +165 -72
  79. package/docs/core-concepts/ai/architecture.mdx +1 -23
  80. package/docs/core-concepts/ai/sdk.mdx +26 -1
  81. package/docs/core-concepts/authentication/architecture.mdx +6 -8
  82. package/docs/core-concepts/authentication/sdk.mdx +387 -91
  83. package/docs/core-concepts/authentication/ui-components/customization.mdx +460 -256
  84. package/docs/core-concepts/authentication/ui-components/nextjs.mdx +50 -24
  85. package/docs/core-concepts/authentication/ui-components/react-router.mdx +18 -19
  86. package/docs/core-concepts/authentication/ui-components/react.mdx +26 -19
  87. package/docs/core-concepts/database/architecture.mdx +58 -21
  88. package/docs/core-concepts/database/pgvector.mdx +138 -0
  89. package/docs/core-concepts/database/sdk.mdx +17 -17
  90. package/docs/core-concepts/deployments/architecture.mdx +152 -0
  91. package/docs/core-concepts/email/architecture.mdx +4 -2
  92. package/docs/core-concepts/functions/architecture.mdx +1 -1
  93. package/docs/core-concepts/functions/sdk.mdx +0 -1
  94. package/docs/core-concepts/realtime/architecture.mdx +1 -1
  95. package/docs/core-concepts/storage/architecture.mdx +1 -1
  96. package/docs/core-concepts/storage/sdk.mdx +25 -25
  97. package/docs/docs.json +14 -6
  98. package/docs/favicon.png +0 -0
  99. package/docs/favicon.svg +3 -18
  100. package/docs/images/changelog/dec-2025/apple-oauth.mp4 +0 -0
  101. package/docs/images/changelog/dec-2025/moreModels.png +0 -0
  102. package/docs/images/changelog/dec-2025/multi-region.webp +0 -0
  103. package/docs/images/changelog/dec-2025/postgres-connection.webp +0 -0
  104. package/docs/images/changelog/dec-2025/realtime2.png +0 -0
  105. package/docs/images/mcp-setup/CC-MCP-1.mp4 +0 -0
  106. package/docs/images/mcp-setup/CC-MCP-2.mp4 +0 -0
  107. package/docs/images/mcp-setup/Cursor-MCP-1.mp4 +0 -0
  108. package/docs/images/mcp-setup/Cursor-MCP-2.mp4 +0 -0
  109. package/docs/images/mcp-setup/Cursor-MCP-3.mp4 +0 -0
  110. package/docs/images/mcp-setup/claude-code-connect.png +0 -0
  111. package/docs/images/mcp-setup/cline-1.png +0 -0
  112. package/docs/images/mcp-setup/cline-2.png +0 -0
  113. package/docs/images/mcp-setup/cline-3.png +0 -0
  114. package/docs/images/mcp-setup/connect-project.png +0 -0
  115. package/docs/images/mcp-setup/copilot-1.png +0 -0
  116. package/docs/images/mcp-setup/copilot-2.png +0 -0
  117. package/docs/images/mcp-setup/copilot-3.png +0 -0
  118. package/docs/images/mcp-setup/mcp-json-1.png +0 -0
  119. package/docs/images/mcp-setup/mcp-json-2.png +0 -0
  120. package/docs/images/mcp-setup/qoder-1.png +0 -0
  121. package/docs/images/mcp-setup/qoder-2.png +0 -0
  122. package/docs/images/mcp-setup/roocode-1.png +0 -0
  123. package/docs/images/mcp-setup/roocode-2.png +0 -0
  124. package/docs/images/mcp-setup/trae-1.png +0 -0
  125. package/docs/images/mcp-setup/trae-2.png +0 -0
  126. package/docs/images/mcp-setup/trae-3.png +0 -0
  127. package/docs/images/mcp-setup/trae-4.png +0 -0
  128. package/docs/images/mcp-setup/trae-5.png +0 -0
  129. package/docs/images/mcp-setup/windsurf-1.png +0 -0
  130. package/docs/images/mcp-setup/windsurf-2.png +0 -0
  131. package/docs/insforge-instructions-sdk.md +7 -3
  132. package/docs/introduction.mdx +9 -8
  133. package/docs/mcp-setup.mdx +332 -0
  134. package/docs/oauth-server.mdx +563 -0
  135. package/docs/partnership.mdx +79 -10
  136. package/docs/quickstart.mdx +1 -1
  137. package/docs/vscode-extension.mdx +74 -0
  138. package/eslint.config.js +1 -0
  139. package/examples/response-examples.md +1 -1
  140. package/frontend/package.json +1 -1
  141. package/frontend/src/App.tsx +8 -3
  142. package/frontend/src/assets/logos/antigravity.svg +1 -0
  143. package/frontend/src/assets/logos/copilot.svg +10 -0
  144. package/frontend/src/assets/logos/deepseek.svg +139 -0
  145. package/frontend/src/assets/logos/kiro.svg +9 -0
  146. package/frontend/src/assets/logos/qoder.svg +4 -0
  147. package/frontend/src/assets/logos/qwen.svg +15 -0
  148. package/frontend/src/components/CodeBlock.tsx +2 -2
  149. package/frontend/src/components/ConnectCTA.tsx +3 -2
  150. package/frontend/src/components/datagrid/DataGrid.tsx +90 -62
  151. package/frontend/src/components/datagrid/datagridTypes.tsx +2 -1
  152. package/frontend/src/components/datagrid/index.ts +1 -1
  153. package/frontend/src/components/index.ts +0 -1
  154. package/frontend/src/components/layout/AppHeader.tsx +4 -27
  155. package/frontend/src/components/layout/AppSidebar.tsx +85 -100
  156. package/frontend/src/components/layout/Layout.tsx +34 -32
  157. package/frontend/src/components/layout/PrimaryMenu.tsx +12 -4
  158. package/frontend/src/components/radix/Select.tsx +151 -151
  159. package/frontend/src/features/ai/components/AIConfigCard.tsx +200 -200
  160. package/frontend/src/features/ai/components/AIEmptyState.tsx +23 -23
  161. package/frontend/src/features/ai/components/ModalityFilterSidebar.tsx +102 -101
  162. package/frontend/src/features/ai/components/ModelSelectionDialog.tsx +135 -135
  163. package/frontend/src/features/ai/components/ModelSelectionGrid.tsx +51 -51
  164. package/frontend/src/features/ai/components/SystemPromptDialog.tsx +118 -118
  165. package/frontend/src/features/ai/components/index.ts +6 -6
  166. package/frontend/src/features/ai/helpers.ts +147 -141
  167. package/frontend/src/features/ai/pages/AIPage.tsx +166 -166
  168. package/frontend/src/features/auth/components/AuthPreview.tsx +96 -96
  169. package/frontend/src/features/auth/components/UsersDataGrid.tsx +55 -31
  170. package/frontend/src/features/auth/components/index.ts +5 -5
  171. package/frontend/src/features/auth/pages/AuthMethodsPage.tsx +275 -275
  172. package/frontend/src/features/dashboard/pages/DashboardPage.tsx +1 -1
  173. package/frontend/src/features/database/components/DatabaseDataGrid.tsx +0 -2
  174. package/frontend/src/features/database/components/ForeignKeyCell.tsx +38 -11
  175. package/frontend/src/features/database/components/ForeignKeyPopover.tsx +18 -8
  176. package/frontend/src/features/database/components/LinkRecordModal.tsx +61 -13
  177. package/frontend/src/features/database/components/RecordFormField.tsx +1 -1
  178. package/frontend/src/features/database/components/TableSidebar.tsx +0 -3
  179. package/frontend/src/features/database/components/TablesEmptyState.tsx +1 -1
  180. package/frontend/src/features/database/components/TemplatePreview.tsx +1 -2
  181. package/frontend/src/features/database/constants.ts +16 -28
  182. package/frontend/src/features/database/hooks/useCSVImport.ts +3 -2
  183. package/frontend/src/features/database/hooks/useRawSQL.ts +3 -2
  184. package/frontend/src/features/database/hooks/useTables.ts +5 -7
  185. package/frontend/src/features/database/pages/FunctionsPage.tsx +0 -5
  186. package/frontend/src/features/database/pages/IndexesPage.tsx +0 -5
  187. package/frontend/src/features/database/pages/PoliciesPage.tsx +0 -5
  188. package/frontend/src/features/database/pages/SQLEditorPage.tsx +2 -2
  189. package/frontend/src/features/database/pages/TriggersPage.tsx +0 -5
  190. package/frontend/src/features/database/services/advance.service.ts +1 -15
  191. package/frontend/src/features/database/services/record.service.ts +4 -20
  192. package/frontend/src/features/database/services/table.service.ts +1 -4
  193. package/frontend/src/features/database/templates/ai-chatbot.ts +6 -6
  194. package/frontend/src/features/database/templates/ecommerce-platform.ts +2 -2
  195. package/frontend/src/features/database/templates/instagram-clone.ts +10 -10
  196. package/frontend/src/features/database/templates/notion-clone.ts +8 -8
  197. package/frontend/src/features/database/templates/reddit-clone.ts +10 -10
  198. package/frontend/src/features/deployments/components/DeploymentRow.tsx +93 -0
  199. package/frontend/src/features/deployments/components/DeploymentsEmptyState.tsx +15 -0
  200. package/frontend/src/features/deployments/hooks/useDeployments.ts +157 -0
  201. package/frontend/src/features/deployments/pages/DeploymentsPage.tsx +318 -0
  202. package/frontend/src/features/deployments/services/deployments.service.ts +63 -0
  203. package/frontend/src/features/functions/components/FunctionRow.tsx +72 -72
  204. package/frontend/src/features/functions/components/FunctionsSidebar.tsx +56 -56
  205. package/frontend/src/features/functions/components/SecretRow.tsx +3 -3
  206. package/frontend/src/features/functions/components/index.ts +5 -5
  207. package/frontend/src/features/functions/hooks/useFunctions.ts +5 -4
  208. package/frontend/src/features/functions/hooks/useSecrets.ts +6 -9
  209. package/frontend/src/features/functions/pages/SecretsPage.tsx +118 -118
  210. package/frontend/src/features/functions/services/function.service.ts +8 -25
  211. package/frontend/src/features/functions/services/secret.service.ts +23 -41
  212. package/frontend/src/features/login/pages/CloudLoginPage.tsx +125 -118
  213. package/frontend/src/features/logs/components/LogDetailPanel.tsx +41 -0
  214. package/frontend/src/features/logs/components/LogsDataGrid.tsx +32 -1
  215. package/frontend/src/features/logs/components/index.ts +1 -0
  216. package/frontend/src/features/logs/pages/LogsPage.tsx +36 -6
  217. package/frontend/src/features/onboard/components/ApiCredentialsSection.tsx +59 -0
  218. package/frontend/src/features/onboard/components/ConnectionStringSection.tsx +180 -0
  219. package/frontend/src/features/onboard/components/McpConnectionSection.tsx +159 -0
  220. package/frontend/src/features/onboard/components/OnboardingController.tsx +68 -0
  221. package/frontend/src/features/onboard/components/OnboardingModal.tsx +121 -267
  222. package/frontend/src/features/onboard/components/ShowPasswordButton.tsx +21 -0
  223. package/frontend/src/features/onboard/components/index.ts +9 -4
  224. package/frontend/src/features/onboard/components/mcp/CursorDeeplinkGenerator.tsx +1 -1
  225. package/frontend/src/features/onboard/components/mcp/QoderDeeplinkGenerator.tsx +36 -0
  226. package/frontend/src/features/onboard/components/mcp/helpers.tsx +123 -98
  227. package/frontend/src/features/onboard/components/mcp/index.ts +4 -3
  228. package/frontend/src/features/onboard/index.ts +17 -13
  229. package/frontend/src/features/settings/pages/SettingsPage.tsx +349 -0
  230. package/frontend/src/features/visualizer/components/AuthNode.tsx +4 -4
  231. package/frontend/src/features/visualizer/components/SchemaVisualizer.tsx +21 -8
  232. package/frontend/src/features/visualizer/pages/VisualizerPage.tsx +10 -1
  233. package/frontend/src/index.css +249 -249
  234. package/frontend/src/lib/contexts/ModalContext.tsx +35 -0
  235. package/frontend/src/lib/hooks/useMetadata.ts +45 -1
  236. package/frontend/src/lib/hooks/useModal.tsx +2 -0
  237. package/frontend/src/lib/routing/AppRoutes.tsx +103 -99
  238. package/frontend/src/lib/services/metadata.service.ts +20 -3
  239. package/frontend/src/lib/utils/menuItems.ts +223 -207
  240. package/frontend/src/lib/utils/utils.ts +196 -196
  241. package/functions/server.ts +315 -315
  242. package/functions/worker-template.js +1 -1
  243. package/openapi/ai.yaml +115 -5
  244. package/openapi/auth.yaml +97 -17
  245. package/openapi/logs.yaml +0 -2
  246. package/openapi/metadata.yaml +0 -2
  247. package/openapi/records.yaml +21 -21
  248. package/openapi/tables.yaml +1 -2
  249. package/package.json +1 -1
  250. package/shared-schemas/package.json +1 -1
  251. package/shared-schemas/src/ai-api.schema.ts +251 -143
  252. package/shared-schemas/src/ai.schema.ts +63 -63
  253. package/shared-schemas/src/auth-api.schema.ts +34 -6
  254. package/shared-schemas/src/auth.schema.ts +17 -10
  255. package/shared-schemas/src/cloud-events.schema.ts +26 -0
  256. package/shared-schemas/src/deployments-api.schema.ts +55 -0
  257. package/shared-schemas/src/deployments.schema.ts +30 -0
  258. package/shared-schemas/src/docs.schema.ts +8 -2
  259. package/shared-schemas/src/email-api.schema.ts +30 -30
  260. package/shared-schemas/src/functions-api.schema.ts +13 -4
  261. package/shared-schemas/src/functions.schema.ts +1 -1
  262. package/shared-schemas/src/index.ts +22 -18
  263. package/shared-schemas/src/metadata.schema.ts +30 -4
  264. package/shared-schemas/src/secrets-api.schema.ts +44 -0
  265. package/shared-schemas/src/secrets.schema.ts +15 -0
  266. package/zeabur/README.md +13 -0
  267. package/zeabur/template.yml +20 -51
  268. package/backend/src/types/profile.ts +0 -55
  269. package/frontend/src/components/ProjectInfoModal.tsx +0 -128
@@ -0,0 +1,159 @@
1
+ import jwt from 'jsonwebtoken';
2
+ import axios from 'axios';
3
+ import { z } from 'zod';
4
+ import { config } from '@/infra/config/app.config.js';
5
+ import { AppError } from '@/api/middlewares/error.js';
6
+ import { ERROR_CODES } from '@/types/error-constants.js';
7
+ import { DatabaseProvider, DatabaseConnectionInfo, DatabasePasswordInfo } from './base.provider.js';
8
+
9
+ /**
10
+ * Zod schema for validating database connection info response
11
+ */
12
+ const DatabaseConnectionInfoSchema = z.object({
13
+ connectionURL: z.string(),
14
+ parameters: z.object({
15
+ host: z.string(),
16
+ port: z.number(),
17
+ database: z.string(),
18
+ user: z.string(),
19
+ password: z.string(),
20
+ sslmode: z.string(),
21
+ }),
22
+ });
23
+
24
+ /**
25
+ * Zod schema for validating database password response
26
+ */
27
+ const DatabasePasswordInfoSchema = z.object({
28
+ databasePassword: z.string(),
29
+ });
30
+
31
+ /**
32
+ * Cloud database provider for fetching database connection info via Insforge cloud backend
33
+ */
34
+ export class CloudDatabaseProvider implements DatabaseProvider {
35
+ private static instance: CloudDatabaseProvider;
36
+
37
+ private constructor() {}
38
+
39
+ public static getInstance(): CloudDatabaseProvider {
40
+ if (!CloudDatabaseProvider.instance) {
41
+ CloudDatabaseProvider.instance = new CloudDatabaseProvider();
42
+ }
43
+ return CloudDatabaseProvider.instance;
44
+ }
45
+
46
+ /**
47
+ * Generate JWT sign token for cloud API authentication
48
+ */
49
+ private generateSignToken(): string {
50
+ const projectId = config.cloud.projectId;
51
+ const jwtSecret = config.app.jwtSecret;
52
+
53
+ if (!projectId || projectId === 'local') {
54
+ throw new AppError(
55
+ 'PROJECT_ID is not configured. Cannot access cloud API without cloud project setup.',
56
+ 500,
57
+ ERROR_CODES.INTERNAL_ERROR
58
+ );
59
+ }
60
+
61
+ if (!jwtSecret) {
62
+ throw new AppError(
63
+ 'JWT_SECRET is not configured. Cannot generate sign token.',
64
+ 500,
65
+ ERROR_CODES.INTERNAL_ERROR
66
+ );
67
+ }
68
+
69
+ return jwt.sign({ sub: projectId }, jwtSecret, { expiresIn: '10m' });
70
+ }
71
+
72
+ /**
73
+ * Get database connection string from cloud backend
74
+ */
75
+ async getDatabaseConnectionString(): Promise<DatabaseConnectionInfo> {
76
+ const signToken = this.generateSignToken();
77
+ const url = `${config.cloud.apiHost}/projects/v1/${config.cloud.projectId}/database-connection-string`;
78
+
79
+ try {
80
+ const response = await axios.get(url, {
81
+ headers: { sign: signToken },
82
+ timeout: 10000,
83
+ });
84
+
85
+ const parsed = DatabaseConnectionInfoSchema.safeParse(response.data);
86
+ if (!parsed.success) {
87
+ throw new AppError(
88
+ `Invalid database connection info response: ${parsed.error.message}`,
89
+ 500,
90
+ ERROR_CODES.INTERNAL_ERROR
91
+ );
92
+ }
93
+
94
+ return parsed.data;
95
+ } catch (error) {
96
+ if (error instanceof AppError) {
97
+ throw error;
98
+ }
99
+ if (axios.isAxiosError(error)) {
100
+ const status = error.response?.status ?? 500;
101
+ const message = error.response?.data?.message ?? error.message;
102
+ throw new AppError(
103
+ `Failed to fetch database connection string: ${message}`,
104
+ status,
105
+ ERROR_CODES.INTERNAL_ERROR
106
+ );
107
+ }
108
+ throw new AppError(
109
+ `Unexpected error fetching database connection string: ${error instanceof Error ? error.message : 'Unknown error'}`,
110
+ 500,
111
+ ERROR_CODES.INTERNAL_ERROR
112
+ );
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Get database password from cloud backend
118
+ */
119
+ async getDatabasePassword(): Promise<DatabasePasswordInfo> {
120
+ const signToken = this.generateSignToken();
121
+ const url = `${config.cloud.apiHost}/projects/v1/${config.cloud.projectId}/database-password`;
122
+
123
+ try {
124
+ const response = await axios.get(url, {
125
+ headers: { sign: signToken },
126
+ timeout: 10000,
127
+ });
128
+
129
+ const parsed = DatabasePasswordInfoSchema.safeParse(response.data);
130
+ if (!parsed.success) {
131
+ throw new AppError(
132
+ `Invalid database password response: ${parsed.error.message}`,
133
+ 500,
134
+ ERROR_CODES.INTERNAL_ERROR
135
+ );
136
+ }
137
+
138
+ return parsed.data;
139
+ } catch (error) {
140
+ if (error instanceof AppError) {
141
+ throw error;
142
+ }
143
+ if (axios.isAxiosError(error)) {
144
+ const status = error.response?.status ?? 500;
145
+ const message = error.response?.data?.message ?? error.message;
146
+ throw new AppError(
147
+ `Failed to fetch database password: ${message}`,
148
+ status,
149
+ ERROR_CODES.INTERNAL_ERROR
150
+ );
151
+ }
152
+ throw new AppError(
153
+ `Unexpected error fetching database password: ${error instanceof Error ? error.message : 'Unknown error'}`,
154
+ 500,
155
+ ERROR_CODES.INTERNAL_ERROR
156
+ );
157
+ }
158
+ }
159
+ }
@@ -0,0 +1,516 @@
1
+ import axios from 'axios';
2
+ import jwt from 'jsonwebtoken';
3
+ import crypto from 'crypto';
4
+ import { isCloudEnvironment } from '@/utils/environment.js';
5
+ import { AppError } from '@/api/middlewares/error.js';
6
+ import { ERROR_CODES } from '@/types/error-constants.js';
7
+ import { SecretService } from '@/services/secrets/secret.service.js';
8
+ import logger from '@/utils/logger.js';
9
+
10
+ interface CloudCredentialsResponse {
11
+ team_id: string;
12
+ vercel_project_id: string;
13
+ bearer_token: string;
14
+ expires_at: string;
15
+ webhook_secret: string | null;
16
+ }
17
+
18
+ interface VercelCredentials {
19
+ token: string;
20
+ teamId: string;
21
+ projectId: string;
22
+ expiresAt: Date | null;
23
+ }
24
+
25
+ export interface VercelDeploymentResult {
26
+ id: string;
27
+ url: string | null;
28
+ state: string;
29
+ readyState: string;
30
+ name: string;
31
+ createdAt: Date;
32
+ error?: {
33
+ code: string;
34
+ message: string;
35
+ };
36
+ }
37
+
38
+ export interface CreateDeploymentOptions {
39
+ name?: string;
40
+ files?: Array<{
41
+ file: string;
42
+ sha: string;
43
+ size: number;
44
+ }>;
45
+ projectSettings?: {
46
+ buildCommand?: string | null;
47
+ outputDirectory?: string | null;
48
+ installCommand?: string | null;
49
+ devCommand?: string | null;
50
+ rootDirectory?: string | null;
51
+ };
52
+ meta?: Record<string, string>;
53
+ }
54
+
55
+ export interface DeploymentFile {
56
+ path: string;
57
+ content: Buffer;
58
+ sha: string;
59
+ size: number;
60
+ }
61
+
62
+ export class VercelProvider {
63
+ private static instance: VercelProvider;
64
+ private cloudCredentials: VercelCredentials | undefined;
65
+ private fetchPromise: Promise<VercelCredentials> | null = null;
66
+ private secretService: SecretService;
67
+
68
+ private constructor() {
69
+ this.secretService = SecretService.getInstance();
70
+ }
71
+
72
+ static getInstance(): VercelProvider {
73
+ if (!VercelProvider.instance) {
74
+ VercelProvider.instance = new VercelProvider();
75
+ }
76
+ return VercelProvider.instance;
77
+ }
78
+
79
+ /**
80
+ * Get Vercel credentials based on environment
81
+ */
82
+ async getCredentials(): Promise<VercelCredentials> {
83
+ if (isCloudEnvironment()) {
84
+ if (
85
+ this.cloudCredentials &&
86
+ (!this.cloudCredentials.expiresAt || new Date() < this.cloudCredentials.expiresAt)
87
+ ) {
88
+ return this.cloudCredentials;
89
+ }
90
+ return await this.fetchCloudCredentials();
91
+ }
92
+
93
+ const token = process.env.VERCEL_TOKEN;
94
+ const teamId = process.env.VERCEL_TEAM_ID;
95
+ const projectId = process.env.VERCEL_PROJECT_ID;
96
+
97
+ if (!token) {
98
+ throw new AppError(
99
+ 'VERCEL_TOKEN not found in environment variables',
100
+ 500,
101
+ ERROR_CODES.INTERNAL_ERROR
102
+ );
103
+ }
104
+ if (!teamId) {
105
+ throw new AppError(
106
+ 'VERCEL_TEAM_ID not found in environment variables',
107
+ 500,
108
+ ERROR_CODES.INTERNAL_ERROR
109
+ );
110
+ }
111
+ if (!projectId) {
112
+ throw new AppError(
113
+ 'VERCEL_PROJECT_ID not found in environment variables',
114
+ 500,
115
+ ERROR_CODES.INTERNAL_ERROR
116
+ );
117
+ }
118
+
119
+ return { token, teamId, projectId, expiresAt: null };
120
+ }
121
+
122
+ /**
123
+ * Check if Vercel is properly configured
124
+ */
125
+ isConfigured(): boolean {
126
+ if (isCloudEnvironment()) {
127
+ return true;
128
+ }
129
+ return !!(
130
+ process.env.VERCEL_TOKEN &&
131
+ process.env.VERCEL_TEAM_ID &&
132
+ process.env.VERCEL_PROJECT_ID
133
+ );
134
+ }
135
+
136
+ /**
137
+ * Fetch credentials from cloud service
138
+ */
139
+ private async fetchCloudCredentials(): Promise<VercelCredentials> {
140
+ if (this.fetchPromise) {
141
+ logger.info('Vercel credentials fetch already in progress, waiting for completion...');
142
+ return this.fetchPromise;
143
+ }
144
+
145
+ this.fetchPromise = (async () => {
146
+ try {
147
+ const projectId = process.env.PROJECT_ID;
148
+ if (!projectId) {
149
+ throw new Error('PROJECT_ID not found in environment variables');
150
+ }
151
+
152
+ const jwtSecret = process.env.JWT_SECRET;
153
+ if (!jwtSecret) {
154
+ throw new Error('JWT_SECRET not found in environment variables');
155
+ }
156
+
157
+ const signature = jwt.sign({ projectId }, jwtSecret, { expiresIn: '1h' });
158
+
159
+ const response = await fetch(
160
+ `${process.env.CLOUD_API_HOST || 'https://api.insforge.dev'}/sites/v1/credentials/${projectId}?sign=${signature}`
161
+ );
162
+
163
+ if (!response.ok) {
164
+ throw new Error(`Failed to fetch Vercel credentials: ${response.statusText}`);
165
+ }
166
+
167
+ const data = (await response.json()) as CloudCredentialsResponse;
168
+
169
+ if (!data.bearer_token || !data.vercel_project_id) {
170
+ throw new Error('Invalid response: missing Vercel credentials');
171
+ }
172
+
173
+ if (data.webhook_secret) {
174
+ await this.storeWebhookSecret(data.webhook_secret);
175
+ }
176
+
177
+ this.cloudCredentials = {
178
+ token: data.bearer_token,
179
+ teamId: data.team_id,
180
+ projectId: data.vercel_project_id,
181
+ expiresAt: new Date(data.expires_at),
182
+ };
183
+
184
+ logger.info('Successfully fetched Vercel credentials from cloud', {
185
+ expiresAt: this.cloudCredentials.expiresAt?.toISOString(),
186
+ });
187
+
188
+ return this.cloudCredentials;
189
+ } catch (error) {
190
+ logger.error('Failed to fetch Vercel credentials', {
191
+ error: error instanceof Error ? error.message : String(error),
192
+ });
193
+ throw error;
194
+ } finally {
195
+ this.fetchPromise = null;
196
+ }
197
+ })();
198
+
199
+ return this.fetchPromise;
200
+ }
201
+
202
+ /**
203
+ * Store webhook secret in secrets service
204
+ */
205
+ private async storeWebhookSecret(webhookSecret: string): Promise<void> {
206
+ const secretKey = 'VERCEL_WEBHOOK_SECRET';
207
+
208
+ try {
209
+ const existingSecret = await this.secretService.getSecretByKey(secretKey);
210
+
211
+ if (existingSecret === webhookSecret) {
212
+ return;
213
+ }
214
+
215
+ if (existingSecret !== null) {
216
+ await this.secretService.updateSecretByKey(secretKey, { value: webhookSecret });
217
+ logger.info('Vercel webhook secret updated');
218
+ } else {
219
+ await this.secretService.createSecret({
220
+ key: secretKey,
221
+ value: webhookSecret,
222
+ isReserved: true,
223
+ });
224
+ logger.info('Vercel webhook secret created');
225
+ }
226
+ } catch (error) {
227
+ logger.warn('Failed to store Vercel webhook secret', {
228
+ error: error instanceof Error ? error.message : String(error),
229
+ });
230
+ }
231
+ }
232
+
233
+ /**
234
+ * Create a new deployment on Vercel
235
+ * POST /v13/deployments
236
+ */
237
+ async createDeployment(options: CreateDeploymentOptions = {}): Promise<VercelDeploymentResult> {
238
+ const credentials = await this.getCredentials();
239
+
240
+ try {
241
+ const response = await axios.post(
242
+ `https://api.vercel.com/v13/deployments?teamId=${credentials.teamId}&skipAutoDetectionConfirmation=1`,
243
+ {
244
+ name: options.name || 'deployment',
245
+ target: 'production',
246
+ project: credentials.projectId,
247
+ files: options.files,
248
+ projectSettings: options.projectSettings,
249
+ meta: options.meta,
250
+ },
251
+ { headers: { Authorization: `Bearer ${credentials.token}` } }
252
+ );
253
+
254
+ const deployment = response.data;
255
+
256
+ logger.info('Vercel deployment created', {
257
+ id: deployment.id,
258
+ url: deployment.url,
259
+ readyState: deployment.readyState,
260
+ });
261
+
262
+ return {
263
+ id: deployment.id,
264
+ url: deployment.url ? `https://${deployment.url}` : null,
265
+ state: deployment.readyState,
266
+ readyState: deployment.readyState,
267
+ name: deployment.name,
268
+ createdAt: new Date(deployment.createdAt),
269
+ };
270
+ } catch (error) {
271
+ logger.error('Failed to create Vercel deployment', {
272
+ error: error instanceof Error ? error.message : String(error),
273
+ });
274
+ throw new AppError('Failed to create Vercel deployment', 500, ERROR_CODES.INTERNAL_ERROR);
275
+ }
276
+ }
277
+
278
+ /**
279
+ * Get deployment status by deployment ID
280
+ * GET /v13/deployments/:id
281
+ */
282
+ async getDeployment(deploymentId: string): Promise<VercelDeploymentResult> {
283
+ const credentials = await this.getCredentials();
284
+
285
+ try {
286
+ const response = await axios.get(
287
+ `https://api.vercel.com/v13/deployments/${deploymentId}?teamId=${credentials.teamId}`,
288
+ { headers: { Authorization: `Bearer ${credentials.token}` } }
289
+ );
290
+ const deployment = response.data;
291
+
292
+ return {
293
+ id: deployment.id,
294
+ url: deployment.url ? `https://${deployment.url}` : null,
295
+ state: deployment.readyState,
296
+ readyState: deployment.readyState,
297
+ name: deployment.name,
298
+ createdAt: new Date(deployment.createdAt),
299
+ error: deployment.errorCode
300
+ ? {
301
+ code: deployment.errorCode,
302
+ message: deployment.errorMessage || 'Unknown error',
303
+ }
304
+ : undefined,
305
+ };
306
+ } catch (error) {
307
+ if (axios.isAxiosError(error) && error.response?.status === 404) {
308
+ throw new AppError(`Deployment not found: ${deploymentId}`, 404, ERROR_CODES.NOT_FOUND);
309
+ }
310
+ logger.error('Failed to get Vercel deployment', {
311
+ error: error instanceof Error ? error.message : String(error),
312
+ deploymentId,
313
+ });
314
+ throw new AppError('Failed to get Vercel deployment', 500, ERROR_CODES.INTERNAL_ERROR);
315
+ }
316
+ }
317
+
318
+ /**
319
+ * Cancel a deployment
320
+ * PATCH /v12/deployments/:id/cancel
321
+ */
322
+ async cancelDeployment(deploymentId: string): Promise<void> {
323
+ const credentials = await this.getCredentials();
324
+
325
+ try {
326
+ await axios.patch(
327
+ `https://api.vercel.com/v12/deployments/${deploymentId}/cancel?teamId=${credentials.teamId}`,
328
+ {},
329
+ { headers: { Authorization: `Bearer ${credentials.token}` } }
330
+ );
331
+ logger.info('Vercel deployment cancelled', { deploymentId });
332
+ } catch (error) {
333
+ logger.error('Failed to cancel Vercel deployment', {
334
+ error: error instanceof Error ? error.message : String(error),
335
+ deploymentId,
336
+ });
337
+ throw new AppError('Failed to cancel Vercel deployment', 500, ERROR_CODES.INTERNAL_ERROR);
338
+ }
339
+ }
340
+
341
+ /**
342
+ * Upsert environment variables for the project
343
+ * POST /v10/projects/:id/env
344
+ */
345
+ async upsertEnvironmentVariables(envVars: Array<{ key: string; value: string }>): Promise<void> {
346
+ const credentials = await this.getCredentials();
347
+
348
+ try {
349
+ const payload = envVars.map((env) => ({
350
+ key: env.key,
351
+ value: env.value,
352
+ type: 'encrypted',
353
+ target: ['production', 'preview', 'development'],
354
+ }));
355
+
356
+ await axios.post(
357
+ `https://api.vercel.com/v10/projects/${credentials.projectId}/env?teamId=${credentials.teamId}&upsert=true`,
358
+ payload,
359
+ { headers: { Authorization: `Bearer ${credentials.token}` } }
360
+ );
361
+
362
+ logger.info('Environment variables upserted', {
363
+ count: envVars.length,
364
+ keys: envVars.map((e) => e.key),
365
+ });
366
+ } catch (error) {
367
+ logger.error('Failed to upsert environment variables', {
368
+ error: error instanceof Error ? error.message : String(error),
369
+ });
370
+ throw new AppError('Failed to upsert environment variables', 500, ERROR_CODES.INTERNAL_ERROR);
371
+ }
372
+ }
373
+
374
+ /**
375
+ * Get all environment variable keys for the project
376
+ * GET /v9/projects/:id/env
377
+ */
378
+ async getEnvironmentVariableKeys(): Promise<string[]> {
379
+ const credentials = await this.getCredentials();
380
+
381
+ try {
382
+ const response = await axios.get(
383
+ `https://api.vercel.com/v9/projects/${credentials.projectId}/env?teamId=${credentials.teamId}`,
384
+ { headers: { Authorization: `Bearer ${credentials.token}` } }
385
+ );
386
+
387
+ const data = response.data as { envs?: Array<{ key: string }> };
388
+ return (data.envs || []).map((env) => env.key);
389
+ } catch (error) {
390
+ logger.warn('Failed to get environment variable keys', {
391
+ error: error instanceof Error ? error.message : String(error),
392
+ });
393
+ return [];
394
+ }
395
+ }
396
+
397
+ /**
398
+ * Clear cached credentials
399
+ */
400
+ clearCredentials(): void {
401
+ this.cloudCredentials = undefined;
402
+ this.fetchPromise = null;
403
+ logger.info('Vercel credentials cache cleared');
404
+ }
405
+
406
+ /**
407
+ * Upload a single file to Vercel
408
+ * POST /v2/files
409
+ */
410
+ async uploadFile(fileContent: Buffer): Promise<string> {
411
+ const credentials = await this.getCredentials();
412
+ const sha = this.computeSha(fileContent);
413
+
414
+ try {
415
+ await axios.post(
416
+ `https://api.vercel.com/v2/files?teamId=${credentials.teamId}`,
417
+ fileContent,
418
+ {
419
+ headers: {
420
+ Authorization: `Bearer ${credentials.token}`,
421
+ 'Content-Type': 'application/octet-stream',
422
+ 'Content-Length': fileContent.length.toString(),
423
+ 'x-vercel-digest': sha,
424
+ },
425
+ }
426
+ );
427
+
428
+ logger.info('File uploaded to Vercel', { sha, size: fileContent.length });
429
+ return sha;
430
+ } catch (error) {
431
+ // 409 Conflict means file already exists (same SHA), which is fine
432
+ if (axios.isAxiosError(error) && error.response?.status === 409) {
433
+ logger.info('File already exists on Vercel', { sha });
434
+ return sha;
435
+ }
436
+ logger.error('Failed to upload file to Vercel', {
437
+ error: error instanceof Error ? error.message : String(error),
438
+ });
439
+ throw new AppError('Failed to upload file to Vercel', 500, ERROR_CODES.INTERNAL_ERROR);
440
+ }
441
+ }
442
+
443
+ /**
444
+ * Upload multiple files to Vercel in parallel
445
+ */
446
+ async uploadFiles(
447
+ files: Array<{ path: string; content: Buffer }>
448
+ ): Promise<Array<{ file: string; sha: string; size: number }>> {
449
+ const uploadPromises = files.map(async ({ path, content }) => {
450
+ const sha = await this.uploadFile(content);
451
+ return {
452
+ file: path,
453
+ sha,
454
+ size: content.length,
455
+ };
456
+ });
457
+
458
+ return Promise.all(uploadPromises);
459
+ }
460
+
461
+ /**
462
+ * Compute SHA-1 hash of file content
463
+ */
464
+ private computeSha(content: Buffer): string {
465
+ return crypto.createHash('sha1').update(content).digest('hex');
466
+ }
467
+
468
+ /**
469
+ * Create deployment using file SHAs (files must be pre-uploaded)
470
+ */
471
+ async createDeploymentWithFiles(
472
+ files: Array<{ file: string; sha: string; size: number }>,
473
+ options: Omit<CreateDeploymentOptions, 'files'> = {}
474
+ ): Promise<VercelDeploymentResult> {
475
+ const credentials = await this.getCredentials();
476
+
477
+ try {
478
+ const response = await axios.post(
479
+ `https://api.vercel.com/v13/deployments?teamId=${credentials.teamId}&skipAutoDetectionConfirmation=1`,
480
+ {
481
+ name: options.name || 'deployment',
482
+ target: 'production',
483
+ project: credentials.projectId,
484
+ files: files,
485
+ projectSettings: options.projectSettings,
486
+ meta: options.meta,
487
+ },
488
+ { headers: { Authorization: `Bearer ${credentials.token}` } }
489
+ );
490
+
491
+ const deployment = response.data;
492
+
493
+ logger.info('Vercel deployment created with file SHAs', {
494
+ id: deployment.id,
495
+ url: deployment.url,
496
+ readyState: deployment.readyState,
497
+ fileCount: files.length,
498
+ });
499
+
500
+ return {
501
+ id: deployment.id,
502
+ url: deployment.url ? `https://${deployment.url}` : null,
503
+ state: deployment.readyState,
504
+ readyState: deployment.readyState,
505
+ name: deployment.name,
506
+ createdAt: new Date(deployment.createdAt),
507
+ };
508
+ } catch (error) {
509
+ logger.error('Failed to create Vercel deployment with files', {
510
+ error: error instanceof Error ? error.message : String(error),
511
+ fileCount: files.length,
512
+ });
513
+ throw new AppError('Failed to create Vercel deployment', 500, ERROR_CODES.INTERNAL_ERROR);
514
+ }
515
+ }
516
+ }