insforge 1.2.10 → 1.3.0

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 (335) hide show
  1. package/.claude-plugin/marketplace.json +20 -20
  2. package/.dockerignore +60 -60
  3. package/.env.example +83 -77
  4. package/.github/ISSUE_TEMPLATE/bug_report.yml +36 -36
  5. package/.github/ISSUE_TEMPLATE/config.yml +11 -11
  6. package/.github/ISSUE_TEMPLATE/feature_request.yml +26 -26
  7. package/.github/PULL_REQUEST_TEMPLATE.md +7 -7
  8. package/.github/copilot-instructions.md +146 -146
  9. package/.github/workflows/build-image.yml +65 -65
  10. package/.github/workflows/ci-premerge-check.yml +23 -23
  11. package/.github/workflows/e2e.yml +63 -63
  12. package/.github/workflows/lint-and-format.yml +32 -32
  13. package/.prettierignore +64 -64
  14. package/CHANGELOG.md +44 -44
  15. package/CLAUDE_PLUGIN.md +104 -104
  16. package/CODE_OF_CONDUCT.md +128 -128
  17. package/CONTRIBUTING.md +125 -125
  18. package/Dockerfile +30 -30
  19. package/GITHUB_OAUTH_SETUP.md +49 -49
  20. package/GOOGLE_OAUTH_SETUP.md +148 -148
  21. package/LICENSE +201 -201
  22. package/README.md +182 -182
  23. package/assets/Dark.svg +23 -23
  24. package/auth/package.json +28 -28
  25. package/auth/src/lib/broadcastService.ts +117 -115
  26. package/auth/src/pages/SignInPage.tsx +60 -57
  27. package/auth/src/pages/SignUpPage.tsx +60 -57
  28. package/auth/tsconfig.json +32 -32
  29. package/auth/tsconfig.node.json +11 -11
  30. package/backend/package.json +78 -75
  31. package/backend/src/api/routes/ai/index.routes.ts +3 -3
  32. package/backend/src/api/routes/auth/index.routes.ts +667 -570
  33. package/backend/src/api/routes/auth/oauth.routes.ts +473 -448
  34. package/backend/src/api/routes/database/advance.routes.ts +37 -16
  35. package/backend/src/api/routes/database/index.routes.ts +78 -1
  36. package/backend/src/api/routes/database/records.routes.ts +10 -10
  37. package/backend/src/api/routes/database/tables.routes.ts +0 -14
  38. package/backend/src/api/routes/docs/index.routes.ts +75 -76
  39. package/backend/src/api/routes/email/index.routes.ts +35 -0
  40. package/backend/src/api/routes/functions/index.routes.ts +18 -12
  41. package/backend/src/api/routes/metadata/index.routes.ts +12 -0
  42. package/backend/src/api/routes/realtime/channels.routes.ts +81 -0
  43. package/backend/src/api/routes/realtime/index.routes.ts +12 -0
  44. package/backend/src/api/routes/realtime/messages.routes.ts +48 -0
  45. package/backend/src/api/routes/realtime/permissions.routes.ts +19 -0
  46. package/backend/src/api/routes/storage/index.routes.ts +18 -12
  47. package/backend/src/api/routes/usage/index.routes.ts +6 -4
  48. package/backend/src/infra/database/database.manager.ts +14 -1
  49. package/backend/src/infra/database/migrations/000_create-base-tables.sql +141 -141
  50. package/backend/src/infra/database/migrations/001_create-helper-functions.sql +40 -40
  51. package/backend/src/infra/database/migrations/002_rename-auth-tables.sql +29 -29
  52. package/backend/src/infra/database/migrations/003_create-users-table.sql +55 -55
  53. package/backend/src/infra/database/migrations/004_add-reload-postgrest-func.sql +23 -23
  54. package/backend/src/infra/database/migrations/005_enable-project-admin-modify-users.sql +29 -29
  55. package/backend/src/infra/database/migrations/006_modify-ai-usage-table.sql +24 -24
  56. package/backend/src/infra/database/migrations/007_drop-metadata-table.sql +1 -1
  57. package/backend/src/infra/database/migrations/008_add-system-tables.sql +76 -76
  58. package/backend/src/infra/database/migrations/009_add-function-secrets.sql +23 -23
  59. package/backend/src/infra/database/migrations/010_modify-ai-config-modalities.sql +93 -93
  60. package/backend/src/infra/database/migrations/011_refactor-secrets-table.sql +15 -15
  61. package/backend/src/infra/database/migrations/012_add-storage-uploaded-by.sql +7 -7
  62. package/backend/src/infra/database/migrations/013_create-auth-schema-functions.sql +44 -44
  63. package/backend/src/infra/database/migrations/014_add-updated-at-trigger-user-table.sql +7 -7
  64. package/backend/src/infra/database/migrations/015_create-auth-config-and-email-otp-tables.sql +59 -59
  65. package/backend/src/infra/database/migrations/016_update-auth-config-and-email-otp.sql +24 -24
  66. package/backend/src/infra/database/migrations/017_create-realtime-schema.sql +233 -0
  67. package/backend/src/infra/realtime/realtime.manager.ts +246 -0
  68. package/backend/src/infra/realtime/webhook-sender.ts +82 -0
  69. package/backend/src/infra/security/token.manager.ts +219 -125
  70. package/backend/src/infra/socket/socket.manager.ts +198 -64
  71. package/backend/src/providers/ai/openrouter.provider.ts +12 -9
  72. package/backend/src/providers/email/base.provider.ts +4 -7
  73. package/backend/src/providers/email/cloud.provider.ts +84 -0
  74. package/backend/src/providers/oauth/apple.provider.ts +266 -0
  75. package/backend/src/providers/oauth/index.ts +1 -0
  76. package/backend/src/server.ts +317 -284
  77. package/backend/src/services/ai/ai-model.service.ts +5 -5
  78. package/backend/src/services/ai/chat-completion.service.ts +4 -4
  79. package/backend/src/services/ai/image-generation.service.ts +3 -3
  80. package/backend/src/services/auth/auth.service.ts +14 -0
  81. package/backend/src/services/database/database-table.service.ts +0 -9
  82. package/backend/src/services/database/database.service.ts +127 -0
  83. package/backend/src/services/email/email.service.ts +5 -7
  84. package/backend/src/services/realtime/index.ts +3 -0
  85. package/backend/src/services/realtime/realtime-auth.service.ts +104 -0
  86. package/backend/src/services/realtime/realtime-channel.service.ts +237 -0
  87. package/backend/src/services/realtime/realtime-message.service.ts +260 -0
  88. package/backend/src/types/auth.ts +11 -0
  89. package/backend/src/types/realtime.ts +18 -0
  90. package/backend/src/types/socket.ts +7 -31
  91. package/backend/src/utils/cookies.ts +35 -0
  92. package/backend/src/utils/s3-config-loader.ts +64 -0
  93. package/backend/src/utils/seed.ts +301 -298
  94. package/backend/src/utils/sql-parser.ts +90 -0
  95. package/backend/tests/README.md +133 -133
  96. package/backend/tests/cleanup-all-test-data.sh +230 -230
  97. package/backend/tests/cloud/test-s3-multitenant.sh +131 -131
  98. package/backend/tests/local/comprehensive-curl-tests.sh +155 -155
  99. package/backend/tests/local/test-ai-config.sh +129 -129
  100. package/backend/tests/local/test-ai-usage.sh +80 -80
  101. package/backend/tests/local/test-auth-router.sh +143 -143
  102. package/backend/tests/local/test-database-router.sh +222 -222
  103. package/backend/tests/local/test-e2e.sh +240 -240
  104. package/backend/tests/local/test-fk-errors.sh +96 -96
  105. package/backend/tests/local/test-functions.sh +123 -123
  106. package/backend/tests/local/test-id-field.sh +200 -200
  107. package/backend/tests/local/test-logs.sh +132 -132
  108. package/backend/tests/local/test-public-bucket.sh +264 -264
  109. package/backend/tests/local/test-secrets.sh +249 -249
  110. package/backend/tests/local/test-serverless-functions.sh.disabled +325 -325
  111. package/backend/tests/local/test-traditional-rest.sh +208 -208
  112. package/backend/tests/manual/README.md +50 -50
  113. package/backend/tests/manual/create-large-table-simple.sql +10 -10
  114. package/backend/tests/manual/seed-large-table.sql +100 -100
  115. package/backend/tests/manual/setup-large-table-extras.sql +33 -33
  116. package/backend/tests/manual/test-bulk-upsert.sh +409 -409
  117. package/backend/tests/manual/test-database-advance.sh +296 -296
  118. package/backend/tests/manual/test-postgrest-stability.sh +191 -191
  119. package/backend/tests/manual/test-rawsql-export-import.sh +411 -411
  120. package/backend/tests/manual/test-rawsql-modes.sh +244 -244
  121. package/backend/tests/manual/test-universal-storage.sh +263 -263
  122. package/backend/tests/manual/test-users.sql +17 -17
  123. package/backend/tests/run-all-tests.sh +139 -139
  124. package/backend/tests/setup.ts +0 -0
  125. package/backend/tests/test-config.sh +338 -338
  126. package/backend/tests/unit/analyze-query.test.ts +697 -0
  127. package/backend/tsconfig.json +22 -22
  128. package/claude-plugin/.claude-plugin/plugin.json +24 -24
  129. package/claude-plugin/README.md +133 -133
  130. package/claude-plugin/skills/insforge-schema-patterns/SKILL.md +270 -270
  131. package/docker-compose.prod.yml +204 -200
  132. package/docker-compose.yml +232 -228
  133. package/docker-init/db/db-init.sql +97 -97
  134. package/docker-init/db/jwt.sql +5 -5
  135. package/docker-init/db/postgresql.conf +16 -16
  136. package/docker-init/logs/vector.yml +236 -236
  137. package/docs/README.md +44 -44
  138. package/docs/agent-docs/real-time.md +269 -0
  139. package/docs/changelog.mdx +119 -67
  140. package/docs/core-concepts/ai/architecture.mdx +372 -372
  141. package/docs/core-concepts/ai/sdk.mdx +213 -213
  142. package/docs/core-concepts/authentication/architecture.mdx +278 -278
  143. package/docs/core-concepts/authentication/sdk.mdx +414 -414
  144. package/docs/core-concepts/authentication/ui-components/customization.mdx +529 -529
  145. package/docs/core-concepts/authentication/ui-components/nextjs.mdx +221 -221
  146. package/docs/core-concepts/authentication/ui-components/react-router.mdx +184 -184
  147. package/docs/core-concepts/authentication/ui-components/react.mdx +129 -129
  148. package/docs/core-concepts/database/architecture.mdx +255 -255
  149. package/docs/core-concepts/database/sdk.mdx +382 -382
  150. package/docs/core-concepts/email/architecture.mdx +101 -0
  151. package/docs/core-concepts/email/sdk.mdx +53 -0
  152. package/docs/core-concepts/functions/architecture.mdx +105 -105
  153. package/docs/core-concepts/functions/sdk.mdx +184 -184
  154. package/docs/core-concepts/realtime/architecture.mdx +446 -0
  155. package/docs/core-concepts/realtime/sdk.mdx +409 -0
  156. package/docs/core-concepts/storage/architecture.mdx +243 -243
  157. package/docs/core-concepts/storage/sdk.mdx +253 -253
  158. package/docs/deployment/README.md +94 -94
  159. package/docs/deployment/deploy-to-aws-ec2.md +564 -564
  160. package/docs/deployment/deploy-to-azure-virtual-machines.md +312 -312
  161. package/docs/deployment/deploy-to-google-cloud-compute-engine.md +613 -613
  162. package/docs/deployment/deploy-to-render.md +441 -441
  163. package/docs/deprecated/insforge-auth-api.md +214 -214
  164. package/docs/deprecated/insforge-auth-sdk.md +99 -99
  165. package/docs/deprecated/insforge-db-api.md +358 -358
  166. package/docs/deprecated/insforge-db-sdk.md +139 -139
  167. package/docs/deprecated/insforge-debug-sdk.md +156 -156
  168. package/docs/deprecated/insforge-debug.md +64 -64
  169. package/docs/deprecated/insforge-instructions.md +123 -123
  170. package/docs/deprecated/insforge-project.md +117 -117
  171. package/docs/deprecated/insforge-storage-api.md +278 -278
  172. package/docs/deprecated/insforge-storage-sdk.md +158 -158
  173. package/docs/docs.json +232 -210
  174. package/docs/examples/framework-guides/nextjs.mdx +131 -131
  175. package/docs/examples/framework-guides/nuxt.mdx +165 -165
  176. package/docs/examples/framework-guides/react.mdx +165 -165
  177. package/docs/examples/framework-guides/svelte.mdx +153 -153
  178. package/docs/examples/framework-guides/vue.mdx +159 -159
  179. package/docs/examples/overview.mdx +67 -67
  180. package/docs/favicon.svg +19 -19
  181. package/docs/images/changelog/dec-2025/ai-integration.png +0 -0
  182. package/docs/images/changelog/dec-2025/ai-models.webp +0 -0
  183. package/docs/images/changelog/dec-2025/alipay-payment.webp +0 -0
  184. package/docs/images/changelog/dec-2025/apple-login.jpg +0 -0
  185. package/docs/images/changelog/dec-2025/mcp-installer.png +0 -0
  186. package/docs/images/changelog/dec-2025/realtime-module.jpg +0 -0
  187. package/docs/images/icons/ai.svg +4 -4
  188. package/docs/images/logos/nextjs.svg +4 -4
  189. package/docs/images/logos/nuxt.svg +4 -4
  190. package/docs/images/logos/react.svg +5 -5
  191. package/docs/images/logos/svelte.svg +4 -4
  192. package/docs/images/logos/vue.svg +5 -5
  193. package/docs/insforge-instructions-sdk.md +89 -88
  194. package/docs/introduction.mdx +45 -45
  195. package/docs/logo/dark.svg +22 -22
  196. package/docs/logo/light.svg +20 -20
  197. package/docs/partnership.mdx +651 -646
  198. package/docs/quickstart.mdx +82 -82
  199. package/docs/showcase.mdx +52 -52
  200. package/docs/snippets/sdk-installation.mdx +21 -21
  201. package/docs/snippets/service-icons.mdx +27 -27
  202. package/examples/oauth/frontend-oauth-example.html +250 -250
  203. package/examples/response-examples.md +443 -443
  204. package/frontend/components.json +17 -17
  205. package/frontend/package.json +69 -69
  206. package/frontend/src/assets/icons/checkbox_checked.svg +6 -6
  207. package/frontend/src/assets/icons/checkbox_undetermined.svg +6 -6
  208. package/frontend/src/assets/icons/checked.svg +3 -3
  209. package/frontend/src/assets/icons/connected.svg +3 -3
  210. package/frontend/src/assets/icons/error.svg +3 -3
  211. package/frontend/src/assets/icons/loader.svg +9 -9
  212. package/frontend/src/assets/icons/pencil.svg +4 -4
  213. package/frontend/src/assets/icons/refresh.svg +4 -4
  214. package/frontend/src/assets/icons/step_active.svg +3 -3
  215. package/frontend/src/assets/icons/step_inactive.svg +11 -11
  216. package/frontend/src/assets/icons/warning.svg +3 -3
  217. package/frontend/src/assets/logos/apple.svg +3 -3
  218. package/frontend/src/assets/logos/claude_code.svg +3 -3
  219. package/frontend/src/assets/logos/cline.svg +6 -6
  220. package/frontend/src/assets/logos/cursor.svg +20 -20
  221. package/frontend/src/assets/logos/discord.svg +8 -8
  222. package/frontend/src/assets/logos/facebook.svg +3 -3
  223. package/frontend/src/assets/logos/gemini.svg +19 -19
  224. package/frontend/src/assets/logos/github.svg +5 -5
  225. package/frontend/src/assets/logos/google.svg +13 -13
  226. package/frontend/src/assets/logos/grok.svg +10 -10
  227. package/frontend/src/assets/logos/insforge_dark.svg +15 -15
  228. package/frontend/src/assets/logos/insforge_light.svg +15 -15
  229. package/frontend/src/assets/logos/instagram.svg +1 -1
  230. package/frontend/src/assets/logos/linkedin.svg +3 -3
  231. package/frontend/src/assets/logos/openai.svg +10 -10
  232. package/frontend/src/assets/logos/roo_code.svg +9 -9
  233. package/frontend/src/assets/logos/spotify.svg +16 -16
  234. package/frontend/src/assets/logos/tiktok.svg +5 -5
  235. package/frontend/src/assets/logos/trae.svg +3 -3
  236. package/frontend/src/assets/logos/windsurf.svg +10 -10
  237. package/frontend/src/assets/logos/x.svg +3 -3
  238. package/frontend/src/components/layout/AppHeader.tsx +9 -10
  239. package/frontend/src/features/auth/components/OAuthConfigDialog.tsx +1 -0
  240. package/frontend/src/features/auth/components/UsersDataGrid.tsx +6 -0
  241. package/frontend/src/features/auth/helpers.tsx +8 -0
  242. package/frontend/src/features/auth/{page → pages}/UsersPage.tsx +0 -28
  243. package/frontend/src/features/database/components/SQLModal.tsx +75 -0
  244. package/frontend/src/features/database/components/TableForm.tsx +0 -4
  245. package/frontend/src/features/database/hooks/useDatabase.ts +66 -0
  246. package/frontend/src/features/database/hooks/useTables.ts +32 -28
  247. package/frontend/src/features/database/index.ts +1 -0
  248. package/frontend/src/features/database/{page → pages}/FunctionsPage.tsx +29 -37
  249. package/frontend/src/features/database/{page → pages}/IndexesPage.tsx +35 -47
  250. package/frontend/src/features/database/{page → pages}/PoliciesPage.tsx +43 -54
  251. package/frontend/src/features/database/{page → pages}/TablesPage.tsx +0 -42
  252. package/frontend/src/features/database/{page → pages}/TriggersPage.tsx +35 -47
  253. package/frontend/src/features/database/services/advance.service.ts +0 -26
  254. package/frontend/src/features/database/services/database.service.ts +55 -0
  255. package/frontend/src/features/database/services/table.service.ts +0 -6
  256. package/frontend/src/features/functions/{page → pages}/FunctionsPage.tsx +21 -44
  257. package/frontend/src/features/functions/{page → pages}/SecretsPage.tsx +11 -9
  258. package/frontend/src/features/logs/hooks/useMcpUsage.ts +13 -66
  259. package/frontend/src/features/realtime/components/ChannelRow.tsx +83 -0
  260. package/frontend/src/features/realtime/components/EditChannelModal.tsx +246 -0
  261. package/frontend/src/features/realtime/components/MessageRow.tsx +85 -0
  262. package/frontend/src/features/realtime/components/RealtimeEmptyState.tsx +30 -0
  263. package/frontend/src/features/realtime/hooks/useRealtime.ts +218 -0
  264. package/frontend/src/features/realtime/index.ts +11 -0
  265. package/frontend/src/features/realtime/pages/RealtimeChannelsPage.tsx +172 -0
  266. package/frontend/src/features/realtime/pages/RealtimeMessagesPage.tsx +211 -0
  267. package/frontend/src/features/realtime/pages/RealtimePermissionsPage.tsx +191 -0
  268. package/frontend/src/features/realtime/services/realtime.service.ts +107 -0
  269. package/frontend/src/features/storage/{page → pages}/StoragePage.tsx +1 -29
  270. package/frontend/src/features/visualizer/components/SchemaVisualizer.tsx +3 -3
  271. package/frontend/src/features/visualizer/{page → pages}/VisualizerPage.tsx +1 -35
  272. package/frontend/src/lib/contexts/SocketContext.tsx +119 -75
  273. package/frontend/src/lib/routing/AppRoutes.tsx +35 -20
  274. package/frontend/src/lib/utils/cloudMessaging.ts +1 -1
  275. package/frontend/src/lib/utils/menuItems.ts +24 -0
  276. package/frontend/src/lib/utils/utils.ts +14 -1
  277. package/frontend/tsconfig.json +25 -25
  278. package/frontend/tsconfig.node.json +9 -9
  279. package/functions/deno.json +24 -24
  280. package/functions/server.ts +315 -315
  281. package/i18n/README.ar.md +130 -130
  282. package/i18n/README.de.md +130 -130
  283. package/i18n/README.es.md +154 -154
  284. package/i18n/README.fr.md +134 -134
  285. package/i18n/README.hi.md +129 -129
  286. package/i18n/README.ja.md +174 -174
  287. package/i18n/README.ko.md +136 -136
  288. package/i18n/README.pt-BR.md +131 -131
  289. package/i18n/README.ru.md +129 -129
  290. package/i18n/README.zh-CN.md +133 -133
  291. package/openapi/ai.yaml +715 -715
  292. package/openapi/auth.yaml +1244 -1244
  293. package/openapi/email.yaml +158 -0
  294. package/openapi/functions.yaml +475 -475
  295. package/openapi/health.yaml +29 -29
  296. package/openapi/logs.yaml +223 -223
  297. package/openapi/metadata.yaml +177 -177
  298. package/openapi/realtime.yaml +699 -0
  299. package/openapi/records.yaml +381 -381
  300. package/openapi/secrets.yaml +370 -370
  301. package/openapi/storage.yaml +875 -875
  302. package/openapi/tables.yaml +463 -463
  303. package/package.json +97 -97
  304. package/shared-schemas/package.json +31 -31
  305. package/shared-schemas/src/ai.schema.ts +63 -59
  306. package/shared-schemas/src/auth-api.schema.ts +352 -339
  307. package/shared-schemas/src/auth.schema.ts +1 -1
  308. package/shared-schemas/src/database-api.schema.ts +32 -1
  309. package/shared-schemas/src/database.schema.ts +39 -0
  310. package/shared-schemas/src/docs.schema.ts +26 -0
  311. package/shared-schemas/src/email-api.schema.ts +30 -0
  312. package/shared-schemas/src/index.ts +4 -0
  313. package/shared-schemas/src/metadata.schema.ts +9 -0
  314. package/shared-schemas/src/realtime-api.schema.ts +111 -0
  315. package/shared-schemas/src/realtime.schema.ts +143 -0
  316. package/shared-schemas/tsconfig.json +21 -21
  317. package/tsconfig.json +7 -7
  318. package/zeabur/README.md +13 -13
  319. package/zeabur/template.yml +1032 -1032
  320. package/.cursor/rules/cursor-rules.mdc +0 -94
  321. package/frontend/src/features/database/hooks/useFullMetadata.ts +0 -18
  322. package/test-gemini.sh +0 -35
  323. package/test-usage-admin.sh +0 -57
  324. package/test-usage.sh +0 -50
  325. /package/frontend/src/features/ai/{page → pages}/AIPage.tsx +0 -0
  326. /package/frontend/src/features/auth/{page → pages}/AuthMethodsPage.tsx +0 -0
  327. /package/frontend/src/features/auth/{page → pages}/ConfigurationPage.tsx +0 -0
  328. /package/frontend/src/features/dashboard/{page → pages}/DashboardPage.tsx +0 -0
  329. /package/frontend/src/features/database/{page → pages}/SQLEditorPage.tsx +0 -0
  330. /package/frontend/src/features/database/{page → pages}/TemplatesPage.tsx +0 -0
  331. /package/frontend/src/features/login/{page → pages}/CloudLoginPage.tsx +0 -0
  332. /package/frontend/src/features/login/{page → pages}/LoginPage.tsx +0 -0
  333. /package/frontend/src/features/logs/{page → pages}/AuditsPage.tsx +0 -0
  334. /package/frontend/src/features/logs/{page → pages}/LogsPage.tsx +0 -0
  335. /package/frontend/src/features/logs/{page → pages}/MCPLogsPage.tsx +0 -0
