@graphoria/server 0.3.0 → 0.5.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/README.md +28 -20
- package/dist/src/ai/mcp/index.d.ts +9 -2
- package/dist/src/ai/mcp/index.d.ts.map +1 -1
- package/dist/src/analyzeQuery/analyzers/selectionAnalyzer.d.ts.map +1 -1
- package/dist/src/analyzeQuery/depthLimit.d.ts +1 -13
- package/dist/src/analyzeQuery/depthLimit.d.ts.map +1 -1
- package/dist/src/authentication/capabilities.d.ts +18 -0
- package/dist/src/authentication/capabilities.d.ts.map +1 -0
- package/dist/src/authentication/capabilities.test.d.ts +2 -0
- package/dist/src/authentication/capabilities.test.d.ts.map +1 -0
- package/dist/src/authentication/jwt.d.ts.map +1 -1
- package/dist/src/authentication/paseto.d.ts.map +1 -1
- package/dist/src/authentication/secrets.d.ts +9 -0
- package/dist/src/authentication/secrets.d.ts.map +1 -0
- package/dist/src/authentication/secrets.test.d.ts +2 -0
- package/dist/src/authentication/secrets.test.d.ts.map +1 -0
- package/dist/src/authentication/tokenRepository.d.ts.map +1 -1
- package/dist/src/config/types/db.d.ts +25 -0
- package/dist/src/config/types/db.d.ts.map +1 -1
- package/dist/src/configuration/getSchemas/index.d.ts +40 -2
- package/dist/src/configuration/getSchemas/index.d.ts.map +1 -1
- package/dist/src/configuration/getSchemas/mergeEntities/index.d.ts +20 -1
- package/dist/src/configuration/getSchemas/mergeEntities/index.d.ts.map +1 -1
- package/dist/src/configuration/gql/gqlAuthOperations.d.ts.map +1 -1
- package/dist/src/configuration/gql/handleGraphQLRequestFactory.d.ts.map +1 -1
- package/dist/src/configuration/gql/handleGraphQLSubscriptionFactory.d.ts.map +1 -1
- package/dist/src/configuration/index.d.ts +30 -1
- package/dist/src/configuration/index.d.ts.map +1 -1
- package/dist/src/configuration/rest/generateOpenAPI.d.ts.map +1 -1
- package/dist/src/configuration/rest/generateOpenAPI.test.d.ts +2 -0
- package/dist/src/configuration/rest/generateOpenAPI.test.d.ts.map +1 -0
- package/dist/src/configuration/rest/handleRESTRequestFactory.d.ts.map +1 -1
- package/dist/src/console/api.d.ts.map +1 -1
- package/dist/src/console/session.d.ts +12 -4
- package/dist/src/console/session.d.ts.map +1 -1
- package/dist/src/cron/index.d.ts.map +1 -1
- package/dist/src/databases/common.d.ts +20 -6
- package/dist/src/databases/common.d.ts.map +1 -1
- package/dist/src/databases/core/executor.d.ts +4 -6
- package/dist/src/databases/core/executor.d.ts.map +1 -1
- package/dist/src/databases/core/function-mapping.d.ts +2 -3
- package/dist/src/databases/core/function-mapping.d.ts.map +1 -1
- package/dist/src/databases/core/procedure-arguments.d.ts +17 -0
- package/dist/src/databases/core/procedure-arguments.d.ts.map +1 -0
- package/dist/src/databases/core/procedure-arguments.test.d.ts +2 -0
- package/dist/src/databases/core/procedure-arguments.test.d.ts.map +1 -0
- package/dist/src/databases/core/query-builder.d.ts +5 -0
- package/dist/src/databases/core/query-builder.d.ts.map +1 -1
- package/dist/src/databases/engines/mssql/connection.d.ts +1 -1
- package/dist/src/databases/engines/mssql/connection.d.ts.map +1 -1
- package/dist/src/databases/engines/mssql/getStructure.d.ts +5 -0
- package/dist/src/databases/engines/mssql/getStructure.d.ts.map +1 -1
- package/dist/src/databases/engines/mssql/query/index.d.ts +2 -2
- package/dist/src/databases/engines/mssql/query/index.d.ts.map +1 -1
- package/dist/src/databases/engines/mysql/connection.d.ts +1 -1
- package/dist/src/databases/engines/mysql/connection.d.ts.map +1 -1
- package/dist/src/databases/engines/mysql/getStructure.d.ts +5 -0
- package/dist/src/databases/engines/mysql/getStructure.d.ts.map +1 -1
- package/dist/src/databases/engines/mysql/query/index.d.ts +2 -2
- package/dist/src/databases/engines/mysql/query/index.d.ts.map +1 -1
- package/dist/src/databases/engines/postgresql/connection.d.ts +1 -1
- package/dist/src/databases/engines/postgresql/connection.d.ts.map +1 -1
- package/dist/src/databases/engines/postgresql/getStructure.d.ts +5 -0
- package/dist/src/databases/engines/postgresql/getStructure.d.ts.map +1 -1
- package/dist/src/databases/engines/postgresql/query/index.d.ts +2 -2
- package/dist/src/databases/engines/postgresql/query/index.d.ts.map +1 -1
- package/dist/src/databases/high-level-operations.d.ts +5 -0
- package/dist/src/databases/high-level-operations.d.ts.map +1 -1
- package/dist/src/databases/metadata/structure.d.ts +5 -0
- package/dist/src/databases/metadata/structure.d.ts.map +1 -1
- package/dist/src/databases/transformers/data-transformers.d.ts +30 -0
- package/dist/src/databases/transformers/data-transformers.d.ts.map +1 -1
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/logging/audit.d.ts +27 -0
- package/dist/src/logging/audit.d.ts.map +1 -0
- package/dist/src/logging/audit.test.d.ts +2 -0
- package/dist/src/logging/audit.test.d.ts.map +1 -0
- package/dist/src/logging/slowQuery.d.ts +29 -0
- package/dist/src/logging/slowQuery.d.ts.map +1 -0
- package/dist/src/logging/slowQuery.test.d.ts +2 -0
- package/dist/src/logging/slowQuery.test.d.ts.map +1 -0
- package/dist/src/observability/health.d.ts +23 -0
- package/dist/src/observability/health.d.ts.map +1 -0
- package/dist/src/observability/health.test.d.ts +2 -0
- package/dist/src/observability/health.test.d.ts.map +1 -0
- package/dist/src/observability/httpMetrics.d.ts +7 -0
- package/dist/src/observability/httpMetrics.d.ts.map +1 -0
- package/dist/src/observability/httpMetrics.test.d.ts +2 -0
- package/dist/src/observability/httpMetrics.test.d.ts.map +1 -0
- package/dist/src/observability/httpTracing.d.ts +11 -0
- package/dist/src/observability/httpTracing.d.ts.map +1 -0
- package/dist/src/observability/httpTracing.test.d.ts +2 -0
- package/dist/src/observability/httpTracing.test.d.ts.map +1 -0
- package/dist/src/observability/metrics.d.ts +81 -0
- package/dist/src/observability/metrics.d.ts.map +1 -0
- package/dist/src/observability/metrics.perf.test.d.ts +2 -0
- package/dist/src/observability/metrics.perf.test.d.ts.map +1 -0
- package/dist/src/observability/metrics.test.d.ts +2 -0
- package/dist/src/observability/metrics.test.d.ts.map +1 -0
- package/dist/src/observability/metricsRoute.d.ts +19 -0
- package/dist/src/observability/metricsRoute.d.ts.map +1 -0
- package/dist/src/observability/metricsRoute.test.d.ts +2 -0
- package/dist/src/observability/metricsRoute.test.d.ts.map +1 -0
- package/dist/src/observability/tracing.d.ts +92 -0
- package/dist/src/observability/tracing.d.ts.map +1 -0
- package/dist/src/observability/tracing.perf.test.d.ts +2 -0
- package/dist/src/observability/tracing.perf.test.d.ts.map +1 -0
- package/dist/src/observability/tracing.test.d.ts +2 -0
- package/dist/src/observability/tracing.test.d.ts.map +1 -0
- package/dist/src/queues/kafka.d.ts +6 -0
- package/dist/src/queues/kafka.d.ts.map +1 -1
- package/dist/src/queues/kafka.test.d.ts +2 -0
- package/dist/src/queues/kafka.test.d.ts.map +1 -0
- package/dist/src/queues/rabbitmq.d.ts.map +1 -1
- package/dist/src/remoteREST/proxy.d.ts.map +1 -1
- package/dist/src/remoteSchemas/proxy.d.ts.map +1 -1
- package/dist/src/singletons/cache/redisClient.d.ts +2 -2
- package/dist/src/singletons/cache/redisClient.d.ts.map +1 -1
- package/dist/src/singletons/databases.d.ts +1 -0
- package/dist/src/singletons/databases.d.ts.map +1 -1
- package/dist/src/singletons/env.d.ts +37 -4
- package/dist/src/singletons/env.d.ts.map +1 -1
- package/dist/src/singletons/queues.d.ts +1 -0
- package/dist/src/singletons/queues.d.ts.map +1 -1
- package/dist/src/subscriptions/strategies/database.d.ts.map +1 -1
- package/dist/src/subscriptions/utils/polling.d.ts +1 -0
- package/dist/src/subscriptions/utils/polling.d.ts.map +1 -1
- package/dist/src/subscriptions/utils/polling.test.d.ts +2 -0
- package/dist/src/subscriptions/utils/polling.test.d.ts.map +1 -0
- package/dist/src/types/env.d.ts +113 -12
- package/dist/src/types/env.d.ts.map +1 -1
- package/dist/src/types/zod/configuration.d.ts +25 -0
- package/dist/src/types/zod/configuration.d.ts.map +1 -1
- package/dist/src/types/zod/db.d.ts +85 -0
- package/dist/src/types/zod/db.d.ts.map +1 -1
- package/dist/src/utils/rateLimit.d.ts.map +1 -1
- package/dist/src/utils/redis.d.ts +17 -0
- package/dist/src/utils/redis.d.ts.map +1 -0
- package/dist/src/utils/redis.test.d.ts +2 -0
- package/dist/src/utils/redis.test.d.ts.map +1 -0
- package/dist/src/utils/responses.d.ts +6 -0
- package/dist/src/utils/responses.d.ts.map +1 -1
- package/dist/src/utils/sessionVariables.d.ts +2 -0
- package/dist/src/utils/sessionVariables.d.ts.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +8 -5
- package/playgrounds/console/index.html +9 -9
- package/playgrounds/graphiql/index.html +549 -686
- package/playgrounds/scalar/index.html +1 -1
- package/src/ai/mcp/index.ts +19 -14
- package/src/analyzeQuery/analyzers/selectionAnalyzer.ts +2 -1
- package/src/analyzeQuery/depthLimit.ts +15 -5
- package/src/authentication/capabilities.ts +56 -0
- package/src/authentication/index.ts +3 -3
- package/src/authentication/jwt.ts +30 -25
- package/src/authentication/paseto.ts +36 -26
- package/src/authentication/secrets.ts +30 -0
- package/src/authentication/tokenRepository.ts +2 -3
- package/src/config/types/db.ts +16 -2
- package/src/configuration/getSchemas/mergeEntities/index.ts +0 -26
- package/src/configuration/gql/gqlAuthOperations.ts +26 -0
- package/src/configuration/gql/handleGraphQLRequestFactory.ts +241 -148
- package/src/configuration/gql/handleGraphQLSubscriptionFactory.ts +9 -0
- package/src/configuration/rest/generateOpenAPI.ts +61 -11
- package/src/configuration/rest/handleRESTRequestFactory.ts +26 -0
- package/src/console/api.ts +64 -24
- package/src/console/session.ts +30 -26
- package/src/cron/index.ts +59 -36
- package/src/databases/common.ts +109 -10
- package/src/databases/core/executor.ts +97 -20
- package/src/databases/core/function-mapping.ts +1 -2
- package/src/databases/core/procedure-arguments.ts +25 -0
- package/src/databases/engines/mssql/connection.ts +0 -3
- package/src/databases/engines/mssql/query/index.ts +4 -1
- package/src/databases/engines/mysql/connection.ts +10 -7
- package/src/databases/engines/mysql/query/index.ts +5 -2
- package/src/databases/engines/postgresql/connection.ts +10 -7
- package/src/databases/engines/postgresql/query/index.ts +5 -2
- package/src/databases/high-level-operations.ts +44 -2
- package/src/index.ts +200 -76
- package/src/logging/audit.ts +86 -0
- package/src/logging/slowQuery.ts +59 -0
- package/src/observability/health.ts +67 -0
- package/src/observability/httpMetrics.ts +37 -0
- package/src/observability/httpTracing.ts +48 -0
- package/src/observability/metrics.ts +243 -0
- package/src/observability/metricsRoute.ts +33 -0
- package/src/observability/tracing.ts +421 -0
- package/src/queues/kafka.ts +45 -2
- package/src/queues/rabbitmq.ts +37 -1
- package/src/remoteREST/proxy.ts +17 -0
- package/src/remoteSchemas/proxy.ts +17 -0
- package/src/singletons/cache/redisClient.ts +4 -5
- package/src/singletons/databases.ts +5 -0
- package/src/singletons/queues.ts +9 -3
- package/src/subscriptions/strategies/database.ts +2 -0
- package/src/subscriptions/utils/polling.ts +80 -21
- package/src/types/env.ts +53 -8
- package/src/utils/rateLimit.ts +6 -1
- package/src/utils/redis.ts +71 -0
- package/src/utils/responses.ts +12 -0
- package/src/utils/sessionVariables.ts +2 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<html lang="en">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
|
-
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0OCIgaGVpZ2h0PSI0NiIgZmlsbD0ibm9uZSIgdmlld0JveD0iMCAwIDQ4IDQ2Ij48cGF0aCBmaWxsPSIjODYzYmZmIiBkPSJNMjUuOTQ2IDQ0LjkzOGMtLjY2NC44NDUtMi4wMjEuMzc1LTIuMDIxLS42OThWMzMuOTM3YTIuMjYgMi4yNiAwIDAgMC0yLjI2Mi0yLjI2MkgxMC4yODdjLS45MiAwLTEuNDU2LTEuMDQtLjkyLTEuNzg4bDcuNDgtMTAuNDcxYzEuMDctMS40OTcgMC0zLjU3OC0xLjg0Mi0zLjU3OEgxLjIzN2MtLjkyIDAtMS40NTYtMS4wNC0uOTItMS43ODhMMTAuMDEzLjQ3NGMuMjE0LS4yOTcuNTU2LS40NzQuOTItLjQ3NGgyOC44OTRjLjkyIDAgMS40NTYgMS4wNC45MiAxLjc4OGwtNy40OCAxMC40NzFjLTEuMDcgMS40OTggMCAzLjU3OSAxLjg0MiAzLjU3OWgxMS4zNzdjLjk0MyAwIDEuNDczIDEuMDg4Ljg5IDEuODNMMjUuOTQ3IDQ0Ljk0eiIgc3R5bGU9ImZpbGw6Izg2M2JmZjtmaWxsOmNvbG9yKGRpc3BsYXktcDMgLjUyNTIgLjIzIDEpO2ZpbGwtb3BhY2l0eToxIi8+PG1hc2sgaWQ9ImEiIHdpZHRoPSI0OCIgaGVpZ2h0PSI0NiIgeD0iMCIgeT0iMCIgbWFza1VuaXRzPSJ1c2VyU3BhY2VPblVzZSIgc3R5bGU9Im1hc2stdHlwZTphbHBoYSI+PHBhdGggZmlsbD0iIzAwMCIgZD0iTTI1Ljg0MiA0NC45MzhjLS42NjQuODQ0LTIuMDIxLjM3NS0yLjAyMS0uNjk4VjMzLjkzN2EyLjI2IDIuMjYgMCAwIDAtMi4yNjItMi4yNjJIMTAuMTgzYy0uOTIgMC0xLjQ1Ni0xLjA0LS45Mi0xLjc4OGw3LjQ4LTEwLjQ3MWMxLjA3LTEuNDk4IDAtMy41NzktMS44NDItMy41NzlIMS4xMzNjLS45MiAwLTEuNDU2LTEuMDQtLjkyLTEuNzg3TDkuOTEuNDczYy4yMTQtLjI5Ny41NTYtLjQ3NC45Mi0uNDc0aDI4Ljg5NGMuOTIgMCAxLjQ1NiAxLjA0LjkyIDEuNzg4bC03LjQ4IDEwLjQ3MWMtMS4wNyAxLjQ5OCAwIDMuNTc4IDEuODQyIDMuNTc4aDExLjM3N2MuOTQzIDAgMS40NzMgMS4wODguODkgMS44MzJMMjUuODQzIDQ0Ljk0eiIgc3R5bGU9ImZpbGw6IzAwMDtmaWxsLW9wYWNpdHk6MSIvPjwvbWFzaz48ZyBtYXNrPSJ1cmwoI2EpIj48ZyBmaWx0ZXI9InVybCgjYikiPjxlbGxpcHNlIGN4PSI1LjUwOCIgY3k9IjE0LjcwNCIgZmlsbD0iI2VkZTZmZiIgcng9IjUuNTA4IiByeT0iMTQuNzA0IiBzdHlsZT0iZmlsbDojZWRlNmZmO2ZpbGw6Y29sb3IoZGlzcGxheS1wMyAuOTI3NSAuOTAzMyAxKTtmaWxsLW9wYWNpdHk6MSIgdHJhbnNmb3JtPSJtYXRyaXgoLjAwMzI0IDEgMSAtLjAwMzI0IC00LjQ3IDMxLjUxNikiLz48L2c+PGcgZmlsdGVyPSJ1cmwoI2MpIj48ZWxsaXBzZSBjeD0iMTAuMzk5IiBjeT0iMjkuODUxIiBmaWxsPSIjZWRlNmZmIiByeD0iMTAuMzk5IiByeT0iMjkuODUxIiBzdHlsZT0iZmlsbDojZWRlNmZmO2ZpbGw6Y29sb3IoZGlzcGxheS1wMyAuOTI3NSAuOTAzMyAxKTtmaWxsLW9wYWNpdHk6MSIgdHJhbnNmb3JtPSJtYXRyaXgoLjAwMzI0IDEgMSAtLjAwMzI0IC0zOS4zMjggNy44ODMpIi8+PC9nPjxnIGZpbHRlcj0idXJsKCNkKSI+PGVsbGlwc2UgY3g9IjUuNTA4IiBjeT0iMzAuNDg3IiBmaWxsPSIjN2UxNGZmIiByeD0iNS41MDgiIHJ5PSIzMC40ODciIHN0eWxlPSJmaWxsOiM3ZTE0ZmY7ZmlsbDpjb2xvcihkaXNwbGF5LXAzIC40OTIyIC4wNzY3IDEpO2ZpbGwtb3BhY2l0eToxIiB0cmFuc2Zvcm09InJvdGF0ZSg4OS44MTQgLTI1LjkxMyAtMTQuNjM5KXNjYWxlKDEgLTEpIi8+PC9nPjxnIGZpbHRlcj0idXJsKCNlKSI+PGVsbGlwc2UgY3g9IjUuNTA4IiBjeT0iMzAuNTk5IiBmaWxsPSIjN2UxNGZmIiByeD0iNS41MDgiIHJ5PSIzMC41OTkiIHN0eWxlPSJmaWxsOiM3ZTE0ZmY7ZmlsbDpjb2xvcihkaXNwbGF5LXAzIC40OTIyIC4wNzY3IDEpO2ZpbGwtb3BhY2l0eToxIiB0cmFuc2Zvcm09InJvdGF0ZSg4OS44MTQgLTMyLjY0NCAtMy4zMzQpc2NhbGUoMSAtMSkiLz48L2c+PGcgZmlsdGVyPSJ1cmwoI2YpIj48ZWxsaXBzZSBjeD0iNS41MDgiIGN5PSIzMC41OTkiIGZpbGw9IiM3ZTE0ZmYiIHJ4PSI1LjUwOCIgcnk9IjMwLjU5OSIgc3R5bGU9ImZpbGw6IzdlMTRmZjtmaWxsOmNvbG9yKGRpc3BsYXktcDMgLjQ5MjIgLjA3NjcgMSk7ZmlsbC1vcGFjaXR5OjEiIHRyYW5zZm9ybT0ibWF0cml4KC4wMDMyNCAxIDEgLS4wMDMyNCAtMzQuMzQgMzAuNDcpIi8+PC9nPjxnIGZpbHRlcj0idXJsKCNnKSI+PGVsbGlwc2UgY3g9IjE0LjA3MiIgY3k9IjIyLjA3OCIgZmlsbD0iI2VkZTZmZiIgcng9IjE0LjA3MiIgcnk9IjIyLjA3OCIgc3R5bGU9ImZpbGw6I2VkZTZmZjtmaWxsOmNvbG9yKGRpc3BsYXktcDMgLjkyNzUgLjkwMzMgMSk7ZmlsbC1vcGFjaXR5OjEiIHRyYW5zZm9ybT0icm90YXRlKDkzLjM1IDI0LjUwNiA0OC40OTMpc2NhbGUoLTEgMSkiLz48L2c+PGcgZmlsdGVyPSJ1cmwoI2gpIj48ZWxsaXBzZSBjeD0iMy40NyIgY3k9IjIxLjUwMSIgZmlsbD0iIzdlMTRmZiIgcng9IjMuNDciIHJ5PSIyMS41MDEiIHN0eWxlPSJmaWxsOiM3ZTE0ZmY7ZmlsbDpjb2xvcihkaXNwbGF5LXAzIC40OTIyIC4wNzY3IDEpO2ZpbGwtb3BhY2l0eToxIiB0cmFuc2Zvcm09InJvdGF0ZSg4OS4wMDkgMjguNzA4IDQ3LjU5KXNjYWxlKC0xIDEpIi8+PC9nPjxnIGZpbHRlcj0idXJsKCNpKSI+PGVsbGlwc2UgY3g9IjMuNDciIGN5PSIyMS41MDEiIGZpbGw9IiM3ZTE0ZmYiIHJ4PSIzLjQ3IiByeT0iMjEuNTAxIiBzdHlsZT0iZmlsbDojN2UxNGZmO2ZpbGw6Y29sb3IoZGlzcGxheS1wMyAuNDkyMiAuMDc2NyAxKTtmaWxsLW9wYWNpdHk6MSIgdHJhbnNmb3JtPSJyb3RhdGUoODkuMDA5IDI4LjcwOCA0Ny41OSlzY2FsZSgtMSAxKSIvPjwvZz48ZyBmaWx0ZXI9InVybCgjaikiPjxlbGxpcHNlIGN4PSIuMzg3IiBjeT0iOC45NzIiIGZpbGw9IiM3ZTE0ZmYiIHJ4PSI0LjQwNyIgcnk9IjI5LjEwOCIgc3R5bGU9ImZpbGw6IzdlMTRmZjtmaWxsOmNvbG9yKGRpc3BsYXktcDMgLjQ5MjIgLjA3NjcgMSk7ZmlsbC1vcGFjaXR5OjEiIHRyYW5zZm9ybT0icm90YXRlKDM5LjUxIC4zODcgOC45NzIpIi8+PC9nPjxnIGZpbHRlcj0idXJsKCNrKSI+PGVsbGlwc2UgY3g9IjQ3LjUyMyIgY3k9Ii02LjA5MiIgZmlsbD0iIzdlMTRmZiIgcng9IjQuNDA3IiByeT0iMjkuMTA4IiBzdHlsZT0iZmlsbDojN2UxNGZmO2ZpbGw6Y29sb3IoZGlzcGxheS1wMyAuNDkyMiAuMDc2NyAxKTtmaWxsLW9wYWNpdHk6MSIgdHJhbnNmb3JtPSJyb3RhdGUoMzcuODkyIDQ3LjUyMyAtNi4wOTIpIi8+PC9nPjxnIGZpbHRlcj0idXJsKCNsKSI+PGVsbGlwc2UgY3g9IjQxLjQxMiIgY3k9IjYuMzMzIiBmaWxsPSIjNDdiZmZmIiByeD0iNS45NzEiIHJ5PSI5LjY2NSIgc3R5bGU9ImZpbGw6IzQ3YmZmZjtmaWxsOmNvbG9yKGRpc3BsYXktcDMgLjI3OTkgLjc0OCAxKTtmaWxsLW9wYWNpdHk6MSIgdHJhbnNmb3JtPSJyb3RhdGUoMzcuODkyIDQxLjQxMiA2LjMzMykiLz48L2c+PGcgZmlsdGVyPSJ1cmwoI20pIj48ZWxsaXBzZSBjeD0iLTEuODc5IiBjeT0iMzguMzMyIiBmaWxsPSIjN2UxNGZmIiByeD0iNC40MDciIHJ5PSIyOS4xMDgiIHN0eWxlPSJmaWxsOiM3ZTE0ZmY7ZmlsbDpjb2xvcihkaXNwbGF5LXAzIC40OTIyIC4wNzY3IDEpO2ZpbGwtb3BhY2l0eToxIiB0cmFuc2Zvcm09InJvdGF0ZSgzNy44OTIgLTEuODggMzguMzMyKSIvPjwvZz48ZyBmaWx0ZXI9InVybCgjbikiPjxlbGxpcHNlIGN4PSItMS44NzkiIGN5PSIzOC4zMzIiIGZpbGw9IiM3ZTE0ZmYiIHJ4PSI0LjQwNyIgcnk9IjI5LjEwOCIgc3R5bGU9ImZpbGw6IzdlMTRmZjtmaWxsOmNvbG9yKGRpc3BsYXktcDMgLjQ5MjIgLjA3NjcgMSk7ZmlsbC1vcGFjaXR5OjEiIHRyYW5zZm9ybT0icm90YXRlKDM3Ljg5MiAtMS44OCAzOC4zMzIpIi8+PC9nPjxnIGZpbHRlcj0idXJsKCNvKSI+PGVsbGlwc2UgY3g9IjM1LjY1MSIgY3k9IjI5LjkwNyIgZmlsbD0iIzdlMTRmZiIgcng9IjQuNDA3IiByeT0iMjkuMTA4IiBzdHlsZT0iZmlsbDojN2UxNGZmO2ZpbGw6Y29sb3IoZGlzcGxheS1wMyAuNDkyMiAuMDc2NyAxKTtmaWxsLW9wYWNpdHk6MSIgdHJhbnNmb3JtPSJyb3RhdGUoMzcuODkyIDM1LjY1MSAyOS45MDcpIi8+PC9nPjxnIGZpbHRlcj0idXJsKCNwKSI+PGVsbGlwc2UgY3g9IjM4LjQxOCIgY3k9IjMyLjQiIGZpbGw9IiM0N2JmZmYiIHJ4PSI1Ljk3MSIgcnk9IjE1LjI5NyIgc3R5bGU9ImZpbGw6IzQ3YmZmZjtmaWxsOmNvbG9yKGRpc3BsYXktcDMgLjI3OTkgLjc0OCAxKTtmaWxsLW9wYWNpdHk6MSIgdHJhbnNmb3JtPSJyb3RhdGUoMzcuODkyIDM4LjQxOCAzMi40KSIvPjwvZz48L2c+PGRlZnM+PGZpbHRlciBpZD0iYiIgd2lkdGg9IjYwLjA0NSIgaGVpZ2h0PSI0MS42NTQiIHg9Ii0xOS43NyIgeT0iMTYuMTQ5IiBjb2xvci1pbnRlcnBvbGF0aW9uLWZpbHRlcnM9InNSR0IiIGZpbHRlclVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PGZlRmxvb2QgZmxvb2Qtb3BhY2l0eT0iMCIgcmVzdWx0PSJCYWNrZ3JvdW5kSW1hZ2VGaXgiLz48ZmVCbGVuZCBpbj0iU291cmNlR3JhcGhpYyIgaW4yPSJCYWNrZ3JvdW5kSW1hZ2VGaXgiIHJlc3VsdD0ic2hhcGUiLz48ZmVHYXVzc2lhbkJsdXIgcmVzdWx0PSJlZmZlY3QxX2ZvcmVncm91bmRCbHVyXzIwMDJfMTcxNTgiIHN0ZERldmlhdGlvbj0iNy42NTkiLz48L2ZpbHRlcj48ZmlsdGVyIGlkPSJjIiB3aWR0aD0iOTAuMzQiIGhlaWdodD0iNTEuNDM3IiB4PSItNTQuNjEzIiB5PSItNy41MzMiIGNvbG9yLWludGVycG9sYXRpb24tZmlsdGVycz0ic1JHQiIgZmlsdGVyVW5pdHM9InVzZXJTcGFjZU9uVXNlIj48ZmVGbG9vZCBmbG9vZC1vcGFjaXR5PSIwIiByZXN1bHQ9IkJhY2tncm91bmRJbWFnZUZpeCIvPjxmZUJsZW5kIGluPSJTb3VyY2VHcmFwaGljIiBpbjI9IkJhY2tncm91bmRJbWFnZUZpeCIgcmVzdWx0PSJzaGFwZSIvPjxmZUdhdXNzaWFuQmx1ciByZXN1bHQ9ImVmZmVjdDFfZm9yZWdyb3VuZEJsdXJfMjAwMl8xNzE1OCIgc3RkRGV2aWF0aW9uPSI3LjY1OSIvPjwvZmlsdGVyPjxmaWx0ZXIgaWQ9ImQiIHdpZHRoPSI3OS4zNTUiIGhlaWdodD0iMjkuNCIgeD0iLTQ5LjY0IiB5PSIyLjAzIiBjb2xvci1pbnRlcnBvbGF0aW9uLWZpbHRlcnM9InNSR0IiIGZpbHRlclVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PGZlRmxvb2QgZmxvb2Qtb3BhY2l0eT0iMCIgcmVzdWx0PSJCYWNrZ3JvdW5kSW1hZ2VGaXgiLz48ZmVCbGVuZCBpbj0iU291cmNlR3JhcGhpYyIgaW4yPSJCYWNrZ3JvdW5kSW1hZ2VGaXgiIHJlc3VsdD0ic2hhcGUiLz48ZmVHYXVzc2lhbkJsdXIgcmVzdWx0PSJlZmZlY3QxX2ZvcmVncm91bmRCbHVyXzIwMDJfMTcxNTgiIHN0ZERldmlhdGlvbj0iNC41OTYiLz48L2ZpbHRlcj48ZmlsdGVyIGlkPSJlIiB3aWR0aD0iNzkuNTc5IiBoZWlnaHQ9IjI5LjQiIHg9Ii00NS4wNDUiIHk9IjIwLjAyOSIgY29sb3ItaW50ZXJwb2xhdGlvbi1maWx0ZXJzPSJzUkdCIiBmaWx0ZXJVbml0cz0idXNlclNwYWNlT25Vc2UiPjxmZUZsb29kIGZsb29kLW9wYWNpdHk9IjAiIHJlc3VsdD0iQmFja2dyb3VuZEltYWdlRml4Ii8+PGZlQmxlbmQgaW49IlNvdXJjZUdyYXBoaWMiIGluMj0iQmFja2dyb3VuZEltYWdlRml4IiByZXN1bHQ9InNoYXBlIi8+PGZlR2F1c3NpYW5CbHVyIHJlc3VsdD0iZWZmZWN0MV9mb3JlZ3JvdW5kQmx1cl8yMDAyXzE3MTU4IiBzdGREZXZpYXRpb249IjQuNTk2Ii8+PC9maWx0ZXI+PGZpbHRlciBpZD0iZiIgd2lkdGg9Ijc5LjU3OSIgaGVpZ2h0PSIyOS40IiB4PSItNDMuNTEzIiB5PSIyMS4xNzgiIGNvbG9yLWludGVycG9sYXRpb24tZmlsdGVycz0ic1JHQiIgZmlsdGVyVW5pdHM9InVzZXJTcGFjZU9uVXNlIj48ZmVGbG9vZCBmbG9vZC1vcGFjaXR5PSIwIiByZXN1bHQ9IkJhY2tncm91bmRJbWFnZUZpeCIvPjxmZUJsZW5kIGluPSJTb3VyY2VHcmFwaGljIiBpbjI9IkJhY2tncm91bmRJbWFnZUZpeCIgcmVzdWx0PSJzaGFwZSIvPjxmZUdhdXNzaWFuQmx1ciByZXN1bHQ9ImVmZmVjdDFfZm9yZWdyb3VuZEJsdXJfMjAwMl8xNzE1OCIgc3RkRGV2aWF0aW9uPSI0LjU5NiIvPjwvZmlsdGVyPjxmaWx0ZXIgaWQ9ImciIHdpZHRoPSI3NC43NDkiIGhlaWdodD0iNTguODUyIiB4PSIxNS43NTYiIHk9Ii0xNy45MDEiIGNvbG9yLWludGVycG9sYXRpb24tZmlsdGVycz0ic1JHQiIgZmlsdGVyVW5pdHM9InVzZXJTcGFjZU9uVXNlIj48ZmVGbG9vZCBmbG9vZC1vcGFjaXR5PSIwIiByZXN1bHQ9IkJhY2tncm91bmRJbWFnZUZpeCIvPjxmZUJsZW5kIGluPSJTb3VyY2VHcmFwaGljIiBpbjI9IkJhY2tncm91bmRJbWFnZUZpeCIgcmVzdWx0PSJzaGFwZSIvPjxmZUdhdXNzaWFuQmx1ciByZXN1bHQ9ImVmZmVjdDFfZm9yZWdyb3VuZEJsdXJfMjAwMl8xNzE1OCIgc3RkRGV2aWF0aW9uPSI3LjY1OSIvPjwvZmlsdGVyPjxmaWx0ZXIgaWQ9ImgiIHdpZHRoPSI2MS4zNzciIGhlaWdodD0iMjUuMzYyIiB4PSIyMy41NDgiIHk9IjIuMjg0IiBjb2xvci1pbnRlcnBvbGF0aW9uLWZpbHRlcnM9InNSR0IiIGZpbHRlclVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PGZlRmxvb2QgZmxvb2Qtb3BhY2l0eT0iMCIgcmVzdWx0PSJCYWNrZ3JvdW5kSW1hZ2VGaXgiLz48ZmVCbGVuZCBpbj0iU291cmNlR3JhcGhpYyIgaW4yPSJCYWNrZ3JvdW5kSW1hZ2VGaXgiIHJlc3VsdD0ic2hhcGUiLz48ZmVHYXVzc2lhbkJsdXIgcmVzdWx0PSJlZmZlY3QxX2ZvcmVncm91bmRCbHVyXzIwMDJfMTcxNTgiIHN0ZERldmlhdGlvbj0iNC41OTYiLz48L2ZpbHRlcj48ZmlsdGVyIGlkPSJpIiB3aWR0aD0iNjEuMzc3IiBoZWlnaHQ9IjI1LjM2MiIgeD0iMjMuNTQ4IiB5PSIyLjI4NCIgY29sb3ItaW50ZXJwb2xhdGlvbi1maWx0ZXJzPSJzUkdCIiBmaWx0ZXJVbml0cz0idXNlclNwYWNlT25Vc2UiPjxmZUZsb29kIGZsb29kLW9wYWNpdHk9IjAiIHJlc3VsdD0iQmFja2dyb3VuZEltYWdlRml4Ii8+PGZlQmxlbmQgaW49IlNvdXJjZUdyYXBoaWMiIGluMj0iQmFja2dyb3VuZEltYWdlRml4IiByZXN1bHQ9InNoYXBlIi8+PGZlR2F1c3NpYW5CbHVyIHJlc3VsdD0iZWZmZWN0MV9mb3JlZ3JvdW5kQmx1cl8yMDAyXzE3MTU4IiBzdGREZXZpYXRpb249IjQuNTk2Ii8+PC9maWx0ZXI+PGZpbHRlciBpZD0iaiIgd2lkdGg9IjU2LjA0NSIgaGVpZ2h0PSI2My42NDkiIHg9Ii0yNy42MzYiIHk9Ii0yMi44NTMiIGNvbG9yLWludGVycG9sYXRpb24tZmlsdGVycz0ic1JHQiIgZmlsdGVyVW5pdHM9InVzZXJTcGFjZU9uVXNlIj48ZmVGbG9vZCBmbG9vZC1vcGFjaXR5PSIwIiByZXN1bHQ9IkJhY2tncm91bmRJbWFnZUZpeCIvPjxmZUJsZW5kIGluPSJTb3VyY2VHcmFwaGljIiBpbjI9IkJhY2tncm91bmRJbWFnZUZpeCIgcmVzdWx0PSJzaGFwZSIvPjxmZUdhdXNzaWFuQmx1ciByZXN1bHQ9ImVmZmVjdDFfZm9yZWdyb3VuZEJsdXJfMjAwMl8xNzE1OCIgc3RkRGV2aWF0aW9uPSI0LjU5NiIvPjwvZmlsdGVyPjxmaWx0ZXIgaWQ9ImsiIHdpZHRoPSI1NC44MTQiIGhlaWdodD0iNjQuNjQ2IiB4PSIyMC4xMTYiIHk9Ii0zOC40MTUiIGNvbG9yLWludGVycG9sYXRpb24tZmlsdGVycz0ic1JHQiIgZmlsdGVyVW5pdHM9InVzZXJTcGFjZU9uVXNlIj48ZmVGbG9vZCBmbG9vZC1vcGFjaXR5PSIwIiByZXN1bHQ9IkJhY2tncm91bmRJbWFnZUZpeCIvPjxmZUJsZW5kIGluPSJTb3VyY2VHcmFwaGljIiBpbjI9IkJhY2tncm91bmRJbWFnZUZpeCIgcmVzdWx0PSJzaGFwZSIvPjxmZUdhdXNzaWFuQmx1ciByZXN1bHQ9ImVmZmVjdDFfZm9yZWdyb3VuZEJsdXJfMjAwMl8xNzE1OCIgc3RkRGV2aWF0aW9uPSI0LjU5NiIvPjwvZmlsdGVyPjxmaWx0ZXIgaWQ9ImwiIHdpZHRoPSIzMy41NDEiIGhlaWdodD0iMzUuMzEzIiB4PSIyNC42NDEiIHk9Ii0xMS4zMjMiIGNvbG9yLWludGVycG9sYXRpb24tZmlsdGVycz0ic1JHQiIgZmlsdGVyVW5pdHM9InVzZXJTcGFjZU9uVXNlIj48ZmVGbG9vZCBmbG9vZC1vcGFjaXR5PSIwIiByZXN1bHQ9IkJhY2tncm91bmRJbWFnZUZpeCIvPjxmZUJsZW5kIGluPSJTb3VyY2VHcmFwaGljIiBpbjI9IkJhY2tncm91bmRJbWFnZUZpeCIgcmVzdWx0PSJzaGFwZSIvPjxmZUdhdXNzaWFuQmx1ciByZXN1bHQ9ImVmZmVjdDFfZm9yZWdyb3VuZEJsdXJfMjAwMl8xNzE1OCIgc3RkRGV2aWF0aW9uPSI0LjU5NiIvPjwvZmlsdGVyPjxmaWx0ZXIgaWQ9Im0iIHdpZHRoPSI1NC44MTQiIGhlaWdodD0iNjQuNjQ2IiB4PSItMjkuMjg2IiB5PSI2LjAwOSIgY29sb3ItaW50ZXJwb2xhdGlvbi1maWx0ZXJzPSJzUkdCIiBmaWx0ZXJVbml0cz0idXNlclNwYWNlT25Vc2UiPjxmZUZsb29kIGZsb29kLW9wYWNpdHk9IjAiIHJlc3VsdD0iQmFja2dyb3VuZEltYWdlRml4Ii8+PGZlQmxlbmQgaW49IlNvdXJjZUdyYXBoaWMiIGluMj0iQmFja2dyb3VuZEltYWdlRml4IiByZXN1bHQ9InNoYXBlIi8+PGZlR2F1c3NpYW5CbHVyIHJlc3VsdD0iZWZmZWN0MV9mb3JlZ3JvdW5kQmx1cl8yMDAyXzE3MTU4IiBzdGREZXZpYXRpb249IjQuNTk2Ii8+PC9maWx0ZXI+PGZpbHRlciBpZD0ibiIgd2lkdGg9IjU0LjgxNCIgaGVpZ2h0PSI2NC42NDYiIHg9Ii0yOS4yODYiIHk9IjYuMDA5IiBjb2xvci1pbnRlcnBvbGF0aW9uLWZpbHRlcnM9InNSR0IiIGZpbHRlclVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PGZlRmxvb2QgZmxvb2Qtb3BhY2l0eT0iMCIgcmVzdWx0PSJCYWNrZ3JvdW5kSW1hZ2VGaXgiLz48ZmVCbGVuZCBpbj0iU291cmNlR3JhcGhpYyIgaW4yPSJCYWNrZ3JvdW5kSW1hZ2VGaXgiIHJlc3VsdD0ic2hhcGUiLz48ZmVHYXVzc2lhbkJsdXIgcmVzdWx0PSJlZmZlY3QxX2ZvcmVncm91bmRCbHVyXzIwMDJfMTcxNTgiIHN0ZERldmlhdGlvbj0iNC41OTYiLz48L2ZpbHRlcj48ZmlsdGVyIGlkPSJvIiB3aWR0aD0iNTQuODE0IiBoZWlnaHQ9IjY0LjY0NiIgeD0iOC4yNDQiIHk9Ii0yLjQxNiIgY29sb3ItaW50ZXJwb2xhdGlvbi1maWx0ZXJzPSJzUkdCIiBmaWx0ZXJVbml0cz0idXNlclNwYWNlT25Vc2UiPjxmZUZsb29kIGZsb29kLW9wYWNpdHk9IjAiIHJlc3VsdD0iQmFja2dyb3VuZEltYWdlRml4Ii8+PGZlQmxlbmQgaW49IlNvdXJjZUdyYXBoaWMiIGluMj0iQmFja2dyb3VuZEltYWdlRml4IiByZXN1bHQ9InNoYXBlIi8+PGZlR2F1c3NpYW5CbHVyIHJlc3VsdD0iZWZmZWN0MV9mb3JlZ3JvdW5kQmx1cl8yMDAyXzE3MTU4IiBzdGREZXZpYXRpb249IjQuNTk2Ii8+PC9maWx0ZXI+PGZpbHRlciBpZD0icCIgd2lkdGg9IjM5LjQwOSIgaGVpZ2h0PSI0My42MjMiIHg9IjE4LjcxMyIgeT0iMTAuNTg4IiBjb2xvci1pbnRlcnBvbGF0aW9uLWZpbHRlcnM9InNSR0IiIGZpbHRlclVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PGZlRmxvb2QgZmxvb2Qtb3BhY2l0eT0iMCIgcmVzdWx0PSJCYWNrZ3JvdW5kSW1hZ2VGaXgiLz48ZmVCbGVuZCBpbj0iU291cmNlR3JhcGhpYyIgaW4yPSJCYWNrZ3JvdW5kSW1hZ2VGaXgiIHJlc3VsdD0ic2hhcGUiLz48ZmVHYXVzc2lhbkJsdXIgcmVzdWx0PSJlZmZlY3QxX2ZvcmVncm91bmRCbHVyXzIwMDJfMTcxNTgiIHN0ZERldmlhdGlvbj0iNC41OTYiLz48L2ZpbHRlcj48L2RlZnM+PC9zdmc+" />
|
|
5
|
+
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI2NCIgaGVpZ2h0PSI2NCIgdmlld0JveD0iMCAwIDY0IDY0IiBmaWxsPSJub25lIiByb2xlPSJpbWciIGFyaWEtbGFiZWw9IkdyYXBob3JpYSI+CiAgPHN0eWxlPi5ibGFkZXtzdHJva2U6IzBGMTcyMH1AbWVkaWEgKHByZWZlcnMtY29sb3Itc2NoZW1lOmRhcmspey5ibGFkZXtzdHJva2U6I0ZCRkNGQ319PC9zdHlsZT4KICA8ZyBzdHJva2U9IiMxMkEzQkYiIHN0cm9rZS13aWR0aD0iNC42IiBzdHJva2UtbGluZWNhcD0icm91bmQiPgogICAgPHBhdGggZD0iTTM5LjMxIDguMDkgQTI1IDI1IDAgMCAxIDU2LjM2IDM3LjYzIj48L3BhdGg+CiAgICA8cGF0aCBkPSJNNDkuMDUgNTAuMjkgQTI1IDI1IDAgMCAxIDE0Ljk1IDUwLjI5Ij48L3BhdGg+CiAgICA8cGF0aCBkPSJNNy42NCAzNy42MyBBMjUgMjUgMCAwIDEgMjQuNjkgOC4wOSI+PC9wYXRoPgogIDwvZz4KICA8ZyBjbGFzcz0iYmxhZGUiIHN0cm9rZS13aWR0aD0iNC44IiBzdHJva2UtbGluZWNhcD0icm91bmQiPgogICAgPHBhdGggZD0iTTM0LjA2IDIzLjc1IEEyNCAyNCAwIDAgMSAzMS4zNyAxNC4wMSI+PC9wYXRoPgogICAgPHBhdGggZD0iTTM4LjExIDM3LjkxIEEyNCAyNCAwIDAgMSA0Ny44OSA0MC40NSI+PC9wYXRoPgogICAgPHBhdGggZD0iTTIzLjgzIDM0LjM0IEEyNCAyNCAwIDAgMSAxNi43NCA0MS41NCI+PC9wYXRoPgogIDwvZz4KICA8Y2lyY2xlIGN4PSIzMiIgY3k9IjciIHI9IjUuMiIgZmlsbD0iIzEyQTNCRiI+PC9jaXJjbGU+CiAgPGNpcmNsZSBjeD0iNTMuNjUiIGN5PSI0NC41IiByPSI1LjIiIGZpbGw9IiMxMkEzQkYiPjwvY2lyY2xlPgogIDxjaXJjbGUgY3g9IjEwLjM1IiBjeT0iNDQuNSIgcj0iNS4yIiBmaWxsPSIjMTJBM0JGIj48L2NpcmNsZT4KICA8Y2lyY2xlIGN4PSIzMiIgY3k9IjMyIiByPSI3IiBmaWxsPSIjMTRCMzg5Ij48L2NpcmNsZT4KPC9zdmc+" />
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
7
|
<title>Graphoria - Scalar Playground</title>
|
|
8
8
|
<style>html,body{height:100%;margin:0;padding:0}#scalar-client{width:100%!important}.missing-endpoint{padding:32px;font-family:monospace}.token-banner{position:fixed;color:#fff;z-index:10001;animation:3s forwards fadeOut;background:#22c55e;border-radius:6px;padding:8px 16px;font-family:system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Noto Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif;font-size:14px;top:10px;right:10px}@keyframes fadeOut{0%,70%{opacity:1}to{opacity:0;visibility:hidden}}
|
package/src/ai/mcp/index.ts
CHANGED
|
@@ -1,23 +1,17 @@
|
|
|
1
|
-
import { timingSafeEqual } from "crypto";
|
|
2
|
-
|
|
3
1
|
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/server";
|
|
4
2
|
|
|
3
|
+
import type { CapabilityAuthorizer } from "../../authentication/capabilities";
|
|
5
4
|
import type { AnalyzedConfiguration } from "../../configuration";
|
|
6
5
|
import type { CreateMcpServerOptions } from "./create-server";
|
|
7
6
|
|
|
8
7
|
import { createMcpServer } from "./create-server";
|
|
9
8
|
import { logger } from "../../logging";
|
|
10
|
-
|
|
11
|
-
const safeCompare = (a: string, b: string): boolean => {
|
|
12
|
-
const bufA = Buffer.from(a);
|
|
13
|
-
const bufB = Buffer.from(b);
|
|
14
|
-
if (bufA.length !== bufB.length) return false;
|
|
15
|
-
return timingSafeEqual(bufA, bufB);
|
|
16
|
-
};
|
|
9
|
+
import { audit } from "../../logging/audit";
|
|
17
10
|
|
|
18
11
|
export type CreateMCPRoutesOptions = CreateMcpServerOptions & {
|
|
19
12
|
requireAdminSecret?: boolean;
|
|
20
|
-
|
|
13
|
+
/** Decides whether the presented header grants `mcp`; without one the gate never opens. */
|
|
14
|
+
authorize?: CapabilityAuthorizer;
|
|
21
15
|
adminSecretHeader?: string;
|
|
22
16
|
};
|
|
23
17
|
|
|
@@ -34,16 +28,27 @@ const jsonRpcError = (status: number, code: number, message: string) =>
|
|
|
34
28
|
},
|
|
35
29
|
);
|
|
36
30
|
|
|
31
|
+
type RequestServer = { requestIP(req: Request): { address: string } | null };
|
|
32
|
+
|
|
37
33
|
const handleMcpPost =
|
|
38
34
|
(analyzedConfiguration: AnalyzedConfiguration, options: CreateMCPRoutesOptions) =>
|
|
39
|
-
async (req: Request) => {
|
|
35
|
+
async (req: Request, bunServer?: RequestServer) => {
|
|
40
36
|
if (options.requireAdminSecret) {
|
|
41
37
|
const headerName = options.adminSecretHeader ?? "x-admin-secret";
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
if (!provided || !expected || !safeCompare(provided, expected)) {
|
|
38
|
+
const grant = options.authorize?.(req.headers.get(headerName), "mcp");
|
|
39
|
+
if (!grant) {
|
|
45
40
|
return jsonRpcError(401, -32001, "Unauthorized: admin secret required");
|
|
46
41
|
}
|
|
42
|
+
const ip = bunServer?.requestIP(req)?.address;
|
|
43
|
+
audit().emit({
|
|
44
|
+
action: "admin_secret.used",
|
|
45
|
+
actor: {
|
|
46
|
+
type: "admin_secret",
|
|
47
|
+
scope: grant.superset ? "all" : "mcp",
|
|
48
|
+
...(ip ? { ip } : {}),
|
|
49
|
+
},
|
|
50
|
+
target: { kind: "mcp" },
|
|
51
|
+
});
|
|
47
52
|
}
|
|
48
53
|
|
|
49
54
|
const server = createMcpServer(analyzedConfiguration, options);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { GraphQLObjectType, GraphQLSchema } from "graphql";
|
|
1
|
+
import { GraphQLNonNull, GraphQLObjectType, GraphQLSchema } from "graphql";
|
|
2
2
|
|
|
3
3
|
import type { FieldNode, SelectionNode } from "graphql";
|
|
4
4
|
import type { MergedEntities } from "../../configuration/getSchemas/mergeEntities";
|
|
@@ -55,6 +55,7 @@ export const analyzeSelections = (
|
|
|
55
55
|
const directives = analyzeDirectives(field.directives || [], generatedVariables);
|
|
56
56
|
|
|
57
57
|
const isRequired =
|
|
58
|
+
fieldType instanceof GraphQLNonNull &&
|
|
58
59
|
directives.filter(
|
|
59
60
|
(dir) => dir.name === "include" || dir.name === "skip" || dir.name === "when",
|
|
60
61
|
).length === 0;
|
|
@@ -77,6 +77,16 @@ function computeDepth(
|
|
|
77
77
|
* const errors = validate(schema, parse(query), [depthLimitRule(10)]);
|
|
78
78
|
* ```
|
|
79
79
|
*/
|
|
80
|
+
/**
|
|
81
|
+
* Identity, not message matching: a caller classifying a rejection needs to
|
|
82
|
+
* tell this rule's error from an ordinary validation error, and the message is
|
|
83
|
+
* not a contract. Weak so a held error never pins the entry.
|
|
84
|
+
*/
|
|
85
|
+
const depthErrors = new WeakSet<GraphQLError>();
|
|
86
|
+
|
|
87
|
+
export const isDepthLimitError = (error: unknown): boolean =>
|
|
88
|
+
error instanceof GraphQLError && depthErrors.has(error);
|
|
89
|
+
|
|
80
90
|
export const depthLimitRule =
|
|
81
91
|
(maxDepth: number) =>
|
|
82
92
|
(context: ValidationContext): ASTVisitor => {
|
|
@@ -92,12 +102,12 @@ export const depthLimitRule =
|
|
|
92
102
|
if (depth > maxDepth) {
|
|
93
103
|
const operationName = def.name?.value ?? "anonymous";
|
|
94
104
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
{ nodes: [def] },
|
|
99
|
-
),
|
|
105
|
+
const error = new GraphQLError(
|
|
106
|
+
`Query depth of ${depth} exceeds the maximum allowed depth of ${maxDepth} (operation: "${operationName}"). Raise MAX_QUERY_DEPTH to allow deeper queries.`,
|
|
107
|
+
{ nodes: [def] },
|
|
100
108
|
);
|
|
109
|
+
depthErrors.add(error);
|
|
110
|
+
context.reportError(error);
|
|
101
111
|
}
|
|
102
112
|
}
|
|
103
113
|
},
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { Env } from "../types/env";
|
|
2
|
+
|
|
3
|
+
import { logger } from "../logging";
|
|
4
|
+
import { matchesAnySecret } from "./secrets";
|
|
5
|
+
|
|
6
|
+
export type Capability = "console:read" | "console:write" | "ai" | "mcp" | "metrics";
|
|
7
|
+
|
|
8
|
+
/** `superset` is true when the admin secret, not a scoped credential, matched. */
|
|
9
|
+
export type CapabilityGrant = { superset: boolean };
|
|
10
|
+
|
|
11
|
+
export type CapabilityAuthorizer = (
|
|
12
|
+
candidate: string | null,
|
|
13
|
+
capability: Capability,
|
|
14
|
+
) => CapabilityGrant | null;
|
|
15
|
+
|
|
16
|
+
type WarnLog = { warn: (obj: object, msg: string) => void };
|
|
17
|
+
|
|
18
|
+
// Each list is checked on its own so the rotation debug line inside
|
|
19
|
+
// matchesAnySecret keeps reporting the index within one credential.
|
|
20
|
+
const scopedSecretsFor = (env: Env, capability: Capability): string[][] => {
|
|
21
|
+
switch (capability) {
|
|
22
|
+
case "console:read":
|
|
23
|
+
return [env.console.writeSecrets, env.console.readSecrets];
|
|
24
|
+
case "console:write":
|
|
25
|
+
return [env.console.writeSecrets];
|
|
26
|
+
case "ai":
|
|
27
|
+
return [env.ai.secrets];
|
|
28
|
+
case "mcp":
|
|
29
|
+
return [env.ai.mcp.secrets];
|
|
30
|
+
case "metrics":
|
|
31
|
+
return [env.metrics.secrets];
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Decides whether a presented secret grants a capability. The admin secret
|
|
37
|
+
* grants all of them and is reported as the superset so the caller can tell
|
|
38
|
+
* the two apart; it is also logged, since a scoped credential would have done.
|
|
39
|
+
*/
|
|
40
|
+
export const createCapabilityAuthorizer = (
|
|
41
|
+
env: Env,
|
|
42
|
+
log: WarnLog = logger("auth"),
|
|
43
|
+
): CapabilityAuthorizer => {
|
|
44
|
+
return (candidate, capability) => {
|
|
45
|
+
if (matchesAnySecret(candidate, env.admin.secrets)) {
|
|
46
|
+
log.warn({ capability }, "admin secret used where a scoped credential would do");
|
|
47
|
+
return { superset: true };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
for (const secrets of scopedSecretsFor(env, capability)) {
|
|
51
|
+
if (matchesAnySecret(candidate, secrets)) return { superset: false };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return null;
|
|
55
|
+
};
|
|
56
|
+
};
|
|
@@ -7,7 +7,7 @@ import { createPASETOService } from "./paseto";
|
|
|
7
7
|
export const createTokenService = (env: Env, strategy: TokenStrategy = "jwt"): TokenService => {
|
|
8
8
|
switch (strategy) {
|
|
9
9
|
case "jwt": {
|
|
10
|
-
if (
|
|
10
|
+
if (env.jwt.secrets.length === 0) {
|
|
11
11
|
throw new Error(
|
|
12
12
|
"JWT_SECRET environment variable is required when using jwt token strategy",
|
|
13
13
|
);
|
|
@@ -15,7 +15,7 @@ export const createTokenService = (env: Env, strategy: TokenStrategy = "jwt"): T
|
|
|
15
15
|
return createJWTService(env);
|
|
16
16
|
}
|
|
17
17
|
case "paseto_local": {
|
|
18
|
-
if (
|
|
18
|
+
if (env.paseto.localKeys.length === 0) {
|
|
19
19
|
throw new Error(
|
|
20
20
|
"PASETO_LOCAL_KEY environment variable is required when using paseto_local token strategy.",
|
|
21
21
|
);
|
|
@@ -23,7 +23,7 @@ export const createTokenService = (env: Env, strategy: TokenStrategy = "jwt"): T
|
|
|
23
23
|
return createPASETOService(env, "local");
|
|
24
24
|
}
|
|
25
25
|
case "paseto_public": {
|
|
26
|
-
if (!env.paseto.secretKey ||
|
|
26
|
+
if (!env.paseto.secretKey || env.paseto.publicKeys.length === 0) {
|
|
27
27
|
throw new Error(
|
|
28
28
|
"PASETO_SECRET_KEY and PASETO_PUBLIC_KEY environment variables are required when using paseto_public token strategy.",
|
|
29
29
|
);
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
import { SignJWT, jwtVerify } from "jose";
|
|
1
|
+
import { SignJWT, errors, jwtVerify } from "jose";
|
|
4
2
|
|
|
5
3
|
import type { JWTPayload } from "jose";
|
|
6
4
|
import type { Env } from "../types/env";
|
|
@@ -9,6 +7,7 @@ import type { TokenRepository } from "./tokenRepository";
|
|
|
9
7
|
import type { TokenGenerationParameters, TokenOptions, TokenResponse, TokenService } from "./types";
|
|
10
8
|
|
|
11
9
|
import { parseDurationToSeconds } from "./duration";
|
|
10
|
+
import { matchesAnySecret } from "./secrets";
|
|
12
11
|
import { createTokenRepository } from "./tokenRepository";
|
|
13
12
|
import { logger } from "../logging";
|
|
14
13
|
|
|
@@ -16,18 +15,6 @@ import { logger } from "../logging";
|
|
|
16
15
|
const ACCESS_TOKEN_AUDIENCE = "access";
|
|
17
16
|
const REFRESH_TOKEN_AUDIENCE = "refresh";
|
|
18
17
|
|
|
19
|
-
// Timing-safe string comparison to prevent timing attacks
|
|
20
|
-
const safeCompare = (a: string, b: string): boolean => {
|
|
21
|
-
try {
|
|
22
|
-
const bufA = Buffer.from(a);
|
|
23
|
-
const bufB = Buffer.from(b);
|
|
24
|
-
if (bufA.length !== bufB.length) return false;
|
|
25
|
-
return timingSafeEqual(bufA, bufB);
|
|
26
|
-
} catch {
|
|
27
|
-
return false;
|
|
28
|
-
}
|
|
29
|
-
};
|
|
30
|
-
|
|
31
18
|
export type { TokenGenerationParameters, TokenOptions, TokenResponse };
|
|
32
19
|
|
|
33
20
|
export type JWTTokenPayload = JWTPayload &
|
|
@@ -43,7 +30,9 @@ export const createJWTService = (
|
|
|
43
30
|
tokenRepositoryOverride?: TokenRepository,
|
|
44
31
|
): TokenService => {
|
|
45
32
|
const tokenRepository = tokenRepositoryOverride ?? createTokenRepository(env.cache.redisUrl);
|
|
46
|
-
|
|
33
|
+
// First entry signs; every entry verifies, so a rotation can overlap.
|
|
34
|
+
const jwtSecrets = env.jwt.secrets.map((secret) => new TextEncoder().encode(secret));
|
|
35
|
+
const signingSecret = jwtSecrets[0];
|
|
47
36
|
|
|
48
37
|
const createToken = async (
|
|
49
38
|
payload: TokenGenerationParameters,
|
|
@@ -67,7 +56,8 @@ export const createJWTService = (
|
|
|
67
56
|
jwt.setNotBefore(options.notBefore);
|
|
68
57
|
}
|
|
69
58
|
|
|
70
|
-
|
|
59
|
+
if (!signingSecret) throw new Error("No JWT secret configured");
|
|
60
|
+
return await jwt.sign(signingSecret);
|
|
71
61
|
} catch (error) {
|
|
72
62
|
logger("auth").child({ strategy: "jwt" }).error({ err: error }, "token creation failed");
|
|
73
63
|
throw new Error("Token creation failed");
|
|
@@ -78,23 +68,38 @@ export const createJWTService = (
|
|
|
78
68
|
token: string,
|
|
79
69
|
options: TokenOptions = {},
|
|
80
70
|
): Promise<T> => {
|
|
81
|
-
const
|
|
71
|
+
const verifyOptions = {
|
|
82
72
|
issuer: options.issuer,
|
|
83
73
|
audience: options.audience,
|
|
84
74
|
algorithms: [ALGORITHM],
|
|
85
75
|
typ: TYP,
|
|
86
|
-
}
|
|
76
|
+
};
|
|
87
77
|
|
|
88
|
-
|
|
78
|
+
let lastError: unknown = new Error("No JWT secret configured");
|
|
79
|
+
for (const [index, secret] of jwtSecrets.entries()) {
|
|
80
|
+
try {
|
|
81
|
+
const { payload } = await jwtVerify(token, secret, verifyOptions);
|
|
82
|
+
if (index > 0) {
|
|
83
|
+
logger("auth")
|
|
84
|
+
.child({ strategy: "jwt" })
|
|
85
|
+
.debug({ index }, "token verified with a previous secret");
|
|
86
|
+
}
|
|
87
|
+
return payload as T;
|
|
88
|
+
} catch (error) {
|
|
89
|
+
// Only a signature mismatch means "try the next secret"; a claim
|
|
90
|
+
// failure came from the secret that did sign this token.
|
|
91
|
+
if (!(error instanceof errors.JWSSignatureVerificationFailed)) throw error;
|
|
92
|
+
lastError = error;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
throw lastError;
|
|
89
96
|
};
|
|
90
97
|
|
|
91
98
|
const verifyTokenAndGetRole = async (
|
|
92
99
|
authHeader: string | null,
|
|
93
100
|
adminSecretHeader: string | null,
|
|
94
101
|
): Promise<string> => {
|
|
95
|
-
|
|
96
|
-
if (env.admin.secret && adminSecretHeader && safeCompare(adminSecretHeader, env.admin.secret))
|
|
97
|
-
return env.superadmin.role;
|
|
102
|
+
if (matchesAnySecret(adminSecretHeader, env.admin.secrets)) return env.superadmin.role;
|
|
98
103
|
|
|
99
104
|
if (!authHeader) return env.anonymousRole;
|
|
100
105
|
|
|
@@ -115,8 +120,8 @@ export const createJWTService = (
|
|
|
115
120
|
authHeader: string | null,
|
|
116
121
|
adminSecretHeader: string | null,
|
|
117
122
|
): Promise<SessionContext> => {
|
|
118
|
-
if (
|
|
119
|
-
return { sub: "superadmin", role: env.superadmin.role };
|
|
123
|
+
if (matchesAnySecret(adminSecretHeader, env.admin.secrets))
|
|
124
|
+
return { sub: "superadmin", role: env.superadmin.role, authMethod: "admin_secret" };
|
|
120
125
|
|
|
121
126
|
if (!authHeader || !authHeader.startsWith("Bearer "))
|
|
122
127
|
return { sub: "anonymous", role: env.anonymousRole };
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { PasetoDecryptionFailed, PasetoSignatureInvalid } from "paseto-ts/lib/errors";
|
|
3
2
|
import { decrypt, encrypt, sign, verify } from "paseto-ts/v4";
|
|
4
3
|
|
|
5
4
|
import type { Env } from "../types/env";
|
|
@@ -8,6 +7,7 @@ import type { TokenRepository } from "./tokenRepository";
|
|
|
8
7
|
import type { TokenGenerationParameters, TokenOptions, TokenResponse, TokenService } from "./types";
|
|
9
8
|
|
|
10
9
|
import { parseDurationToSeconds, toPasetoDuration } from "./duration";
|
|
10
|
+
import { matchesAnySecret } from "./secrets";
|
|
11
11
|
import { createTokenRepository } from "./tokenRepository";
|
|
12
12
|
import { logger } from "../logging";
|
|
13
13
|
|
|
@@ -15,18 +15,6 @@ import { logger } from "../logging";
|
|
|
15
15
|
const ACCESS_TOKEN_AUDIENCE = "access";
|
|
16
16
|
const REFRESH_TOKEN_AUDIENCE = "refresh";
|
|
17
17
|
|
|
18
|
-
// Timing-safe string comparison to prevent timing attacks
|
|
19
|
-
const safeCompare = (a: string, b: string): boolean => {
|
|
20
|
-
try {
|
|
21
|
-
const bufA = Buffer.from(a);
|
|
22
|
-
const bufB = Buffer.from(b);
|
|
23
|
-
if (bufA.length !== bufB.length) return false;
|
|
24
|
-
return timingSafeEqual(bufA, bufB);
|
|
25
|
-
} catch {
|
|
26
|
-
return false;
|
|
27
|
-
}
|
|
28
|
-
};
|
|
29
|
-
|
|
30
18
|
type PasetoPayload = {
|
|
31
19
|
sub: string;
|
|
32
20
|
role: string;
|
|
@@ -56,7 +44,12 @@ export const createPASETOService = (
|
|
|
56
44
|
): TokenService => {
|
|
57
45
|
const tokenRepository = tokenRepositoryOverride ?? createTokenRepository(env.cache.redisUrl);
|
|
58
46
|
|
|
59
|
-
//
|
|
47
|
+
// In local mode the first key encrypts and every key decrypts. In public
|
|
48
|
+
// mode the secret key signs and every public key verifies. Either way a
|
|
49
|
+
// rotation can overlap.
|
|
50
|
+
const verifyingKeys = mode === "local" ? env.paseto.localKeys : env.paseto.publicKeys;
|
|
51
|
+
const signingKey = mode === "local" ? env.paseto.localKeys[0] : env.paseto.secretKey;
|
|
52
|
+
|
|
60
53
|
const createPasetoToken = async (
|
|
61
54
|
payload: Record<string, unknown>,
|
|
62
55
|
expiry: string,
|
|
@@ -66,19 +59,37 @@ export const createPASETOService = (
|
|
|
66
59
|
exp: toPasetoDuration(expiry),
|
|
67
60
|
};
|
|
68
61
|
|
|
62
|
+
if (!signingKey) throw new Error("No PASETO key configured");
|
|
69
63
|
if (mode === "local") {
|
|
70
|
-
return encrypt(
|
|
64
|
+
return encrypt(signingKey, pasetoPayload);
|
|
71
65
|
}
|
|
72
|
-
return sign(
|
|
66
|
+
return sign(signingKey, pasetoPayload);
|
|
73
67
|
};
|
|
74
68
|
|
|
75
69
|
const verifyPasetoToken = async (token: string): Promise<PasetoPayload> => {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
70
|
+
let lastError: unknown = new Error("No PASETO key configured");
|
|
71
|
+
for (const [index, key] of verifyingKeys.entries()) {
|
|
72
|
+
try {
|
|
73
|
+
const { payload } =
|
|
74
|
+
mode === "local"
|
|
75
|
+
? await decrypt<PasetoPayload>(key, token)
|
|
76
|
+
: await verify<PasetoPayload>(key, token);
|
|
77
|
+
if (index > 0) {
|
|
78
|
+
logger("auth")
|
|
79
|
+
.child({ strategy: "paseto" })
|
|
80
|
+
.debug({ index }, "token verified with a previous key");
|
|
81
|
+
}
|
|
82
|
+
return payload;
|
|
83
|
+
} catch (error) {
|
|
84
|
+
// Only a key mismatch means "try the next key"; a claim failure came
|
|
85
|
+
// from the key that did produce this token.
|
|
86
|
+
const keyMismatch =
|
|
87
|
+
error instanceof PasetoDecryptionFailed || error instanceof PasetoSignatureInvalid;
|
|
88
|
+
if (!keyMismatch) throw error;
|
|
89
|
+
lastError = error;
|
|
90
|
+
}
|
|
79
91
|
}
|
|
80
|
-
|
|
81
|
-
return payload;
|
|
92
|
+
throw lastError;
|
|
82
93
|
};
|
|
83
94
|
|
|
84
95
|
const createToken = async (
|
|
@@ -132,8 +143,7 @@ export const createPASETOService = (
|
|
|
132
143
|
authHeader: string | null,
|
|
133
144
|
adminSecretHeader: string | null,
|
|
134
145
|
): Promise<string> => {
|
|
135
|
-
if (
|
|
136
|
-
return env.superadmin.role;
|
|
146
|
+
if (matchesAnySecret(adminSecretHeader, env.admin.secrets)) return env.superadmin.role;
|
|
137
147
|
|
|
138
148
|
if (!authHeader) return env.anonymousRole;
|
|
139
149
|
|
|
@@ -153,8 +163,8 @@ export const createPASETOService = (
|
|
|
153
163
|
authHeader: string | null,
|
|
154
164
|
adminSecretHeader: string | null,
|
|
155
165
|
): Promise<SessionContext> => {
|
|
156
|
-
if (
|
|
157
|
-
return { sub: "superadmin", role: env.superadmin.role };
|
|
166
|
+
if (matchesAnySecret(adminSecretHeader, env.admin.secrets))
|
|
167
|
+
return { sub: "superadmin", role: env.superadmin.role, authMethod: "admin_secret" };
|
|
158
168
|
|
|
159
169
|
if (!authHeader || !authHeader.startsWith("Bearer "))
|
|
160
170
|
return { sub: "anonymous", role: env.anonymousRole };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { timingSafeEqual } from "crypto";
|
|
2
|
+
|
|
3
|
+
import { logger } from "../logging";
|
|
4
|
+
|
|
5
|
+
// Timing-safe string comparison to prevent timing attacks
|
|
6
|
+
export const safeCompare = (a: string, b: string): boolean => {
|
|
7
|
+
try {
|
|
8
|
+
const bufA = Buffer.from(a);
|
|
9
|
+
const bufB = Buffer.from(b);
|
|
10
|
+
if (bufA.length !== bufB.length) return false;
|
|
11
|
+
return timingSafeEqual(bufA, bufB);
|
|
12
|
+
} catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* True when `candidate` equals any entry of a rotating secret set. The first
|
|
19
|
+
* entry is the current secret; a match further along means a caller still
|
|
20
|
+
* holds a previous one, which is logged so the operator knows when it is safe
|
|
21
|
+
* to drop it.
|
|
22
|
+
*/
|
|
23
|
+
export const matchesAnySecret = (candidate: string | null, secrets: string[]): boolean => {
|
|
24
|
+
if (!candidate) return false;
|
|
25
|
+
|
|
26
|
+
const index = secrets.findIndex((secret) => safeCompare(candidate, secret));
|
|
27
|
+
if (index > 0) logger("auth").debug({ index }, "matched a previous secret");
|
|
28
|
+
|
|
29
|
+
return index >= 0;
|
|
30
|
+
};
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { RedisClient as ValkeyClient } from "bun";
|
|
2
|
-
|
|
3
1
|
import { parseDurationToMs } from "./duration";
|
|
4
2
|
import { logger } from "../logging";
|
|
3
|
+
import { createRedisClient } from "../utils/redis";
|
|
5
4
|
|
|
6
5
|
export type TokenRepository = {
|
|
7
6
|
saveJti(jti: string, expiresIn: string): Promise<void>;
|
|
@@ -80,4 +79,4 @@ export const createTokenRepository = (redisUrl: string): TokenRepository =>
|
|
|
80
79
|
// Bun's RedisClient exposes the same hset/hmget/expire surface we need but
|
|
81
80
|
// its declared types are wider than TokenRepositoryClient. The cast is the
|
|
82
81
|
// structural-typing bridge and is intentional.
|
|
83
|
-
createTokenRepositoryWithClient(
|
|
82
|
+
createTokenRepositoryWithClient(createRedisClient(redisUrl) as unknown as TokenRepositoryClient);
|
package/src/config/types/db.ts
CHANGED
|
@@ -51,14 +51,28 @@ export const RelationshipConditionZod = z
|
|
|
51
51
|
operator: z.enum(RELATIONSHIP_CONDITION_OPERATORS).optional().default("eq"),
|
|
52
52
|
/** Literal compared against the column (omit for `is_null` / `is_not_null`) */
|
|
53
53
|
value: z.union([z.string(), z.number(), z.boolean()]).optional(),
|
|
54
|
+
/**
|
|
55
|
+
* Column on an ancestor table, compared against this condition's column
|
|
56
|
+
* instead of a literal. The ancestor must sit above the relationship in the
|
|
57
|
+
* query path; the nearest match wins, and a query that does not traverse it
|
|
58
|
+
* is rejected rather than joined without the predicate.
|
|
59
|
+
*/
|
|
60
|
+
ancestor: z
|
|
61
|
+
.strictObject({ schema: z.string(), name: z.string(), column: z.string() })
|
|
62
|
+
.optional(),
|
|
54
63
|
})
|
|
55
64
|
.refine((cond) => (cond.source === undefined) !== (cond.target === undefined), {
|
|
56
65
|
message: 'Relationship condition must set exactly one of "source" or "target"',
|
|
57
66
|
})
|
|
58
67
|
.refine(
|
|
59
68
|
(cond) =>
|
|
60
|
-
cond.operator === "is_null" || cond.operator === "is_not_null"
|
|
61
|
-
|
|
69
|
+
cond.operator === "is_null" || cond.operator === "is_not_null"
|
|
70
|
+
? cond.ancestor === undefined
|
|
71
|
+
: (cond.value === undefined) !== (cond.ancestor === undefined),
|
|
72
|
+
{
|
|
73
|
+
message:
|
|
74
|
+
'Relationship condition requires exactly one of "value" or "ancestor", and neither operand may be an "ancestor" when operator is is_null/is_not_null',
|
|
75
|
+
},
|
|
62
76
|
);
|
|
63
77
|
|
|
64
78
|
export type RelationshipCondition = z.input<typeof RelationshipConditionZod>;
|
|
@@ -7,23 +7,6 @@ import type { TypedOperation } from "../../../types/zod/operation";
|
|
|
7
7
|
|
|
8
8
|
import { EntitySource, createResolverEntry } from "../../../types/resolver";
|
|
9
9
|
import { columnFieldName } from "../../../databases/transformers/graphqlName";
|
|
10
|
-
import { SqlTypeCategory, categorizeSqlType } from "../../../databases/sqlTypeUtils";
|
|
11
|
-
|
|
12
|
-
const dataTypeToOpenApiType = (dataType: string): string => {
|
|
13
|
-
const category = categorizeSqlType(dataType);
|
|
14
|
-
|
|
15
|
-
switch (category) {
|
|
16
|
-
case SqlTypeCategory.INTEGER:
|
|
17
|
-
case SqlTypeCategory.FLOAT:
|
|
18
|
-
return "number";
|
|
19
|
-
case SqlTypeCategory.BOOLEAN:
|
|
20
|
-
return "boolean";
|
|
21
|
-
case SqlTypeCategory.DATE_TIME:
|
|
22
|
-
case SqlTypeCategory.STRING:
|
|
23
|
-
default:
|
|
24
|
-
return "string";
|
|
25
|
-
}
|
|
26
|
-
};
|
|
27
10
|
|
|
28
11
|
export const mergeEntities = (
|
|
29
12
|
entityOfRole: EntitiesOfRole,
|
|
@@ -236,15 +219,6 @@ export const mergeEntities = (
|
|
|
236
219
|
*/
|
|
237
220
|
columnSqlName: (table: string, fieldName: string): string =>
|
|
238
221
|
queriesMap[table]?.columns.find((c) => columnFieldName(c) === fieldName)?.name ?? fieldName,
|
|
239
|
-
getColumnTypeForOpenApi: (table: string, column: string) => {
|
|
240
|
-
const tableObj = queriesMap[table];
|
|
241
|
-
|
|
242
|
-
const columnObj = tableObj?.columns.find((c) => c.name === column);
|
|
243
|
-
|
|
244
|
-
if (!columnObj) return "string";
|
|
245
|
-
|
|
246
|
-
return dataTypeToOpenApiType(columnObj.dataType);
|
|
247
|
-
},
|
|
248
222
|
};
|
|
249
223
|
};
|
|
250
224
|
|
|
@@ -5,6 +5,7 @@ import type { SessionContext } from "../../utils/sessionVariables";
|
|
|
5
5
|
|
|
6
6
|
import { resolveVariableRef } from "../../analyzeQuery/resolveVariables";
|
|
7
7
|
import { checkUserCredentials } from "../../databases";
|
|
8
|
+
import { actorFromSession, audit } from "../../logging/audit";
|
|
8
9
|
import { getTokenService } from "../../singletons/authentication";
|
|
9
10
|
|
|
10
11
|
// Handle auth mutations: auth_login / auth_refresh / auth_logout
|
|
@@ -35,9 +36,23 @@ export const handleAuthMutation = async (
|
|
|
35
36
|
passwordValue.toString(),
|
|
36
37
|
);
|
|
37
38
|
|
|
39
|
+
const actor = { type: "credentials", sub: usernameValue.toString() } as const;
|
|
38
40
|
if (!result.valid) {
|
|
41
|
+
audit().emit({
|
|
42
|
+
action: "auth.login",
|
|
43
|
+
outcome: "failure",
|
|
44
|
+
actor,
|
|
45
|
+
target: { kind: "auth", via: "graphql" },
|
|
46
|
+
reason: "Invalid username or password",
|
|
47
|
+
});
|
|
39
48
|
throw new Error("Invalid username or password");
|
|
40
49
|
}
|
|
50
|
+
audit().emit({
|
|
51
|
+
action: "auth.login",
|
|
52
|
+
outcome: "success",
|
|
53
|
+
actor,
|
|
54
|
+
target: { kind: "auth", via: "graphql", role: result.role },
|
|
55
|
+
});
|
|
41
56
|
|
|
42
57
|
const data = await getTokenService().createTokenPair({
|
|
43
58
|
sub: usernameValue.toString(),
|
|
@@ -104,9 +119,11 @@ export const handleAuthMutation = async (
|
|
|
104
119
|
|
|
105
120
|
if (field.name === "auth_logout") {
|
|
106
121
|
const tokenService = getTokenService();
|
|
122
|
+
const revoked: string[] = [];
|
|
107
123
|
|
|
108
124
|
if (session?.jti) {
|
|
109
125
|
await tokenService.revoke(session.jti);
|
|
126
|
+
revoked.push(session.jti);
|
|
110
127
|
}
|
|
111
128
|
|
|
112
129
|
const refreshCookie = req?.cookies?.get("refresh_token");
|
|
@@ -116,6 +133,7 @@ export const handleAuthMutation = async (
|
|
|
116
133
|
audience: "refresh",
|
|
117
134
|
});
|
|
118
135
|
await tokenService.revoke(refreshPayload.jti);
|
|
136
|
+
revoked.push(refreshPayload.jti);
|
|
119
137
|
} catch {
|
|
120
138
|
// tampered or expired cookie — nothing to revoke
|
|
121
139
|
}
|
|
@@ -125,6 +143,14 @@ export const handleAuthMutation = async (
|
|
|
125
143
|
req.cookies.delete("refresh_token");
|
|
126
144
|
}
|
|
127
145
|
|
|
146
|
+
if (revoked.length > 0) {
|
|
147
|
+
audit().emit({
|
|
148
|
+
action: "auth.logout",
|
|
149
|
+
actor: actorFromSession(session),
|
|
150
|
+
target: { kind: "auth", via: "graphql", revoked },
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
128
154
|
return {
|
|
129
155
|
data: {
|
|
130
156
|
[field.alias || field.name]: true,
|