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
@@ -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
- * BOTH MODES block:
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
- * STRICT MODE blocks:
74
- * - ALL operations on system tables (tables starting with _)
75
- * - DROP or RENAME operations on users table
76
- *
77
- * RELAXED MODE allows:
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, mode: 'strict' | 'relaxed' = 'strict'): string {
84
- // Both modes: Block database-level operations
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
- // Check for RENAME TO system table
100
- const renameToSystemTablePattern = /RENAME\s+TO\s+(?:\w+\.)?["']?_\w+/im;
101
- if (renameToSystemTablePattern.test(query)) {
102
- throw new AppError(
103
- 'Cannot rename tables to system table names (tables starting with underscore)',
104
- 403,
105
- ERROR_CODES.FORBIDDEN
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 update_updated_at_column();
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
- columnName: col.column_name,
379
- type: convertSqlTypeToColumnType(col.data_type),
380
- isNullable: col.is_nullable === 'YES',
381
- isPrimaryKey: pkSet.has(col.column_name),
382
- isUnique: pkSet.has(col.column_name) || uniqueSet.has(col.column_name),
383
- defaultValue: this.parseDefaultValue(col.column_default),
384
- ...(foreignKeyMap.has(col.column_name) && {
385
- foreignKey: foreignKeyMap.get(col.column_name),
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
- const constraintName = `fk_${col.columnName}_${fk.referenceTable}_${fk.referenceColumn}`;
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.quoteIdentifier(fk.referenceTable)}(${this.quoteIdentifier(fk.referenceColumn)}) ON DELETE ${onDelete} ON UPDATE ${onUpdate}`;
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.quoteIdentifier(fk.referenceTable)}(${this.quoteIdentifier(fk.referenceColumn)}) ON DELETE ${onDelete} ON UPDATE ${onUpdate}`;
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 (fk.foreign_table.startsWith('_')) {
789
- // hiden internal table.
790
- return;
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: fk.foreign_table,
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
+ }