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.
Files changed (395) hide show
  1. package/.dockerignore +58 -0
  2. package/.env.example +49 -0
  3. package/.github/ISSUE_TEMPLATE/bug_report.yml +83 -0
  4. package/.github/ISSUE_TEMPLATE/config.yml +11 -0
  5. package/.github/ISSUE_TEMPLATE/feature_request.yml +79 -0
  6. package/.github/copilot-instructions.md +147 -0
  7. package/.github/workflows/build-image.yml +65 -0
  8. package/.github/workflows/ci-premerge-check.yml +24 -0
  9. package/.github/workflows/deploy-aws.yml +130 -0
  10. package/.github/workflows/lint-and-format.yml +33 -0
  11. package/.prettierignore +65 -0
  12. package/.prettierrc +9 -0
  13. package/CHANGELOG.md +3 -0
  14. package/CONTRIBUTING.md +126 -0
  15. package/Dockerfile +27 -0
  16. package/GITHUB_OAUTH_SETUP.md +49 -0
  17. package/GOOGLE_OAUTH_SETUP.md +148 -0
  18. package/LICENSE +201 -0
  19. package/README.md +134 -0
  20. package/assets/Dark.svg +23 -0
  21. package/assets/archDiagram.png +0 -0
  22. package/assets/banner.png +0 -0
  23. package/assets/mcpInstallv2.png +0 -0
  24. package/assets/sampleResponse.png +0 -0
  25. package/assets/signin.png +0 -0
  26. package/assets/userflow.png +0 -0
  27. package/backend/migrations/000_create-base-tables.sql +142 -0
  28. package/backend/migrations/001_create-helper-functions.sql +41 -0
  29. package/backend/migrations/002_rename-auth-tables.sql +30 -0
  30. package/backend/migrations/003_create-users-table.sql +56 -0
  31. package/backend/migrations/004_add-reload-postgrest-func.sql +24 -0
  32. package/backend/migrations/005_enable-project-admin-modify-users.sql +30 -0
  33. package/backend/migrations/006_modify-ai-usage-table.sql +25 -0
  34. package/backend/migrations/007_drop-metadata-table.sql +2 -0
  35. package/backend/migrations/008_add-system-tables.sql +77 -0
  36. package/backend/migrations/009_add-function-secrets.sql +24 -0
  37. package/backend/migrations/010_modify-ai-config-modalities.sql +93 -0
  38. package/backend/migrations/011_refactor-secrets-table.sql +15 -0
  39. package/backend/migrations/012_add-storage-uploaded-by.sql +8 -0
  40. package/backend/package.json +75 -0
  41. package/backend/src/api/middleware/auth.ts +240 -0
  42. package/backend/src/api/middleware/error.ts +231 -0
  43. package/backend/src/api/middleware/upload.ts +59 -0
  44. package/backend/src/api/routes/agent.ts +29 -0
  45. package/backend/src/api/routes/ai.ts +472 -0
  46. package/backend/src/api/routes/auth.oauth.ts +482 -0
  47. package/backend/src/api/routes/auth.ts +386 -0
  48. package/backend/src/api/routes/database.advance.ts +275 -0
  49. package/backend/src/api/routes/database.records.ts +246 -0
  50. package/backend/src/api/routes/database.tables.ts +161 -0
  51. package/backend/src/api/routes/docs.ts +66 -0
  52. package/backend/src/api/routes/functions.ts +183 -0
  53. package/backend/src/api/routes/logs.ts +150 -0
  54. package/backend/src/api/routes/metadata.ts +160 -0
  55. package/backend/src/api/routes/openapi.ts +82 -0
  56. package/backend/src/api/routes/secrets.ts +199 -0
  57. package/backend/src/api/routes/storage.ts +547 -0
  58. package/backend/src/api/routes/usage.ts +96 -0
  59. package/backend/src/core/ai/chat.ts +207 -0
  60. package/backend/src/core/ai/client.ts +242 -0
  61. package/backend/src/core/ai/config.ts +187 -0
  62. package/backend/src/core/ai/image.ts +156 -0
  63. package/backend/src/core/ai/model.ts +117 -0
  64. package/backend/src/core/ai/usage.ts +290 -0
  65. package/backend/src/core/auth/auth.ts +781 -0
  66. package/backend/src/core/auth/oauth.ts +398 -0
  67. package/backend/src/core/database/advance.ts +1074 -0
  68. package/backend/src/core/database/manager.ts +178 -0
  69. package/backend/src/core/database/table.ts +772 -0
  70. package/backend/src/core/documentation/agent.ts +689 -0
  71. package/backend/src/core/documentation/openapi.ts +856 -0
  72. package/backend/src/core/functions/functions.ts +310 -0
  73. package/backend/src/core/logs/analytics.ts +76 -0
  74. package/backend/src/core/logs/audit.ts +255 -0
  75. package/backend/src/core/logs/providers/base.provider.ts +83 -0
  76. package/backend/src/core/logs/providers/cloudwatch.provider.ts +510 -0
  77. package/backend/src/core/logs/providers/localdb.provider.ts +246 -0
  78. package/backend/src/core/secrets/encryption.ts +58 -0
  79. package/backend/src/core/secrets/secrets.ts +410 -0
  80. package/backend/src/core/socket/socket.ts +388 -0
  81. package/backend/src/core/socket/types.ts +79 -0
  82. package/backend/src/core/storage/storage.ts +923 -0
  83. package/backend/src/server.ts +288 -0
  84. package/backend/src/types/ai.ts +46 -0
  85. package/backend/src/types/auth.ts +90 -0
  86. package/backend/src/types/database.ts +136 -0
  87. package/backend/src/types/error-constants.ts +86 -0
  88. package/backend/src/types/logs.ts +47 -0
  89. package/backend/src/types/profile.ts +55 -0
  90. package/backend/src/types/storage.ts +23 -0
  91. package/backend/src/utils/cloud-token.ts +39 -0
  92. package/backend/src/utils/constants.ts +1 -0
  93. package/backend/src/utils/environment.ts +35 -0
  94. package/backend/src/utils/helpers.ts +49 -0
  95. package/backend/src/utils/logger.ts +13 -0
  96. package/backend/src/utils/response.ts +62 -0
  97. package/backend/src/utils/seed.ts +205 -0
  98. package/backend/src/utils/sql-parser.ts +63 -0
  99. package/backend/src/utils/uuid.ts +9 -0
  100. package/backend/src/utils/validations.ts +129 -0
  101. package/backend/tests/README.md +134 -0
  102. package/backend/tests/cleanup-all-test-data.sh +231 -0
  103. package/backend/tests/cloud/test-s3-multitenant.sh +132 -0
  104. package/backend/tests/local/comprehensive-curl-tests.sh +156 -0
  105. package/backend/tests/local/test-auth-router.sh +144 -0
  106. package/backend/tests/local/test-database-router.sh +222 -0
  107. package/backend/tests/local/test-e2e.sh +241 -0
  108. package/backend/tests/local/test-fk-errors.sh +97 -0
  109. package/backend/tests/local/test-id-field.sh +201 -0
  110. package/backend/tests/local/test-public-bucket.sh +265 -0
  111. package/backend/tests/local/test-secrets.sh +248 -0
  112. package/backend/tests/local/test-serverless-functions.sh.disabled +325 -0
  113. package/backend/tests/local/test-traditional-rest.sh +209 -0
  114. package/backend/tests/manual/README.md +51 -0
  115. package/backend/tests/manual/create-large-table-simple.sql +11 -0
  116. package/backend/tests/manual/seed-large-table.sql +101 -0
  117. package/backend/tests/manual/setup-large-table-extras.sql +34 -0
  118. package/backend/tests/manual/test-better-auth.sh +303 -0
  119. package/backend/tests/manual/test-bulk-upsert.sh +410 -0
  120. package/backend/tests/manual/test-database-advance.sh +297 -0
  121. package/backend/tests/manual/test-postgrest-stability.sh +192 -0
  122. package/backend/tests/manual/test-rawsql-export-import.sh +412 -0
  123. package/backend/tests/manual/test-universal-storage.sh +264 -0
  124. package/backend/tests/manual/test-users.sql +18 -0
  125. package/backend/tests/run-all-tests.sh +140 -0
  126. package/backend/tests/setup.ts +22 -0
  127. package/backend/tests/test-config.sh +303 -0
  128. package/backend/tsconfig.json +23 -0
  129. package/backend/tsup.config.ts +18 -0
  130. package/backend/vitest.config.ts +22 -0
  131. package/docker-compose.prod.yml +145 -0
  132. package/docker-compose.yml +167 -0
  133. package/docker-init/db/db-init.sql +125 -0
  134. package/docker-init/db/jwt.sql +5 -0
  135. package/docker-init/db/logs.sql +9 -0
  136. package/docker-init/db/postgresql.conf +17 -0
  137. package/docs/deprecated/insforge-auth-api.md +215 -0
  138. package/docs/deprecated/insforge-auth-sdk.md +100 -0
  139. package/docs/deprecated/insforge-db-api.md +359 -0
  140. package/docs/deprecated/insforge-db-sdk.md +140 -0
  141. package/docs/deprecated/insforge-debug-sdk.md +157 -0
  142. package/docs/deprecated/insforge-debug.md +65 -0
  143. package/docs/deprecated/insforge-instructions.md +124 -0
  144. package/docs/deprecated/insforge-project.md +118 -0
  145. package/docs/deprecated/insforge-storage-api.md +279 -0
  146. package/docs/deprecated/insforge-storage-sdk.md +159 -0
  147. package/docs/insforge-instructions-sdk.md +407 -0
  148. package/eslint.config.js +317 -0
  149. package/examples/oauth/frontend-oauth-example.html +251 -0
  150. package/examples/response-examples.md +444 -0
  151. package/frontend/README.md +112 -0
  152. package/frontend/components.json +17 -0
  153. package/frontend/index.html +13 -0
  154. package/frontend/package.json +63 -0
  155. package/frontend/public/favicon.ico +0 -0
  156. package/frontend/src/App.tsx +106 -0
  157. package/frontend/src/assets/icons/checkbox_checked.svg +6 -0
  158. package/frontend/src/assets/icons/checkbox_undetermined.svg +6 -0
  159. package/frontend/src/assets/icons/checked.svg +3 -0
  160. package/frontend/src/assets/icons/error.svg +3 -0
  161. package/frontend/src/assets/icons/pencil.svg +4 -0
  162. package/frontend/src/assets/icons/refresh.svg +4 -0
  163. package/frontend/src/assets/icons/step_active.svg +3 -0
  164. package/frontend/src/assets/icons/step_inactive.svg +11 -0
  165. package/frontend/src/assets/icons/warning.svg +3 -0
  166. package/frontend/src/assets/logos/amazon.svg +1 -0
  167. package/frontend/src/assets/logos/claude_code.svg +3 -0
  168. package/frontend/src/assets/logos/cline.svg +6 -0
  169. package/frontend/src/assets/logos/cursor.svg +20 -0
  170. package/frontend/src/assets/logos/discord.svg +9 -0
  171. package/frontend/src/assets/logos/gemini.svg +19 -0
  172. package/frontend/src/assets/logos/github.svg +5 -0
  173. package/frontend/src/assets/logos/google.svg +13 -0
  174. package/frontend/src/assets/logos/grok.svg +10 -0
  175. package/frontend/src/assets/logos/insforge_dark.svg +15 -0
  176. package/frontend/src/assets/logos/insforge_light.svg +15 -0
  177. package/frontend/src/assets/logos/openai.svg +10 -0
  178. package/frontend/src/assets/logos/roo_code.svg +9 -0
  179. package/frontend/src/assets/logos/trae.svg +3 -0
  180. package/frontend/src/assets/logos/windsurf.svg +10 -0
  181. package/frontend/src/components/ButtonWithLoading.tsx +27 -0
  182. package/frontend/src/components/Checkbox.tsx +61 -0
  183. package/frontend/src/components/CodeBlock.tsx +32 -0
  184. package/frontend/src/components/ConfirmDialog.tsx +96 -0
  185. package/frontend/src/components/CopyButton.tsx +69 -0
  186. package/frontend/src/components/DeleteActionButton.tsx +42 -0
  187. package/frontend/src/components/EmptyState.tsx +41 -0
  188. package/frontend/src/components/ErrorState.tsx +35 -0
  189. package/frontend/src/components/FeatureSidebar.tsx +126 -0
  190. package/frontend/src/components/FeatureSidebarItem.tsx +101 -0
  191. package/frontend/src/components/JsonHighlight.tsx +61 -0
  192. package/frontend/src/components/LoadingState.tsx +16 -0
  193. package/frontend/src/components/PaginationControls.tsx +54 -0
  194. package/frontend/src/components/PromptDialog.tsx +68 -0
  195. package/frontend/src/components/SearchInput.tsx +90 -0
  196. package/frontend/src/components/SelectionClearButton.tsx +26 -0
  197. package/frontend/src/components/Stepper.tsx +139 -0
  198. package/frontend/src/components/ThemeToggle.tsx +58 -0
  199. package/frontend/src/components/TypeBadge.tsx +20 -0
  200. package/frontend/src/components/datagrid/DataGrid.tsx +264 -0
  201. package/frontend/src/components/datagrid/DefaultCellRenderer.tsx +114 -0
  202. package/frontend/src/components/datagrid/IdCell.tsx +44 -0
  203. package/frontend/src/components/datagrid/SortableHeader.tsx +74 -0
  204. package/frontend/src/components/datagrid/cell-editors/BooleanCellEditor.tsx +54 -0
  205. package/frontend/src/components/datagrid/cell-editors/DateCellEditor.tsx +483 -0
  206. package/frontend/src/components/datagrid/cell-editors/JsonCellEditor.tsx +362 -0
  207. package/frontend/src/components/datagrid/cell-editors/TextCellEditor.tsx +38 -0
  208. package/frontend/src/components/datagrid/cell-editors/index.ts +14 -0
  209. package/frontend/src/components/datagrid/cell-editors/types.ts +43 -0
  210. package/frontend/src/components/datagrid/datagridTypes.tsx +72 -0
  211. package/frontend/src/components/datagrid/index.tsx +20 -0
  212. package/frontend/src/components/index.ts +39 -0
  213. package/frontend/src/components/layout/AppHeader.tsx +146 -0
  214. package/frontend/src/components/layout/AppSidebar.tsx +190 -0
  215. package/frontend/src/components/layout/CloudLayout.tsx +95 -0
  216. package/frontend/src/components/layout/Layout.tsx +43 -0
  217. package/frontend/src/components/radix/Alert.tsx +45 -0
  218. package/frontend/src/components/radix/AlertDialog.tsx +115 -0
  219. package/frontend/src/components/radix/Avatar.tsx +45 -0
  220. package/frontend/src/components/radix/Badge.tsx +33 -0
  221. package/frontend/src/components/radix/Button.tsx +50 -0
  222. package/frontend/src/components/radix/Card.tsx +58 -0
  223. package/frontend/src/components/radix/Dialog.tsx +98 -0
  224. package/frontend/src/components/radix/DropdownMenu.tsx +185 -0
  225. package/frontend/src/components/radix/Form.tsx +167 -0
  226. package/frontend/src/components/radix/Input.tsx +22 -0
  227. package/frontend/src/components/radix/Label.tsx +19 -0
  228. package/frontend/src/components/radix/Popover.tsx +29 -0
  229. package/frontend/src/components/radix/ScrollArea.tsx +44 -0
  230. package/frontend/src/components/radix/Select.tsx +151 -0
  231. package/frontend/src/components/radix/Separator.tsx +26 -0
  232. package/frontend/src/components/radix/Sheet.tsx +119 -0
  233. package/frontend/src/components/radix/Skeleton.tsx +7 -0
  234. package/frontend/src/components/radix/Switch.tsx +29 -0
  235. package/frontend/src/components/radix/Tabs.tsx +50 -0
  236. package/frontend/src/components/radix/Textarea.tsx +21 -0
  237. package/frontend/src/components/radix/Tooltip.tsx +28 -0
  238. package/frontend/src/features/ai/components/AIConfigCard.tsx +154 -0
  239. package/frontend/src/features/ai/components/AIConfigDialog.tsx +76 -0
  240. package/frontend/src/features/ai/components/AIConfigForm.tsx +222 -0
  241. package/frontend/src/features/ai/components/AIEmptyState.tsx +18 -0
  242. package/frontend/src/features/ai/components/fields/ModalityField.tsx +87 -0
  243. package/frontend/src/features/ai/components/fields/ModelSelectionField.tsx +134 -0
  244. package/frontend/src/features/ai/components/fields/SystemPromptField.tsx +33 -0
  245. package/frontend/src/features/ai/helpers.ts +155 -0
  246. package/frontend/src/features/ai/hooks/useAIConfigs.ts +221 -0
  247. package/frontend/src/features/ai/hooks/useAIUsage.ts +77 -0
  248. package/frontend/src/features/ai/page/AIPage.tsx +178 -0
  249. package/frontend/src/features/ai/services/ai.service.ts +148 -0
  250. package/frontend/src/features/auth/components/AddOAuthDialog.tsx +106 -0
  251. package/frontend/src/features/auth/components/AuthMethodTab.tsx +238 -0
  252. package/frontend/src/features/auth/components/OAuthConfigDialog.tsx +303 -0
  253. package/frontend/src/features/auth/components/OAuthEmptyState.tsx +15 -0
  254. package/frontend/src/features/auth/components/UserFormDialog.tsx +248 -0
  255. package/frontend/src/features/auth/components/UsersDataGrid.tsx +183 -0
  256. package/frontend/src/features/auth/components/UsersTab.tsx +114 -0
  257. package/frontend/src/features/auth/hooks/useOAuthConfig.ts +129 -0
  258. package/frontend/src/features/auth/hooks/useUsers.ts +57 -0
  259. package/frontend/src/features/auth/index.ts +9 -0
  260. package/frontend/src/features/auth/page/AuthenticationPage.tsx +169 -0
  261. package/frontend/src/features/auth/services/auth.service.ts +112 -0
  262. package/frontend/src/features/auth/services/oauth.service.ts +49 -0
  263. package/frontend/src/features/dashboard/page/DashboardPage.tsx +194 -0
  264. package/frontend/src/features/database/components/ColumnTypeSelect.tsx +64 -0
  265. package/frontend/src/features/database/components/DatabaseDataGrid.tsx +282 -0
  266. package/frontend/src/features/database/components/ForeignKeyCell.tsx +187 -0
  267. package/frontend/src/features/database/components/ForeignKeyPopover.tsx +378 -0
  268. package/frontend/src/features/database/components/LinkRecordModal.tsx +288 -0
  269. package/frontend/src/features/database/components/RecordFormDialog.tsx +164 -0
  270. package/frontend/src/features/database/components/RecordFormField.tsx +568 -0
  271. package/frontend/src/features/database/components/TableEmptyState.tsx +21 -0
  272. package/frontend/src/features/database/components/TableForm.tsx +656 -0
  273. package/frontend/src/features/database/components/TableFormColumn.tsx +137 -0
  274. package/frontend/src/features/database/components/TableListSkeleton.tsx +9 -0
  275. package/frontend/src/features/database/components/TableSidebar.tsx +47 -0
  276. package/frontend/src/features/database/constants.ts +26 -0
  277. package/frontend/src/features/database/helpers.ts +125 -0
  278. package/frontend/src/features/database/hooks/UseLinkModal.tsx +78 -0
  279. package/frontend/src/features/database/index.ts +12 -0
  280. package/frontend/src/features/database/page/DatabasePage.tsx +626 -0
  281. package/frontend/src/features/database/schema.ts +25 -0
  282. package/frontend/src/features/database/services/database.service.ts +216 -0
  283. package/frontend/src/features/functions/components/FunctionEmptyState.tsx +15 -0
  284. package/frontend/src/features/functions/components/FunctionRow.tsx +71 -0
  285. package/frontend/src/features/functions/components/FunctionViewer.tsx +46 -0
  286. package/frontend/src/features/functions/components/FunctionsContent.tsx +88 -0
  287. package/frontend/src/features/functions/components/FunctionsSidebar.tsx +56 -0
  288. package/frontend/src/features/functions/components/SecretEmptyState.tsx +23 -0
  289. package/frontend/src/features/functions/components/SecretRow.tsx +68 -0
  290. package/frontend/src/features/functions/components/SecretsContent.tsx +120 -0
  291. package/frontend/src/features/functions/hooks/useFunctions.ts +106 -0
  292. package/frontend/src/features/functions/page/FunctionsPage.tsx +28 -0
  293. package/frontend/src/features/functions/services/functions.service.ts +48 -0
  294. package/frontend/src/features/login/components/AuthErrorBoundary.tsx +87 -0
  295. package/frontend/src/features/login/components/PrivateRoute.tsx +24 -0
  296. package/frontend/src/features/login/page/CloudLoginPage.tsx +93 -0
  297. package/frontend/src/features/login/page/LoginPage.tsx +174 -0
  298. package/frontend/src/features/logs/components/AnalyticsLogsTable.tsx +313 -0
  299. package/frontend/src/features/logs/components/LogsTable.tsx +199 -0
  300. package/frontend/src/features/logs/hooks/useAuditLogs.ts +39 -0
  301. package/frontend/src/features/logs/index.ts +5 -0
  302. package/frontend/src/features/logs/page/AnalyticsLogsPage.tsx +530 -0
  303. package/frontend/src/features/logs/page/AuditsPage.tsx +192 -0
  304. package/frontend/src/features/logs/services/log.service.ts +171 -0
  305. package/frontend/src/features/metadata/hooks/useMetadata.ts +53 -0
  306. package/frontend/src/features/metadata/index.ts +0 -0
  307. package/frontend/src/features/metadata/page/MetadataPage.tsx +136 -0
  308. package/frontend/src/features/metadata/services/metadata.service.ts +17 -0
  309. package/frontend/src/features/onboard/components/CompletionCard.tsx +41 -0
  310. package/frontend/src/features/onboard/components/OnboardButton.tsx +84 -0
  311. package/frontend/src/features/onboard/components/StepContent.tsx +91 -0
  312. package/frontend/src/features/onboard/components/TestConnectionStep.tsx +53 -0
  313. package/frontend/src/features/onboard/components/mcp/CursorDeeplinkGenerator.tsx +35 -0
  314. package/frontend/src/features/onboard/components/mcp/McpInstallation.tsx +144 -0
  315. package/frontend/src/features/onboard/components/mcp/index.ts +4 -0
  316. package/frontend/src/features/onboard/components/mcp/mcp-helper.tsx +98 -0
  317. package/frontend/src/features/onboard/index.ts +3 -0
  318. package/frontend/src/features/onboard/page/OnBoardPage.tsx +104 -0
  319. package/frontend/src/features/onboard/types.ts +8 -0
  320. package/frontend/src/features/secrets/hooks/useSecrets.ts +139 -0
  321. package/frontend/src/features/secrets/services/secrets.service.ts +57 -0
  322. package/frontend/src/features/storage/components/BucketEmptyState.tsx +19 -0
  323. package/frontend/src/features/storage/components/BucketFormDialog.tsx +194 -0
  324. package/frontend/src/features/storage/components/BucketListSkeleton.tsx +17 -0
  325. package/frontend/src/features/storage/components/FilePreviewDialog.tsx +287 -0
  326. package/frontend/src/features/storage/components/StorageDataGrid.tsx +239 -0
  327. package/frontend/src/features/storage/components/StorageManager.tsx +236 -0
  328. package/frontend/src/features/storage/components/StorageSidebar.tsx +44 -0
  329. package/frontend/src/features/storage/components/UploadToast.tsx +46 -0
  330. package/frontend/src/features/storage/index.ts +3 -0
  331. package/frontend/src/features/storage/page/StoragePage.tsx +553 -0
  332. package/frontend/src/features/storage/services/storage.service.ts +144 -0
  333. package/frontend/src/features/visualizer/components/AuthNode.tsx +107 -0
  334. package/frontend/src/features/visualizer/components/BucketNode.tsx +34 -0
  335. package/frontend/src/features/visualizer/components/SchemaVisualizer.tsx +359 -0
  336. package/frontend/src/features/visualizer/components/TableNode.tsx +152 -0
  337. package/frontend/src/features/visualizer/components/VisualizerSkeleton.tsx +24 -0
  338. package/frontend/src/features/visualizer/components/index.ts +5 -0
  339. package/frontend/src/features/visualizer/page/VisualizerPage.tsx +127 -0
  340. package/frontend/src/index.css +248 -0
  341. package/frontend/src/lib/api/client.ts +163 -0
  342. package/frontend/src/lib/contexts/AuthContext.tsx +157 -0
  343. package/frontend/src/lib/contexts/OnboardStepContext.tsx +68 -0
  344. package/frontend/src/lib/contexts/SocketContext.tsx +303 -0
  345. package/frontend/src/lib/contexts/ThemeContext.tsx +125 -0
  346. package/frontend/src/lib/hooks/useAuth.ts +4 -0
  347. package/frontend/src/lib/hooks/useConfirm.ts +55 -0
  348. package/frontend/src/lib/hooks/useInterval.ts +27 -0
  349. package/frontend/src/lib/hooks/useMediaQuery.ts +59 -0
  350. package/frontend/src/lib/hooks/useOnboardingCompletion.ts +29 -0
  351. package/frontend/src/lib/hooks/usePagination.ts +27 -0
  352. package/frontend/src/lib/hooks/useTimeout.ts +27 -0
  353. package/frontend/src/lib/hooks/useToast.tsx +229 -0
  354. package/frontend/src/lib/utils/constants.ts +38 -0
  355. package/frontend/src/lib/utils/utils.ts +165 -0
  356. package/frontend/src/lib/utils/validation-schemas.ts +126 -0
  357. package/frontend/src/main.tsx +16 -0
  358. package/frontend/src/rdg.css +194 -0
  359. package/frontend/src/vite-env.d.ts +12 -0
  360. package/frontend/tailwind.config.js +97 -0
  361. package/frontend/tsconfig.json +26 -0
  362. package/frontend/tsconfig.node.json +10 -0
  363. package/frontend/vite.config.ts +37 -0
  364. package/frontend/vitest.config.ts +36 -0
  365. package/functions/deno.json +25 -0
  366. package/functions/server.ts +290 -0
  367. package/functions/worker-template.js +126 -0
  368. package/openapi/ai.yaml +689 -0
  369. package/openapi/auth.yaml +563 -0
  370. package/openapi/functions.yaml +476 -0
  371. package/openapi/health.yaml +30 -0
  372. package/openapi/logs.yaml +224 -0
  373. package/openapi/metadata.yaml +178 -0
  374. package/openapi/records.yaml +382 -0
  375. package/openapi/secrets.yaml +371 -0
  376. package/openapi/storage.yaml +876 -0
  377. package/openapi/tables.yaml +464 -0
  378. package/package.json +88 -0
  379. package/shared-schemas/package.json +31 -0
  380. package/shared-schemas/src/ai-api.schema.ts +167 -0
  381. package/shared-schemas/src/ai.schema.ts +54 -0
  382. package/shared-schemas/src/auth-api.schema.ts +193 -0
  383. package/shared-schemas/src/auth.schema.ts +94 -0
  384. package/shared-schemas/src/database-api.schema.ts +259 -0
  385. package/shared-schemas/src/database.schema.ts +69 -0
  386. package/shared-schemas/src/functions-api.schema.ts +25 -0
  387. package/shared-schemas/src/functions.schema.ts +16 -0
  388. package/shared-schemas/src/index.ts +13 -0
  389. package/shared-schemas/src/logs-api.schema.ts +49 -0
  390. package/shared-schemas/src/logs.schema.ts +14 -0
  391. package/shared-schemas/src/metadata.schema.ts +56 -0
  392. package/shared-schemas/src/storage-api.schema.ts +65 -0
  393. package/shared-schemas/src/storage.schema.ts +19 -0
  394. package/shared-schemas/tsconfig.json +21 -0
  395. 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
+ }