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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import OpenAI from 'openai';
|
|
2
2
|
import { AIUsageService } from './ai-usage.service.js';
|
|
3
3
|
import { AIConfigService } from './ai-config.service.js';
|
|
4
|
-
import {
|
|
4
|
+
import { OpenRouterProvider } from '@/providers/ai/openrouter.provider.js';
|
|
5
5
|
import type {
|
|
6
6
|
AIConfigurationSchema,
|
|
7
7
|
ChatCompletionResponse,
|
|
@@ -14,7 +14,7 @@ export class ChatCompletionService {
|
|
|
14
14
|
private static instance: ChatCompletionService;
|
|
15
15
|
private aiUsageService = AIUsageService.getInstance();
|
|
16
16
|
private aiConfigService = AIConfigService.getInstance();
|
|
17
|
-
private
|
|
17
|
+
private openRouterProvider = OpenRouterProvider.getInstance();
|
|
18
18
|
|
|
19
19
|
private constructor() {}
|
|
20
20
|
|
|
@@ -106,7 +106,7 @@ export class ChatCompletionService {
|
|
|
106
106
|
};
|
|
107
107
|
|
|
108
108
|
// Send request with automatic renewal and retry logic
|
|
109
|
-
const response = await this.
|
|
109
|
+
const response = await this.openRouterProvider.sendRequest((client) =>
|
|
110
110
|
client.chat.completions.create(request)
|
|
111
111
|
);
|
|
112
112
|
|
|
@@ -173,7 +173,7 @@ export class ChatCompletionService {
|
|
|
173
173
|
};
|
|
174
174
|
|
|
175
175
|
// Send request with automatic renewal and retry logic
|
|
176
|
-
const stream = await this.
|
|
176
|
+
const stream = await this.openRouterProvider.sendRequest((client) =>
|
|
177
177
|
client.chat.completions.create(request)
|
|
178
178
|
);
|
|
179
179
|
|
|
@@ -2,7 +2,7 @@ import OpenAI from 'openai';
|
|
|
2
2
|
|
|
3
3
|
import { AIUsageService } from './ai-usage.service.js';
|
|
4
4
|
import { AIConfigService } from './ai-config.service.js';
|
|
5
|
-
import {
|
|
5
|
+
import { OpenRouterProvider } from '@/providers/ai/openrouter.provider.js';
|
|
6
6
|
import type {
|
|
7
7
|
AIConfigurationSchema,
|
|
8
8
|
ImageGenerationRequest,
|
|
@@ -14,7 +14,7 @@ import { OpenRouterImageMessage } from '@/types/ai.js';
|
|
|
14
14
|
export class ImageGenerationService {
|
|
15
15
|
private static aiUsageService = AIUsageService.getInstance();
|
|
16
16
|
private static aiConfigService = AIConfigService.getInstance();
|
|
17
|
-
private static
|
|
17
|
+
private static openRouterProvider = OpenRouterProvider.getInstance();
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Validate model and get config
|
|
@@ -75,7 +75,7 @@ export class ImageGenerationService {
|
|
|
75
75
|
};
|
|
76
76
|
|
|
77
77
|
// Send request with automatic renewal and retry logic
|
|
78
|
-
const response = (await this.
|
|
78
|
+
const response = (await this.openRouterProvider.sendRequest((client) =>
|
|
79
79
|
client.chat.completions.create(
|
|
80
80
|
request as OpenAI.Chat.ChatCompletionCreateParamsNonStreaming
|
|
81
81
|
)
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
LinkedInUserInfo,
|
|
34
34
|
DiscordUserInfo,
|
|
35
35
|
XUserInfo,
|
|
36
|
+
AppleUserInfo,
|
|
36
37
|
UserRecord,
|
|
37
38
|
OAuthUserData,
|
|
38
39
|
} from '@/types/auth.js';
|
|
@@ -42,6 +43,7 @@ import { AppError } from '@/api/middlewares/error.js';
|
|
|
42
43
|
import { ERROR_CODES } from '@/types/error-constants.js';
|
|
43
44
|
import { EmailService } from '@/services/email/email.service.js';
|
|
44
45
|
import { XOAuthProvider } from '@/providers/oauth/x.provider.js';
|
|
46
|
+
import { AppleOAuthProvider } from '@/providers/oauth/apple.provider.js';
|
|
45
47
|
|
|
46
48
|
/**
|
|
47
49
|
* Simplified JWT-based auth service
|
|
@@ -62,6 +64,7 @@ export class AuthService {
|
|
|
62
64
|
private facebookOAuthProvider: FacebookOAuthProvider;
|
|
63
65
|
private microsoftOAuthProvider: MicrosoftOAuthProvider;
|
|
64
66
|
private xOAuthProvider: XOAuthProvider;
|
|
67
|
+
private appleOAuthProvider: AppleOAuthProvider;
|
|
65
68
|
|
|
66
69
|
private constructor() {
|
|
67
70
|
this.adminEmail = process.env.ADMIN_EMAIL ?? '';
|
|
@@ -82,6 +85,7 @@ export class AuthService {
|
|
|
82
85
|
this.facebookOAuthProvider = FacebookOAuthProvider.getInstance();
|
|
83
86
|
this.microsoftOAuthProvider = MicrosoftOAuthProvider.getInstance();
|
|
84
87
|
this.xOAuthProvider = XOAuthProvider.getInstance();
|
|
88
|
+
this.appleOAuthProvider = AppleOAuthProvider.getInstance();
|
|
85
89
|
|
|
86
90
|
logger.info('AuthService initialized');
|
|
87
91
|
}
|
|
@@ -670,6 +674,7 @@ export class AuthService {
|
|
|
670
674
|
| MicrosoftUserInfo
|
|
671
675
|
| FacebookUserInfo
|
|
672
676
|
| XUserInfo
|
|
677
|
+
| AppleUserInfo
|
|
673
678
|
| Record<string, unknown>
|
|
674
679
|
): Promise<CreateSessionResponse> {
|
|
675
680
|
const pool = this.getPool();
|
|
@@ -777,6 +782,7 @@ export class AuthService {
|
|
|
777
782
|
| MicrosoftUserInfo
|
|
778
783
|
| FacebookUserInfo
|
|
779
784
|
| XUserInfo
|
|
785
|
+
| AppleUserInfo
|
|
780
786
|
| Record<string, unknown>,
|
|
781
787
|
avatarUrl: string
|
|
782
788
|
): Promise<CreateSessionResponse> {
|
|
@@ -870,6 +876,8 @@ export class AuthService {
|
|
|
870
876
|
return this.microsoftOAuthProvider.generateOAuthUrl(state);
|
|
871
877
|
case 'x':
|
|
872
878
|
return this.xOAuthProvider.generateOAuthUrl(state);
|
|
879
|
+
case 'apple':
|
|
880
|
+
return this.appleOAuthProvider.generateOAuthUrl(state);
|
|
873
881
|
default:
|
|
874
882
|
throw new Error(`OAuth provider ${provider} is not implemented yet.`);
|
|
875
883
|
}
|
|
@@ -906,6 +914,9 @@ export class AuthService {
|
|
|
906
914
|
case 'x':
|
|
907
915
|
userData = await this.xOAuthProvider.handleCallback(payload);
|
|
908
916
|
break;
|
|
917
|
+
case 'apple':
|
|
918
|
+
userData = await this.appleOAuthProvider.handleCallback(payload);
|
|
919
|
+
break;
|
|
909
920
|
default:
|
|
910
921
|
throw new Error(`OAuth provider ${provider} is not implemented yet.`);
|
|
911
922
|
}
|
|
@@ -949,6 +960,9 @@ export class AuthService {
|
|
|
949
960
|
case 'x':
|
|
950
961
|
userData = this.xOAuthProvider.handleSharedCallback(payloadData);
|
|
951
962
|
break;
|
|
963
|
+
case 'apple':
|
|
964
|
+
userData = this.appleOAuthProvider.handleSharedCallback(payloadData);
|
|
965
|
+
break;
|
|
952
966
|
case 'microsoft':
|
|
953
967
|
default:
|
|
954
968
|
throw new Error(`OAuth provider ${provider} is not supported for shared callback.`);
|
|
@@ -268,15 +268,6 @@ export class DatabaseTableService {
|
|
|
268
268
|
}
|
|
269
269
|
}
|
|
270
270
|
|
|
271
|
-
/**
|
|
272
|
-
* Get all table schemas
|
|
273
|
-
*/
|
|
274
|
-
async getAllTableSchemas(): Promise<GetTableSchemaResponse[]> {
|
|
275
|
-
const tables = await this.listTables();
|
|
276
|
-
const schemas = await Promise.all(tables.map((table) => this.getTableSchema(table)));
|
|
277
|
-
return schemas;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
271
|
/**
|
|
281
272
|
* Get table schema
|
|
282
273
|
*/
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { DatabaseManager } from '@/infra/database/database.manager.js';
|
|
2
|
+
import type {
|
|
3
|
+
DatabaseFunctionsResponse,
|
|
4
|
+
DatabaseIndexesResponse,
|
|
5
|
+
DatabasePoliciesResponse,
|
|
6
|
+
DatabaseTriggersResponse,
|
|
7
|
+
} from '@insforge/shared-schemas';
|
|
8
|
+
|
|
9
|
+
export class DatabaseService {
|
|
10
|
+
private static instance: DatabaseService;
|
|
11
|
+
private dbManager = DatabaseManager.getInstance();
|
|
12
|
+
|
|
13
|
+
private constructor() {}
|
|
14
|
+
|
|
15
|
+
public static getInstance(): DatabaseService {
|
|
16
|
+
if (!DatabaseService.instance) {
|
|
17
|
+
DatabaseService.instance = new DatabaseService();
|
|
18
|
+
}
|
|
19
|
+
return DatabaseService.instance;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Get all database functions (excluding system and extension functions)
|
|
24
|
+
*/
|
|
25
|
+
async getFunctions(): Promise<DatabaseFunctionsResponse> {
|
|
26
|
+
const pool = this.dbManager.getPool();
|
|
27
|
+
|
|
28
|
+
const result = await pool.query(`
|
|
29
|
+
SELECT
|
|
30
|
+
p.proname as "functionName",
|
|
31
|
+
pg_get_functiondef(p.oid) as "functionDef",
|
|
32
|
+
p.prokind as "kind"
|
|
33
|
+
FROM pg_proc p
|
|
34
|
+
JOIN pg_namespace n ON p.pronamespace = n.oid
|
|
35
|
+
WHERE n.nspname = 'public'
|
|
36
|
+
AND p.prokind IN ('f', 'p', 'w')
|
|
37
|
+
AND NOT EXISTS (
|
|
38
|
+
SELECT 1 FROM pg_depend d
|
|
39
|
+
JOIN pg_extension e ON d.refobjid = e.oid
|
|
40
|
+
WHERE d.objid = p.oid
|
|
41
|
+
)
|
|
42
|
+
ORDER BY p.proname
|
|
43
|
+
`);
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
functions: result.rows,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Get all indexes across all tables (excluding system tables)
|
|
52
|
+
*/
|
|
53
|
+
async getIndexes(): Promise<DatabaseIndexesResponse> {
|
|
54
|
+
const pool = this.dbManager.getPool();
|
|
55
|
+
|
|
56
|
+
const result = await pool.query(`
|
|
57
|
+
SELECT
|
|
58
|
+
pi.tablename as "tableName",
|
|
59
|
+
pi.indexname as "indexName",
|
|
60
|
+
pi.indexdef as "indexDef",
|
|
61
|
+
idx.indisunique as "isUnique",
|
|
62
|
+
idx.indisprimary as "isPrimary"
|
|
63
|
+
FROM pg_indexes pi
|
|
64
|
+
JOIN pg_class cls ON cls.relname = pi.indexname
|
|
65
|
+
AND cls.relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = pi.schemaname)
|
|
66
|
+
JOIN pg_index idx ON idx.indexrelid = cls.oid
|
|
67
|
+
WHERE pi.schemaname = 'public'
|
|
68
|
+
AND pi.tablename NOT LIKE '\\_%' ESCAPE '\\'
|
|
69
|
+
ORDER BY pi.tablename, pi.indexname
|
|
70
|
+
`);
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
indexes: result.rows,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Get all RLS policies across all tables (excluding system tables)
|
|
79
|
+
*/
|
|
80
|
+
async getPolicies(): Promise<DatabasePoliciesResponse> {
|
|
81
|
+
const pool = this.dbManager.getPool();
|
|
82
|
+
|
|
83
|
+
const result = await pool.query(`
|
|
84
|
+
SELECT
|
|
85
|
+
tablename as "tableName",
|
|
86
|
+
policyname as "policyName",
|
|
87
|
+
cmd,
|
|
88
|
+
roles,
|
|
89
|
+
qual,
|
|
90
|
+
with_check as "withCheck"
|
|
91
|
+
FROM pg_policies
|
|
92
|
+
WHERE schemaname = 'public'
|
|
93
|
+
AND tablename NOT LIKE '\\_%' ESCAPE '\\'
|
|
94
|
+
ORDER BY tablename, policyname
|
|
95
|
+
`);
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
policies: result.rows,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Get all triggers across all tables (excluding system tables)
|
|
104
|
+
*/
|
|
105
|
+
async getTriggers(): Promise<DatabaseTriggersResponse> {
|
|
106
|
+
const pool = this.dbManager.getPool();
|
|
107
|
+
|
|
108
|
+
const result = await pool.query(`
|
|
109
|
+
SELECT
|
|
110
|
+
event_object_table as "tableName",
|
|
111
|
+
trigger_name as "triggerName",
|
|
112
|
+
action_timing as "actionTiming",
|
|
113
|
+
event_manipulation as "eventManipulation",
|
|
114
|
+
action_orientation as "actionOrientation",
|
|
115
|
+
action_condition as "actionCondition",
|
|
116
|
+
action_statement as "actionStatement"
|
|
117
|
+
FROM information_schema.triggers
|
|
118
|
+
WHERE event_object_schema = 'public'
|
|
119
|
+
AND event_object_table NOT LIKE '\\_%' ESCAPE '\\'
|
|
120
|
+
ORDER BY event_object_table, trigger_name
|
|
121
|
+
`);
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
triggers: result.rows,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { EmailProvider } from '@/providers/email/base.provider.js';
|
|
2
2
|
import { CloudEmailProvider } from '@/providers/email/cloud.provider.js';
|
|
3
3
|
import { EmailTemplate } from '@/types/email.js';
|
|
4
|
+
import { SendRawEmailRequest } from '@insforge/shared-schemas';
|
|
4
5
|
import logger from '@/utils/logger.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -53,17 +54,14 @@ export class EmailService {
|
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
/**
|
|
56
|
-
* Send raw email
|
|
57
|
-
* @param
|
|
58
|
-
* @param subject - Email subject
|
|
59
|
-
* @param html - HTML email body
|
|
60
|
-
* @param text - Plain text email body (optional)
|
|
57
|
+
* Send custom/raw email
|
|
58
|
+
* @param options - Email options (to, subject, html, cc, bcc, from, replyTo)
|
|
61
59
|
*/
|
|
62
|
-
public async sendRaw(
|
|
60
|
+
public async sendRaw(options: SendRawEmailRequest): Promise<void> {
|
|
63
61
|
if (!this.provider.sendRaw) {
|
|
64
62
|
throw new Error('Current email provider does not support raw email sending');
|
|
65
63
|
}
|
|
66
|
-
return this.provider.sendRaw(
|
|
64
|
+
return this.provider.sendRaw(options);
|
|
67
65
|
}
|
|
68
66
|
|
|
69
67
|
/**
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { Pool, PoolClient } from 'pg';
|
|
2
|
+
import { DatabaseManager } from '@/infra/database/database.manager.js';
|
|
3
|
+
import logger from '@/utils/logger.js';
|
|
4
|
+
import { RoleSchema } from '@insforge/shared-schemas';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Handles channel authorization by checking RLS policies on the messages table.
|
|
8
|
+
*
|
|
9
|
+
* Permission Model (Supabase pattern):
|
|
10
|
+
* - SELECT on messages = 'join' permission (can subscribe to channel)
|
|
11
|
+
* - INSERT on messages = 'send' permission (can publish to channel)
|
|
12
|
+
*
|
|
13
|
+
* Developers define RLS policies on realtime.messages that check:
|
|
14
|
+
* - current_setting('request.jwt.claim.sub', true) = user ID
|
|
15
|
+
* - current_setting('request.jwt.claim.role', true) = user role
|
|
16
|
+
* - channel_name for channel-specific access
|
|
17
|
+
*/
|
|
18
|
+
export class RealtimeAuthService {
|
|
19
|
+
private static instance: RealtimeAuthService;
|
|
20
|
+
private pool: Pool | null = null;
|
|
21
|
+
|
|
22
|
+
private constructor() {}
|
|
23
|
+
|
|
24
|
+
static getInstance(): RealtimeAuthService {
|
|
25
|
+
if (!RealtimeAuthService.instance) {
|
|
26
|
+
RealtimeAuthService.instance = new RealtimeAuthService();
|
|
27
|
+
}
|
|
28
|
+
return RealtimeAuthService.instance;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
private getPool(): Pool {
|
|
32
|
+
if (!this.pool) {
|
|
33
|
+
this.pool = DatabaseManager.getInstance().getPool();
|
|
34
|
+
}
|
|
35
|
+
return this.pool;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Check if user has permission to subscribe to a channel.
|
|
40
|
+
* Tests SELECT permission on channels table via RLS.
|
|
41
|
+
*
|
|
42
|
+
* @param channelName - The channel to check access for
|
|
43
|
+
* @param userId - The user ID (undefined for anonymous users)
|
|
44
|
+
* @param role - The database role to use (authenticated or anon)
|
|
45
|
+
* @returns true if user can subscribe, false otherwise
|
|
46
|
+
*/
|
|
47
|
+
async checkSubscribePermission(
|
|
48
|
+
channelName: string,
|
|
49
|
+
userId: string | undefined,
|
|
50
|
+
role: RoleSchema
|
|
51
|
+
): Promise<boolean> {
|
|
52
|
+
const client = await this.getPool().connect();
|
|
53
|
+
try {
|
|
54
|
+
// Begin transaction to ensure settings persist across queries
|
|
55
|
+
await client.query('BEGIN');
|
|
56
|
+
// Switch to specified role to enforce RLS policies
|
|
57
|
+
await client.query(`SET LOCAL ROLE ${role}`);
|
|
58
|
+
await this.setUserContext(client, userId, channelName);
|
|
59
|
+
|
|
60
|
+
// Test SELECT permission via RLS on channels table
|
|
61
|
+
const result = await client.query(
|
|
62
|
+
`SELECT 1 FROM realtime.channels
|
|
63
|
+
WHERE enabled = TRUE
|
|
64
|
+
AND (pattern = $1 OR $1 LIKE pattern)
|
|
65
|
+
LIMIT 1`,
|
|
66
|
+
[channelName]
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
// Commit transaction
|
|
70
|
+
await client.query('COMMIT');
|
|
71
|
+
|
|
72
|
+
// If query returns a row, user has permission
|
|
73
|
+
return result.rowCount !== null && result.rowCount > 0;
|
|
74
|
+
} catch (error) {
|
|
75
|
+
// Rollback transaction on error
|
|
76
|
+
await client.query('ROLLBACK').catch(() => {});
|
|
77
|
+
logger.debug('Subscribe permission denied', { channelName, userId, error });
|
|
78
|
+
return false;
|
|
79
|
+
} finally {
|
|
80
|
+
// Reset role back to default before releasing connection
|
|
81
|
+
await client.query('RESET ROLE');
|
|
82
|
+
client.release();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Set user context variables for RLS policy evaluation.
|
|
88
|
+
* Can be used by other services that need to execute queries with user context.
|
|
89
|
+
*/
|
|
90
|
+
async setUserContext(
|
|
91
|
+
client: PoolClient,
|
|
92
|
+
userId: string | undefined,
|
|
93
|
+
channelName: string
|
|
94
|
+
): Promise<void> {
|
|
95
|
+
if (userId) {
|
|
96
|
+
await client.query("SELECT set_config('request.jwt.claim.sub', $1, true)", [userId]);
|
|
97
|
+
} else {
|
|
98
|
+
await client.query("SELECT set_config('request.jwt.claim.sub', '', true)");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Set the channel being accessed (used by realtime.channel_name())
|
|
102
|
+
await client.query("SELECT set_config('realtime.channel_name', $1, true)", [channelName]);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { Pool } from 'pg';
|
|
2
|
+
import { DatabaseManager } from '@/infra/database/database.manager.js';
|
|
3
|
+
import { AppError } from '@/api/middlewares/error.js';
|
|
4
|
+
import { ERROR_CODES } from '@/types/error-constants.js';
|
|
5
|
+
import logger from '@/utils/logger.js';
|
|
6
|
+
import type {
|
|
7
|
+
RealtimeChannel,
|
|
8
|
+
CreateChannelRequest,
|
|
9
|
+
UpdateChannelRequest,
|
|
10
|
+
RealtimeMetadataSchema,
|
|
11
|
+
RlsPolicy,
|
|
12
|
+
RealtimePermissionsResponse,
|
|
13
|
+
} from '@insforge/shared-schemas';
|
|
14
|
+
|
|
15
|
+
const SYSTEM_POLICIES = ['project_admin_policy'];
|
|
16
|
+
|
|
17
|
+
export class RealtimeChannelService {
|
|
18
|
+
private static instance: RealtimeChannelService;
|
|
19
|
+
private pool: Pool | null = null;
|
|
20
|
+
|
|
21
|
+
private constructor() {}
|
|
22
|
+
|
|
23
|
+
static getInstance(): RealtimeChannelService {
|
|
24
|
+
if (!RealtimeChannelService.instance) {
|
|
25
|
+
RealtimeChannelService.instance = new RealtimeChannelService();
|
|
26
|
+
}
|
|
27
|
+
return RealtimeChannelService.instance;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private getPool(): Pool {
|
|
31
|
+
if (!this.pool) {
|
|
32
|
+
this.pool = DatabaseManager.getInstance().getPool();
|
|
33
|
+
}
|
|
34
|
+
return this.pool;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async list(): Promise<RealtimeChannel[]> {
|
|
38
|
+
const result = await this.getPool().query(`
|
|
39
|
+
SELECT
|
|
40
|
+
id,
|
|
41
|
+
pattern,
|
|
42
|
+
description,
|
|
43
|
+
webhook_urls as "webhookUrls",
|
|
44
|
+
enabled,
|
|
45
|
+
created_at as "createdAt",
|
|
46
|
+
updated_at as "updatedAt"
|
|
47
|
+
FROM realtime.channels
|
|
48
|
+
ORDER BY created_at DESC
|
|
49
|
+
`);
|
|
50
|
+
return result.rows;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async getById(id: string): Promise<RealtimeChannel | null> {
|
|
54
|
+
const result = await this.getPool().query(
|
|
55
|
+
`SELECT
|
|
56
|
+
id,
|
|
57
|
+
pattern,
|
|
58
|
+
description,
|
|
59
|
+
webhook_urls as "webhookUrls",
|
|
60
|
+
enabled,
|
|
61
|
+
created_at as "createdAt",
|
|
62
|
+
updated_at as "updatedAt"
|
|
63
|
+
FROM realtime.channels
|
|
64
|
+
WHERE id = $1`,
|
|
65
|
+
[id]
|
|
66
|
+
);
|
|
67
|
+
return result.rows[0] || null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Find a channel by name (exact match or wildcard pattern match).
|
|
72
|
+
* For wildcard patterns like "order:%", checks if channelName matches the pattern.
|
|
73
|
+
* Returns the matching channel if found and enabled, null otherwise.
|
|
74
|
+
*/
|
|
75
|
+
async getByName(channelName: string): Promise<RealtimeChannel | null> {
|
|
76
|
+
const result = await this.getPool().query(
|
|
77
|
+
`SELECT
|
|
78
|
+
id,
|
|
79
|
+
pattern,
|
|
80
|
+
description,
|
|
81
|
+
webhook_urls as "webhookUrls",
|
|
82
|
+
enabled,
|
|
83
|
+
created_at as "createdAt",
|
|
84
|
+
updated_at as "updatedAt"
|
|
85
|
+
FROM realtime.channels
|
|
86
|
+
WHERE enabled = TRUE
|
|
87
|
+
AND (pattern = $1 OR $1 LIKE pattern)
|
|
88
|
+
ORDER BY pattern = $1 DESC
|
|
89
|
+
LIMIT 1`,
|
|
90
|
+
[channelName]
|
|
91
|
+
);
|
|
92
|
+
return result.rows[0] || null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async create(input: CreateChannelRequest): Promise<RealtimeChannel> {
|
|
96
|
+
this.validateChannelPattern(input.pattern);
|
|
97
|
+
|
|
98
|
+
const result = await this.getPool().query(
|
|
99
|
+
`INSERT INTO realtime.channels (
|
|
100
|
+
pattern, description, webhook_urls, enabled
|
|
101
|
+
) VALUES ($1, $2, $3, $4)
|
|
102
|
+
RETURNING
|
|
103
|
+
id,
|
|
104
|
+
pattern,
|
|
105
|
+
description,
|
|
106
|
+
webhook_urls as "webhookUrls",
|
|
107
|
+
enabled,
|
|
108
|
+
created_at as "createdAt",
|
|
109
|
+
updated_at as "updatedAt"`,
|
|
110
|
+
[input.pattern, input.description || null, input.webhookUrls || null, input.enabled ?? true]
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
logger.info('Realtime channel created', { pattern: input.pattern });
|
|
114
|
+
return result.rows[0];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async update(id: string, input: UpdateChannelRequest): Promise<RealtimeChannel> {
|
|
118
|
+
const existing = await this.getById(id);
|
|
119
|
+
if (!existing) {
|
|
120
|
+
throw new AppError('Channel not found', 404, ERROR_CODES.NOT_FOUND);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (input.pattern) {
|
|
124
|
+
this.validateChannelPattern(input.pattern);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const result = await this.getPool().query(
|
|
128
|
+
`UPDATE realtime.channels
|
|
129
|
+
SET
|
|
130
|
+
pattern = COALESCE($2, pattern),
|
|
131
|
+
description = COALESCE($3, description),
|
|
132
|
+
webhook_urls = COALESCE($4, webhook_urls),
|
|
133
|
+
enabled = COALESCE($5, enabled)
|
|
134
|
+
WHERE id = $1
|
|
135
|
+
RETURNING
|
|
136
|
+
id,
|
|
137
|
+
pattern,
|
|
138
|
+
description,
|
|
139
|
+
webhook_urls as "webhookUrls",
|
|
140
|
+
enabled,
|
|
141
|
+
created_at as "createdAt",
|
|
142
|
+
updated_at as "updatedAt"`,
|
|
143
|
+
[id, input.pattern, input.description, input.webhookUrls, input.enabled]
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
logger.info('Realtime channel updated', { id });
|
|
147
|
+
return result.rows[0];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async delete(id: string): Promise<void> {
|
|
151
|
+
const existing = await this.getById(id);
|
|
152
|
+
if (!existing) {
|
|
153
|
+
throw new AppError('Channel not found', 404, ERROR_CODES.NOT_FOUND);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
await this.getPool().query('DELETE FROM realtime.channels WHERE id = $1', [id]);
|
|
157
|
+
logger.info('Realtime channel deleted', { id, pattern: existing.pattern });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Get realtime metadata including channels and permissions
|
|
162
|
+
*/
|
|
163
|
+
async getMetadata(): Promise<RealtimeMetadataSchema> {
|
|
164
|
+
const [channels, permissions] = await Promise.all([this.list(), this.getPermissions()]);
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
channels,
|
|
168
|
+
permissions,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ============================================================================
|
|
173
|
+
// Permissions Methods
|
|
174
|
+
// ============================================================================
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Get RLS policies for a table in the realtime schema, excluding system policies
|
|
178
|
+
*/
|
|
179
|
+
private async getPolicies(tableName: string): Promise<RlsPolicy[]> {
|
|
180
|
+
const result = await this.getPool().query(
|
|
181
|
+
`SELECT
|
|
182
|
+
policyname as "policyName",
|
|
183
|
+
tablename as "tableName",
|
|
184
|
+
cmd as "command",
|
|
185
|
+
roles,
|
|
186
|
+
qual as "using",
|
|
187
|
+
with_check as "withCheck"
|
|
188
|
+
FROM pg_policies
|
|
189
|
+
WHERE schemaname = 'realtime'
|
|
190
|
+
AND tablename = $1
|
|
191
|
+
ORDER BY policyname`,
|
|
192
|
+
[tableName]
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
// Filter out system policies
|
|
196
|
+
return result.rows.filter((policy) => !SYSTEM_POLICIES.includes(policy.policyName));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Get all realtime permissions (RLS policies for channels and messages tables)
|
|
201
|
+
*
|
|
202
|
+
* - Subscribe permission: RLS policies on realtime.channels (SELECT)
|
|
203
|
+
* - Publish permission: RLS policies on realtime.messages (INSERT)
|
|
204
|
+
*/
|
|
205
|
+
async getPermissions(): Promise<RealtimePermissionsResponse> {
|
|
206
|
+
const [channelsPolicies, messagesPolicies] = await Promise.all([
|
|
207
|
+
this.getPolicies('channels'),
|
|
208
|
+
this.getPolicies('messages'),
|
|
209
|
+
]);
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
subscribe: {
|
|
213
|
+
policies: channelsPolicies,
|
|
214
|
+
},
|
|
215
|
+
publish: {
|
|
216
|
+
policies: messagesPolicies,
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ============================================================================
|
|
222
|
+
// Validation
|
|
223
|
+
// ============================================================================
|
|
224
|
+
|
|
225
|
+
private validateChannelPattern(pattern: string): void {
|
|
226
|
+
// Allow alphanumeric, colons, hyphens, and % for wildcards
|
|
227
|
+
// Note: underscore is not allowed as it's a SQL wildcard character
|
|
228
|
+
const validPattern = /^[a-zA-Z0-9-]+(:[a-zA-Z0-9%:-]+)*$/;
|
|
229
|
+
if (!validPattern.test(pattern)) {
|
|
230
|
+
throw new AppError(
|
|
231
|
+
'Invalid channel pattern. Use alphanumeric characters, colons, hyphens, and % for wildcards.',
|
|
232
|
+
400,
|
|
233
|
+
ERROR_CODES.INVALID_INPUT
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|