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
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
} from '@insforge/shared-schemas';
|
|
10
10
|
import logger from '@/utils/logger.js';
|
|
11
11
|
import { ERROR_CODES } from '@/types/error-constants.js';
|
|
12
|
-
import { parseSQLStatements } from '@/utils/sql-parser.js';
|
|
12
|
+
import { parseSQLStatements, checkAuthSchemaOperations } from '@/utils/sql-parser.js';
|
|
13
13
|
import { validateTableName } from '@/utils/validations.js';
|
|
14
14
|
import format from 'pg-format';
|
|
15
15
|
import { parse } from 'csv-parse/sync';
|
|
@@ -66,28 +66,26 @@ export class DatabaseAdvanceService {
|
|
|
66
66
|
/**
|
|
67
67
|
* Sanitize query with strict or relaxed mode
|
|
68
68
|
*
|
|
69
|
-
*
|
|
69
|
+
* Blocks:
|
|
70
70
|
* - DROP DATABASE, CREATE DATABASE, ALTER DATABASE
|
|
71
71
|
* - pg_catalog and information_schema access
|
|
72
|
+
* - DELETE operations on auth schema (prevents user deletion via raw SQL)
|
|
73
|
+
* - TRUNCATE operations on auth schema (prevents mass user deletion)
|
|
74
|
+
* - DROP operations on auth schema (prevents destruction of tables, indexes, triggers, functions, views, sequences, schemas, policies, types, domains)
|
|
72
75
|
*
|
|
73
|
-
*
|
|
74
|
-
* -
|
|
75
|
-
* -
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
* - SELECT and INSERT into system tables and users table
|
|
79
|
-
* RELAXED MODE blocks:
|
|
80
|
-
* - UPDATE/DELETE/DROP/CREATE/ALTER system tables
|
|
81
|
-
* - UPDATE/DELETE/DROP/RENAME users table
|
|
76
|
+
* Allows:
|
|
77
|
+
* - SELECT queries on auth schema (for reading user data)
|
|
78
|
+
* - INSERT operations on auth schema (for test users)
|
|
79
|
+
* - CREATE TRIGGER on auth tables (for automatic profile creation, etc.)
|
|
80
|
+
* - Other DDL operations on auth schema (ALTER TABLE for indexes, etc.)
|
|
82
81
|
*/
|
|
83
|
-
sanitizeQuery(query: string,
|
|
84
|
-
//
|
|
82
|
+
sanitizeQuery(query: string, _mode: 'strict' | 'relaxed' = 'strict'): string {
|
|
83
|
+
// Block database-level operations
|
|
85
84
|
const dangerousPatterns = [
|
|
86
85
|
/DROP\s+DATABASE/i,
|
|
87
86
|
/CREATE\s+DATABASE/i,
|
|
88
87
|
/ALTER\s+DATABASE/i,
|
|
89
88
|
/pg_catalog/i,
|
|
90
|
-
/information_schema/i,
|
|
91
89
|
];
|
|
92
90
|
|
|
93
91
|
for (const pattern of dangerousPatterns) {
|
|
@@ -96,47 +94,13 @@ export class DatabaseAdvanceService {
|
|
|
96
94
|
}
|
|
97
95
|
}
|
|
98
96
|
|
|
99
|
-
//
|
|
100
|
-
const
|
|
101
|
-
if (
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Check for DROP or RENAME operations on 'users' table
|
|
110
|
-
const usersTablePattern =
|
|
111
|
-
/(?:^|\n|;)\s*(?:DROP\s+(?:TABLE\s+)?(?:IF\s+EXISTS\s+)?(?:\w+\.)?["']?users["']?|ALTER\s+TABLE\s+(?:IF\s+EXISTS\s+)?(?:\w+\.)?["']?users["']?\s+RENAME\s+TO)/im;
|
|
112
|
-
if (usersTablePattern.test(query)) {
|
|
113
|
-
throw new AppError('Cannot drop or rename the users table', 403, ERROR_CODES.FORBIDDEN);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
if (mode === 'strict') {
|
|
117
|
-
// Check for system table operations (tables starting with underscore)
|
|
118
|
-
// This pattern checks each statement in multi-statement queries, including schema-qualified names
|
|
119
|
-
const systemTablePattern =
|
|
120
|
-
/(?:^|\n|;)\s*(?:CREATE|ALTER|DROP|INSERT\s+INTO|UPDATE|DELETE\s+FROM|TRUNCATE)\s+(?:TABLE\s+)?(?:IF\s+(?:NOT\s+)?EXISTS\s+)?(?:\w+\.)?["']?_\w+/im;
|
|
121
|
-
if (systemTablePattern.test(query)) {
|
|
122
|
-
throw new AppError(
|
|
123
|
-
'Cannot modify or create system tables (tables starting with underscore)',
|
|
124
|
-
403,
|
|
125
|
-
ERROR_CODES.FORBIDDEN
|
|
126
|
-
);
|
|
127
|
-
}
|
|
128
|
-
} else {
|
|
129
|
-
// Relaxed mode: Allow only SELECT and INSERT into system tables and users table
|
|
130
|
-
// Block UPDATE, DELETE, DROP, CREATE, ALTER, TRUNCATE
|
|
131
|
-
const systemTableDestructivePattern =
|
|
132
|
-
/(?:^|\n|;)\s*(?:CREATE|ALTER|DROP|TRUNCATE|UPDATE|DELETE\s+FROM)\s+(?:TABLE\s+)?(?:IF\s+(?:NOT\s+)?EXISTS\s+)?(?:\w+\.)?["']?_\w+/im;
|
|
133
|
-
if (systemTableDestructivePattern.test(query)) {
|
|
134
|
-
throw new AppError(
|
|
135
|
-
'Cannot UPDATE/DELETE/DROP/CREATE/ALTER system tables (tables starting with underscore)',
|
|
136
|
-
403,
|
|
137
|
-
ERROR_CODES.FORBIDDEN
|
|
138
|
-
);
|
|
139
|
-
}
|
|
97
|
+
// Use parser-based check for auth schema operations
|
|
98
|
+
const authError = checkAuthSchemaOperations(query);
|
|
99
|
+
if (authError) {
|
|
100
|
+
logger.warn('Blocked operation on auth schema', {
|
|
101
|
+
query: query.substring(0, 100),
|
|
102
|
+
});
|
|
103
|
+
throw new AppError(authError, 403, ERROR_CODES.FORBIDDEN);
|
|
140
104
|
}
|
|
141
105
|
|
|
142
106
|
return query;
|
|
@@ -30,8 +30,6 @@ const reservedColumns = {
|
|
|
30
30
|
updated_at: ColumnType.DATETIME,
|
|
31
31
|
};
|
|
32
32
|
|
|
33
|
-
const userTableFrozenColumns = ['nickname', 'avatar_url'];
|
|
34
|
-
|
|
35
33
|
const SAFE_FUNCS = new Set(['now()', 'gen_random_uuid()']);
|
|
36
34
|
|
|
37
35
|
function getSafeDollarQuotedLiteral(s: string) {
|
|
@@ -105,7 +103,6 @@ export class DatabaseTableService {
|
|
|
105
103
|
FROM information_schema.tables
|
|
106
104
|
WHERE table_schema = 'public'
|
|
107
105
|
AND table_type = 'BASE TABLE'
|
|
108
|
-
AND table_name NOT LIKE '\\_%'
|
|
109
106
|
`
|
|
110
107
|
);
|
|
111
108
|
|
|
@@ -122,15 +119,6 @@ export class DatabaseTableService {
|
|
|
122
119
|
): Promise<CreateTableResponse> {
|
|
123
120
|
// Validate table name
|
|
124
121
|
validateIdentifier(table_name, 'table');
|
|
125
|
-
// Prevent creation of system tables
|
|
126
|
-
if (table_name.startsWith('_')) {
|
|
127
|
-
throw new AppError(
|
|
128
|
-
'Cannot create system tables',
|
|
129
|
-
403,
|
|
130
|
-
ERROR_CODES.FORBIDDEN,
|
|
131
|
-
'Table names starting with underscore are reserved for system tables'
|
|
132
|
-
);
|
|
133
|
-
}
|
|
134
122
|
|
|
135
123
|
// Filter out reserved fields with matching types, throw error for mismatched types
|
|
136
124
|
const validatedColumns = this.validateReservedFields(columns);
|
|
@@ -245,7 +233,7 @@ export class DatabaseTableService {
|
|
|
245
233
|
`
|
|
246
234
|
CREATE TRIGGER ${this.quoteIdentifier(table_name + '_update_timestamp')}
|
|
247
235
|
BEFORE UPDATE ON ${this.quoteIdentifier(table_name)}
|
|
248
|
-
FOR EACH ROW EXECUTE FUNCTION
|
|
236
|
+
FOR EACH ROW EXECUTE FUNCTION system.update_updated_at();
|
|
249
237
|
`
|
|
250
238
|
);
|
|
251
239
|
|
|
@@ -304,6 +292,7 @@ export class DatabaseTableService {
|
|
|
304
292
|
SELECT
|
|
305
293
|
column_name,
|
|
306
294
|
data_type,
|
|
295
|
+
udt_name,
|
|
307
296
|
is_nullable,
|
|
308
297
|
column_default,
|
|
309
298
|
character_maximum_length
|
|
@@ -374,17 +363,21 @@ export class DatabaseTableService {
|
|
|
374
363
|
|
|
375
364
|
return {
|
|
376
365
|
tableName: table,
|
|
377
|
-
columns: columns.map((col: ColumnInfo) =>
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
366
|
+
columns: columns.map((col: ColumnInfo) => {
|
|
367
|
+
// For USER-DEFINED types (extensions like pgvector), use udt_name
|
|
368
|
+
const effectiveType = col.data_type === 'USER-DEFINED' ? col.udt_name : col.data_type;
|
|
369
|
+
return {
|
|
370
|
+
columnName: col.column_name,
|
|
371
|
+
type: convertSqlTypeToColumnType(effectiveType),
|
|
372
|
+
isNullable: col.is_nullable === 'YES',
|
|
373
|
+
isPrimaryKey: pkSet.has(col.column_name),
|
|
374
|
+
isUnique: pkSet.has(col.column_name) || uniqueSet.has(col.column_name),
|
|
375
|
+
defaultValue: this.parseDefaultValue(col.column_default),
|
|
376
|
+
...(foreignKeyMap.has(col.column_name) && {
|
|
377
|
+
foreignKey: foreignKeyMap.get(col.column_name),
|
|
378
|
+
}),
|
|
379
|
+
};
|
|
380
|
+
}),
|
|
388
381
|
recordCount: row_count,
|
|
389
382
|
};
|
|
390
383
|
} finally {
|
|
@@ -402,16 +395,6 @@ export class DatabaseTableService {
|
|
|
402
395
|
const { addColumns, dropColumns, updateColumns, addForeignKeys, dropForeignKeys, renameTable } =
|
|
403
396
|
operations;
|
|
404
397
|
|
|
405
|
-
// Prevent modification of system tables
|
|
406
|
-
if (tableName.startsWith('_')) {
|
|
407
|
-
throw new AppError(
|
|
408
|
-
'System tables cannot be modified',
|
|
409
|
-
403,
|
|
410
|
-
ERROR_CODES.DATABASE_FORBIDDEN,
|
|
411
|
-
'System tables cannot be modified. System tables are prefixed with underscore.'
|
|
412
|
-
);
|
|
413
|
-
}
|
|
414
|
-
|
|
415
398
|
const client = await this.getPool().connect();
|
|
416
399
|
try {
|
|
417
400
|
// Check if table exists
|
|
@@ -492,14 +475,6 @@ export class DatabaseTableService {
|
|
|
492
475
|
`You cannot drop the system column '${col}'`
|
|
493
476
|
);
|
|
494
477
|
}
|
|
495
|
-
if (tableName === 'users' && userTableFrozenColumns.includes(col)) {
|
|
496
|
-
throw new AppError(
|
|
497
|
-
'cannot drop frozen users columns',
|
|
498
|
-
403,
|
|
499
|
-
ERROR_CODES.FORBIDDEN,
|
|
500
|
-
`You cannot drop the frozen users column '${col}'`
|
|
501
|
-
);
|
|
502
|
-
}
|
|
503
478
|
await client.query(
|
|
504
479
|
`
|
|
505
480
|
ALTER TABLE ${safeTableName}
|
|
@@ -522,14 +497,6 @@ export class DatabaseTableService {
|
|
|
522
497
|
`You cannot update the system column '${column.columnName}'`
|
|
523
498
|
);
|
|
524
499
|
}
|
|
525
|
-
if (tableName === 'users' && userTableFrozenColumns.includes(column.columnName)) {
|
|
526
|
-
throw new AppError(
|
|
527
|
-
'cannot update frozen user columns',
|
|
528
|
-
403,
|
|
529
|
-
ERROR_CODES.FORBIDDEN,
|
|
530
|
-
`You cannot update the frozen users column '${column.columnName}'`
|
|
531
|
-
);
|
|
532
|
-
}
|
|
533
500
|
|
|
534
501
|
// Handle default value changes
|
|
535
502
|
if (column.defaultValue !== undefined) {
|
|
@@ -620,19 +587,6 @@ export class DatabaseTableService {
|
|
|
620
587
|
}
|
|
621
588
|
|
|
622
589
|
if (renameTable && renameTable.newTableName) {
|
|
623
|
-
if (tableName === 'users') {
|
|
624
|
-
throw new AppError('Cannot rename users table', 403, ERROR_CODES.FORBIDDEN);
|
|
625
|
-
}
|
|
626
|
-
// Prevent renaming to system tables
|
|
627
|
-
if (renameTable.newTableName.startsWith('_')) {
|
|
628
|
-
throw new AppError(
|
|
629
|
-
'Cannot rename to system table',
|
|
630
|
-
403,
|
|
631
|
-
ERROR_CODES.FORBIDDEN,
|
|
632
|
-
'Table names starting with underscore are reserved for system tables'
|
|
633
|
-
);
|
|
634
|
-
}
|
|
635
|
-
|
|
636
590
|
const safeNewTableName = this.quoteIdentifier(renameTable.newTableName);
|
|
637
591
|
// Rename the table
|
|
638
592
|
await client.query(
|
|
@@ -669,19 +623,6 @@ export class DatabaseTableService {
|
|
|
669
623
|
* Delete a table
|
|
670
624
|
*/
|
|
671
625
|
async deleteTable(table: string): Promise<DeleteTableResponse> {
|
|
672
|
-
// Prevent deletion of system tables
|
|
673
|
-
if (table.startsWith('_')) {
|
|
674
|
-
throw new AppError(
|
|
675
|
-
'System tables cannot be deleted',
|
|
676
|
-
403,
|
|
677
|
-
ERROR_CODES.DATABASE_FORBIDDEN,
|
|
678
|
-
'System tables cannot be deleted. System tables are prefixed with underscore.'
|
|
679
|
-
);
|
|
680
|
-
}
|
|
681
|
-
if (table === 'users') {
|
|
682
|
-
throw new AppError('Cannot delete users table', 403, ERROR_CODES.DATABASE_FORBIDDEN);
|
|
683
|
-
}
|
|
684
|
-
|
|
685
626
|
const client = await this.getPool().connect();
|
|
686
627
|
try {
|
|
687
628
|
await client.query(`DROP TABLE IF EXISTS ${this.quoteIdentifier(table)} CASCADE`);
|
|
@@ -712,6 +653,15 @@ export class DatabaseTableService {
|
|
|
712
653
|
return `"${identifier.replace(/"/g, '""')}"`;
|
|
713
654
|
}
|
|
714
655
|
|
|
656
|
+
// Quote a table reference, with special handling for auth.users
|
|
657
|
+
private quoteTableReference(tableRef: string): string {
|
|
658
|
+
// Only allow auth.users as a cross-schema reference
|
|
659
|
+
if (tableRef === 'auth.users') {
|
|
660
|
+
return '"auth"."users"';
|
|
661
|
+
}
|
|
662
|
+
return this.quoteIdentifier(tableRef);
|
|
663
|
+
}
|
|
664
|
+
|
|
715
665
|
private validateReservedFields(columns: ColumnSchema[]): ColumnSchema[] {
|
|
716
666
|
return columns.filter((col: ColumnSchema) => {
|
|
717
667
|
const reservedType = reservedColumns[col.columnName as keyof typeof reservedColumns];
|
|
@@ -743,14 +693,16 @@ export class DatabaseTableService {
|
|
|
743
693
|
}
|
|
744
694
|
// Store foreign_key in a const to avoid repeated non-null assertions
|
|
745
695
|
const fk = col.foreignKey;
|
|
746
|
-
|
|
696
|
+
// Use "auth_users" in constraint name for auth.users references
|
|
697
|
+
const safeTableName = fk.referenceTable === 'auth.users' ? 'auth_users' : fk.referenceTable;
|
|
698
|
+
const constraintName = `fk_${col.columnName}_${safeTableName}_${fk.referenceColumn}`;
|
|
747
699
|
const onDelete = fk.onDelete || 'RESTRICT';
|
|
748
700
|
const onUpdate = fk.onUpdate || 'RESTRICT';
|
|
749
701
|
|
|
750
702
|
if (include_source_column) {
|
|
751
|
-
return `CONSTRAINT ${this.quoteIdentifier(constraintName)} FOREIGN KEY (${this.quoteIdentifier(col.columnName)}) REFERENCES ${this.
|
|
703
|
+
return `CONSTRAINT ${this.quoteIdentifier(constraintName)} FOREIGN KEY (${this.quoteIdentifier(col.columnName)}) REFERENCES ${this.quoteTableReference(fk.referenceTable)}(${this.quoteIdentifier(fk.referenceColumn)}) ON DELETE ${onDelete} ON UPDATE ${onUpdate}`;
|
|
752
704
|
} else {
|
|
753
|
-
return `CONSTRAINT ${this.quoteIdentifier(constraintName)} REFERENCES ${this.
|
|
705
|
+
return `CONSTRAINT ${this.quoteIdentifier(constraintName)} REFERENCES ${this.quoteTableReference(fk.referenceTable)}(${this.quoteIdentifier(fk.referenceColumn)}) ON DELETE ${onDelete} ON UPDATE ${onUpdate}`;
|
|
754
706
|
}
|
|
755
707
|
}
|
|
756
708
|
|
|
@@ -760,6 +712,7 @@ export class DatabaseTableService {
|
|
|
760
712
|
SELECT
|
|
761
713
|
tc.constraint_name,
|
|
762
714
|
kcu.column_name as from_column,
|
|
715
|
+
ccu.table_schema AS foreign_schema,
|
|
763
716
|
ccu.table_name AS foreign_table,
|
|
764
717
|
ccu.column_name AS foreign_column,
|
|
765
718
|
rc.delete_rule as on_delete,
|
|
@@ -770,7 +723,6 @@ export class DatabaseTableService {
|
|
|
770
723
|
AND tc.table_schema = kcu.table_schema
|
|
771
724
|
JOIN information_schema.constraint_column_usage AS ccu
|
|
772
725
|
ON ccu.constraint_name = tc.constraint_name
|
|
773
|
-
AND ccu.table_schema = tc.table_schema
|
|
774
726
|
JOIN information_schema.referential_constraints AS rc
|
|
775
727
|
ON rc.constraint_name = tc.constraint_name
|
|
776
728
|
AND rc.constraint_schema = tc.table_schema
|
|
@@ -785,13 +737,14 @@ export class DatabaseTableService {
|
|
|
785
737
|
// Create a map of column names to their foreign key info
|
|
786
738
|
const foreignKeyMap = new Map<string, ForeignKeyInfo>();
|
|
787
739
|
foreignKeys.forEach((fk: ForeignKeyRow) => {
|
|
788
|
-
if (
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
740
|
+
// Prefix table name with schema if not public (e.g., "auth.users")
|
|
741
|
+
const referenceTable =
|
|
742
|
+
fk.foreign_schema !== 'public'
|
|
743
|
+
? `${fk.foreign_schema}.${fk.foreign_table}`
|
|
744
|
+
: fk.foreign_table;
|
|
792
745
|
foreignKeyMap.set(fk.from_column, {
|
|
793
746
|
constraint_name: fk.constraint_name,
|
|
794
|
-
referenceTable
|
|
747
|
+
referenceTable,
|
|
795
748
|
referenceColumn: fk.foreign_column,
|
|
796
749
|
onDelete: fk.on_delete as OnDeleteActionSchema,
|
|
797
750
|
onUpdate: fk.on_update as OnUpdateActionSchema,
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import axios, { AxiosResponse } from 'axios';
|
|
2
|
+
import http from 'http';
|
|
3
|
+
import https from 'https';
|
|
4
|
+
import { TokenManager } from '@/infra/security/token.manager.js';
|
|
5
|
+
import { SecretService } from '@/services/secrets/secret.service.js';
|
|
6
|
+
import logger from '@/utils/logger.js';
|
|
7
|
+
|
|
8
|
+
const postgrestUrl = process.env.POSTGREST_BASE_URL || 'http://localhost:5430';
|
|
9
|
+
|
|
10
|
+
// Connection pooling for PostgREST
|
|
11
|
+
const httpAgent = new http.Agent({
|
|
12
|
+
keepAlive: true,
|
|
13
|
+
keepAliveMsecs: 5000,
|
|
14
|
+
maxSockets: 20,
|
|
15
|
+
maxFreeSockets: 5,
|
|
16
|
+
timeout: 10000,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const httpsAgent = new https.Agent({
|
|
20
|
+
keepAlive: true,
|
|
21
|
+
keepAliveMsecs: 5000,
|
|
22
|
+
maxSockets: 20,
|
|
23
|
+
maxFreeSockets: 5,
|
|
24
|
+
timeout: 10000,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const postgrestAxios = axios.create({
|
|
28
|
+
httpAgent,
|
|
29
|
+
httpsAgent,
|
|
30
|
+
timeout: 10000,
|
|
31
|
+
maxRedirects: 0,
|
|
32
|
+
headers: {
|
|
33
|
+
Connection: 'keep-alive',
|
|
34
|
+
'Keep-Alive': 'timeout=5, max=10',
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export interface ProxyRequest {
|
|
39
|
+
method: string;
|
|
40
|
+
path: string;
|
|
41
|
+
query?: Record<string, unknown>;
|
|
42
|
+
headers?: Record<string, string | string[] | undefined>;
|
|
43
|
+
body?: unknown;
|
|
44
|
+
apiKey?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface ProxyResponse {
|
|
48
|
+
data: unknown;
|
|
49
|
+
status: number;
|
|
50
|
+
headers: Record<string, unknown>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Headers that should not be forwarded to the client
|
|
55
|
+
*/
|
|
56
|
+
const EXCLUDED_HEADERS = new Set([
|
|
57
|
+
'content-length',
|
|
58
|
+
'transfer-encoding',
|
|
59
|
+
'connection',
|
|
60
|
+
'content-encoding',
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
export class PostgrestProxyService {
|
|
64
|
+
private static instance: PostgrestProxyService;
|
|
65
|
+
private tokenManager = TokenManager.getInstance();
|
|
66
|
+
private secretService = SecretService.getInstance();
|
|
67
|
+
private adminToken: string;
|
|
68
|
+
|
|
69
|
+
private constructor() {
|
|
70
|
+
this.adminToken = this.tokenManager.generateAdminToken();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
public static getInstance(): PostgrestProxyService {
|
|
74
|
+
if (!PostgrestProxyService.instance) {
|
|
75
|
+
PostgrestProxyService.instance = new PostgrestProxyService();
|
|
76
|
+
}
|
|
77
|
+
return PostgrestProxyService.instance;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Filter headers for forwarding to client (excludes problematic ones)
|
|
82
|
+
*/
|
|
83
|
+
static filterHeaders(headers: Record<string, unknown>): Record<string, string> {
|
|
84
|
+
const filtered: Record<string, string> = {};
|
|
85
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
86
|
+
if (!EXCLUDED_HEADERS.has(key.toLowerCase()) && value !== undefined) {
|
|
87
|
+
filtered[key] = value as string;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return filtered;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Forward request to PostgREST with retry logic
|
|
95
|
+
*/
|
|
96
|
+
async forward(request: ProxyRequest): Promise<ProxyResponse> {
|
|
97
|
+
const targetUrl = `${postgrestUrl}${request.path}`;
|
|
98
|
+
|
|
99
|
+
const axiosConfig: {
|
|
100
|
+
method: string;
|
|
101
|
+
url: string;
|
|
102
|
+
params?: Record<string, unknown>;
|
|
103
|
+
headers: Record<string, string | string[] | undefined>;
|
|
104
|
+
data?: unknown;
|
|
105
|
+
} = {
|
|
106
|
+
method: request.method,
|
|
107
|
+
url: targetUrl,
|
|
108
|
+
params: request.query,
|
|
109
|
+
headers: {
|
|
110
|
+
...request.headers,
|
|
111
|
+
host: undefined,
|
|
112
|
+
'content-length': undefined,
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// Use admin token if valid API key provided
|
|
117
|
+
if (request.apiKey) {
|
|
118
|
+
const isValid = await this.secretService.verifyApiKey(request.apiKey);
|
|
119
|
+
if (isValid) {
|
|
120
|
+
axiosConfig.headers.authorization = `Bearer ${this.adminToken}`;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (request.body !== undefined) {
|
|
125
|
+
axiosConfig.data = request.body;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Retry logic
|
|
129
|
+
let response: AxiosResponse | undefined;
|
|
130
|
+
let lastError: unknown;
|
|
131
|
+
const maxRetries = 3;
|
|
132
|
+
|
|
133
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
134
|
+
try {
|
|
135
|
+
response = await postgrestAxios(axiosConfig);
|
|
136
|
+
break;
|
|
137
|
+
} catch (error) {
|
|
138
|
+
lastError = error;
|
|
139
|
+
const shouldRetry = axios.isAxiosError(error) && !error.response && attempt < maxRetries;
|
|
140
|
+
|
|
141
|
+
if (shouldRetry) {
|
|
142
|
+
logger.warn(`PostgREST request failed, retrying (attempt ${attempt}/${maxRetries})`, {
|
|
143
|
+
url: targetUrl,
|
|
144
|
+
errorCode: (error as NodeJS.ErrnoException).code,
|
|
145
|
+
message: (error as Error).message,
|
|
146
|
+
});
|
|
147
|
+
const backoffDelay = Math.min(200 * Math.pow(2.5, attempt - 1), 1000);
|
|
148
|
+
await new Promise((resolve) => setTimeout(resolve, backoffDelay));
|
|
149
|
+
} else {
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (!response) {
|
|
156
|
+
throw lastError || new Error('Failed to get response from PostgREST');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
data: response.data,
|
|
161
|
+
status: response.status,
|
|
162
|
+
headers: response.headers as Record<string, unknown>,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
}
|