@@ -2,17 +2,19 @@ import { Server as HttpServer } from 'http';
2
2
  import { Server as SocketIOServer, Socket } from 'socket.io';
3
3
  import logger from '@/utils/logger.js';
4
4
  import { TokenManager } from '@/infra/security/token.manager.js';
5
- import {
6
- ServerEvents,
7
- ClientEvents,
5
+ import { ServerEvents, ClientEvents, SocketMetadata, NotificationPayload } from '@/types/socket.js';
6
+ import type {
7
+ SubscribeChannelPayload,
8
+ PublishEventPayload,
8
9
  SocketMessage,
9
- SocketMetadata,
10
- NotificationPayload,
11
- SubscribePayload,
12
- UnsubscribePayload,
13
- } from '@/types/socket.js';
10
+ SocketMessageMeta,
11
+ SubscribeResponse,
12
+ UnsubscribeChannelPayload,
13
+ } from '@insforge/shared-schemas';
14
14
  import { AppError } from '@/api/middlewares/error.js';
15
15
  import { ERROR_CODES, NEXT_ACTION } from '@/types/error-constants.js';
16
+ import { RealtimeAuthService } from '@/services/realtime/realtime-auth.service.js';
17
+ import { RealtimeMessageService } from '@/services/realtime/realtime-message.service.js';
16
18
 
