insforge 0.3.1
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/.dockerignore +58 -0
- package/.env.example +49 -0
- package/.github/ISSUE_TEMPLATE/bug_report.yml +83 -0
- package/.github/ISSUE_TEMPLATE/config.yml +11 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +79 -0
- package/.github/copilot-instructions.md +147 -0
- package/.github/workflows/build-image.yml +65 -0
- package/.github/workflows/ci-premerge-check.yml +24 -0
- package/.github/workflows/deploy-aws.yml +130 -0
- package/.github/workflows/lint-and-format.yml +33 -0
- package/.prettierignore +65 -0
- package/.prettierrc +9 -0
- package/CHANGELOG.md +3 -0
- package/CONTRIBUTING.md +126 -0
- package/Dockerfile +27 -0
- package/GITHUB_OAUTH_SETUP.md +49 -0
- package/GOOGLE_OAUTH_SETUP.md +148 -0
- package/LICENSE +201 -0
- package/README.md +134 -0
- package/assets/Dark.svg +23 -0
- package/assets/archDiagram.png +0 -0
- package/assets/banner.png +0 -0
- package/assets/mcpInstallv2.png +0 -0
- package/assets/sampleResponse.png +0 -0
- package/assets/signin.png +0 -0
- package/assets/userflow.png +0 -0
- package/backend/migrations/000_create-base-tables.sql +142 -0
- package/backend/migrations/001_create-helper-functions.sql +41 -0
- package/backend/migrations/002_rename-auth-tables.sql +30 -0
- package/backend/migrations/003_create-users-table.sql +56 -0
- package/backend/migrations/004_add-reload-postgrest-func.sql +24 -0
- package/backend/migrations/005_enable-project-admin-modify-users.sql +30 -0
- package/backend/migrations/006_modify-ai-usage-table.sql +25 -0
- package/backend/migrations/007_drop-metadata-table.sql +2 -0
- package/backend/migrations/008_add-system-tables.sql +77 -0
- package/backend/migrations/009_add-function-secrets.sql +24 -0
- package/backend/migrations/010_modify-ai-config-modalities.sql +93 -0
- package/backend/migrations/011_refactor-secrets-table.sql +15 -0
- package/backend/migrations/012_add-storage-uploaded-by.sql +8 -0
- package/backend/package.json +75 -0
- package/backend/src/api/middleware/auth.ts +240 -0
- package/backend/src/api/middleware/error.ts +231 -0
- package/backend/src/api/middleware/upload.ts +59 -0
- package/backend/src/api/routes/agent.ts +29 -0
- package/backend/src/api/routes/ai.ts +472 -0
- package/backend/src/api/routes/auth.oauth.ts +482 -0
- package/backend/src/api/routes/auth.ts +386 -0
- package/backend/src/api/routes/database.advance.ts +275 -0
- package/backend/src/api/routes/database.records.ts +246 -0
- package/backend/src/api/routes/database.tables.ts +161 -0
- package/backend/src/api/routes/docs.ts +66 -0
- package/backend/src/api/routes/functions.ts +183 -0
- package/backend/src/api/routes/logs.ts +150 -0
- package/backend/src/api/routes/metadata.ts +160 -0
- package/backend/src/api/routes/openapi.ts +82 -0
- package/backend/src/api/routes/secrets.ts +199 -0
- package/backend/src/api/routes/storage.ts +547 -0
- package/backend/src/api/routes/usage.ts +96 -0
- package/backend/src/core/ai/chat.ts +207 -0
- package/backend/src/core/ai/client.ts +242 -0
- package/backend/src/core/ai/config.ts +187 -0
- package/backend/src/core/ai/image.ts +156 -0
- package/backend/src/core/ai/model.ts +117 -0
- package/backend/src/core/ai/usage.ts +290 -0
- package/backend/src/core/auth/auth.ts +781 -0
- package/backend/src/core/auth/oauth.ts +398 -0
- package/backend/src/core/database/advance.ts +1074 -0
- package/backend/src/core/database/manager.ts +178 -0
- package/backend/src/core/database/table.ts +772 -0
- package/backend/src/core/documentation/agent.ts +689 -0
- package/backend/src/core/documentation/openapi.ts +856 -0
- package/backend/src/core/functions/functions.ts +310 -0
- package/backend/src/core/logs/analytics.ts +76 -0
- package/backend/src/core/logs/audit.ts +255 -0
- package/backend/src/core/logs/providers/base.provider.ts +83 -0
- package/backend/src/core/logs/providers/cloudwatch.provider.ts +510 -0
- package/backend/src/core/logs/providers/localdb.provider.ts +246 -0
- package/backend/src/core/secrets/encryption.ts +58 -0
- package/backend/src/core/secrets/secrets.ts +410 -0
- package/backend/src/core/socket/socket.ts +388 -0
- package/backend/src/core/socket/types.ts +79 -0
- package/backend/src/core/storage/storage.ts +923 -0
- package/backend/src/server.ts +288 -0
- package/backend/src/types/ai.ts +46 -0
- package/backend/src/types/auth.ts +90 -0
- package/backend/src/types/database.ts +136 -0
- package/backend/src/types/error-constants.ts +86 -0
- package/backend/src/types/logs.ts +47 -0
- package/backend/src/types/profile.ts +55 -0
- package/backend/src/types/storage.ts +23 -0
- package/backend/src/utils/cloud-token.ts +39 -0
- package/backend/src/utils/constants.ts +1 -0
- package/backend/src/utils/environment.ts +35 -0
- package/backend/src/utils/helpers.ts +49 -0
- package/backend/src/utils/logger.ts +13 -0
- package/backend/src/utils/response.ts +62 -0
- package/backend/src/utils/seed.ts +205 -0
- package/backend/src/utils/sql-parser.ts +63 -0
- package/backend/src/utils/uuid.ts +9 -0
- package/backend/src/utils/validations.ts +129 -0
- package/backend/tests/README.md +134 -0
- package/backend/tests/cleanup-all-test-data.sh +231 -0
- package/backend/tests/cloud/test-s3-multitenant.sh +132 -0
- package/backend/tests/local/comprehensive-curl-tests.sh +156 -0
- package/backend/tests/local/test-auth-router.sh +144 -0
- package/backend/tests/local/test-database-router.sh +222 -0
- package/backend/tests/local/test-e2e.sh +241 -0
- package/backend/tests/local/test-fk-errors.sh +97 -0
- package/backend/tests/local/test-id-field.sh +201 -0
- package/backend/tests/local/test-public-bucket.sh +265 -0
- package/backend/tests/local/test-secrets.sh +248 -0
- package/backend/tests/local/test-serverless-functions.sh.disabled +325 -0
- package/backend/tests/local/test-traditional-rest.sh +209 -0
- package/backend/tests/manual/README.md +51 -0
- package/backend/tests/manual/create-large-table-simple.sql +11 -0
- package/backend/tests/manual/seed-large-table.sql +101 -0
- package/backend/tests/manual/setup-large-table-extras.sql +34 -0
- package/backend/tests/manual/test-better-auth.sh +303 -0
- package/backend/tests/manual/test-bulk-upsert.sh +410 -0
- package/backend/tests/manual/test-database-advance.sh +297 -0
- package/backend/tests/manual/test-postgrest-stability.sh +192 -0
- package/backend/tests/manual/test-rawsql-export-import.sh +412 -0
- package/backend/tests/manual/test-universal-storage.sh +264 -0
- package/backend/tests/manual/test-users.sql +18 -0
- package/backend/tests/run-all-tests.sh +140 -0
- package/backend/tests/setup.ts +22 -0
- package/backend/tests/test-config.sh +303 -0
- package/backend/tsconfig.json +23 -0
- package/backend/tsup.config.ts +18 -0
- package/backend/vitest.config.ts +22 -0
- package/docker-compose.prod.yml +145 -0
- package/docker-compose.yml +167 -0
- package/docker-init/db/db-init.sql +125 -0
- package/docker-init/db/jwt.sql +5 -0
- package/docker-init/db/logs.sql +9 -0
- package/docker-init/db/postgresql.conf +17 -0
- package/docs/deprecated/insforge-auth-api.md +215 -0
- package/docs/deprecated/insforge-auth-sdk.md +100 -0
- package/docs/deprecated/insforge-db-api.md +359 -0
- package/docs/deprecated/insforge-db-sdk.md +140 -0
- package/docs/deprecated/insforge-debug-sdk.md +157 -0
- package/docs/deprecated/insforge-debug.md +65 -0
- package/docs/deprecated/insforge-instructions.md +124 -0
- package/docs/deprecated/insforge-project.md +118 -0
- package/docs/deprecated/insforge-storage-api.md +279 -0
- package/docs/deprecated/insforge-storage-sdk.md +159 -0
- package/docs/insforge-instructions-sdk.md +407 -0
- package/eslint.config.js +317 -0
- package/examples/oauth/frontend-oauth-example.html +251 -0
- package/examples/response-examples.md +444 -0
- package/frontend/README.md +112 -0
- package/frontend/components.json +17 -0
- package/frontend/index.html +13 -0
- package/frontend/package.json +63 -0
- package/frontend/public/favicon.ico +0 -0
- package/frontend/src/App.tsx +106 -0
- package/frontend/src/assets/icons/checkbox_checked.svg +6 -0
- package/frontend/src/assets/icons/checkbox_undetermined.svg +6 -0
- package/frontend/src/assets/icons/checked.svg +3 -0
- package/frontend/src/assets/icons/error.svg +3 -0
- package/frontend/src/assets/icons/pencil.svg +4 -0
- package/frontend/src/assets/icons/refresh.svg +4 -0
- package/frontend/src/assets/icons/step_active.svg +3 -0
- package/frontend/src/assets/icons/step_inactive.svg +11 -0
- package/frontend/src/assets/icons/warning.svg +3 -0
- package/frontend/src/assets/logos/amazon.svg +1 -0
- package/frontend/src/assets/logos/claude_code.svg +3 -0
- package/frontend/src/assets/logos/cline.svg +6 -0
- package/frontend/src/assets/logos/cursor.svg +20 -0
- package/frontend/src/assets/logos/discord.svg +9 -0
- package/frontend/src/assets/logos/gemini.svg +19 -0
- package/frontend/src/assets/logos/github.svg +5 -0
- package/frontend/src/assets/logos/google.svg +13 -0
- package/frontend/src/assets/logos/grok.svg +10 -0
- package/frontend/src/assets/logos/insforge_dark.svg +15 -0
- package/frontend/src/assets/logos/insforge_light.svg +15 -0
- package/frontend/src/assets/logos/openai.svg +10 -0
- package/frontend/src/assets/logos/roo_code.svg +9 -0
- package/frontend/src/assets/logos/trae.svg +3 -0
- package/frontend/src/assets/logos/windsurf.svg +10 -0
- package/frontend/src/components/ButtonWithLoading.tsx +27 -0
- package/frontend/src/components/Checkbox.tsx +61 -0
- package/frontend/src/components/CodeBlock.tsx +32 -0
- package/frontend/src/components/ConfirmDialog.tsx +96 -0
- package/frontend/src/components/CopyButton.tsx +69 -0
- package/frontend/src/components/DeleteActionButton.tsx +42 -0
- package/frontend/src/components/EmptyState.tsx +41 -0
- package/frontend/src/components/ErrorState.tsx +35 -0
- package/frontend/src/components/FeatureSidebar.tsx +126 -0
- package/frontend/src/components/FeatureSidebarItem.tsx +101 -0
- package/frontend/src/components/JsonHighlight.tsx +61 -0
- package/frontend/src/components/LoadingState.tsx +16 -0
- package/frontend/src/components/PaginationControls.tsx +54 -0
- package/frontend/src/components/PromptDialog.tsx +68 -0
- package/frontend/src/components/SearchInput.tsx +90 -0
- package/frontend/src/components/SelectionClearButton.tsx +26 -0
- package/frontend/src/components/Stepper.tsx +139 -0
- package/frontend/src/components/ThemeToggle.tsx +58 -0
- package/frontend/src/components/TypeBadge.tsx +20 -0
- package/frontend/src/components/datagrid/DataGrid.tsx +264 -0
- package/frontend/src/components/datagrid/DefaultCellRenderer.tsx +114 -0
- package/frontend/src/components/datagrid/IdCell.tsx +44 -0
- package/frontend/src/components/datagrid/SortableHeader.tsx +74 -0
- package/frontend/src/components/datagrid/cell-editors/BooleanCellEditor.tsx +54 -0
- package/frontend/src/components/datagrid/cell-editors/DateCellEditor.tsx +483 -0
- package/frontend/src/components/datagrid/cell-editors/JsonCellEditor.tsx +362 -0
- package/frontend/src/components/datagrid/cell-editors/TextCellEditor.tsx +38 -0
- package/frontend/src/components/datagrid/cell-editors/index.ts +14 -0
- package/frontend/src/components/datagrid/cell-editors/types.ts +43 -0
- package/frontend/src/components/datagrid/datagridTypes.tsx +72 -0
- package/frontend/src/components/datagrid/index.tsx +20 -0
- package/frontend/src/components/index.ts +39 -0
- package/frontend/src/components/layout/AppHeader.tsx +146 -0
- package/frontend/src/components/layout/AppSidebar.tsx +190 -0
- package/frontend/src/components/layout/CloudLayout.tsx +95 -0
- package/frontend/src/components/layout/Layout.tsx +43 -0
- package/frontend/src/components/radix/Alert.tsx +45 -0
- package/frontend/src/components/radix/AlertDialog.tsx +115 -0
- package/frontend/src/components/radix/Avatar.tsx +45 -0
- package/frontend/src/components/radix/Badge.tsx +33 -0
- package/frontend/src/components/radix/Button.tsx +50 -0
- package/frontend/src/components/radix/Card.tsx +58 -0
- package/frontend/src/components/radix/Dialog.tsx +98 -0
- package/frontend/src/components/radix/DropdownMenu.tsx +185 -0
- package/frontend/src/components/radix/Form.tsx +167 -0
- package/frontend/src/components/radix/Input.tsx +22 -0
- package/frontend/src/components/radix/Label.tsx +19 -0
- package/frontend/src/components/radix/Popover.tsx +29 -0
- package/frontend/src/components/radix/ScrollArea.tsx +44 -0
- package/frontend/src/components/radix/Select.tsx +151 -0
- package/frontend/src/components/radix/Separator.tsx +26 -0
- package/frontend/src/components/radix/Sheet.tsx +119 -0
- package/frontend/src/components/radix/Skeleton.tsx +7 -0
- package/frontend/src/components/radix/Switch.tsx +29 -0
- package/frontend/src/components/radix/Tabs.tsx +50 -0
- package/frontend/src/components/radix/Textarea.tsx +21 -0
- package/frontend/src/components/radix/Tooltip.tsx +28 -0
- package/frontend/src/features/ai/components/AIConfigCard.tsx +154 -0
- package/frontend/src/features/ai/components/AIConfigDialog.tsx +76 -0
- package/frontend/src/features/ai/components/AIConfigForm.tsx +222 -0
- package/frontend/src/features/ai/components/AIEmptyState.tsx +18 -0
- package/frontend/src/features/ai/components/fields/ModalityField.tsx +87 -0
- package/frontend/src/features/ai/components/fields/ModelSelectionField.tsx +134 -0
- package/frontend/src/features/ai/components/fields/SystemPromptField.tsx +33 -0
- package/frontend/src/features/ai/helpers.ts +155 -0
- package/frontend/src/features/ai/hooks/useAIConfigs.ts +221 -0
- package/frontend/src/features/ai/hooks/useAIUsage.ts +77 -0
- package/frontend/src/features/ai/page/AIPage.tsx +178 -0
- package/frontend/src/features/ai/services/ai.service.ts +148 -0
- package/frontend/src/features/auth/components/AddOAuthDialog.tsx +106 -0
- package/frontend/src/features/auth/components/AuthMethodTab.tsx +238 -0
- package/frontend/src/features/auth/components/OAuthConfigDialog.tsx +303 -0
- package/frontend/src/features/auth/components/OAuthEmptyState.tsx +15 -0
- package/frontend/src/features/auth/components/UserFormDialog.tsx +248 -0
- package/frontend/src/features/auth/components/UsersDataGrid.tsx +183 -0
- package/frontend/src/features/auth/components/UsersTab.tsx +114 -0
- package/frontend/src/features/auth/hooks/useOAuthConfig.ts +129 -0
- package/frontend/src/features/auth/hooks/useUsers.ts +57 -0
- package/frontend/src/features/auth/index.ts +9 -0
- package/frontend/src/features/auth/page/AuthenticationPage.tsx +169 -0
- package/frontend/src/features/auth/services/auth.service.ts +112 -0
- package/frontend/src/features/auth/services/oauth.service.ts +49 -0
- package/frontend/src/features/dashboard/page/DashboardPage.tsx +194 -0
- package/frontend/src/features/database/components/ColumnTypeSelect.tsx +64 -0
- package/frontend/src/features/database/components/DatabaseDataGrid.tsx +282 -0
- package/frontend/src/features/database/components/ForeignKeyCell.tsx +187 -0
- package/frontend/src/features/database/components/ForeignKeyPopover.tsx +378 -0
- package/frontend/src/features/database/components/LinkRecordModal.tsx +288 -0
- package/frontend/src/features/database/components/RecordFormDialog.tsx +164 -0
- package/frontend/src/features/database/components/RecordFormField.tsx +568 -0
- package/frontend/src/features/database/components/TableEmptyState.tsx +21 -0
- package/frontend/src/features/database/components/TableForm.tsx +656 -0
- package/frontend/src/features/database/components/TableFormColumn.tsx +137 -0
- package/frontend/src/features/database/components/TableListSkeleton.tsx +9 -0
- package/frontend/src/features/database/components/TableSidebar.tsx +47 -0
- package/frontend/src/features/database/constants.ts +26 -0
- package/frontend/src/features/database/helpers.ts +125 -0
- package/frontend/src/features/database/hooks/UseLinkModal.tsx +78 -0
- package/frontend/src/features/database/index.ts +12 -0
- package/frontend/src/features/database/page/DatabasePage.tsx +626 -0
- package/frontend/src/features/database/schema.ts +25 -0
- package/frontend/src/features/database/services/database.service.ts +216 -0
- package/frontend/src/features/functions/components/FunctionEmptyState.tsx +15 -0
- package/frontend/src/features/functions/components/FunctionRow.tsx +71 -0
- package/frontend/src/features/functions/components/FunctionViewer.tsx +46 -0
- package/frontend/src/features/functions/components/FunctionsContent.tsx +88 -0
- package/frontend/src/features/functions/components/FunctionsSidebar.tsx +56 -0
- package/frontend/src/features/functions/components/SecretEmptyState.tsx +23 -0
- package/frontend/src/features/functions/components/SecretRow.tsx +68 -0
- package/frontend/src/features/functions/components/SecretsContent.tsx +120 -0
- package/frontend/src/features/functions/hooks/useFunctions.ts +106 -0
- package/frontend/src/features/functions/page/FunctionsPage.tsx +28 -0
- package/frontend/src/features/functions/services/functions.service.ts +48 -0
- package/frontend/src/features/login/components/AuthErrorBoundary.tsx +87 -0
- package/frontend/src/features/login/components/PrivateRoute.tsx +24 -0
- package/frontend/src/features/login/page/CloudLoginPage.tsx +93 -0
- package/frontend/src/features/login/page/LoginPage.tsx +174 -0
- package/frontend/src/features/logs/components/AnalyticsLogsTable.tsx +313 -0
- package/frontend/src/features/logs/components/LogsTable.tsx +199 -0
- package/frontend/src/features/logs/hooks/useAuditLogs.ts +39 -0
- package/frontend/src/features/logs/index.ts +5 -0
- package/frontend/src/features/logs/page/AnalyticsLogsPage.tsx +530 -0
- package/frontend/src/features/logs/page/AuditsPage.tsx +192 -0
- package/frontend/src/features/logs/services/log.service.ts +171 -0
- package/frontend/src/features/metadata/hooks/useMetadata.ts +53 -0
- package/frontend/src/features/metadata/index.ts +0 -0
- package/frontend/src/features/metadata/page/MetadataPage.tsx +136 -0
- package/frontend/src/features/metadata/services/metadata.service.ts +17 -0
- package/frontend/src/features/onboard/components/CompletionCard.tsx +41 -0
- package/frontend/src/features/onboard/components/OnboardButton.tsx +84 -0
- package/frontend/src/features/onboard/components/StepContent.tsx +91 -0
- package/frontend/src/features/onboard/components/TestConnectionStep.tsx +53 -0
- package/frontend/src/features/onboard/components/mcp/CursorDeeplinkGenerator.tsx +35 -0
- package/frontend/src/features/onboard/components/mcp/McpInstallation.tsx +144 -0
- package/frontend/src/features/onboard/components/mcp/index.ts +4 -0
- package/frontend/src/features/onboard/components/mcp/mcp-helper.tsx +98 -0
- package/frontend/src/features/onboard/index.ts +3 -0
- package/frontend/src/features/onboard/page/OnBoardPage.tsx +104 -0
- package/frontend/src/features/onboard/types.ts +8 -0
- package/frontend/src/features/secrets/hooks/useSecrets.ts +139 -0
- package/frontend/src/features/secrets/services/secrets.service.ts +57 -0
- package/frontend/src/features/storage/components/BucketEmptyState.tsx +19 -0
- package/frontend/src/features/storage/components/BucketFormDialog.tsx +194 -0
- package/frontend/src/features/storage/components/BucketListSkeleton.tsx +17 -0
- package/frontend/src/features/storage/components/FilePreviewDialog.tsx +287 -0
- package/frontend/src/features/storage/components/StorageDataGrid.tsx +239 -0
- package/frontend/src/features/storage/components/StorageManager.tsx +236 -0
- package/frontend/src/features/storage/components/StorageSidebar.tsx +44 -0
- package/frontend/src/features/storage/components/UploadToast.tsx +46 -0
- package/frontend/src/features/storage/index.ts +3 -0
- package/frontend/src/features/storage/page/StoragePage.tsx +553 -0
- package/frontend/src/features/storage/services/storage.service.ts +144 -0
- package/frontend/src/features/visualizer/components/AuthNode.tsx +107 -0
- package/frontend/src/features/visualizer/components/BucketNode.tsx +34 -0
- package/frontend/src/features/visualizer/components/SchemaVisualizer.tsx +359 -0
- package/frontend/src/features/visualizer/components/TableNode.tsx +152 -0
- package/frontend/src/features/visualizer/components/VisualizerSkeleton.tsx +24 -0
- package/frontend/src/features/visualizer/components/index.ts +5 -0
- package/frontend/src/features/visualizer/page/VisualizerPage.tsx +127 -0
- package/frontend/src/index.css +248 -0
- package/frontend/src/lib/api/client.ts +163 -0
- package/frontend/src/lib/contexts/AuthContext.tsx +157 -0
- package/frontend/src/lib/contexts/OnboardStepContext.tsx +68 -0
- package/frontend/src/lib/contexts/SocketContext.tsx +303 -0
- package/frontend/src/lib/contexts/ThemeContext.tsx +125 -0
- package/frontend/src/lib/hooks/useAuth.ts +4 -0
- package/frontend/src/lib/hooks/useConfirm.ts +55 -0
- package/frontend/src/lib/hooks/useInterval.ts +27 -0
- package/frontend/src/lib/hooks/useMediaQuery.ts +59 -0
- package/frontend/src/lib/hooks/useOnboardingCompletion.ts +29 -0
- package/frontend/src/lib/hooks/usePagination.ts +27 -0
- package/frontend/src/lib/hooks/useTimeout.ts +27 -0
- package/frontend/src/lib/hooks/useToast.tsx +229 -0
- package/frontend/src/lib/utils/constants.ts +38 -0
- package/frontend/src/lib/utils/utils.ts +165 -0
- package/frontend/src/lib/utils/validation-schemas.ts +126 -0
- package/frontend/src/main.tsx +16 -0
- package/frontend/src/rdg.css +194 -0
- package/frontend/src/vite-env.d.ts +12 -0
- package/frontend/tailwind.config.js +97 -0
- package/frontend/tsconfig.json +26 -0
- package/frontend/tsconfig.node.json +10 -0
- package/frontend/vite.config.ts +37 -0
- package/frontend/vitest.config.ts +36 -0
- package/functions/deno.json +25 -0
- package/functions/server.ts +290 -0
- package/functions/worker-template.js +126 -0
- package/openapi/ai.yaml +689 -0
- package/openapi/auth.yaml +563 -0
- package/openapi/functions.yaml +476 -0
- package/openapi/health.yaml +30 -0
- package/openapi/logs.yaml +224 -0
- package/openapi/metadata.yaml +178 -0
- package/openapi/records.yaml +382 -0
- package/openapi/secrets.yaml +371 -0
- package/openapi/storage.yaml +876 -0
- package/openapi/tables.yaml +464 -0
- package/package.json +88 -0
- package/shared-schemas/package.json +31 -0
- package/shared-schemas/src/ai-api.schema.ts +167 -0
- package/shared-schemas/src/ai.schema.ts +54 -0
- package/shared-schemas/src/auth-api.schema.ts +193 -0
- package/shared-schemas/src/auth.schema.ts +94 -0
- package/shared-schemas/src/database-api.schema.ts +259 -0
- package/shared-schemas/src/database.schema.ts +69 -0
- package/shared-schemas/src/functions-api.schema.ts +25 -0
- package/shared-schemas/src/functions.schema.ts +16 -0
- package/shared-schemas/src/index.ts +13 -0
- package/shared-schemas/src/logs-api.schema.ts +49 -0
- package/shared-schemas/src/logs.schema.ts +14 -0
- package/shared-schemas/src/metadata.schema.ts +56 -0
- package/shared-schemas/src/storage-api.schema.ts +65 -0
- package/shared-schemas/src/storage.schema.ts +19 -0
- package/shared-schemas/tsconfig.json +21 -0
- package/tsconfig.json +8 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { Pool } from 'pg';
|
|
2
|
+
import { LogSource, AnalyticsLogRecord, LogSourceStats } from '@/types/logs.js';
|
|
3
|
+
import logger from '@/utils/logger.js';
|
|
4
|
+
import { BaseAnalyticsProvider } from './base.provider.js';
|
|
5
|
+
|
|
6
|
+
export class LocalDBProvider extends BaseAnalyticsProvider {
|
|
7
|
+
private pool!: Pool;
|
|
8
|
+
|
|
9
|
+
async initialize(): Promise<void> {
|
|
10
|
+
this.pool = new Pool({
|
|
11
|
+
host: process.env.POSTGRES_HOST || 'localhost',
|
|
12
|
+
port: parseInt(process.env.POSTGRES_PORT || '5432'),
|
|
13
|
+
database: '_insforge',
|
|
14
|
+
user: process.env.POSTGRES_USER || 'postgres',
|
|
15
|
+
password: process.env.POSTGRES_PASSWORD || 'postgres',
|
|
16
|
+
max: 10,
|
|
17
|
+
idleTimeoutMillis: 30000,
|
|
18
|
+
connectionTimeoutMillis: 2000,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const client = await this.pool.connect();
|
|
23
|
+
client.release();
|
|
24
|
+
logger.info('Analytics database connection established');
|
|
25
|
+
} catch (error) {
|
|
26
|
+
logger.error('Failed to connect to analytics database', {
|
|
27
|
+
error: error instanceof Error ? error.message : String(error),
|
|
28
|
+
});
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async getLogSources(): Promise<LogSource[]> {
|
|
34
|
+
const client = await this.pool.connect();
|
|
35
|
+
try {
|
|
36
|
+
const result = await client.query(`
|
|
37
|
+
SELECT id, name, token
|
|
38
|
+
FROM _analytics.sources
|
|
39
|
+
ORDER BY name
|
|
40
|
+
`);
|
|
41
|
+
|
|
42
|
+
const sourcesWithData: LogSource[] = [];
|
|
43
|
+
for (const source of result.rows) {
|
|
44
|
+
const tableName = `log_events_${source.token.replace(/-/g, '_')}`;
|
|
45
|
+
try {
|
|
46
|
+
const countResult = await client.query(`
|
|
47
|
+
SELECT COUNT(*) as count
|
|
48
|
+
FROM _analytics.${tableName}
|
|
49
|
+
`);
|
|
50
|
+
const count = parseInt(countResult.rows[0].count);
|
|
51
|
+
if (count > 0) {
|
|
52
|
+
sourcesWithData.push({
|
|
53
|
+
...source,
|
|
54
|
+
name: this.getDisplayName(source.name),
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
} catch (error) {
|
|
58
|
+
logger.warn(`Source ${source.name} has no accessible data`, {
|
|
59
|
+
error: error instanceof Error ? error.message : String(error),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return sourcesWithData;
|
|
64
|
+
} finally {
|
|
65
|
+
client.release();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async getLogsBySource(
|
|
70
|
+
sourceName: string,
|
|
71
|
+
limit: number = 100,
|
|
72
|
+
beforeTimestamp?: string
|
|
73
|
+
): Promise<{
|
|
74
|
+
logs: AnalyticsLogRecord[];
|
|
75
|
+
total: number;
|
|
76
|
+
tableName: string;
|
|
77
|
+
}> {
|
|
78
|
+
const client = await this.pool.connect();
|
|
79
|
+
try {
|
|
80
|
+
// Convert display name to internal name for query
|
|
81
|
+
const internalSourceName = this.getInternalName(sourceName);
|
|
82
|
+
|
|
83
|
+
// First, get the source token to determine the table name
|
|
84
|
+
const sourceResult = await client.query(
|
|
85
|
+
`SELECT token FROM _analytics.sources WHERE name = $1`,
|
|
86
|
+
[internalSourceName]
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
if (sourceResult.rows.length === 0) {
|
|
90
|
+
throw new Error(`Log source '${sourceName}' not found`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const token = sourceResult.rows[0].token;
|
|
94
|
+
const tableName = `log_events_${token.replace(/-/g, '_')}`;
|
|
95
|
+
|
|
96
|
+
// Build the query with timestamp-based pagination
|
|
97
|
+
// If no beforeTimestamp provided, use current time
|
|
98
|
+
const beforeTs = beforeTimestamp || new Date().toISOString();
|
|
99
|
+
|
|
100
|
+
const query = `
|
|
101
|
+
SELECT id, event_message, timestamp, body
|
|
102
|
+
FROM _analytics.${tableName}
|
|
103
|
+
WHERE timestamp < $1
|
|
104
|
+
ORDER BY timestamp DESC
|
|
105
|
+
LIMIT $2
|
|
106
|
+
`;
|
|
107
|
+
const params = [beforeTs, limit];
|
|
108
|
+
|
|
109
|
+
const logsResult = await client.query(query, params);
|
|
110
|
+
|
|
111
|
+
// Get total count of all records (not filtered by timestamp)
|
|
112
|
+
const countQuery = `SELECT COUNT(*) as count FROM _analytics.${tableName}`;
|
|
113
|
+
const countResult = await client.query(countQuery);
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
logs: logsResult.rows,
|
|
117
|
+
total: parseInt(countResult.rows[0].count),
|
|
118
|
+
tableName: `_analytics.${tableName}`,
|
|
119
|
+
};
|
|
120
|
+
} finally {
|
|
121
|
+
client.release();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async getLogSourceStats(): Promise<LogSourceStats[]> {
|
|
126
|
+
const client = await this.pool.connect();
|
|
127
|
+
try {
|
|
128
|
+
const sources = await this.getLogSources();
|
|
129
|
+
const stats: LogSourceStats[] = [];
|
|
130
|
+
|
|
131
|
+
for (const source of sources) {
|
|
132
|
+
const tableName = `log_events_${source.token.replace(/-/g, '_')}`;
|
|
133
|
+
try {
|
|
134
|
+
const result = await client.query(`
|
|
135
|
+
SELECT
|
|
136
|
+
COUNT(*) as count,
|
|
137
|
+
MAX(timestamp) as last_activity
|
|
138
|
+
FROM _analytics.${tableName}
|
|
139
|
+
`);
|
|
140
|
+
stats.push({
|
|
141
|
+
source: source.name,
|
|
142
|
+
count: parseInt(result.rows[0].count),
|
|
143
|
+
lastActivity: result.rows[0].last_activity || '',
|
|
144
|
+
});
|
|
145
|
+
} catch (error) {
|
|
146
|
+
logger.warn(`Failed to get stats for source ${source.name}`, {
|
|
147
|
+
error: error instanceof Error ? error.message : String(error),
|
|
148
|
+
});
|
|
149
|
+
stats.push({
|
|
150
|
+
source: source.name,
|
|
151
|
+
count: 0,
|
|
152
|
+
lastActivity: '',
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return stats.sort((a, b) => b.count - a.count);
|
|
157
|
+
} finally {
|
|
158
|
+
client.release();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async searchLogs(
|
|
163
|
+
query: string,
|
|
164
|
+
sourceName?: string,
|
|
165
|
+
limit: number = 100,
|
|
166
|
+
offset: number = 0
|
|
167
|
+
): Promise<{
|
|
168
|
+
logs: (AnalyticsLogRecord & { source: string })[];
|
|
169
|
+
total: number;
|
|
170
|
+
}> {
|
|
171
|
+
const client = await this.pool.connect();
|
|
172
|
+
try {
|
|
173
|
+
let sources: LogSource[];
|
|
174
|
+
|
|
175
|
+
if (sourceName) {
|
|
176
|
+
// Convert display name to internal name for query
|
|
177
|
+
const internalSourceName = this.getInternalName(sourceName);
|
|
178
|
+
const sourceResult = await client.query(
|
|
179
|
+
`SELECT id, name, token FROM _analytics.sources WHERE name = $1`,
|
|
180
|
+
[internalSourceName]
|
|
181
|
+
);
|
|
182
|
+
// Apply name mapping to the result
|
|
183
|
+
sources = sourceResult.rows.map((source) => ({
|
|
184
|
+
...source,
|
|
185
|
+
name: this.getDisplayName(source.name),
|
|
186
|
+
}));
|
|
187
|
+
} else {
|
|
188
|
+
// getLogSources already returns mapped names
|
|
189
|
+
sources = await this.getLogSources();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const results: (AnalyticsLogRecord & { source: string })[] = [];
|
|
193
|
+
let totalCount = 0;
|
|
194
|
+
|
|
195
|
+
for (const source of sources) {
|
|
196
|
+
const tableName = `log_events_${source.token.replace(/-/g, '_')}`;
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
// Search in event_message and body fields
|
|
200
|
+
const searchResult = await client.query(
|
|
201
|
+
`SELECT id, event_message, timestamp, body, $1 as source
|
|
202
|
+
FROM _analytics.${tableName}
|
|
203
|
+
WHERE event_message ILIKE $2
|
|
204
|
+
OR body::text ILIKE $2
|
|
205
|
+
ORDER BY timestamp DESC
|
|
206
|
+
LIMIT $3 OFFSET $4`,
|
|
207
|
+
[source.name, `%${query}%`, limit, offset]
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
results.push(...searchResult.rows);
|
|
211
|
+
|
|
212
|
+
// Get count for this source
|
|
213
|
+
const countResult = await client.query(
|
|
214
|
+
`SELECT COUNT(*) as count
|
|
215
|
+
FROM _analytics.${tableName}
|
|
216
|
+
WHERE event_message ILIKE $1
|
|
217
|
+
OR body::text ILIKE $1`,
|
|
218
|
+
[`%${query}%`]
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
totalCount += parseInt(countResult.rows[0].count);
|
|
222
|
+
} catch (error) {
|
|
223
|
+
logger.warn(`Failed to search in source ${source.name}`, {
|
|
224
|
+
error: error instanceof Error ? error.message : String(error),
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Sort combined results by timestamp
|
|
230
|
+
results.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
logs: results.slice(0, limit),
|
|
234
|
+
total: totalCount,
|
|
235
|
+
};
|
|
236
|
+
} finally {
|
|
237
|
+
client.release();
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async close(): Promise<void> {
|
|
242
|
+
if (this.pool) {
|
|
243
|
+
await this.pool.end();
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Encryption utilities for secrets management
|
|
5
|
+
*/
|
|
6
|
+
export class EncryptionUtils {
|
|
7
|
+
private static encryptionKey: Buffer | null = null;
|
|
8
|
+
|
|
9
|
+
private static getEncryptionKey(): Buffer {
|
|
10
|
+
if (!this.encryptionKey) {
|
|
11
|
+
const key = process.env.ENCRYPTION_KEY || process.env.JWT_SECRET;
|
|
12
|
+
if (!key) {
|
|
13
|
+
throw new Error('ENCRYPTION_KEY or JWT_SECRET must be set for secrets encryption');
|
|
14
|
+
}
|
|
15
|
+
this.encryptionKey = crypto.createHash('sha256').update(key).digest();
|
|
16
|
+
}
|
|
17
|
+
return this.encryptionKey;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Encrypt a value using AES-256-GCM
|
|
22
|
+
*/
|
|
23
|
+
static encrypt(value: string): string {
|
|
24
|
+
const encryptionKey = this.getEncryptionKey();
|
|
25
|
+
const iv = crypto.randomBytes(16);
|
|
26
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', encryptionKey, iv);
|
|
27
|
+
|
|
28
|
+
let encrypted = cipher.update(value, 'utf8', 'hex');
|
|
29
|
+
encrypted += cipher.final('hex');
|
|
30
|
+
|
|
31
|
+
const authTag = cipher.getAuthTag();
|
|
32
|
+
|
|
33
|
+
return iv.toString('hex') + ':' + authTag.toString('hex') + ':' + encrypted;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Decrypt a value using AES-256-GCM
|
|
38
|
+
*/
|
|
39
|
+
static decrypt(ciphertext: string): string {
|
|
40
|
+
const encryptionKey = this.getEncryptionKey();
|
|
41
|
+
const parts = ciphertext.split(':');
|
|
42
|
+
if (parts.length !== 3) {
|
|
43
|
+
throw new Error('Invalid ciphertext format');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const iv = Buffer.from(parts[0], 'hex');
|
|
47
|
+
const authTag = Buffer.from(parts[1], 'hex');
|
|
48
|
+
const encrypted = parts[2];
|
|
49
|
+
|
|
50
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', encryptionKey, iv);
|
|
51
|
+
decipher.setAuthTag(authTag);
|
|
52
|
+
|
|
53
|
+
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
|
|
54
|
+
decrypted += decipher.final('utf8');
|
|
55
|
+
|
|
56
|
+
return decrypted;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
import { Pool } from 'pg';
|
|
2
|
+
import crypto from 'crypto';
|
|
3
|
+
import { DatabaseManager } from '@/core/database/manager.js';
|
|
4
|
+
import logger from '@/utils/logger.js';
|
|
5
|
+
import { EncryptionUtils } from './encryption.js';
|
|
6
|
+
|
|
7
|
+
export interface SecretSchema {
|
|
8
|
+
id: string;
|
|
9
|
+
key: string;
|
|
10
|
+
isActive: boolean;
|
|
11
|
+
isReserved: boolean;
|
|
12
|
+
lastUsedAt: Date | null;
|
|
13
|
+
expiresAt: Date | null;
|
|
14
|
+
createdAt: Date;
|
|
15
|
+
updatedAt: Date;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface CreateSecretInput {
|
|
19
|
+
key: string;
|
|
20
|
+
value: string;
|
|
21
|
+
isReserved?: boolean;
|
|
22
|
+
expiresAt?: Date;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface UpdateSecretInput {
|
|
26
|
+
value?: string;
|
|
27
|
+
isActive?: boolean;
|
|
28
|
+
isReserved?: boolean;
|
|
29
|
+
expiresAt?: Date | null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class SecretsService {
|
|
33
|
+
private pool: Pool | null = null;
|
|
34
|
+
|
|
35
|
+
constructor() {
|
|
36
|
+
// Encryption is now handled by the shared EncryptionUtils
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private getPool(): Pool {
|
|
40
|
+
if (!this.pool) {
|
|
41
|
+
this.pool = DatabaseManager.getInstance().getPool();
|
|
42
|
+
}
|
|
43
|
+
return this.pool;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Create a new secret
|
|
48
|
+
*/
|
|
49
|
+
async createSecret(input: CreateSecretInput): Promise<{ id: string }> {
|
|
50
|
+
const client = await this.getPool().connect();
|
|
51
|
+
try {
|
|
52
|
+
const encryptedValue = EncryptionUtils.encrypt(input.value);
|
|
53
|
+
|
|
54
|
+
const result = await client.query(
|
|
55
|
+
`INSERT INTO _secrets (key, value_ciphertext, is_reserved, expires_at)
|
|
56
|
+
VALUES ($1, $2, $3, $4)
|
|
57
|
+
RETURNING id`,
|
|
58
|
+
[input.key, encryptedValue, input.isReserved || false, input.expiresAt || null]
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
logger.info('Secret created', { id: result.rows[0].id, key: input.key });
|
|
62
|
+
return { id: result.rows[0].id };
|
|
63
|
+
} catch (error) {
|
|
64
|
+
logger.error('Failed to create secret', { error, key: input.key });
|
|
65
|
+
throw new Error('Failed to create secret');
|
|
66
|
+
} finally {
|
|
67
|
+
client.release();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Get a decrypted secret by ID
|
|
73
|
+
*/
|
|
74
|
+
async getSecretById(id: string): Promise<string | null> {
|
|
75
|
+
const client = await this.getPool().connect();
|
|
76
|
+
try {
|
|
77
|
+
const result = await client.query(
|
|
78
|
+
`UPDATE _secrets
|
|
79
|
+
SET last_used_at = NOW()
|
|
80
|
+
WHERE id = $1 AND is_active = true
|
|
81
|
+
AND (expires_at IS NULL OR expires_at > NOW())
|
|
82
|
+
RETURNING value_ciphertext`,
|
|
83
|
+
[id]
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
if (result.rows.length === 0) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const decryptedValue = EncryptionUtils.decrypt(result.rows[0].value_ciphertext);
|
|
91
|
+
logger.info('Secret retrieved', { id });
|
|
92
|
+
return decryptedValue;
|
|
93
|
+
} catch (error) {
|
|
94
|
+
logger.error('Failed to get secret', { error, id });
|
|
95
|
+
throw new Error('Failed to get secret');
|
|
96
|
+
} finally {
|
|
97
|
+
client.release();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Get a decrypted secret by key
|
|
103
|
+
*/
|
|
104
|
+
async getSecretByKey(key: string): Promise<string | null> {
|
|
105
|
+
const client = await this.getPool().connect();
|
|
106
|
+
try {
|
|
107
|
+
const result = await client.query(
|
|
108
|
+
`UPDATE _secrets
|
|
109
|
+
SET last_used_at = NOW()
|
|
110
|
+
WHERE key = $1 AND is_active = true
|
|
111
|
+
AND (expires_at IS NULL OR expires_at > NOW())
|
|
112
|
+
RETURNING value_ciphertext`,
|
|
113
|
+
[key]
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
if (result.rows.length === 0) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const decryptedValue = EncryptionUtils.decrypt(result.rows[0].value_ciphertext);
|
|
121
|
+
logger.info('Secret retrieved by key', { key });
|
|
122
|
+
return decryptedValue;
|
|
123
|
+
} catch (error) {
|
|
124
|
+
logger.error('Failed to get secret by key', { error, key });
|
|
125
|
+
throw new Error('Failed to get secret');
|
|
126
|
+
} finally {
|
|
127
|
+
client.release();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* List all secrets (without decrypting values)
|
|
133
|
+
*/
|
|
134
|
+
async listSecrets(): Promise<SecretSchema[]> {
|
|
135
|
+
const client = await this.getPool().connect();
|
|
136
|
+
try {
|
|
137
|
+
const result = await client.query(
|
|
138
|
+
`SELECT
|
|
139
|
+
id,
|
|
140
|
+
key,
|
|
141
|
+
is_active as "isActive",
|
|
142
|
+
is_reserved as "isReserved",
|
|
143
|
+
last_used_at as "lastUsedAt",
|
|
144
|
+
expires_at as "expiresAt",
|
|
145
|
+
created_at as "createdAt",
|
|
146
|
+
updated_at as "updatedAt"
|
|
147
|
+
FROM _secrets
|
|
148
|
+
ORDER BY created_at DESC`
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
return result.rows;
|
|
152
|
+
} catch (error) {
|
|
153
|
+
logger.error('Failed to list secrets', { error });
|
|
154
|
+
throw new Error('Failed to list secrets');
|
|
155
|
+
} finally {
|
|
156
|
+
client.release();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Update a secret
|
|
162
|
+
*/
|
|
163
|
+
async updateSecret(id: string, input: UpdateSecretInput): Promise<boolean> {
|
|
164
|
+
const client = await this.getPool().connect();
|
|
165
|
+
try {
|
|
166
|
+
const updates: string[] = [];
|
|
167
|
+
const values: (string | boolean | Date | null)[] = [];
|
|
168
|
+
let paramCount = 1;
|
|
169
|
+
|
|
170
|
+
if (input.value !== undefined) {
|
|
171
|
+
const encryptedValue = EncryptionUtils.encrypt(input.value);
|
|
172
|
+
updates.push(`value_ciphertext = $${paramCount++}`);
|
|
173
|
+
values.push(encryptedValue);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (input.isActive !== undefined) {
|
|
177
|
+
updates.push(`is_active = $${paramCount++}`);
|
|
178
|
+
values.push(input.isActive);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (input.isReserved !== undefined) {
|
|
182
|
+
updates.push(`is_reserved = $${paramCount++}`);
|
|
183
|
+
values.push(input.isReserved);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (input.expiresAt !== undefined) {
|
|
187
|
+
updates.push(`expires_at = $${paramCount++}`);
|
|
188
|
+
values.push(input.expiresAt);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
values.push(id);
|
|
192
|
+
|
|
193
|
+
const result = await client.query(
|
|
194
|
+
`UPDATE _secrets
|
|
195
|
+
SET ${updates.join(', ')}
|
|
196
|
+
WHERE id = $${paramCount}`,
|
|
197
|
+
values
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
const success = (result.rowCount ?? 0) > 0;
|
|
201
|
+
if (success) {
|
|
202
|
+
logger.info('Secret updated', { id });
|
|
203
|
+
}
|
|
204
|
+
return success;
|
|
205
|
+
} catch (error) {
|
|
206
|
+
logger.error('Failed to update secret', { error, id });
|
|
207
|
+
throw new Error('Failed to update secret');
|
|
208
|
+
} finally {
|
|
209
|
+
client.release();
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Check if a secret value matches the stored value
|
|
215
|
+
*/
|
|
216
|
+
async checkSecretByKey(key: string, value: string): Promise<boolean> {
|
|
217
|
+
const client = await this.getPool().connect();
|
|
218
|
+
try {
|
|
219
|
+
const result = await client.query(
|
|
220
|
+
`SELECT value_ciphertext FROM _secrets
|
|
221
|
+
WHERE key = $1
|
|
222
|
+
AND is_active = true
|
|
223
|
+
AND (expires_at IS NULL OR expires_at > NOW())
|
|
224
|
+
LIMIT 1`,
|
|
225
|
+
[key]
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
if (result.rows.length === 0) {
|
|
229
|
+
logger.warn('Secret not found for verification', { key });
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const decryptedValue = EncryptionUtils.decrypt(result.rows[0].value_ciphertext);
|
|
234
|
+
const matches = decryptedValue === value;
|
|
235
|
+
|
|
236
|
+
// Update last_used_at if the check was successful
|
|
237
|
+
if (matches) {
|
|
238
|
+
await client.query(
|
|
239
|
+
`UPDATE _secrets
|
|
240
|
+
SET last_used_at = NOW()
|
|
241
|
+
WHERE key = $1
|
|
242
|
+
AND is_active = true`,
|
|
243
|
+
[key]
|
|
244
|
+
);
|
|
245
|
+
logger.info('Secret check successful', { key });
|
|
246
|
+
} else {
|
|
247
|
+
logger.warn('Secret check failed - value mismatch', { key });
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return matches;
|
|
251
|
+
} catch (error) {
|
|
252
|
+
logger.error('Failed to check secret', { error, key });
|
|
253
|
+
return false;
|
|
254
|
+
} finally {
|
|
255
|
+
client.release();
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Delete a secret
|
|
261
|
+
*/
|
|
262
|
+
async deleteSecret(id: string): Promise<boolean> {
|
|
263
|
+
const client = await this.getPool().connect();
|
|
264
|
+
try {
|
|
265
|
+
// Check if secret is reserved first
|
|
266
|
+
const checkResult = await client.query('SELECT is_reserved FROM _secrets WHERE id = $1', [
|
|
267
|
+
id,
|
|
268
|
+
]);
|
|
269
|
+
|
|
270
|
+
if (checkResult.rows.length > 0 && checkResult.rows[0].is_reserved) {
|
|
271
|
+
throw new Error('Cannot delete reserved secret');
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const result = await client.query('DELETE FROM _secrets WHERE id = $1', [id]);
|
|
275
|
+
|
|
276
|
+
const success = (result.rowCount ?? 0) > 0;
|
|
277
|
+
if (success) {
|
|
278
|
+
logger.info('Secret deleted', { id });
|
|
279
|
+
}
|
|
280
|
+
return success;
|
|
281
|
+
} catch (error) {
|
|
282
|
+
logger.error('Failed to delete secret', { error, id });
|
|
283
|
+
throw new Error('Failed to delete secret');
|
|
284
|
+
} finally {
|
|
285
|
+
client.release();
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Rotate a secret (create new value, keep old for grace period)
|
|
291
|
+
*/
|
|
292
|
+
async rotateSecret(id: string, newValue: string): Promise<{ newId: string }> {
|
|
293
|
+
const client = await this.getPool().connect();
|
|
294
|
+
try {
|
|
295
|
+
await client.query('BEGIN');
|
|
296
|
+
|
|
297
|
+
const oldSecretResult = await client.query(`SELECT key FROM _secrets WHERE id = $1`, [id]);
|
|
298
|
+
|
|
299
|
+
if (oldSecretResult.rows.length === 0) {
|
|
300
|
+
throw new Error('Secret not found');
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const secretKey = oldSecretResult.rows[0].key;
|
|
304
|
+
|
|
305
|
+
await client.query(
|
|
306
|
+
`UPDATE _secrets
|
|
307
|
+
SET is_active = false,
|
|
308
|
+
expires_at = NOW() + INTERVAL '24 hours'
|
|
309
|
+
WHERE id = $1`,
|
|
310
|
+
[id]
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
const encryptedValue = EncryptionUtils.encrypt(newValue);
|
|
314
|
+
const newSecretResult = await client.query(
|
|
315
|
+
`INSERT INTO _secrets (key, value_ciphertext)
|
|
316
|
+
VALUES ($1, $2)
|
|
317
|
+
RETURNING id`,
|
|
318
|
+
[secretKey, encryptedValue]
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
await client.query('COMMIT');
|
|
322
|
+
|
|
323
|
+
logger.info('Secret rotated', {
|
|
324
|
+
oldId: id,
|
|
325
|
+
newId: newSecretResult.rows[0].id,
|
|
326
|
+
key: secretKey,
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
return { newId: newSecretResult.rows[0].id };
|
|
330
|
+
} catch (error) {
|
|
331
|
+
await client.query('ROLLBACK');
|
|
332
|
+
logger.error('Failed to rotate secret', { error, id });
|
|
333
|
+
throw new Error('Failed to rotate secret');
|
|
334
|
+
} finally {
|
|
335
|
+
client.release();
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Clean up expired secrets
|
|
341
|
+
*/
|
|
342
|
+
async cleanupExpiredSecrets(): Promise<number> {
|
|
343
|
+
const client = await this.getPool().connect();
|
|
344
|
+
try {
|
|
345
|
+
const result = await client.query(
|
|
346
|
+
`DELETE FROM _secrets
|
|
347
|
+
WHERE expires_at IS NOT NULL
|
|
348
|
+
AND expires_at < NOW()
|
|
349
|
+
RETURNING id`
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
const deletedCount = result.rowCount ?? 0;
|
|
353
|
+
if (deletedCount > 0) {
|
|
354
|
+
logger.info('Expired secrets cleaned up', { count: deletedCount });
|
|
355
|
+
}
|
|
356
|
+
return deletedCount;
|
|
357
|
+
} catch (error) {
|
|
358
|
+
logger.error('Failed to cleanup expired secrets', { error });
|
|
359
|
+
throw new Error('Failed to cleanup expired secrets');
|
|
360
|
+
} finally {
|
|
361
|
+
client.release();
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Generate a new API key with 'ik_' prefix (Insforge Key)
|
|
367
|
+
*/
|
|
368
|
+
generateApiKey(): string {
|
|
369
|
+
return 'ik_' + crypto.randomBytes(32).toString('hex');
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Verify API key against database
|
|
374
|
+
*/
|
|
375
|
+
async verifyApiKey(apiKey: string): Promise<boolean> {
|
|
376
|
+
if (!apiKey) {
|
|
377
|
+
return false;
|
|
378
|
+
}
|
|
379
|
+
return this.checkSecretByKey('API_KEY', apiKey);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Initialize API key on startup
|
|
384
|
+
* Seeds from environment variable if database is empty
|
|
385
|
+
*/
|
|
386
|
+
async initializeApiKey(): Promise<string> {
|
|
387
|
+
let apiKey = await this.getSecretByKey('API_KEY');
|
|
388
|
+
|
|
389
|
+
if (!apiKey) {
|
|
390
|
+
// Check if ACCESS_API_KEY is provided via environment
|
|
391
|
+
const envApiKey = process.env.ACCESS_API_KEY;
|
|
392
|
+
|
|
393
|
+
if (envApiKey && envApiKey.trim() !== '') {
|
|
394
|
+
// Use the provided API key from environment, ensure it has 'ik_' prefix
|
|
395
|
+
apiKey = envApiKey.startsWith('ik_') ? envApiKey : 'ik_' + envApiKey;
|
|
396
|
+
await this.createSecret({ key: 'API_KEY', value: apiKey, isReserved: true });
|
|
397
|
+
logger.info('✅ API key initialized from ACCESS_API_KEY environment variable');
|
|
398
|
+
} else {
|
|
399
|
+
// Generate a new API key if none provided
|
|
400
|
+
apiKey = this.generateApiKey();
|
|
401
|
+
await this.createSecret({ key: 'API_KEY', value: apiKey, isReserved: true });
|
|
402
|
+
logger.info('✅ API key generated and stored');
|
|
403
|
+
}
|
|
404
|
+
} else {
|
|
405
|
+
logger.info('✅ API key exists in database');
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return apiKey;
|
|
409
|
+
}
|
|
410
|
+
}
|