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
|
@@ -109,7 +109,7 @@ export class StorageService {
|
|
|
109
109
|
// This query finds all files matching the pattern and extracts the counter number
|
|
110
110
|
const result = await this.getPool().query(
|
|
111
111
|
`
|
|
112
|
-
SELECT key FROM
|
|
112
|
+
SELECT key FROM storage.objects
|
|
113
113
|
WHERE bucket = $1
|
|
114
114
|
AND (key = $2 OR key LIKE $3)
|
|
115
115
|
`,
|
|
@@ -169,7 +169,7 @@ export class StorageService {
|
|
|
169
169
|
// Save metadata to database and return the timestamp in one operation
|
|
170
170
|
const result = await client.query(
|
|
171
171
|
`
|
|
172
|
-
INSERT INTO
|
|
172
|
+
INSERT INTO storage.objects (bucket, key, size, mime_type, uploaded_by)
|
|
173
173
|
VALUES ($1, $2, $3, $4, $5)
|
|
174
174
|
RETURNING uploaded_at as "uploadedAt"
|
|
175
175
|
`,
|
|
@@ -207,7 +207,7 @@ export class StorageService {
|
|
|
207
207
|
this.validateKey(key);
|
|
208
208
|
|
|
209
209
|
const result = await this.getPool().query(
|
|
210
|
-
'SELECT * FROM
|
|
210
|
+
'SELECT * FROM storage.objects WHERE bucket = $1 AND key = $2',
|
|
211
211
|
[bucket, key]
|
|
212
212
|
);
|
|
213
213
|
|
|
@@ -249,7 +249,7 @@ export class StorageService {
|
|
|
249
249
|
// Check permissions
|
|
250
250
|
if (!isAdmin) {
|
|
251
251
|
const fileResult = await client.query(
|
|
252
|
-
'SELECT uploaded_by FROM
|
|
252
|
+
'SELECT uploaded_by FROM storage.objects WHERE bucket = $1 AND key = $2',
|
|
253
253
|
[bucket, key]
|
|
254
254
|
);
|
|
255
255
|
|
|
@@ -273,10 +273,10 @@ export class StorageService {
|
|
|
273
273
|
await this.provider.deleteObject(bucket, key);
|
|
274
274
|
|
|
275
275
|
// Delete from database
|
|
276
|
-
const result = await client.query(
|
|
277
|
-
bucket,
|
|
278
|
-
key
|
|
279
|
-
|
|
276
|
+
const result = await client.query(
|
|
277
|
+
'DELETE FROM storage.objects WHERE bucket = $1 AND key = $2',
|
|
278
|
+
[bucket, key]
|
|
279
|
+
);
|
|
280
280
|
|
|
281
281
|
return result.rowCount !== null && result.rowCount > 0;
|
|
282
282
|
} finally {
|
|
@@ -295,8 +295,8 @@ export class StorageService {
|
|
|
295
295
|
|
|
296
296
|
const client = await this.getPool().connect();
|
|
297
297
|
try {
|
|
298
|
-
let query = 'SELECT * FROM
|
|
299
|
-
let countQuery = 'SELECT COUNT(*) as count FROM
|
|
298
|
+
let query = 'SELECT * FROM storage.objects WHERE bucket = $1';
|
|
299
|
+
let countQuery = 'SELECT COUNT(*) as count FROM storage.objects WHERE bucket = $1';
|
|
300
300
|
const params: (string | number)[] = [bucket];
|
|
301
301
|
let paramIndex = 2;
|
|
302
302
|
|
|
@@ -338,7 +338,7 @@ export class StorageService {
|
|
|
338
338
|
|
|
339
339
|
async isBucketPublic(bucket: string): Promise<boolean> {
|
|
340
340
|
const result = await this.getPool().query(
|
|
341
|
-
'SELECT public FROM
|
|
341
|
+
'SELECT public FROM storage.buckets WHERE name = $1',
|
|
342
342
|
[bucket]
|
|
343
343
|
);
|
|
344
344
|
return result.rows[0]?.public || false;
|
|
@@ -348,7 +348,7 @@ export class StorageService {
|
|
|
348
348
|
const client = await this.getPool().connect();
|
|
349
349
|
try {
|
|
350
350
|
// Check if bucket exists
|
|
351
|
-
const bucketResult = await client.query('SELECT name FROM
|
|
351
|
+
const bucketResult = await client.query('SELECT name FROM storage.buckets WHERE name = $1', [
|
|
352
352
|
bucket,
|
|
353
353
|
]);
|
|
354
354
|
|
|
@@ -356,9 +356,9 @@ export class StorageService {
|
|
|
356
356
|
throw new Error(`Bucket "${bucket}" does not exist`);
|
|
357
357
|
}
|
|
358
358
|
|
|
359
|
-
// Update bucket visibility in
|
|
359
|
+
// Update bucket visibility in storage.buckets table
|
|
360
360
|
await client.query(
|
|
361
|
-
'UPDATE
|
|
361
|
+
'UPDATE storage.buckets SET public = $1, updated_at = CURRENT_TIMESTAMP WHERE name = $2',
|
|
362
362
|
[isPublic, bucket]
|
|
363
363
|
);
|
|
364
364
|
|
|
@@ -370,9 +370,9 @@ export class StorageService {
|
|
|
370
370
|
}
|
|
371
371
|
|
|
372
372
|
async listBuckets(): Promise<StorageBucketSchema[]> {
|
|
373
|
-
// Get all buckets with their metadata from
|
|
373
|
+
// Get all buckets with their metadata from storage.buckets table
|
|
374
374
|
const result = await this.getPool().query(
|
|
375
|
-
'SELECT name, public, created_at as "createdAt" FROM
|
|
375
|
+
'SELECT name, public, created_at as "createdAt" FROM storage.buckets ORDER BY name'
|
|
376
376
|
);
|
|
377
377
|
|
|
378
378
|
return result.rows as StorageBucketSchema[];
|
|
@@ -384,7 +384,7 @@ export class StorageService {
|
|
|
384
384
|
const client = await this.getPool().connect();
|
|
385
385
|
try {
|
|
386
386
|
// Check if bucket already exists
|
|
387
|
-
const existing = await client.query('SELECT name FROM
|
|
387
|
+
const existing = await client.query('SELECT name FROM storage.buckets WHERE name = $1', [
|
|
388
388
|
bucket,
|
|
389
389
|
]);
|
|
390
390
|
|
|
@@ -392,8 +392,8 @@ export class StorageService {
|
|
|
392
392
|
throw new Error(`Bucket "${bucket}" already exists`);
|
|
393
393
|
}
|
|
394
394
|
|
|
395
|
-
// Insert bucket into
|
|
396
|
-
await client.query('INSERT INTO
|
|
395
|
+
// Insert bucket into storage.buckets table
|
|
396
|
+
await client.query('INSERT INTO storage.buckets (name, public) VALUES ($1, $2)', [
|
|
397
397
|
bucket,
|
|
398
398
|
isPublic,
|
|
399
399
|
]);
|
|
@@ -414,7 +414,7 @@ export class StorageService {
|
|
|
414
414
|
const client = await this.getPool().connect();
|
|
415
415
|
try {
|
|
416
416
|
// Check if bucket exists
|
|
417
|
-
const bucketResult = await client.query('SELECT name FROM
|
|
417
|
+
const bucketResult = await client.query('SELECT name FROM storage.buckets WHERE name = $1', [
|
|
418
418
|
bucket,
|
|
419
419
|
]);
|
|
420
420
|
|
|
@@ -425,8 +425,8 @@ export class StorageService {
|
|
|
425
425
|
// Delete bucket using backend (handles all files)
|
|
426
426
|
await this.provider.deleteBucket(bucket);
|
|
427
427
|
|
|
428
|
-
// Delete from storage table (cascade will handle
|
|
429
|
-
await client.query('DELETE FROM
|
|
428
|
+
// Delete from storage table (cascade will handle storage.objects entries)
|
|
429
|
+
await client.query('DELETE FROM storage.buckets WHERE name = $1', [bucket]);
|
|
430
430
|
|
|
431
431
|
// Update storage metadata
|
|
432
432
|
// Metadata is now updated on-demand
|
|
@@ -451,7 +451,7 @@ export class StorageService {
|
|
|
451
451
|
const client = await this.getPool().connect();
|
|
452
452
|
try {
|
|
453
453
|
// Check if bucket exists
|
|
454
|
-
const bucketResult = await client.query('SELECT name FROM
|
|
454
|
+
const bucketResult = await client.query('SELECT name FROM storage.buckets WHERE name = $1', [
|
|
455
455
|
bucket,
|
|
456
456
|
]);
|
|
457
457
|
|
|
@@ -503,7 +503,7 @@ export class StorageService {
|
|
|
503
503
|
try {
|
|
504
504
|
// Check if already confirmed
|
|
505
505
|
const existingResult = await client.query(
|
|
506
|
-
'SELECT key FROM
|
|
506
|
+
'SELECT key FROM storage.objects WHERE bucket = $1 AND key = $2',
|
|
507
507
|
[bucket, key]
|
|
508
508
|
);
|
|
509
509
|
|
|
@@ -514,7 +514,7 @@ export class StorageService {
|
|
|
514
514
|
// Save metadata to database and return the timestamp in one operation
|
|
515
515
|
const result = await client.query(
|
|
516
516
|
`
|
|
517
|
-
INSERT INTO
|
|
517
|
+
INSERT INTO storage.objects (bucket, key, size, mime_type, uploaded_by)
|
|
518
518
|
VALUES ($1, $2, $3, $4, $5)
|
|
519
519
|
RETURNING uploaded_at as "uploadedAt"
|
|
520
520
|
`,
|
|
@@ -548,9 +548,9 @@ export class StorageService {
|
|
|
548
548
|
* Get storage metadata
|
|
549
549
|
*/
|
|
550
550
|
async getMetadata(): Promise<StorageMetadataSchema> {
|
|
551
|
-
// Get storage buckets from
|
|
551
|
+
// Get storage buckets from storage.buckets table
|
|
552
552
|
const result = await this.getPool().query(
|
|
553
|
-
'SELECT name, public, created_at as "createdAt" FROM
|
|
553
|
+
'SELECT name, public, created_at as "createdAt" FROM storage.buckets ORDER BY name'
|
|
554
554
|
);
|
|
555
555
|
|
|
556
556
|
const storageBuckets = result.rows as StorageBucketSchema[];
|
|
@@ -572,7 +572,7 @@ export class StorageService {
|
|
|
572
572
|
try {
|
|
573
573
|
// Query to get object count for each bucket
|
|
574
574
|
const result = await this.getPool().query(
|
|
575
|
-
'SELECT bucket, COUNT(*) as count FROM
|
|
575
|
+
'SELECT bucket, COUNT(*) as count FROM storage.objects GROUP BY bucket'
|
|
576
576
|
);
|
|
577
577
|
|
|
578
578
|
const bucketCounts = result.rows as { bucket: string; count: string }[];
|
|
@@ -595,11 +595,11 @@ export class StorageService {
|
|
|
595
595
|
|
|
596
596
|
private async getStorageSizeInGB(): Promise<number> {
|
|
597
597
|
try {
|
|
598
|
-
// Query the
|
|
598
|
+
// Query the storage.objects table to sum all file sizes
|
|
599
599
|
const result = await this.getPool().query(
|
|
600
600
|
`
|
|
601
601
|
SELECT COALESCE(SUM(size), 0) as total_size
|
|
602
|
-
FROM
|
|
602
|
+
FROM storage.objects
|
|
603
603
|
`
|
|
604
604
|
);
|
|
605
605
|
|
|
@@ -54,7 +54,7 @@ export class UsageService {
|
|
|
54
54
|
async recordMCPUsage(toolName: string, success: boolean = true): Promise<{ created_at: string }> {
|
|
55
55
|
try {
|
|
56
56
|
const result = await this.getPool().query(
|
|
57
|
-
`INSERT INTO
|
|
57
|
+
`INSERT INTO system.mcp_usage (tool_name, success)
|
|
58
58
|
VALUES ($1, $2)
|
|
59
59
|
RETURNING created_at`,
|
|
60
60
|
[toolName, success]
|
|
@@ -75,7 +75,7 @@ export class UsageService {
|
|
|
75
75
|
try {
|
|
76
76
|
const result = await this.getPool().query(
|
|
77
77
|
`SELECT tool_name, success, created_at
|
|
78
|
-
FROM
|
|
78
|
+
FROM system.mcp_usage
|
|
79
79
|
WHERE success = $1
|
|
80
80
|
ORDER BY created_at DESC
|
|
81
81
|
LIMIT $2`,
|
|
@@ -98,7 +98,7 @@ export class UsageService {
|
|
|
98
98
|
// Get MCP tool usage count
|
|
99
99
|
const mcpResult = await this.getPool().query(
|
|
100
100
|
`SELECT COUNT(*) as count
|
|
101
|
-
FROM
|
|
101
|
+
FROM system.mcp_usage
|
|
102
102
|
WHERE success = true
|
|
103
103
|
AND created_at >= $1
|
|
104
104
|
AND created_at < $2`,
|
|
@@ -112,7 +112,7 @@ export class UsageService {
|
|
|
112
112
|
|
|
113
113
|
// Get total storage size
|
|
114
114
|
const storageResult = await this.getPool().query(
|
|
115
|
-
`SELECT COALESCE(SUM(size), 0) as total_size FROM
|
|
115
|
+
`SELECT COALESCE(SUM(size), 0) as total_size FROM storage.objects`
|
|
116
116
|
);
|
|
117
117
|
|
|
118
118
|
// Get AI usage breakdown by model (only billable metrics)
|
|
@@ -122,8 +122,8 @@ export class UsageService {
|
|
|
122
122
|
COALESCE(SUM(u.input_tokens), 0) as total_input_tokens,
|
|
123
123
|
COALESCE(SUM(u.output_tokens), 0) as total_output_tokens,
|
|
124
124
|
COALESCE(SUM(u.image_count), 0) as total_images
|
|
125
|
-
FROM
|
|
126
|
-
LEFT JOIN
|
|
125
|
+
FROM ai.usage u
|
|
126
|
+
LEFT JOIN ai.configs c ON u.config_id = c.id
|
|
127
127
|
WHERE u.created_at >= $1 AND u.created_at < $2
|
|
128
128
|
GROUP BY COALESCE(u.model_id, c.model_id)
|
|
129
129
|
ORDER BY (COALESCE(SUM(u.input_tokens), 0) + COALESCE(SUM(u.output_tokens), 0)) DESC`,
|
package/backend/src/types/ai.ts
CHANGED
|
@@ -11,6 +11,14 @@ export interface OpenRouterImageMessage {
|
|
|
11
11
|
};
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
export interface OpenRouterAudioMessage {
|
|
15
|
+
type: 'input_audio';
|
|
16
|
+
input_audio: {
|
|
17
|
+
data: string; // Base64-encoded audio data
|
|
18
|
+
format: string; // wav, mp3, aiff, aac, ogg, flac, m4a
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
14
22
|
// ============= OpenRouter API Types =============
|
|
15
23
|
|
|
16
24
|
export interface RawOpenRouterModel {
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
// Type definitions for database user records
|
|
2
|
+
|
|
2
3
|
export interface UserRecord {
|
|
3
4
|
id: string;
|
|
4
5
|
email: string;
|
|
5
|
-
|
|
6
|
+
profile: Record<string, unknown> | null;
|
|
7
|
+
metadata: Record<string, unknown> | null;
|
|
6
8
|
email_verified: boolean;
|
|
9
|
+
is_project_admin: boolean;
|
|
10
|
+
is_anonymous: boolean;
|
|
7
11
|
created_at: string;
|
|
8
12
|
updated_at: string;
|
|
9
13
|
password?: string;
|
|
@@ -102,6 +102,7 @@ export type ForeignKeyInfo = ForeignKeySchema & {
|
|
|
102
102
|
export interface ForeignKeyRow {
|
|
103
103
|
constraint_name: string;
|
|
104
104
|
from_column: string;
|
|
105
|
+
foreign_schema: string;
|
|
105
106
|
foreign_table: string;
|
|
106
107
|
foreign_column: string;
|
|
107
108
|
on_delete: string;
|
|
@@ -112,6 +113,7 @@ export interface ForeignKeyRow {
|
|
|
112
113
|
export interface ColumnInfo {
|
|
113
114
|
column_name: string;
|
|
114
115
|
data_type: string;
|
|
116
|
+
udt_name: string;
|
|
115
117
|
is_nullable: string;
|
|
116
118
|
column_default: string | null;
|
|
117
119
|
character_maximum_length: number | null;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Backend-only types for deployments
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Deployment status constants
|
|
5
|
+
* WAITING -> UPLOADING -> (Vercel statuses: QUEUED/BUILDING/READY/ERROR/CANCELED)
|
|
6
|
+
*/
|
|
7
|
+
export const DeploymentStatus = {
|
|
8
|
+
// InsForge internal statuses
|
|
9
|
+
WAITING: 'WAITING', // Record created, waiting for client to upload zip to S3
|
|
10
|
+
UPLOADING: 'UPLOADING', // Server is downloading from S3 and uploading to Vercel
|
|
11
|
+
// Vercel statuses (stored directly)
|
|
12
|
+
QUEUED: 'QUEUED',
|
|
13
|
+
BUILDING: 'BUILDING',
|
|
14
|
+
READY: 'READY',
|
|
15
|
+
ERROR: 'ERROR',
|
|
16
|
+
CANCELED: 'CANCELED',
|
|
17
|
+
} as const;
|
|
18
|
+
|
|
19
|
+
export type DeploymentStatusType = (typeof DeploymentStatus)[keyof typeof DeploymentStatus];
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Internal deployment record with Date objects (database returns Date, not string)
|
|
23
|
+
*/
|
|
24
|
+
export interface DeploymentRecord {
|
|
25
|
+
id: string;
|
|
26
|
+
providerDeploymentId: string | null; // Provider's deployment ID, null until deployment starts
|
|
27
|
+
provider: string;
|
|
28
|
+
status: DeploymentStatusType;
|
|
29
|
+
url: string | null;
|
|
30
|
+
metadata: Record<string, unknown> | null;
|
|
31
|
+
createdAt: Date;
|
|
32
|
+
updatedAt: Date;
|
|
33
|
+
}
|
|
@@ -10,7 +10,7 @@ export interface StorageRecord {
|
|
|
10
10
|
uploaded_at: string;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
// Bucket record from
|
|
13
|
+
// Bucket record from storage.buckets table
|
|
14
14
|
export type BucketRecord = Omit<StorageBucketSchema, 'created_at'>;
|
|
15
15
|
|
|
16
16
|
// Form field types for file uploads
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Webhook types for external integrations
|
|
2
|
+
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// Vercel Webhooks
|
|
5
|
+
// ============================================================================
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Vercel webhook event types we handle for deployments
|
|
9
|
+
*/
|
|
10
|
+
export type VercelDeploymentEventType =
|
|
11
|
+
| 'deployment.created'
|
|
12
|
+
| 'deployment.succeeded'
|
|
13
|
+
| 'deployment.error'
|
|
14
|
+
| 'deployment.canceled';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Map Vercel webhook event types to our deployment status
|
|
18
|
+
*/
|
|
19
|
+
export const VERCEL_EVENT_TO_STATUS: Record<VercelDeploymentEventType, string> = {
|
|
20
|
+
'deployment.created': 'BUILDING',
|
|
21
|
+
'deployment.succeeded': 'READY',
|
|
22
|
+
'deployment.error': 'ERROR',
|
|
23
|
+
'deployment.canceled': 'CANCELED',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Vercel webhook payload structure for deployment events
|
|
28
|
+
*/
|
|
29
|
+
export interface VercelWebhookPayload {
|
|
30
|
+
type: string;
|
|
31
|
+
id: string;
|
|
32
|
+
createdAt: string;
|
|
33
|
+
payload: {
|
|
34
|
+
team?: { id: string };
|
|
35
|
+
user?: { id: string };
|
|
36
|
+
deployment: {
|
|
37
|
+
id: string;
|
|
38
|
+
url: string;
|
|
39
|
+
name: string;
|
|
40
|
+
meta?: Record<string, unknown>;
|
|
41
|
+
};
|
|
42
|
+
target?: string;
|
|
43
|
+
project?: { id: string };
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -1,35 +1,34 @@
|
|
|
1
|
-
import { Response } from 'express';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
*
|
|
11
|
-
* @param
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
*
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
1
|
+
import { Request, Response } from 'express';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Cookie names
|
|
5
|
+
*/
|
|
6
|
+
export const REFRESH_TOKEN_COOKIE_NAME = 'insforge_refresh_token';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Set an auth cookie on response
|
|
10
|
+
* @param name - Cookie name
|
|
11
|
+
* @param value - Cookie value
|
|
12
|
+
*/
|
|
13
|
+
export function setAuthCookie(req: Request, res: Response, name: string, value: string): void {
|
|
14
|
+
res.cookie(name, value, {
|
|
15
|
+
httpOnly: true,
|
|
16
|
+
secure: req.secure,
|
|
17
|
+
sameSite: 'none',
|
|
18
|
+
path: '/api/auth',
|
|
19
|
+
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Clear an auth cookie on response
|
|
25
|
+
* IMPORTANT: Must use the same options (especially path) as when setting the cookie
|
|
26
|
+
*/
|
|
27
|
+
export function clearAuthCookie(req: Request, res: Response, name: string): void {
|
|
28
|
+
res.clearCookie(name, {
|
|
29
|
+
httpOnly: true,
|
|
30
|
+
secure: req.secure,
|
|
31
|
+
sameSite: 'none',
|
|
32
|
+
path: '/api/auth',
|
|
33
|
+
});
|
|
34
|
+
}
|
|
@@ -18,20 +18,6 @@ export function isOAuthSharedKeysAvailable(): boolean {
|
|
|
18
18
|
return isCloudEnvironment();
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
/**
|
|
22
|
-
* Check if running in development mode
|
|
23
|
-
*/
|
|
24
|
-
export function isDevelopment(): boolean {
|
|
25
|
-
return process.env.NODE_ENV === 'development' || !process.env.NODE_ENV;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Check if running in production mode
|
|
30
|
-
*/
|
|
31
|
-
export function isProduction(): boolean {
|
|
32
|
-
return process.env.NODE_ENV === 'production';
|
|
33
|
-
}
|
|
34
|
-
|
|
35
21
|
/**
|
|
36
22
|
* Get the API base URL from environment variable or default to localhost
|
|
37
23
|
* @returns The API base URL
|
|
@@ -1,64 +1,64 @@
|
|
|
1
|
-
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
|
|
2
|
-
import logger from '@/utils/logger.js';
|
|
3
|
-
|
|
4
|
-
// TODO: make these configurable in env variables in cloud backend
|
|
5
|
-
const CONFIG_BUCKET = process.env.AWS_CONFIG_BUCKET || 'insforge-config';
|
|
6
|
-
const CONFIG_REGION = process.env.AWS_CONFIG_REGION || 'us-east-2';
|
|
7
|
-
|
|
8
|
-
let s3Client: S3Client | null = null;
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Get or create S3 client for config loading
|
|
12
|
-
*/
|
|
13
|
-
function getS3Client(): S3Client {
|
|
14
|
-
if (s3Client) {
|
|
15
|
-
return s3Client;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const s3Config: {
|
|
19
|
-
region: string;
|
|
20
|
-
credentials?: { accessKeyId: string; secretAccessKey: string };
|
|
21
|
-
} = {
|
|
22
|
-
region: CONFIG_REGION,
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
// Use explicit credentials if provided, otherwise IAM role
|
|
26
|
-
if (process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY) {
|
|
27
|
-
s3Config.credentials = {
|
|
28
|
-
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
|
29
|
-
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
s3Client = new S3Client(s3Config);
|
|
34
|
-
return s3Client;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Fetches a JSON config file from the S3 config bucket
|
|
39
|
-
* @param key - The S3 object key (e.g., 'default-ai-models.json')
|
|
40
|
-
* @returns Parsed JSON content or null if fetch fails
|
|
41
|
-
*/
|
|
42
|
-
export async function fetchS3Config<T>(key: string): Promise<T | null> {
|
|
43
|
-
try {
|
|
44
|
-
const command = new GetObjectCommand({
|
|
45
|
-
Bucket: CONFIG_BUCKET,
|
|
46
|
-
Key: key,
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
const response = await getS3Client().send(command);
|
|
50
|
-
const body = await response.Body?.transformToString();
|
|
51
|
-
|
|
52
|
-
if (!body) {
|
|
53
|
-
logger.warn(`Empty config file from S3: ${key}`);
|
|
54
|
-
return null;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return JSON.parse(body) as T;
|
|
58
|
-
} catch (error) {
|
|
59
|
-
logger.warn(`Failed to fetch config from S3: ${key}`, {
|
|
60
|
-
error: error instanceof Error ? error.message : String(error),
|
|
61
|
-
});
|
|
62
|
-
return null;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
1
|
+
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
|
|
2
|
+
import logger from '@/utils/logger.js';
|
|
3
|
+
|
|
4
|
+
// TODO: make these configurable in env variables in cloud backend
|
|
5
|
+
const CONFIG_BUCKET = process.env.AWS_CONFIG_BUCKET || 'insforge-config';
|
|
6
|
+
const CONFIG_REGION = process.env.AWS_CONFIG_REGION || 'us-east-2';
|
|
7
|
+
|
|
8
|
+
let s3Client: S3Client | null = null;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Get or create S3 client for config loading
|
|
12
|
+
*/
|
|
13
|
+
function getS3Client(): S3Client {
|
|
14
|
+
if (s3Client) {
|
|
15
|
+
return s3Client;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const s3Config: {
|
|
19
|
+
region: string;
|
|
20
|
+
credentials?: { accessKeyId: string; secretAccessKey: string };
|
|
21
|
+
} = {
|
|
22
|
+
region: CONFIG_REGION,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// Use explicit credentials if provided, otherwise IAM role
|
|
26
|
+
if (process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY) {
|
|
27
|
+
s3Config.credentials = {
|
|
28
|
+
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
|
29
|
+
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
s3Client = new S3Client(s3Config);
|
|
34
|
+
return s3Client;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Fetches a JSON config file from the S3 config bucket
|
|
39
|
+
* @param key - The S3 object key (e.g., 'default-ai-models.json')
|
|
40
|
+
* @returns Parsed JSON content or null if fetch fails
|
|
41
|
+
*/
|
|
42
|
+
export async function fetchS3Config<T>(key: string): Promise<T | null> {
|
|
43
|
+
try {
|
|
44
|
+
const command = new GetObjectCommand({
|
|
45
|
+
Bucket: CONFIG_BUCKET,
|
|
46
|
+
Key: key,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const response = await getS3Client().send(command);
|
|
50
|
+
const body = await response.Body?.transformToString();
|
|
51
|
+
|
|
52
|
+
if (!body) {
|
|
53
|
+
logger.warn(`Empty config file from S3: ${key}`);
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return JSON.parse(body) as T;
|
|
58
|
+
} catch (error) {
|
|
59
|
+
logger.warn(`Failed to fetch config from S3: ${key}`, {
|
|
60
|
+
error: error instanceof Error ? error.message : String(error),
|
|
61
|
+
});
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|