17
19
  const tokenManager = TokenManager.getInstance();
18
20
 
@@ -188,14 +190,22 @@ export class SocketManager {
188
190
  * Setup handlers for client events
189
191
  */
190
192
  private setupClientEventHandlers(socket: Socket): void {
191
- // Handle subscription requests
192
- socket.on(ClientEvents.SUBSCRIBE, (payload: SubscribePayload) => {
193
- this.handleSubscribe(socket, payload);
193
+ // Handle realtime channel subscribe with ack callback
194
+ socket.on(
195
+ ClientEvents.REALTIME_SUBSCRIBE,
196
+ (payload: SubscribeChannelPayload, ack: (response: SubscribeResponse) => void) => {
197
+ void this.handleRealtimeSubscribe(socket, payload, ack);
198
+ }
199
+ );
200
+
201
+ // Handle realtime channel unsubscribe (fire-and-forget, no ack needed)
202
+ socket.on(ClientEvents.REALTIME_UNSUBSCRIBE, (payload: UnsubscribeChannelPayload) => {
203
+ this.handleRealtimeUnsubscribe(socket, payload);
194
204
  });
195
205
 
196
- // Handle unsubscription requests
197
- socket.on(ClientEvents.UNSUBSCRIBE, (payload: UnsubscribePayload) => {
198
- this.handleUnsubscribe(socket, payload);
206
+ // Handle realtime publish (client-initiated messages)
207
+ socket.on(ClientEvents.REALTIME_PUBLISH, (payload: PublishEventPayload) => {
208
+ void this.handleRealtimePublish(socket, payload);
199
209
  });
200
210
 
201
211
  // Update last activity on any event
@@ -208,70 +218,181 @@ export class SocketManager {
208
218
  }
209
219
 
210
220
  /**
211
- * Handle channel subscription
221
+ * Handle realtime channel subscribe request
212
222
  */
213
- private handleSubscribe(socket: Socket, payload: SubscribePayload): void {
214
- const metadata = this.socketMetadata.get(socket.id);
215
- if (!metadata) {
216
- return;
223
+ private async handleRealtimeSubscribe(
224
+ socket: Socket,
225
+ payload: SubscribeChannelPayload,
226
+ ack?: (response: SubscribeResponse) => void
227
+ ): Promise<void> {
228
+ const authService = RealtimeAuthService.getInstance();
229
+ const { channel } = payload;
230
+ const userId = socket.data.user?.id;
231
+ const userRole = socket.data.user?.role;
232
+
233
+ try {
234
+ // Check subscribe permission via RLS SELECT policy
235
+ const canSubscribe = await authService.checkSubscribePermission(channel, userId, userRole);
236
+
237
+ if (!canSubscribe) {
238
+ ack?.({
239
+ ok: false,
240
+ channel,
241
+ error: { code: 'UNAUTHORIZED', message: 'Not authorized to subscribe to this channel' },
242
+ });
243
+ return;
244
+ }
245
+
246
+ const roomName = `realtime:${channel}`;
247
+ await socket.join(roomName);
248
+
249
+ const metadata = this.socketMetadata.get(socket.id);
250
+ if (metadata) {
251
+ metadata.subscriptions.add(roomName);
252
+ }
253
+
254
+ ack?.({ ok: true, channel });
255
+
256
+ logger.debug('Socket subscribed to realtime channel', {
257
+ socketId: socket.id,
258
+ channel,
259
+ });
260
+ } catch (error) {
261
+ logger.error('Error handling realtime subscribe', { error, channel });
262
+ ack?.({
263
+ ok: false,
264
+ channel,
265
+ error: { code: 'INTERNAL_ERROR', message: 'Failed to subscribe to channel' },
266
+ });
217
267
  }
268
+ }
218
269
 
219
- void socket.join(payload.channel);
220
- metadata.subscriptions.add(payload.channel);
270
+ /**
271
+ * Handle realtime channel unsubscribe request (fire-and-forget)
272
+ */
273
+ private handleRealtimeUnsubscribe(socket: Socket, payload: UnsubscribeChannelPayload): void {
274
+ const { channel } = payload;
275
+ const roomName = `realtime:${channel}`;
221
276
 
222
- logger.debug('Socket subscribed to channel', {
223
- socketId: socket.id,
224
- channel: payload.channel,
225
- });
277
+ void socket.leave(roomName);
278
+
279
+ const metadata = this.socketMetadata.get(socket.id);
280
+ if (metadata) {
281
+ metadata.subscriptions.delete(roomName);
282
+ }
283
+
284
+ logger.debug('Socket unsubscribed from realtime channel', { socketId: socket.id, channel });
226
285
  }
227
286
 
228
287
  /**
229
- * Handle channel unsubscription
288
+ * Handle realtime publish request (client-initiated message)
289
+ * Inserts message to DB - trigger handles pg_notify, broadcast, and stats update.
230
290
  */
231
- private handleUnsubscribe(socket: Socket, payload: UnsubscribePayload): void {
291
+ private async handleRealtimePublish(socket: Socket, payload: PublishEventPayload): Promise<void> {
292
+ const { channel, event, payload: eventPayload } = payload;
293
+ const userId = socket.data.user?.id;
294
+ const userRole = socket.data.user?.role;
295
+
296
+ // Check if client has subscribed to this channel
297
+ const roomName = `realtime:${channel}`;
232
298
  const metadata = this.socketMetadata.get(socket.id);
233
- if (!metadata) {
299
+ if (!metadata?.subscriptions.has(roomName)) {
300
+ socket.emit(ServerEvents.REALTIME_ERROR, {
301
+ channel,
302
+ code: 'NOT_SUBSCRIBED',
303
+ message: 'Must subscribe to channel before publishing messages',
304
+ });
234
305
  return;
235
306
  }
236
307
 
237
- void socket.leave(payload.channel);
238
- metadata.subscriptions.delete(payload.channel);
308
+ try {
309
+ // Insert message directly - trigger will handle pg_notify and broadcasting
310
+ const messageService = RealtimeMessageService.getInstance();
311
+ const result = await messageService.insertMessage(
312
+ channel,
313
+ event,
314
+ eventPayload,
315
+ userId,
316
+ userRole
317
+ );
318
+
319
+ if (!result) {
320
+ socket.emit(ServerEvents.REALTIME_ERROR, {
321
+ channel,
322
+ code: 'UNAUTHORIZED',
323
+ message: 'Not authorized to publish to this channel',
324
+ });
325
+ return;
326
+ }
239
327
 
240
- logger.debug('Socket unsubscribed from channel', {
241
- socketId: socket.id,
242
- channel: payload.channel,
243
- });
328
+ logger.debug('Client message inserted', {
329
+ socketId: socket.id,
330
+ channel,
331
+ event,
332
+ });
333
+ } catch (error) {
334
+ logger.error('Error handling realtime publish', { error, channel });
335
+ socket.emit(ServerEvents.REALTIME_ERROR, {
336
+ channel,
337
+ code: 'INTERNAL_ERROR',
338
+ message: 'Failed to publish message',
339
+ });
340
+ }
244
341
  }
245
342
 
246
343
  /**
247
- * Emit event to specific socket with type safety
344
+ * Build a SocketMessage with meta and payload
248
345
  */
249
- emitToSocket<T>(socket: Socket, event: ServerEvents, payload: T): void {
250
- const message: SocketMessage<T> = {
251
- type: event,
252
- payload,
253
- timestamp: Date.now(),
254
- id: this.generateMessageId(),
255
- };
346
+ private buildSocketMessage<T extends object>(
347
+ payload: T,
348
+ meta: Omit<SocketMessageMeta, 'messageId' | 'timestamp'> & { messageId?: string }
349
+ ): SocketMessage & T {
350
+ return {
351
+ ...payload,
352
+ meta: {
353
+ ...meta,
354
+ messageId: meta.messageId || this.generateMessageId(),
355
+ timestamp: new Date().toISOString(),
356
+ },
357
+ } as SocketMessage & T;
358
+ }
359
+
360
+ /**
361
+ * Emit message to specific socket
362
+ */
363
+ emitToSocket<T extends object>(
364
+ socket: Socket,
365
+ event: string,
366
+ payload: T,
367
+ senderType: 'system' | 'user' = 'system',
368
+ senderId?: string,
369
+ messageId?: string
370
+ ): void {
371
+ const message = this.buildSocketMessage(payload, {
372
+ channel: socket.id,
373
+ senderType,
374
+ senderId,
375
+ messageId,
376
+ });
256
377
  socket.emit(event, message);
257
378
  }
258
379
 
259
380
  /**
260
381
  * Broadcast to all connected clients
261
382
  */
262
- broadcastToAll<T>(event: ServerEvents, payload: T): void {
383
+ broadcastToAll<T extends object>(
384
+ event: string,
385
+ payload: T,
386
+ senderType: 'system' | 'user' = 'system',
387
+ senderId?: string,
388
+ messageId?: string
389
+ ): void {
263
390
  if (!this.io) {
264
391
  logger.warn('Socket.IO server not initialized');
265
392
  return;
266
393
  }
267
394
 
268
- const message: SocketMessage<T> = {
269
- type: event,
270
- payload,
271
- timestamp: Date.now(),
272
- id: this.generateMessageId(),
273
- };
274
-
395
+ const message = this.buildSocketMessage(payload, { senderType, senderId, messageId });
275
396
  this.io.emit(event, message);
276
397
 
277
398
  logger.info('Broadcasted message to all clients', {
@@ -283,25 +404,38 @@ export class SocketManager {
283
404
  /**
284
405
  * Broadcast to specific room
285
406
  */
286
- broadcastToRoom<T>(room: string, event: ServerEvents, payload?: T): void {
407
+ broadcastToRoom<T extends object>(
408
+ room: string,
409
+ event: string,
410
+ payload: T,
411
+ senderType: 'system' | 'user',
412
+ senderId?: string,
413
+ messageId?: string
414
+ ): void {
287
415
  if (!this.io) {
288
416
  logger.warn('Socket.IO server not initialized');
289
417
  return;
290
418
  }
291
419
 
292
- const message: SocketMessage<T> = {
293
- type: event,
294
- payload,
295
- timestamp: Date.now(),
296
- id: this.generateMessageId(),
297
- };
298
-
420
+ const message = this.buildSocketMessage(payload, {
421
+ channel: room,
422
+ senderType,
423
+ senderId,
424
+ messageId,
425
+ });
299
426
  this.io.to(room).emit(event, message);
300
427
 
301
- logger.info('Broadcasted message to room', {
302
- event,
303
- room,
304
- });
428
+ logger.debug('Broadcasted message to room', { event, room });
429
+ }
430
+
431
+ /**
432
+ * Get the number of sockets in a room
433
+ */
434
+ getRoomSize(room: string): number {
435
+ if (!this.io) {
436
+ return 0;
437
+ }
438
+ return this.io.sockets.adapter.rooms.get(room)?.size || 0;
305
439
  }
306
440
 
307
441
  /**
@@ -368,11 +502,11 @@ export class SocketManager {
368
502
  close(): void {
369
503
  if (this.io) {
370
504
  // Notify all clients about server shutdown
371
- this.broadcastToAll<NotificationPayload>(ServerEvents.NOTIFICATION, {
505
+ this.broadcastToAll(ServerEvents.NOTIFICATION, {
372
506
  level: 'warning',
373
507
  title: 'Server Shutdown',
374
508
  message: 'Server is shutting down',
375
- });
509
+ } as NotificationPayload);
376
510
 
377
511
  // Close all connections
378
512
  void this.io.close();
@@ -41,8 +41,8 @@ interface OpenRouterLimitation {
41
41
  };
42
42
  }
43
43
 
44
- export class AIClientService {
45
- private static instance: AIClientService;
44
+ export class OpenRouterProvider {
45
+ private static instance: OpenRouterProvider;
46
46
  private cloudCredentials: CloudCredentials | undefined;
47
47
  private openRouterClient: OpenAI | null = null;
48
48
  private currentApiKey: string | undefined;
@@ -51,11 +51,11 @@ export class AIClientService {
51
51
 
52
52
  private constructor() {}
53
53
 
54
- static getInstance(): AIClientService {
55
- if (!AIClientService.instance) {
56
- AIClientService.instance = new AIClientService();
54
+ static getInstance(): OpenRouterProvider {
55
+ if (!OpenRouterProvider.instance) {
56
+ OpenRouterProvider.instance = new OpenRouterProvider();
57
57
  }
58
- return AIClientService.instance;
58
+ return OpenRouterProvider.instance;
59
59
  }
60
60
 
61
61
  /**
@@ -100,9 +100,9 @@ export class AIClientService {
100
100
 
101
101
  /**
102
102
  * Get the OpenAI client, creating or updating it as needed
103
- * This is the main method services should use
103
+ * Used internally by sendRequest()
104
104
  */
105
- async getClient(): Promise<OpenAI> {
105
+ private async getClient(): Promise<OpenAI> {
106
106
  if (!this.openRouterClient) {
107
107
  this.openRouterClient = this.createClient(await this.getApiKey());
108
108
  return this.openRouterClient;
@@ -349,6 +349,9 @@ export class AIClientService {
349
349
  logger.info(`Received ${error.status} insufficient credits, renewing API key...`);
350
350
  await this.renewCloudApiKey();
351
351
 
352
+ // Get fresh client with renewed API key
353
+ const renewedClient = await this.getClient();
354
+
352
355
  // Retry with exponential backoff (3 attempts)
353
356
  const maxRetries = 3;
354
357
 
@@ -360,7 +363,7 @@ export class AIClientService {
360
363
  );
361
364
  await new Promise((resolve) => setTimeout(resolve, backoffMs));
362
365
 
363
- const result = await request(client);
366
+ const result = await request(renewedClient);
364
367
  logger.info('Request succeeded after API key renewal');
365
368
  return result;
366
369
  } catch (retryError) {
@@ -1,4 +1,5 @@
1
1
  import { EmailTemplate } from '@/types/email.js';
2
+ import { SendRawEmailRequest } from '@insforge/shared-schemas';
2
3
 
3
4
  /**
4
5
  * Email provider interface
@@ -25,14 +26,10 @@ export interface EmailProvider {
25
26
  ): Promise<void>;
26
27
 
27
28
  /**
28
- * Send raw email with custom subject and body
29
- * Optional - not all providers may support this
30
- * @param to - Recipient email address
31
- * @param subject - Email subject
32
- * @param html - HTML email body
33
- * @param text - Plain text email body (optional)
29
+ * Send custom/raw email (optional - not all providers may support this)
30
+ * @param options - Email options (to, subject, html, cc, bcc, from, replyTo)
34
31
  */
35
- sendRaw?(to: string, subject: string, html: string, text?: string): Promise<void>;
32
+ sendRaw?(options: SendRawEmailRequest): Promise<void>;
36
33
 
37
34
  /**
38
35
  * Check if provider supports template-based emails
@@ -5,6 +5,7 @@ import logger from '@/utils/logger.js';
5
5
  import { AppError } from '@/api/middlewares/error.js';
6
6
  import { ERROR_CODES } from '@/types/error-constants.js';
7
7
  import { EmailTemplate } from '@/types/email.js';
8
+ import { SendRawEmailRequest } from '@insforge/shared-schemas';
8
9
  import { EmailProvider } from './base.provider.js';
9
10
 
10
11
  /**
@@ -184,4 +185,87 @@ export class CloudEmailProvider implements EmailProvider {
184
185
  );
185
186
  }
186
187
  }
188
+
189
+ /**
190
+ * Send custom/raw email via cloud backend
191
+ */
192
+ async sendRaw(options: SendRawEmailRequest): Promise<void> {
193
+ try {
194
+ const projectId = config.cloud.projectId;
195
+ const apiHost = config.cloud.apiHost;
196
+ const signToken = this.generateSignToken();
197
+
198
+ const url = `${apiHost}/email/v1/${projectId}/send-on-demand`;
199
+ const response = await axios.post(url, options, {
200
+ headers: {
201
+ 'Content-Type': 'application/json',
202
+ sign: signToken,
203
+ },
204
+ timeout: 10000,
205
+ });
206
+
207
+ if (response.data?.success) {
208
+ logger.info('Raw email sent successfully', { projectId });
209
+ } else {
210
+ throw new AppError(
211
+ 'Email service returned unsuccessful response',
212
+ 500,
213
+ ERROR_CODES.INTERNAL_ERROR
214
+ );
215
+ }
216
+ } catch (error) {
217
+ if (axios.isAxiosError(error)) {
218
+ const status = error.response?.status;
219
+ const message = error.response?.data?.message || error.message;
220
+
221
+ logger.error('Failed to send raw email via cloud backend', {
222
+ projectId: config.cloud.projectId,
223
+ status,
224
+ message,
225
+ });
226
+
227
+ if (status === 401) {
228
+ throw new AppError(
229
+ 'Authentication failed with cloud email service.',
230
+ status,
231
+ ERROR_CODES.AUTH_UNAUTHORIZED
232
+ );
233
+ } else if (status === 403) {
234
+ throw new AppError(
235
+ 'Custom email service is not available for free plan. Please upgrade to use this feature.',
236
+ status,
237
+ ERROR_CODES.FORBIDDEN
238
+ );
239
+ } else if (status === 429) {
240
+ throw new AppError(
241
+ 'Email rate limit exceeded. Starter plan is limited 10 emails per hour, and Pro plan is limited 50 emails per hour',
242
+ status,
243
+ ERROR_CODES.RATE_LIMITED
244
+ );
245
+ } else if (status === 400) {
246
+ throw new AppError(
247
+ `Invalid email request: ${message}`,
248
+ status,
249
+ ERROR_CODES.INVALID_INPUT
250
+ );
251
+ } else {
252
+ throw new AppError(
253
+ `Failed to send email: ${message}`,
254
+ status || 500,
255
+ ERROR_CODES.INTERNAL_ERROR
256
+ );
257
+ }
258
+ }
259
+
260
+ if (error instanceof AppError) {
261
+ throw error;
262
+ }
263
+
264
+ throw new AppError(
265
+ `Failed to send email: ${error instanceof Error ? error.message : 'Unknown error'}`,
266
+ 500,
267
+ ERROR_CODES.INTERNAL_ERROR
268
+ );
269
+ }
270
+ }
187
271
  }