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.
- package/.claude-plugin/marketplace.json +20 -20
- package/.dockerignore +60 -60
- package/.env.example +83 -77
- package/.github/ISSUE_TEMPLATE/bug_report.yml +36 -36
- package/.github/ISSUE_TEMPLATE/config.yml +11 -11
- package/.github/ISSUE_TEMPLATE/feature_request.yml +26 -26
- package/.github/PULL_REQUEST_TEMPLATE.md +7 -7
- package/.github/copilot-instructions.md +146 -146
- package/.github/workflows/build-image.yml +65 -65
- package/.github/workflows/ci-premerge-check.yml +23 -23
- package/.github/workflows/e2e.yml +63 -63
- package/.github/workflows/lint-and-format.yml +32 -32
- package/.prettierignore +64 -64
- package/CHANGELOG.md +44 -44
- package/CLAUDE_PLUGIN.md +104 -104
- package/CODE_OF_CONDUCT.md +128 -128
- package/CONTRIBUTING.md +125 -125
- package/Dockerfile +30 -30
- package/GITHUB_OAUTH_SETUP.md +49 -49
- package/GOOGLE_OAUTH_SETUP.md +148 -148
- package/LICENSE +201 -201
- package/README.md +182 -182
- package/assets/Dark.svg +23 -23
- package/auth/package.json +28 -28
- package/auth/src/lib/broadcastService.ts +117 -115
- package/auth/src/pages/SignInPage.tsx +60 -57
- package/auth/src/pages/SignUpPage.tsx +60 -57
- package/auth/tsconfig.json +32 -32
- package/auth/tsconfig.node.json +11 -11
- package/backend/package.json +78 -75
- package/backend/src/api/routes/ai/index.routes.ts +3 -3
- package/backend/src/api/routes/auth/index.routes.ts +667 -570
- package/backend/src/api/routes/auth/oauth.routes.ts +473 -448
- package/backend/src/api/routes/database/advance.routes.ts +37 -16
- package/backend/src/api/routes/database/index.routes.ts +78 -1
- package/backend/src/api/routes/database/records.routes.ts +10 -10
- package/backend/src/api/routes/database/tables.routes.ts +0 -14
- package/backend/src/api/routes/docs/index.routes.ts +75 -76
- package/backend/src/api/routes/email/index.routes.ts +35 -0
- package/backend/src/api/routes/functions/index.routes.ts +18 -12
- package/backend/src/api/routes/metadata/index.routes.ts +12 -0
- package/backend/src/api/routes/realtime/channels.routes.ts +81 -0
- package/backend/src/api/routes/realtime/index.routes.ts +12 -0
- package/backend/src/api/routes/realtime/messages.routes.ts +48 -0
- package/backend/src/api/routes/realtime/permissions.routes.ts +19 -0
- package/backend/src/api/routes/storage/index.routes.ts +18 -12
- package/backend/src/api/routes/usage/index.routes.ts +6 -4
- package/backend/src/infra/database/database.manager.ts +14 -1
- package/backend/src/infra/database/migrations/000_create-base-tables.sql +141 -141
- package/backend/src/infra/database/migrations/001_create-helper-functions.sql +40 -40
- package/backend/src/infra/database/migrations/002_rename-auth-tables.sql +29 -29
- package/backend/src/infra/database/migrations/003_create-users-table.sql +55 -55
- package/backend/src/infra/database/migrations/004_add-reload-postgrest-func.sql +23 -23
- package/backend/src/infra/database/migrations/005_enable-project-admin-modify-users.sql +29 -29
- package/backend/src/infra/database/migrations/006_modify-ai-usage-table.sql +24 -24
- package/backend/src/infra/database/migrations/007_drop-metadata-table.sql +1 -1
- package/backend/src/infra/database/migrations/008_add-system-tables.sql +76 -76
- package/backend/src/infra/database/migrations/009_add-function-secrets.sql +23 -23
- package/backend/src/infra/database/migrations/010_modify-ai-config-modalities.sql +93 -93
- package/backend/src/infra/database/migrations/011_refactor-secrets-table.sql +15 -15
- package/backend/src/infra/database/migrations/012_add-storage-uploaded-by.sql +7 -7
- package/backend/src/infra/database/migrations/013_create-auth-schema-functions.sql +44 -44
- package/backend/src/infra/database/migrations/014_add-updated-at-trigger-user-table.sql +7 -7
- package/backend/src/infra/database/migrations/015_create-auth-config-and-email-otp-tables.sql +59 -59
- package/backend/src/infra/database/migrations/016_update-auth-config-and-email-otp.sql +24 -24
- package/backend/src/infra/database/migrations/017_create-realtime-schema.sql +233 -0
- package/backend/src/infra/realtime/realtime.manager.ts +246 -0
- package/backend/src/infra/realtime/webhook-sender.ts +82 -0
- package/backend/src/infra/security/token.manager.ts +219 -125
- package/backend/src/infra/socket/socket.manager.ts +198 -64
- package/backend/src/providers/ai/openrouter.provider.ts +12 -9
- package/backend/src/providers/email/base.provider.ts +4 -7
- package/backend/src/providers/email/cloud.provider.ts +84 -0
- package/backend/src/providers/oauth/apple.provider.ts +266 -0
- package/backend/src/providers/oauth/index.ts +1 -0
- package/backend/src/server.ts +317 -284
- package/backend/src/services/ai/ai-model.service.ts +5 -5
- package/backend/src/services/ai/chat-completion.service.ts +4 -4
- package/backend/src/services/ai/image-generation.service.ts +3 -3
- package/backend/src/services/auth/auth.service.ts +14 -0
- package/backend/src/services/database/database-table.service.ts +0 -9
- package/backend/src/services/database/database.service.ts +127 -0
- package/backend/src/services/email/email.service.ts +5 -7
- package/backend/src/services/realtime/index.ts +3 -0
- package/backend/src/services/realtime/realtime-auth.service.ts +104 -0
- package/backend/src/services/realtime/realtime-channel.service.ts +237 -0
- package/backend/src/services/realtime/realtime-message.service.ts +260 -0
- package/backend/src/types/auth.ts +11 -0
- package/backend/src/types/realtime.ts +18 -0
- package/backend/src/types/socket.ts +7 -31
- package/backend/src/utils/cookies.ts +35 -0
- package/backend/src/utils/s3-config-loader.ts +64 -0
- package/backend/src/utils/seed.ts +301 -298
- package/backend/src/utils/sql-parser.ts +90 -0
- package/backend/tests/README.md +133 -133
- package/backend/tests/cleanup-all-test-data.sh +230 -230
- package/backend/tests/cloud/test-s3-multitenant.sh +131 -131
- package/backend/tests/local/comprehensive-curl-tests.sh +155 -155
- package/backend/tests/local/test-ai-config.sh +129 -129
- package/backend/tests/local/test-ai-usage.sh +80 -80
- package/backend/tests/local/test-auth-router.sh +143 -143
- package/backend/tests/local/test-database-router.sh +222 -222
- package/backend/tests/local/test-e2e.sh +240 -240
- package/backend/tests/local/test-fk-errors.sh +96 -96
- package/backend/tests/local/test-functions.sh +123 -123
- package/backend/tests/local/test-id-field.sh +200 -200
- package/backend/tests/local/test-logs.sh +132 -132
- package/backend/tests/local/test-public-bucket.sh +264 -264
- package/backend/tests/local/test-secrets.sh +249 -249
- package/backend/tests/local/test-serverless-functions.sh.disabled +325 -325
- package/backend/tests/local/test-traditional-rest.sh +208 -208
- package/backend/tests/manual/README.md +50 -50
- package/backend/tests/manual/create-large-table-simple.sql +10 -10
- package/backend/tests/manual/seed-large-table.sql +100 -100
- package/backend/tests/manual/setup-large-table-extras.sql +33 -33
- package/backend/tests/manual/test-bulk-upsert.sh +409 -409
- package/backend/tests/manual/test-database-advance.sh +296 -296
- package/backend/tests/manual/test-postgrest-stability.sh +191 -191
- package/backend/tests/manual/test-rawsql-export-import.sh +411 -411
- package/backend/tests/manual/test-rawsql-modes.sh +244 -244
- package/backend/tests/manual/test-universal-storage.sh +263 -263
- package/backend/tests/manual/test-users.sql +17 -17
- package/backend/tests/run-all-tests.sh +139 -139
- package/backend/tests/setup.ts +0 -0
- package/backend/tests/test-config.sh +338 -338
- package/backend/tests/unit/analyze-query.test.ts +697 -0
- package/backend/tsconfig.json +22 -22
- package/claude-plugin/.claude-plugin/plugin.json +24 -24
- package/claude-plugin/README.md +133 -133
- package/claude-plugin/skills/insforge-schema-patterns/SKILL.md +270 -270
- package/docker-compose.prod.yml +204 -200
- package/docker-compose.yml +232 -228
- package/docker-init/db/db-init.sql +97 -97
- package/docker-init/db/jwt.sql +5 -5
- package/docker-init/db/postgresql.conf +16 -16
- package/docker-init/logs/vector.yml +236 -236
- package/docs/README.md +44 -44
- package/docs/agent-docs/real-time.md +269 -0
- package/docs/changelog.mdx +119 -67
- package/docs/core-concepts/ai/architecture.mdx +372 -372
- package/docs/core-concepts/ai/sdk.mdx +213 -213
- package/docs/core-concepts/authentication/architecture.mdx +278 -278
- package/docs/core-concepts/authentication/sdk.mdx +414 -414
- package/docs/core-concepts/authentication/ui-components/customization.mdx +529 -529
- package/docs/core-concepts/authentication/ui-components/nextjs.mdx +221 -221
- package/docs/core-concepts/authentication/ui-components/react-router.mdx +184 -184
- package/docs/core-concepts/authentication/ui-components/react.mdx +129 -129
- package/docs/core-concepts/database/architecture.mdx +255 -255
- package/docs/core-concepts/database/sdk.mdx +382 -382
- package/docs/core-concepts/email/architecture.mdx +101 -0
- package/docs/core-concepts/email/sdk.mdx +53 -0
- package/docs/core-concepts/functions/architecture.mdx +105 -105
- package/docs/core-concepts/functions/sdk.mdx +184 -184
- package/docs/core-concepts/realtime/architecture.mdx +446 -0
- package/docs/core-concepts/realtime/sdk.mdx +409 -0
- package/docs/core-concepts/storage/architecture.mdx +243 -243
- package/docs/core-concepts/storage/sdk.mdx +253 -253
- package/docs/deployment/README.md +94 -94
- package/docs/deployment/deploy-to-aws-ec2.md +564 -564
- package/docs/deployment/deploy-to-azure-virtual-machines.md +312 -312
- package/docs/deployment/deploy-to-google-cloud-compute-engine.md +613 -613
- package/docs/deployment/deploy-to-render.md +441 -441
- package/docs/deprecated/insforge-auth-api.md +214 -214
- package/docs/deprecated/insforge-auth-sdk.md +99 -99
- package/docs/deprecated/insforge-db-api.md +358 -358
- package/docs/deprecated/insforge-db-sdk.md +139 -139
- package/docs/deprecated/insforge-debug-sdk.md +156 -156
- package/docs/deprecated/insforge-debug.md +64 -64
- package/docs/deprecated/insforge-instructions.md +123 -123
- package/docs/deprecated/insforge-project.md +117 -117
- package/docs/deprecated/insforge-storage-api.md +278 -278
- package/docs/deprecated/insforge-storage-sdk.md +158 -158
- package/docs/docs.json +232 -210
- package/docs/examples/framework-guides/nextjs.mdx +131 -131
- package/docs/examples/framework-guides/nuxt.mdx +165 -165
- package/docs/examples/framework-guides/react.mdx +165 -165
- package/docs/examples/framework-guides/svelte.mdx +153 -153
- package/docs/examples/framework-guides/vue.mdx +159 -159
- package/docs/examples/overview.mdx +67 -67
- package/docs/favicon.svg +19 -19
- package/docs/images/changelog/dec-2025/ai-integration.png +0 -0
- package/docs/images/changelog/dec-2025/ai-models.webp +0 -0
- package/docs/images/changelog/dec-2025/alipay-payment.webp +0 -0
- package/docs/images/changelog/dec-2025/apple-login.jpg +0 -0
- package/docs/images/changelog/dec-2025/mcp-installer.png +0 -0
- package/docs/images/changelog/dec-2025/realtime-module.jpg +0 -0
- package/docs/images/icons/ai.svg +4 -4
- package/docs/images/logos/nextjs.svg +4 -4
- package/docs/images/logos/nuxt.svg +4 -4
- package/docs/images/logos/react.svg +5 -5
- package/docs/images/logos/svelte.svg +4 -4
- package/docs/images/logos/vue.svg +5 -5
- package/docs/insforge-instructions-sdk.md +89 -88
- package/docs/introduction.mdx +45 -45
- package/docs/logo/dark.svg +22 -22
- package/docs/logo/light.svg +20 -20
- package/docs/partnership.mdx +651 -646
- package/docs/quickstart.mdx +82 -82
- package/docs/showcase.mdx +52 -52
- package/docs/snippets/sdk-installation.mdx +21 -21
- package/docs/snippets/service-icons.mdx +27 -27
- package/examples/oauth/frontend-oauth-example.html +250 -250
- package/examples/response-examples.md +443 -443
- package/frontend/components.json +17 -17
- package/frontend/package.json +69 -69
- package/frontend/src/assets/icons/checkbox_checked.svg +6 -6
- package/frontend/src/assets/icons/checkbox_undetermined.svg +6 -6
- package/frontend/src/assets/icons/checked.svg +3 -3
- package/frontend/src/assets/icons/connected.svg +3 -3
- package/frontend/src/assets/icons/error.svg +3 -3
- package/frontend/src/assets/icons/loader.svg +9 -9
- package/frontend/src/assets/icons/pencil.svg +4 -4
- package/frontend/src/assets/icons/refresh.svg +4 -4
- package/frontend/src/assets/icons/step_active.svg +3 -3
- package/frontend/src/assets/icons/step_inactive.svg +11 -11
- package/frontend/src/assets/icons/warning.svg +3 -3
- package/frontend/src/assets/logos/apple.svg +3 -3
- package/frontend/src/assets/logos/claude_code.svg +3 -3
- package/frontend/src/assets/logos/cline.svg +6 -6
- package/frontend/src/assets/logos/cursor.svg +20 -20
- package/frontend/src/assets/logos/discord.svg +8 -8
- package/frontend/src/assets/logos/facebook.svg +3 -3
- package/frontend/src/assets/logos/gemini.svg +19 -19
- package/frontend/src/assets/logos/github.svg +5 -5
- package/frontend/src/assets/logos/google.svg +13 -13
- package/frontend/src/assets/logos/grok.svg +10 -10
- package/frontend/src/assets/logos/insforge_dark.svg +15 -15
- package/frontend/src/assets/logos/insforge_light.svg +15 -15
- package/frontend/src/assets/logos/instagram.svg +1 -1
- package/frontend/src/assets/logos/linkedin.svg +3 -3
- package/frontend/src/assets/logos/openai.svg +10 -10
- package/frontend/src/assets/logos/roo_code.svg +9 -9
- package/frontend/src/assets/logos/spotify.svg +16 -16
- package/frontend/src/assets/logos/tiktok.svg +5 -5
- package/frontend/src/assets/logos/trae.svg +3 -3
- package/frontend/src/assets/logos/windsurf.svg +10 -10
- package/frontend/src/assets/logos/x.svg +3 -3
- package/frontend/src/components/layout/AppHeader.tsx +9 -10
- package/frontend/src/features/auth/components/OAuthConfigDialog.tsx +1 -0
- package/frontend/src/features/auth/components/UsersDataGrid.tsx +6 -0
- package/frontend/src/features/auth/helpers.tsx +8 -0
- package/frontend/src/features/auth/{page → pages}/UsersPage.tsx +0 -28
- package/frontend/src/features/database/components/SQLModal.tsx +75 -0
- package/frontend/src/features/database/components/TableForm.tsx +0 -4
- package/frontend/src/features/database/hooks/useDatabase.ts +66 -0
- package/frontend/src/features/database/hooks/useTables.ts +32 -28
- package/frontend/src/features/database/index.ts +1 -0
- package/frontend/src/features/database/{page → pages}/FunctionsPage.tsx +29 -37
- package/frontend/src/features/database/{page → pages}/IndexesPage.tsx +35 -47
- package/frontend/src/features/database/{page → pages}/PoliciesPage.tsx +43 -54
- package/frontend/src/features/database/{page → pages}/TablesPage.tsx +0 -42
- package/frontend/src/features/database/{page → pages}/TriggersPage.tsx +35 -47
- package/frontend/src/features/database/services/advance.service.ts +0 -26
- package/frontend/src/features/database/services/database.service.ts +55 -0
- package/frontend/src/features/database/services/table.service.ts +0 -6
- package/frontend/src/features/functions/{page → pages}/FunctionsPage.tsx +21 -44
- package/frontend/src/features/functions/{page → pages}/SecretsPage.tsx +11 -9
- package/frontend/src/features/logs/hooks/useMcpUsage.ts +13 -66
- package/frontend/src/features/realtime/components/ChannelRow.tsx +83 -0
- package/frontend/src/features/realtime/components/EditChannelModal.tsx +246 -0
- package/frontend/src/features/realtime/components/MessageRow.tsx +85 -0
- package/frontend/src/features/realtime/components/RealtimeEmptyState.tsx +30 -0
- package/frontend/src/features/realtime/hooks/useRealtime.ts +218 -0
- package/frontend/src/features/realtime/index.ts +11 -0
- package/frontend/src/features/realtime/pages/RealtimeChannelsPage.tsx +172 -0
- package/frontend/src/features/realtime/pages/RealtimeMessagesPage.tsx +211 -0
- package/frontend/src/features/realtime/pages/RealtimePermissionsPage.tsx +191 -0
- package/frontend/src/features/realtime/services/realtime.service.ts +107 -0
- package/frontend/src/features/storage/{page → pages}/StoragePage.tsx +1 -29
- package/frontend/src/features/visualizer/components/SchemaVisualizer.tsx +3 -3
- package/frontend/src/features/visualizer/{page → pages}/VisualizerPage.tsx +1 -35
- package/frontend/src/lib/contexts/SocketContext.tsx +119 -75
- package/frontend/src/lib/routing/AppRoutes.tsx +35 -20
- package/frontend/src/lib/utils/cloudMessaging.ts +1 -1
- package/frontend/src/lib/utils/menuItems.ts +24 -0
- package/frontend/src/lib/utils/utils.ts +14 -1
- package/frontend/tsconfig.json +25 -25
- package/frontend/tsconfig.node.json +9 -9
- package/functions/deno.json +24 -24
- package/functions/server.ts +315 -315
- package/i18n/README.ar.md +130 -130
- package/i18n/README.de.md +130 -130
- package/i18n/README.es.md +154 -154
- package/i18n/README.fr.md +134 -134
- package/i18n/README.hi.md +129 -129
- package/i18n/README.ja.md +174 -174
- package/i18n/README.ko.md +136 -136
- package/i18n/README.pt-BR.md +131 -131
- package/i18n/README.ru.md +129 -129
- package/i18n/README.zh-CN.md +133 -133
- package/openapi/ai.yaml +715 -715
- package/openapi/auth.yaml +1244 -1244
- package/openapi/email.yaml +158 -0
- package/openapi/functions.yaml +475 -475
- package/openapi/health.yaml +29 -29
- package/openapi/logs.yaml +223 -223
- package/openapi/metadata.yaml +177 -177
- package/openapi/realtime.yaml +699 -0
- package/openapi/records.yaml +381 -381
- package/openapi/secrets.yaml +370 -370
- package/openapi/storage.yaml +875 -875
- package/openapi/tables.yaml +463 -463
- package/package.json +97 -97
- package/shared-schemas/package.json +31 -31
- package/shared-schemas/src/ai.schema.ts +63 -59
- package/shared-schemas/src/auth-api.schema.ts +352 -339
- package/shared-schemas/src/auth.schema.ts +1 -1
- package/shared-schemas/src/database-api.schema.ts +32 -1
- package/shared-schemas/src/database.schema.ts +39 -0
- package/shared-schemas/src/docs.schema.ts +26 -0
- package/shared-schemas/src/email-api.schema.ts +30 -0
- package/shared-schemas/src/index.ts +4 -0
- package/shared-schemas/src/metadata.schema.ts +9 -0
- package/shared-schemas/src/realtime-api.schema.ts +111 -0
- package/shared-schemas/src/realtime.schema.ts +143 -0
- package/shared-schemas/tsconfig.json +21 -21
- package/tsconfig.json +7 -7
- package/zeabur/README.md +13 -13
- package/zeabur/template.yml +1032 -1032
- package/.cursor/rules/cursor-rules.mdc +0 -94
- package/frontend/src/features/database/hooks/useFullMetadata.ts +0 -18
- package/test-gemini.sh +0 -35
- package/test-usage-admin.sh +0 -57
- package/test-usage.sh +0 -50
- /package/frontend/src/features/ai/{page → pages}/AIPage.tsx +0 -0
- /package/frontend/src/features/auth/{page → pages}/AuthMethodsPage.tsx +0 -0
- /package/frontend/src/features/auth/{page → pages}/ConfigurationPage.tsx +0 -0
- /package/frontend/src/features/dashboard/{page → pages}/DashboardPage.tsx +0 -0
- /package/frontend/src/features/database/{page → pages}/SQLEditorPage.tsx +0 -0
- /package/frontend/src/features/database/{page → pages}/TemplatesPage.tsx +0 -0
- /package/frontend/src/features/login/{page → pages}/CloudLoginPage.tsx +0 -0
- /package/frontend/src/features/login/{page → pages}/LoginPage.tsx +0 -0
- /package/frontend/src/features/logs/{page → pages}/AuditsPage.tsx +0 -0
- /package/frontend/src/features/logs/{page → pages}/LogsPage.tsx +0 -0
- /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
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
*
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if (error instanceof AppError) {
|
|
113
|
-
throw error;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
+
}
|