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.
- package/CHANGELOG.md +2 -0
- package/auth/package.json +5 -3
- package/auth/src/lib/broadcastService.ts +115 -117
- package/auth/src/lib/insforge.ts +8 -0
- package/auth/src/main.tsx +2 -4
- package/auth/src/pages/SignInPage.tsx +60 -60
- package/auth/src/pages/SignUpPage.tsx +60 -60
- package/auth/src/pages/VerifyEmailPage.tsx +18 -0
- package/auth/tsconfig.json +2 -1
- package/backend/package.json +10 -6
- package/backend/src/api/middlewares/rate-limiters.ts +127 -127
- package/backend/src/api/routes/ai/index.routes.ts +475 -468
- package/backend/src/api/routes/auth/index.routes.ts +85 -32
- package/backend/src/api/routes/auth/oauth.routes.ts +11 -6
- package/backend/src/api/routes/database/index.routes.ts +2 -0
- package/backend/src/api/routes/database/records.routes.ts +39 -175
- package/backend/src/api/routes/database/rpc.routes.ts +69 -0
- package/backend/src/api/routes/deployments/index.routes.ts +192 -0
- package/backend/src/api/routes/docs/index.routes.ts +3 -2
- package/backend/src/api/routes/email/index.routes.ts +35 -35
- package/backend/src/api/routes/functions/index.routes.ts +3 -3
- package/backend/src/api/routes/metadata/index.routes.ts +26 -0
- package/backend/src/api/routes/webhooks/index.routes.ts +109 -0
- package/backend/src/infra/database/database.manager.ts +0 -10
- package/backend/src/infra/database/migrations/018_schema-rework.sql +441 -0
- package/backend/src/infra/database/migrations/019_create-deployments-table.sql +36 -0
- package/backend/src/infra/database/migrations/020_add-audio-modality.sql +11 -0
- package/backend/src/infra/database/migrations/bootstrap/bootstrap-migrations.js +103 -0
- package/backend/src/infra/security/token.manager.ts +1 -4
- package/backend/src/providers/ai/openrouter.provider.ts +12 -3
- package/backend/src/providers/database/base.provider.ts +39 -0
- package/backend/src/providers/database/cloud.provider.ts +159 -0
- package/backend/src/providers/deployments/vercel.provider.ts +516 -0
- package/backend/src/server.ts +19 -7
- package/backend/src/services/ai/ai-config.service.ts +6 -6
- package/backend/src/services/ai/ai-model.service.ts +60 -60
- package/backend/src/services/ai/ai-usage.service.ts +7 -7
- package/backend/src/services/ai/chat-completion.service.ts +415 -220
- package/backend/src/services/ai/helpers.ts +64 -64
- package/backend/src/services/ai/index.ts +13 -13
- package/backend/src/services/auth/auth-config.service.ts +4 -4
- package/backend/src/services/auth/auth-otp.service.ts +6 -6
- package/backend/src/services/auth/auth.service.ts +134 -74
- package/backend/src/services/auth/index.ts +4 -4
- package/backend/src/services/auth/oauth-config.service.ts +12 -12
- package/backend/src/services/database/database-advance.service.ts +19 -55
- package/backend/src/services/database/database-table.service.ts +38 -85
- package/backend/src/services/database/postgrest-proxy.service.ts +165 -0
- package/backend/src/services/deployments/deployment.service.ts +693 -0
- package/backend/src/services/functions/function.service.ts +61 -41
- package/backend/src/services/logs/audit.service.ts +10 -10
- package/backend/src/services/secrets/secret.service.ts +101 -27
- package/backend/src/services/storage/storage.service.ts +30 -30
- package/backend/src/services/usage/usage.service.ts +6 -6
- package/backend/src/types/ai.ts +8 -0
- package/backend/src/types/auth.ts +5 -1
- package/backend/src/types/database.ts +2 -0
- package/backend/src/types/deployments.ts +33 -0
- package/backend/src/types/storage.ts +1 -1
- package/backend/src/types/webhooks.ts +45 -0
- package/backend/src/utils/cookies.ts +34 -35
- package/backend/src/utils/environment.ts +0 -14
- package/backend/src/utils/s3-config-loader.ts +64 -64
- package/backend/src/utils/seed.ts +334 -301
- package/backend/src/utils/sql-parser.ts +126 -0
- package/backend/src/utils/utils.ts +114 -114
- package/backend/src/utils/validations.ts +10 -10
- package/backend/tests/local/test-rpc.sh +141 -0
- package/backend/tests/local/test-secrets.sh +1 -1
- package/backend/tests/manual/test-ai-model-plugins.sh +258 -0
- package/backend/tests/manual/test-rawsql-modes.sh +24 -24
- package/backend/tests/unit/database-advance.test.ts +326 -0
- package/backend/tests/unit/helpers.test.ts +2 -2
- package/claude-plugin/skills/insforge-schema-patterns/SKILL.md +13 -10
- package/docker-compose.prod.yml +1 -1
- package/docker-compose.yml +1 -1
- package/docs/agent-docs/deployment.md +79 -0
- package/docs/changelog.mdx +165 -72
- package/docs/core-concepts/ai/architecture.mdx +1 -23
- package/docs/core-concepts/ai/sdk.mdx +26 -1
- package/docs/core-concepts/authentication/architecture.mdx +6 -8
- package/docs/core-concepts/authentication/sdk.mdx +387 -91
- package/docs/core-concepts/authentication/ui-components/customization.mdx +460 -256
- package/docs/core-concepts/authentication/ui-components/nextjs.mdx +50 -24
- package/docs/core-concepts/authentication/ui-components/react-router.mdx +18 -19
- package/docs/core-concepts/authentication/ui-components/react.mdx +26 -19
- package/docs/core-concepts/database/architecture.mdx +58 -21
- package/docs/core-concepts/database/pgvector.mdx +138 -0
- package/docs/core-concepts/database/sdk.mdx +17 -17
- package/docs/core-concepts/deployments/architecture.mdx +152 -0
- package/docs/core-concepts/email/architecture.mdx +4 -2
- package/docs/core-concepts/functions/architecture.mdx +1 -1
- package/docs/core-concepts/functions/sdk.mdx +0 -1
- package/docs/core-concepts/realtime/architecture.mdx +1 -1
- package/docs/core-concepts/storage/architecture.mdx +1 -1
- package/docs/core-concepts/storage/sdk.mdx +25 -25
- package/docs/docs.json +14 -6
- package/docs/favicon.png +0 -0
- package/docs/favicon.svg +3 -18
- package/docs/images/changelog/dec-2025/apple-oauth.mp4 +0 -0
- package/docs/images/changelog/dec-2025/moreModels.png +0 -0
- package/docs/images/changelog/dec-2025/multi-region.webp +0 -0
- package/docs/images/changelog/dec-2025/postgres-connection.webp +0 -0
- package/docs/images/changelog/dec-2025/realtime2.png +0 -0
- package/docs/images/mcp-setup/CC-MCP-1.mp4 +0 -0
- package/docs/images/mcp-setup/CC-MCP-2.mp4 +0 -0
- package/docs/images/mcp-setup/Cursor-MCP-1.mp4 +0 -0
- package/docs/images/mcp-setup/Cursor-MCP-2.mp4 +0 -0
- package/docs/images/mcp-setup/Cursor-MCP-3.mp4 +0 -0
- package/docs/images/mcp-setup/claude-code-connect.png +0 -0
- package/docs/images/mcp-setup/cline-1.png +0 -0
- package/docs/images/mcp-setup/cline-2.png +0 -0
- package/docs/images/mcp-setup/cline-3.png +0 -0
- package/docs/images/mcp-setup/connect-project.png +0 -0
- package/docs/images/mcp-setup/copilot-1.png +0 -0
- package/docs/images/mcp-setup/copilot-2.png +0 -0
- package/docs/images/mcp-setup/copilot-3.png +0 -0
- package/docs/images/mcp-setup/mcp-json-1.png +0 -0
- package/docs/images/mcp-setup/mcp-json-2.png +0 -0
- package/docs/images/mcp-setup/qoder-1.png +0 -0
- package/docs/images/mcp-setup/qoder-2.png +0 -0
- package/docs/images/mcp-setup/roocode-1.png +0 -0
- package/docs/images/mcp-setup/roocode-2.png +0 -0
- package/docs/images/mcp-setup/trae-1.png +0 -0
- package/docs/images/mcp-setup/trae-2.png +0 -0
- package/docs/images/mcp-setup/trae-3.png +0 -0
- package/docs/images/mcp-setup/trae-4.png +0 -0
- package/docs/images/mcp-setup/trae-5.png +0 -0
- package/docs/images/mcp-setup/windsurf-1.png +0 -0
- package/docs/images/mcp-setup/windsurf-2.png +0 -0
- package/docs/insforge-instructions-sdk.md +7 -3
- package/docs/introduction.mdx +9 -8
- package/docs/mcp-setup.mdx +332 -0
- package/docs/oauth-server.mdx +563 -0
- package/docs/partnership.mdx +79 -10
- package/docs/quickstart.mdx +1 -1
- package/docs/vscode-extension.mdx +74 -0
- package/eslint.config.js +1 -0
- package/examples/response-examples.md +1 -1
- package/frontend/package.json +1 -1
- package/frontend/src/App.tsx +8 -3
- package/frontend/src/assets/logos/antigravity.svg +1 -0
- package/frontend/src/assets/logos/copilot.svg +10 -0
- package/frontend/src/assets/logos/deepseek.svg +139 -0
- package/frontend/src/assets/logos/kiro.svg +9 -0
- package/frontend/src/assets/logos/qoder.svg +4 -0
- package/frontend/src/assets/logos/qwen.svg +15 -0
- package/frontend/src/components/CodeBlock.tsx +2 -2
- package/frontend/src/components/ConnectCTA.tsx +3 -2
- package/frontend/src/components/datagrid/DataGrid.tsx +90 -62
- package/frontend/src/components/datagrid/datagridTypes.tsx +2 -1
- package/frontend/src/components/datagrid/index.ts +1 -1
- package/frontend/src/components/index.ts +0 -1
- package/frontend/src/components/layout/AppHeader.tsx +4 -27
- package/frontend/src/components/layout/AppSidebar.tsx +85 -100
- package/frontend/src/components/layout/Layout.tsx +34 -32
- package/frontend/src/components/layout/PrimaryMenu.tsx +12 -4
- package/frontend/src/components/radix/Select.tsx +151 -151
- package/frontend/src/features/ai/components/AIConfigCard.tsx +200 -200
- package/frontend/src/features/ai/components/AIEmptyState.tsx +23 -23
- package/frontend/src/features/ai/components/ModalityFilterSidebar.tsx +102 -101
- package/frontend/src/features/ai/components/ModelSelectionDialog.tsx +135 -135
- package/frontend/src/features/ai/components/ModelSelectionGrid.tsx +51 -51
- package/frontend/src/features/ai/components/SystemPromptDialog.tsx +118 -118
- package/frontend/src/features/ai/components/index.ts +6 -6
- package/frontend/src/features/ai/helpers.ts +147 -141
- package/frontend/src/features/ai/pages/AIPage.tsx +166 -166
- package/frontend/src/features/auth/components/AuthPreview.tsx +96 -96
- package/frontend/src/features/auth/components/UsersDataGrid.tsx +55 -31
- package/frontend/src/features/auth/components/index.ts +5 -5
- package/frontend/src/features/auth/pages/AuthMethodsPage.tsx +275 -275
- package/frontend/src/features/dashboard/pages/DashboardPage.tsx +1 -1
- package/frontend/src/features/database/components/DatabaseDataGrid.tsx +0 -2
- package/frontend/src/features/database/components/ForeignKeyCell.tsx +38 -11
- package/frontend/src/features/database/components/ForeignKeyPopover.tsx +18 -8
- package/frontend/src/features/database/components/LinkRecordModal.tsx +61 -13
- package/frontend/src/features/database/components/RecordFormField.tsx +1 -1
- package/frontend/src/features/database/components/TableSidebar.tsx +0 -3
- package/frontend/src/features/database/components/TablesEmptyState.tsx +1 -1
- package/frontend/src/features/database/components/TemplatePreview.tsx +1 -2
- package/frontend/src/features/database/constants.ts +16 -28
- package/frontend/src/features/database/hooks/useCSVImport.ts +3 -2
- package/frontend/src/features/database/hooks/useRawSQL.ts +3 -2
- package/frontend/src/features/database/hooks/useTables.ts +5 -7
- package/frontend/src/features/database/pages/FunctionsPage.tsx +0 -5
- package/frontend/src/features/database/pages/IndexesPage.tsx +0 -5
- package/frontend/src/features/database/pages/PoliciesPage.tsx +0 -5
- package/frontend/src/features/database/pages/SQLEditorPage.tsx +2 -2
- package/frontend/src/features/database/pages/TriggersPage.tsx +0 -5
- package/frontend/src/features/database/services/advance.service.ts +1 -15
- package/frontend/src/features/database/services/record.service.ts +4 -20
- package/frontend/src/features/database/services/table.service.ts +1 -4
- package/frontend/src/features/database/templates/ai-chatbot.ts +6 -6
- package/frontend/src/features/database/templates/ecommerce-platform.ts +2 -2
- package/frontend/src/features/database/templates/instagram-clone.ts +10 -10
- package/frontend/src/features/database/templates/notion-clone.ts +8 -8
- package/frontend/src/features/database/templates/reddit-clone.ts +10 -10
- package/frontend/src/features/deployments/components/DeploymentRow.tsx +93 -0
- package/frontend/src/features/deployments/components/DeploymentsEmptyState.tsx +15 -0
- package/frontend/src/features/deployments/hooks/useDeployments.ts +157 -0
- package/frontend/src/features/deployments/pages/DeploymentsPage.tsx +318 -0
- package/frontend/src/features/deployments/services/deployments.service.ts +63 -0
- package/frontend/src/features/functions/components/FunctionRow.tsx +72 -72
- package/frontend/src/features/functions/components/FunctionsSidebar.tsx +56 -56
- package/frontend/src/features/functions/components/SecretRow.tsx +3 -3
- package/frontend/src/features/functions/components/index.ts +5 -5
- package/frontend/src/features/functions/hooks/useFunctions.ts +5 -4
- package/frontend/src/features/functions/hooks/useSecrets.ts +6 -9
- package/frontend/src/features/functions/pages/SecretsPage.tsx +118 -118
- package/frontend/src/features/functions/services/function.service.ts +8 -25
- package/frontend/src/features/functions/services/secret.service.ts +23 -41
- package/frontend/src/features/login/pages/CloudLoginPage.tsx +125 -118
- package/frontend/src/features/logs/components/LogDetailPanel.tsx +41 -0
- package/frontend/src/features/logs/components/LogsDataGrid.tsx +32 -1
- package/frontend/src/features/logs/components/index.ts +1 -0
- package/frontend/src/features/logs/pages/LogsPage.tsx +36 -6
- package/frontend/src/features/onboard/components/ApiCredentialsSection.tsx +59 -0
- package/frontend/src/features/onboard/components/ConnectionStringSection.tsx +180 -0
- package/frontend/src/features/onboard/components/McpConnectionSection.tsx +159 -0
- package/frontend/src/features/onboard/components/OnboardingController.tsx +68 -0
- package/frontend/src/features/onboard/components/OnboardingModal.tsx +121 -267
- package/frontend/src/features/onboard/components/ShowPasswordButton.tsx +21 -0
- package/frontend/src/features/onboard/components/index.ts +9 -4
- package/frontend/src/features/onboard/components/mcp/CursorDeeplinkGenerator.tsx +1 -1
- package/frontend/src/features/onboard/components/mcp/QoderDeeplinkGenerator.tsx +36 -0
- package/frontend/src/features/onboard/components/mcp/helpers.tsx +123 -98
- package/frontend/src/features/onboard/components/mcp/index.ts +4 -3
- package/frontend/src/features/onboard/index.ts +17 -13
- package/frontend/src/features/settings/pages/SettingsPage.tsx +349 -0
- package/frontend/src/features/visualizer/components/AuthNode.tsx +4 -4
- package/frontend/src/features/visualizer/components/SchemaVisualizer.tsx +21 -8
- package/frontend/src/features/visualizer/pages/VisualizerPage.tsx +10 -1
- package/frontend/src/index.css +249 -249
- package/frontend/src/lib/contexts/ModalContext.tsx +35 -0
- package/frontend/src/lib/hooks/useMetadata.ts +45 -1
- package/frontend/src/lib/hooks/useModal.tsx +2 -0
- package/frontend/src/lib/routing/AppRoutes.tsx +103 -99
- package/frontend/src/lib/services/metadata.service.ts +20 -3
- package/frontend/src/lib/utils/menuItems.ts +223 -207
- package/frontend/src/lib/utils/utils.ts +196 -196
- package/functions/server.ts +315 -315
- package/functions/worker-template.js +1 -1
- package/openapi/ai.yaml +115 -5
- package/openapi/auth.yaml +97 -17
- package/openapi/logs.yaml +0 -2
- package/openapi/metadata.yaml +0 -2
- package/openapi/records.yaml +21 -21
- package/openapi/tables.yaml +1 -2
- package/package.json +1 -1
- package/shared-schemas/package.json +1 -1
- package/shared-schemas/src/ai-api.schema.ts +251 -143
- package/shared-schemas/src/ai.schema.ts +63 -63
- package/shared-schemas/src/auth-api.schema.ts +34 -6
- package/shared-schemas/src/auth.schema.ts +17 -10
- package/shared-schemas/src/cloud-events.schema.ts +26 -0
- package/shared-schemas/src/deployments-api.schema.ts +55 -0
- package/shared-schemas/src/deployments.schema.ts +30 -0
- package/shared-schemas/src/docs.schema.ts +8 -2
- package/shared-schemas/src/email-api.schema.ts +30 -30
- package/shared-schemas/src/functions-api.schema.ts +13 -4
- package/shared-schemas/src/functions.schema.ts +1 -1
- package/shared-schemas/src/index.ts +22 -18
- package/shared-schemas/src/metadata.schema.ts +30 -4
- package/shared-schemas/src/secrets-api.schema.ts +44 -0
- package/shared-schemas/src/secrets.schema.ts +15 -0
- package/zeabur/README.md +13 -0
- package/zeabur/template.yml +20 -51
- package/backend/src/types/profile.ts +0 -55
- 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
|
+
}
|