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
@@ -0,0 +1,246 @@
1
+ import type { Client } from 'pg';
2
+ import { SocketManager } from '@/infra/socket/socket.manager.js';
3
+ import { WebhookSender } from './webhook-sender.js';
4
+ import { DatabaseManager } from '@/infra/database/database.manager.js';
5
+ import { RealtimeChannelService } from '@/services/realtime/realtime-channel.service.js';
6
+ import { RealtimeMessageService } from '@/services/realtime/realtime-message.service.js';
7
+ import logger from '@/utils/logger.js';
8
+ import type { RealtimeMessage, RealtimeChannel, WebhookMessage } from '@insforge/shared-schemas';
9
+ import type { DeliveryResult } from '@/types/realtime.js';
10
+
11
+ /**
12
+ * RealtimeManager - Listens to pg_notify and publishes messages to WebSocket/webhooks
13
+ *
14
+ * This is a singleton that:
15
+ * 1. Maintains a dedicated PostgreSQL connection for LISTEN
16
+ * 2. Receives notifications from realtime.publish() function
17
+ * 3. Publishes messages to WebSocket clients (via Socket.IO rooms)
18
+ * 4. Publishes messages to webhook URLs (via HTTP POST)
19
+ * 5. Updates message records with delivery statistics
20
+ */
21
+ export class RealtimeManager {
22
+ private static instance: RealtimeManager;
23
+ private listenerClient: Client | null = null;
24
+ private isConnected = false;
25
+ private reconnectTimeout: NodeJS.Timeout | null = null;
26
+ private reconnectAttempts = 0;
27
+ private readonly maxReconnectAttempts = 10;
28
+ private readonly baseReconnectDelay = 5000;
29
+ private webhookSender: WebhookSender;
30
+
31
+ private constructor() {
32
+ this.webhookSender = new WebhookSender();
33
+ }
34
+
35
+ static getInstance(): RealtimeManager {
36
+ if (!RealtimeManager.instance) {
37
+ RealtimeManager.instance = new RealtimeManager();
38
+ }
39
+ return RealtimeManager.instance;
40
+ }
41
+
42
+ /**
43
+ * Initialize the realtime manager and start listening for pg_notify
44
+ */
45
+ async initialize(): Promise<void> {
46
+ if (this.isConnected) {
47
+ return;
48
+ }
49
+
50
+ // Create a dedicated client for LISTEN (cannot use pooled connections)
51
+ this.listenerClient = DatabaseManager.getInstance().createClient();
52
+
53
+ try {
54
+ await this.listenerClient.connect();
55
+ await this.listenerClient.query('LISTEN realtime_message');
56
+ this.isConnected = true;
57
+ this.reconnectAttempts = 0;
58
+
59
+ this.listenerClient.on('notification', (msg) => {
60
+ if (msg.channel === 'realtime_message' && msg.payload) {
61
+ void this.handlePGNotification(msg.payload);
62
+ }
63
+ });
64
+
65
+ this.listenerClient.on('error', (error) => {
66
+ logger.error('RealtimeManager connection error', { error: error.message });
67
+ this.handleDisconnect();
68
+ });
69
+
70
+ this.listenerClient.on('end', () => {
71
+ logger.warn('RealtimeManager connection ended');
72
+ this.handleDisconnect();
73
+ });
74
+
75
+ logger.info('RealtimeManager initialized and listening');
76
+ } catch (error) {
77
+ logger.error('Failed to initialize RealtimeManager', { error });
78
+ this.handleDisconnect();
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Handle incoming pg_notify notification
84
+ * Payload is just the message_id (UUID string) to bypass 8KB limit
85
+ */
86
+ private async handlePGNotification(messageId: string): Promise<void> {
87
+ try {
88
+ // 1. Fetch message and channel in parallel
89
+ // channelId is guaranteed non-null for fresh messages (publish/insertMessage validate channel)
90
+ const message = await RealtimeMessageService.getInstance().getById(messageId);
91
+
92
+ if (!message || !message.channelId) {
93
+ logger.warn('Message not found or invalid for realtime notification', { messageId });
94
+ return;
95
+ }
96
+
97
+ // 2. Look up channel configuration (for enabled check and webhook URLs)
98
+ const channel = await RealtimeChannelService.getInstance().getById(message.channelId);
99
+
100
+ if (!channel?.enabled) {
101
+ logger.debug('Channel not found or disabled, skipping', { channelId: message.channelId });
102
+ return;
103
+ }
104
+
105
+ // 3. Publish to WebSocket and/or Webhooks
106
+ const result = await this.publishMessage(message, channel);
107
+
108
+ // 4. Update message record with delivery stats
109
+ await RealtimeMessageService.getInstance().updateDeliveryStats(messageId, result);
110
+
111
+ logger.debug('Realtime message published', {
112
+ messageId,
113
+ channelName: message.channelName,
114
+ eventName: message.eventName,
115
+ ...result,
116
+ });
117
+ } catch (error) {
118
+ logger.error('Failed to publish realtime message', { error, messageId });
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Publish message to WebSocket clients and webhook URLs
124
+ */
125
+ private async publishMessage(
126
+ message: RealtimeMessage,
127
+ channel: RealtimeChannel
128
+ ): Promise<DeliveryResult> {
129
+ const result: DeliveryResult = {
130
+ wsAudienceCount: 0,
131
+ whAudienceCount: 0,
132
+ whDeliveredCount: 0,
133
+ };
134
+
135
+ // Publish to WebSocket clients
136
+ result.wsAudienceCount = this.publishToWebSocket(message);
137
+
138
+ // Publish to Webhook URLs if configured
139
+ if (channel.webhookUrls && channel.webhookUrls.length > 0) {
140
+ const webhookPayload: WebhookMessage = {
141
+ messageId: message.id,
142
+ channel: message.channelName,
143
+ eventName: message.eventName,
144
+ payload: message.payload,
145
+ };
146
+ const whResult = await this.publishToWebhooks(channel.webhookUrls, webhookPayload);
147
+ result.whAudienceCount = whResult.audienceCount;
148
+ result.whDeliveredCount = whResult.deliveredCount;
149
+ }
150
+
151
+ return result;
152
+ }
153
+
154
+ /**
155
+ * Publish message to WebSocket clients subscribed to the channel
156
+ * Returns the number of clients in the room (audience count)
157
+ */
158
+ private publishToWebSocket(message: RealtimeMessage): number {
159
+ const socketManager = SocketManager.getInstance();
160
+ const roomName = `realtime:${message.channelName}`;
161
+
162
+ const audienceCount = socketManager.getRoomSize(roomName);
163
+
164
+ if (audienceCount > 0) {
165
+ socketManager.broadcastToRoom(
166
+ roomName,
167
+ message.eventName,
168
+ message.payload,
169
+ message.senderType,
170
+ message.senderId ?? undefined,
171
+ message.id
172
+ );
173
+ }
174
+
175
+ return audienceCount;
176
+ }
177
+
178
+ /**
179
+ * Publish message to all configured webhook URLs
180
+ */
181
+ private async publishToWebhooks(
182
+ urls: string[],
183
+ message: WebhookMessage
184
+ ): Promise<{ audienceCount: number; deliveredCount: number }> {
185
+ const audienceCount = urls.length;
186
+ const results = await this.webhookSender.sendToAll(urls, message);
187
+ const deliveredCount = results.filter((r) => r.success).length;
188
+
189
+ return { audienceCount, deliveredCount };
190
+ }
191
+
192
+ /**
193
+ * Handle disconnection and attempt reconnection
194
+ */
195
+ private handleDisconnect(): void {
196
+ this.isConnected = false;
197
+
198
+ if (this.listenerClient) {
199
+ this.listenerClient.removeAllListeners();
200
+ this.listenerClient = null;
201
+ }
202
+
203
+ // Reconnect with exponential backoff
204
+ if (this.reconnectAttempts < this.maxReconnectAttempts) {
205
+ const delay = this.baseReconnectDelay * Math.pow(2, this.reconnectAttempts);
206
+ this.reconnectAttempts++;
207
+
208
+ if (!this.reconnectTimeout) {
209
+ this.reconnectTimeout = setTimeout(() => {
210
+ this.reconnectTimeout = null;
211
+ logger.info(
212
+ `Attempting to reconnect RealtimeManager (attempt ${this.reconnectAttempts})...`
213
+ );
214
+ void this.initialize();
215
+ }, delay);
216
+ }
217
+ } else {
218
+ logger.error('RealtimeManager max reconnect attempts reached');
219
+ }
220
+ }
221
+
222
+ /**
223
+ * Close the realtime manager connection
224
+ */
225
+ async close(): Promise<void> {
226
+ if (this.reconnectTimeout) {
227
+ clearTimeout(this.reconnectTimeout);
228
+ this.reconnectTimeout = null;
229
+ }
230
+
231
+ if (this.listenerClient) {
232
+ this.listenerClient.removeAllListeners();
233
+ await this.listenerClient.end();
234
+ this.listenerClient = null;
235
+ this.isConnected = false;
236
+ logger.info('RealtimeManager closed');
237
+ }
238
+ }
239
+
240
+ /**
241
+ * Check if the manager is connected and healthy
242
+ */
243
+ isHealthy(): boolean {
244
+ return this.isConnected;
245
+ }
246
+ }
@@ -0,0 +1,82 @@
1
+ import axios, { AxiosError } from 'axios';
2
+ import logger from '@/utils/logger.js';
3
+ import type { WebhookMessage } from '@insforge/shared-schemas';
4
+
5
+ export interface WebhookResult {
6
+ url: string;
7
+ success: boolean;
8
+ statusCode?: number;
9
+ error?: string;
10
+ }
11
+
12
+ /**
13
+ * WebhookSender - Handles HTTP delivery of realtime messages to webhook endpoints
14
+ */
15
+ export class WebhookSender {
16
+ private readonly timeout = 10000; // 10 seconds
17
+ private readonly maxRetries = 2;
18
+
19
+ /**
20
+ * Send message to all webhook URLs in parallel
21
+ */
22
+ async sendToAll(urls: string[], message: WebhookMessage): Promise<WebhookResult[]> {
23
+ const promises = urls.map((url) => this.send(url, message));
24
+ return Promise.all(promises);
25
+ }
26
+
27
+ /**
28
+ * Send message to a single webhook URL with retry logic
29
+ */
30
+ private async send(url: string, message: WebhookMessage): Promise<WebhookResult> {
31
+ let lastError: string | undefined;
32
+
33
+ for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
34
+ try {
35
+ const response = await axios.post(url, message.payload, {
36
+ timeout: this.timeout,
37
+ headers: {
38
+ 'Content-Type': 'application/json',
39
+ 'X-InsForge-Event': message.eventName,
40
+ 'X-InsForge-Channel': message.channel,
41
+ 'X-InsForge-Message-Id': message.messageId,
42
+ },
43
+ });
44
+
45
+ return {
46
+ url,
47
+ success: response.status >= 200 && response.status < 300,
48
+ statusCode: response.status,
49
+ };
50
+ } catch (error) {
51
+ const axiosError = error as AxiosError;
52
+ lastError = axiosError.message;
53
+
54
+ if (axiosError.response) {
55
+ // Server responded with error status - don't retry
56
+ return {
57
+ url,
58
+ success: false,
59
+ statusCode: axiosError.response.status,
60
+ error: `HTTP ${axiosError.response.status}`,
61
+ };
62
+ }
63
+
64
+ // Network error - retry with backoff
65
+ if (attempt < this.maxRetries) {
66
+ await this.delay(1000 * (attempt + 1)); // 1s, 2s
67
+ }
68
+ }
69
+ }
70
+ logger.warn('Webhook delivery failed after retries', { url, error: lastError });
71
+
72
+ return {
73
+ url,
74
+ success: false,
75
+ error: lastError,
76
+ };
77
+ }
78
+
79
+ private delay(ms: number): Promise<void> {
80
+ return new Promise((resolve) => setTimeout(resolve, ms));
81
+ }
82
+ }
@@ -1,125 +1,219 @@
1
- import jwt from 'jsonwebtoken';
2
- import { createRemoteJWKSet, JWTPayload, jwtVerify } from 'jose';
3
- import { AppError } from '@/api/middlewares/error.js';
4
- import { ERROR_CODES, NEXT_ACTION } from '@/types/error-constants.js';
5
- import type { TokenPayloadSchema } from '@insforge/shared-schemas';
6
-
7
- const JWT_SECRET = process.env.JWT_SECRET ?? '';
8
- const JWT_EXPIRES_IN = '7d';
9
-
10
- /**
11
- * Create JWKS instance with caching and timeout configuration
12
- * The instance will automatically cache keys and handle refetching
13
- */
14
- const cloudApiHost = process.env.CLOUD_API_HOST || 'https://api.insforge.dev';
15
- const JWKS = createRemoteJWKSet(new URL(`${cloudApiHost}/.well-known/jwks.json`), {
16
- timeoutDuration: 10000, // 10 second timeout for HTTP requests
17
- cooldownDuration: 30000, // 30 seconds cooldown after successful fetch
18
- cacheMaxAge: 600000, // Maximum 10 minutes between refetches
19
- });
20
-
21
- /**
22
- * TokenManager - Handles JWT token operations
23
- * Infrastructure layer for token generation and verification
24
- */
25
- export class TokenManager {
26
- private static instance: TokenManager;
27
-
28
- private constructor() {
29
- if (!process.env.JWT_SECRET) {
30
- throw new Error('JWT_SECRET environment variable is required');
31
- }
32
- }
33
-
34
- public static getInstance(): TokenManager {
35
- if (!TokenManager.instance) {
36
- TokenManager.instance = new TokenManager();
37
- }
38
- return TokenManager.instance;
39
- }
40
-
41
- /**
42
- * Generate JWT token for users and admins
43
- */
44
- generateToken(payload: TokenPayloadSchema): string {
45
- return jwt.sign(payload, JWT_SECRET, {
46
- algorithm: 'HS256',
47
- expiresIn: JWT_EXPIRES_IN,
48
- });
49
- }
50
-
51
- /**
52
- * Generate anonymous JWT token (never expires)
53
- */
54
- generateAnonToken(): string {
55
- const payload = {
56
- sub: '12345678-1234-5678-90ab-cdef12345678',
57
- email: 'anon@insforge.com',
58
- role: 'anon',
59
- };
60
- return jwt.sign(payload, JWT_SECRET, {
61
- algorithm: 'HS256',
62
- // No expiresIn means token never expires
63
- });
64
- }
65
-
66
- /**
67
- * Verify JWT token
68
- */
69
- verifyToken(token: string): TokenPayloadSchema {
70
- try {
71
- const decoded = jwt.verify(token, JWT_SECRET) as TokenPayloadSchema;
72
- return {
73
- sub: decoded.sub,
74
- email: decoded.email,
75
- role: decoded.role || 'authenticated',
76
- };
77
- } catch {
78
- throw new AppError('Invalid token', 401, ERROR_CODES.AUTH_UNAUTHORIZED);
79
- }
80
- }
81
-
82
- /**
83
- * Verify cloud backend JWT token
84
- * Validates JWT tokens from api.insforge.dev using JWKS
85
- */
86
- async verifyCloudToken(token: string): Promise<{ projectId: string; payload: JWTPayload }> {
87
- try {
88
- // JWKS handles caching internally, no need to manage it manually
89
- const { payload } = await jwtVerify(token, JWKS, {
90
- algorithms: ['RS256', 'RS384', 'RS512', 'ES256', 'ES384', 'ES512'],
91
- });
92
-
93
- // Verify project_id matches if configured
94
- const tokenProjectId = payload['projectId'] as string;
95
- const expectedProjectId = process.env.PROJECT_ID;
96
-
97
- if (expectedProjectId && tokenProjectId !== expectedProjectId) {
98
- throw new AppError(
99
- 'Project ID mismatch',
100
- 403,
101
- ERROR_CODES.AUTH_UNAUTHORIZED,
102
- NEXT_ACTION.CHECK_TOKEN
103
- );
104
- }
105
-
106
- return {
107
- projectId: tokenProjectId || expectedProjectId || 'local',
108
- payload,
109
- };
110
- } catch (error) {
111
- // Re-throw AppError as-is
112
- if (error instanceof AppError) {
113
- throw error;
114
- }
115
-
116
- // Wrap other JWT errors
117
- throw new AppError(
118
- `Invalid cloud authorization code: ${error instanceof Error ? error.message : 'Unknown error'}`,
119
- 401,
120
- ERROR_CODES.AUTH_INVALID_CREDENTIALS,
121
- NEXT_ACTION.CHECK_TOKEN
122
- );
123
- }
124
- }
125
- }
1
+ import jwt from 'jsonwebtoken';
2
+ import crypto from 'crypto';
3
+ import { createRemoteJWKSet, JWTPayload, jwtVerify } from 'jose';
4
+ import { AppError } from '@/api/middlewares/error.js';
5
+ import { ERROR_CODES, NEXT_ACTION } from '@/types/error-constants.js';
6
+ import type { TokenPayloadSchema } from '@insforge/shared-schemas';
7
+
8
+ const JWT_SECRET = process.env.JWT_SECRET ?? '';
9
+ // TODO: Change access token expiration time to 15 min
10
+ const JWT_EXPIRES_IN = '7d';
11
+ const REFRESH_TOKEN_EXPIRES_IN = '7d';
12
+
13
+ /**
14
+ * Refresh token payload interface
15
+ */
16
+ export interface RefreshTokenPayload {
17
+ sub: string;
18
+ type: 'refresh';
19
+ iss: string;
20
+ }
21
+
22
+ /**
23
+ * Create JWKS instance with caching and timeout configuration
24
+ * The instance will automatically cache keys and handle refetching
25
+ */
26
+ const cloudApiHost = process.env.CLOUD_API_HOST || 'https://api.insforge.dev';
27
+ const JWKS = createRemoteJWKSet(new URL(`${cloudApiHost}/.well-known/jwks.json`), {
28
+ timeoutDuration: 10000, // 10 second timeout for HTTP requests
29
+ cooldownDuration: 30000, // 30 seconds cooldown after successful fetch
30
+ cacheMaxAge: 600000, // Maximum 10 minutes between refetches
31
+ });
32
+
33
+ /**
34
+ * TokenManager - Handles JWT token operations
35
+ * Infrastructure layer for token generation and verification
36
+ */
37
+ export class TokenManager {
38
+ private static instance: TokenManager;
39
+
40
+ private constructor() {
41
+ if (!process.env.JWT_SECRET) {
42
+ throw new Error('JWT_SECRET environment variable is required');
43
+ }
44
+ }
45
+
46
+ public static getInstance(): TokenManager {
47
+ if (!TokenManager.instance) {
48
+ TokenManager.instance = new TokenManager();
49
+ }
50
+ return TokenManager.instance;
51
+ }
52
+
53
+ /**
54
+ * Generate JWT access token for users and admins
55
+ */
56
+ generateToken(payload: TokenPayloadSchema): string {
57
+ return jwt.sign(payload, JWT_SECRET, {
58
+ algorithm: 'HS256',
59
+ expiresIn: JWT_EXPIRES_IN,
60
+ });
61
+ }
62
+
63
+ /**
64
+ * Generate admin JWT token (never expires)
65
+ * Used for internal API key authenticated requests to PostgREST
66
+ */
67
+ generateAdminToken(): string {
68
+ const payload = {
69
+ sub: 'project-admin-with-api-key',
70
+ email: 'project-admin@email.com',
71
+ role: 'project_admin',
72
+ };
73
+ return jwt.sign(payload, JWT_SECRET, {
74
+ algorithm: 'HS256',
75
+ // No expiresIn means token never expires
76
+ });
77
+ }
78
+
79
+ /**
80
+ * Generate refresh token for secure session management
81
+ */
82
+ generateRefreshToken(userId: string): string {
83
+ const refreshPayload: RefreshTokenPayload = {
84
+ sub: userId,
85
+ type: 'refresh',
86
+ iss: 'insforge',
87
+ };
88
+ return jwt.sign(refreshPayload, JWT_SECRET, {
89
+ algorithm: 'HS256',
90
+ expiresIn: REFRESH_TOKEN_EXPIRES_IN,
91
+ });
92
+ }
93
+
94
+ /**
95
+ * Verify refresh token and return payload
96
+ * Ensures the token is a valid refresh token (not an access token)
97
+ */
98
+ verifyRefreshToken(token: string): RefreshTokenPayload {
99
+ try {
100
+ const decoded = jwt.verify(token, JWT_SECRET, {
101
+ algorithms: ['HS256'],
102
+ issuer: 'insforge',
103
+ }) as RefreshTokenPayload;
104
+
105
+ // Ensure this is a refresh token, not an access token
106
+ if (decoded.type !== 'refresh' || !decoded.sub) {
107
+ throw new AppError('Invalid refresh token type', 401, ERROR_CODES.AUTH_UNAUTHORIZED);
108
+ }
109
+
110
+ return decoded;
111
+ } catch (error) {
112
+ if (error instanceof AppError) {
113
+ throw error;
114
+ }
115
+ throw new AppError('Invalid or expired refresh token', 401, ERROR_CODES.AUTH_UNAUTHORIZED);
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Generate anonymous JWT token (never expires)
121
+ */
122
+ generateAnonToken(): string {
123
+ const payload = {
124
+ sub: '12345678-1234-5678-90ab-cdef12345678',
125
+ email: 'anon@insforge.com',
126
+ role: 'anon',
127
+ };
128
+ return jwt.sign(payload, JWT_SECRET, {
129
+ algorithm: 'HS256',
130
+ // No expiresIn means token never expires
131
+ });
132
+ }
133
+
134
+ /**
135
+ * Verify JWT token
136
+ */
137
+ verifyToken(token: string): TokenPayloadSchema {
138
+ try {
139
+ const decoded = jwt.verify(token, JWT_SECRET) as TokenPayloadSchema;
140
+ return {
141
+ sub: decoded.sub,
142
+ email: decoded.email,
143
+ role: decoded.role || 'authenticated',
144
+ };
145
+ } catch {
146
+ throw new AppError('Invalid token', 401, ERROR_CODES.AUTH_UNAUTHORIZED);
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Verify cloud backend JWT token
152
+ * Validates JWT tokens from api.insforge.dev using JWKS
153
+ */
154
+ async verifyCloudToken(token: string): Promise<{ projectId: string; payload: JWTPayload }> {
155
+ try {
156
+ // JWKS handles caching internally, no need to manage it manually
157
+ const { payload } = await jwtVerify(token, JWKS, {
158
+ algorithms: ['RS256', 'RS384', 'RS512', 'ES256', 'ES384', 'ES512'],
159
+ });
160
+
161
+ // Verify project_id matches if configured
162
+ const tokenProjectId = payload['projectId'] as string;
163
+ const expectedProjectId = process.env.PROJECT_ID;
164
+
165
+ if (expectedProjectId && tokenProjectId !== expectedProjectId) {
166
+ throw new AppError(
167
+ 'Project ID mismatch',
168
+ 403,
169
+ ERROR_CODES.AUTH_UNAUTHORIZED,
170
+ NEXT_ACTION.CHECK_TOKEN
171
+ );
172
+ }
173
+
174
+ return {
175
+ projectId: tokenProjectId || expectedProjectId || 'local',
176
+ payload,
177
+ };
178
+ } catch (error) {
179
+ // Re-throw AppError as-is
180
+ if (error instanceof AppError) {
181
+ throw error;
182
+ }
183
+
184
+ // Wrap other JWT errors
185
+ throw new AppError(
186
+ `Invalid cloud authorization code: ${error instanceof Error ? error.message : 'Unknown error'}`,
187
+ 401,
188
+ ERROR_CODES.AUTH_INVALID_CREDENTIALS,
189
+ NEXT_ACTION.CHECK_TOKEN
190
+ );
191
+ }
192
+ }
193
+
194
+ /**
195
+ * Generate CSRF token derived from refresh token using HMAC
196
+ */
197
+ generateCsrfToken(refreshToken: string): string {
198
+ return crypto
199
+ .createHmac('sha256', JWT_SECRET)
200
+ .update(refreshToken)
201
+ .digest('hex');
202
+ }
203
+
204
+ /**
205
+ * Verify CSRF token by re-computing from refresh token
206
+ * Uses timing-safe comparison to prevent timing attacks
207
+ */
208
+ verifyCsrfToken(csrfHeader: string | undefined, refreshToken: string): boolean {
209
+ if (!csrfHeader || !refreshToken) {
210
+ return false;
211
+ }
212
+ const expectedCsrf = this.generateCsrfToken(refreshToken);
213
+ try {
214
+ return crypto.timingSafeEqual(Buffer.from(csrfHeader), Buffer.from(expectedCsrf));
215
+ } catch {
216
+ return false;
217
+ }
218
+ }
219
+ }