Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions apps/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ pnpm test:watch # watch mode
| `PG_DATABASE` | PostgreSQL database name | `mx_core` |
| `PG_MAX_POOL_SIZE` | PostgreSQL connection pool size | `20` |
| `PG_SSL` | Enable PostgreSQL SSL | `false` |
| `REDIS_CONNECTION_STRING` | Full Redis URI; use `rediss://` for TLS (`REDIS_CONNECTION` and `REDIS_URL` are accepted aliases) | — |
| `REDIS_HOST` | Redis host | `localhost` |
| `REDIS_PORT` | Redis port | `6379` |
| `REDIS_PASSWORD` | Redis password | — |
Expand All @@ -245,6 +246,12 @@ pnpm test:watch # watch mode
| `TZ` | Timezone | `Asia/Shanghai` |
| `DISABLE_CACHE` | Disable Redis caching | `false` |

For managed Redis providers such as Upstash, pass the provider URI as
`REDIS_CONNECTION_STRING=rediss://user:password@host:port`. The `rediss://`
scheme enables TLS. If the provider exposes the same URI as `REDIS_URL`, it is
accepted directly; `REDIS_CONNECTION_STRING` takes precedence when both are
set.

Configuration can also be provided via CLI arguments or YAML files. See
`src/app.config.ts` for the full config schema.

Expand Down
54 changes: 10 additions & 44 deletions apps/core/src/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ import { load as yamlLoad } from 'js-yaml'
import nodeMachineId from 'node-machine-id'

import { isDebugMode, isDev } from './global/env.global'
import {
parseRedisConnectionString,
resolveRedisConnectionStringEnv,
} from './utils/redis-config.util'
import { parseBooleanishValue } from './utils/tool.util'

const { machineIdSync } = nodeMachineId
Expand All @@ -30,49 +34,7 @@ const {
const ENV_JWT_SECRET = JWT_SECRET || JWTSECRET
const ENCRYPT_KEY_FROM_ENV = MX_ENCRYPT_KEY || ENV_ENCRYPT_KEY
const ENCRYPT_ENABLE_FROM_ENV = MX_ENCRYPT_ENABLE || ENV_ENCRYPT_ENABLE

function parseRedisConnectionString(input: string) {
const raw = String(input).trim()
if (!raw) return null

const withProtocol =
raw.includes('://') || raw.startsWith('redis:') || raw.startsWith('rediss:')
? raw
: `redis://${raw}`

const url = new URL(withProtocol)

// `redis:` / `rediss:`
if (url.protocol !== 'redis:' && url.protocol !== 'rediss:') {
throw new Error(`Invalid redis connection string protocol: ${url.protocol}`)
}

const host = url.hostname
const port = url.port ? Number(url.port) : undefined
const username = url.username ? decodeURIComponent(url.username) : undefined
const password = url.password ? decodeURIComponent(url.password) : undefined

const dbFromPath = url.pathname?.replace(/^\//, '')
const db =
dbFromPath && /^\d+$/.test(dbFromPath) ? Number(dbFromPath) : undefined

// Build a sanitized URL (no auth) for downstream clients.
const origin =
url.port && url.port.length > 0
? `${url.protocol}//${url.hostname}:${url.port}`
: `${url.protocol}//${url.hostname}`
const sanitizedUrl = `${origin}${url.pathname || ''}${url.search || ''}`

return {
url: sanitizedUrl,
host,
port,
username,
password,
db,
tls: url.protocol === 'rediss:',
}
}
const REDIS_CONNECTION_STRING_FROM_ENV = resolveRedisConnectionStringEnv()

function applyArgvEnvFallback(argv: Record<string, any>) {
// Fallback rule:
Expand Down Expand Up @@ -120,7 +82,11 @@ const commander = program
.option('--demo', 'enable demo mode')

// redis
.option('--redis_connection_string <string>', 'redis connection string')
.option(
'--redis_connection_string <string>',
'redis connection string',
REDIS_CONNECTION_STRING_FROM_ENV,
)
.option('--redis_host <string>', 'redis host')
.option('--redis_port <number>', 'redis port')
.option('--redis_password <string>', 'redis password')
Expand Down
63 changes: 63 additions & 0 deletions apps/core/src/utils/redis-config.util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
const REDIS_CONNECTION_ENV_KEYS = [
'REDIS_CONNECTION_STRING',
'REDIS_CONNECTION',
'REDIS_URL',
] as const

type RedisConnectionEnv = Partial<
Record<(typeof REDIS_CONNECTION_ENV_KEYS)[number], string>
>

export function resolveRedisConnectionStringEnv(
env: RedisConnectionEnv = process.env,
) {
for (const key of REDIS_CONNECTION_ENV_KEYS) {
const value = env[key]?.trim()
if (value) return value
}

return undefined
}

export function parseRedisConnectionString(input: string) {
const raw = String(input).trim()
if (!raw) return null

const withProtocol =
raw.includes('://') || raw.startsWith('redis:') || raw.startsWith('rediss:')
? raw
: `redis://${raw}`

const url = new URL(withProtocol)

if (url.protocol !== 'redis:' && url.protocol !== 'rediss:') {
throw new Error(`Invalid redis connection string protocol: ${url.protocol}`)
}

const host = url.hostname
const port = url.port ? Number(url.port) : undefined
const username = url.username ? decodeURIComponent(url.username) : undefined
const password = url.password ? decodeURIComponent(url.password) : undefined

const dbFromPath = url.pathname?.replace(/^\//, '')
const db =
dbFromPath && /^\d+$/.test(dbFromPath) ? Number(dbFromPath) : undefined

// Keep credentials out of the URL passed downstream and provide them as
// explicit client options instead.
const origin =
url.port && url.port.length > 0
? `${url.protocol}//${url.hostname}:${url.port}`
: `${url.protocol}//${url.hostname}`
const sanitizedUrl = `${origin}${url.pathname || ''}${url.search || ''}`

return {
url: sanitizedUrl,
host,
port,
username,
password,
db,
tls: url.protocol === 'rediss:',
}
}
53 changes: 53 additions & 0 deletions apps/core/test/src/utils/redis-config.util.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { describe, expect, it } from 'vitest'

import {
parseRedisConnectionString,
resolveRedisConnectionStringEnv,
} from '~/utils/redis-config.util'

describe('redis config utilities', () => {
it('accepts the provider-standard REDIS_URL alias', () => {
expect(
resolveRedisConnectionStringEnv({
REDIS_URL: 'rediss://default:secret@redis.example.com:6380',
}),
).toBe('rediss://default:secret@redis.example.com:6380')
})

it('prefers the canonical connection string over aliases', () => {
expect(
resolveRedisConnectionStringEnv({
REDIS_CONNECTION_STRING: 'redis://canonical:6379',
REDIS_CONNECTION: 'redis://legacy:6379',
REDIS_URL: 'rediss://provider:6380',
}),
).toBe('redis://canonical:6379')
})

it('parses rediss URLs as TLS connections without retaining credentials in the URL', () => {
const encodedAuth = ['default', ['sample', '%40', 'value'].join('')].join(
':',
)
const decodedAuth = ['sample', '@', 'value'].join('')

expect(
parseRedisConnectionString(
`rediss://${encodedAuth}@redis.example.com:6380/2`,
),
).toEqual({
url: 'rediss://redis.example.com:6380/2',
host: 'redis.example.com',
port: 6380,
username: 'default',
password: decodedAuth,
db: 2,
tls: true,
})
})

it('rejects non-Redis URL protocols', () => {
expect(() =>
parseRedisConnectionString('https://redis.example.com'),
).toThrow('Invalid redis connection string protocol: https:')
})
})
7 changes: 6 additions & 1 deletion docker-entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,14 @@ mask_redis_password() {
#
# Keep a tiny bit of provenance for logs.
REDIS_CONNECTION_STRING_ORIG="${REDIS_CONNECTION_STRING:-}"
REDIS_CONNECTION_ORIG="${REDIS_CONNECTION:-}"
REDIS_URL_ORIG="${REDIS_URL:-}"
apply_env_alias CONFIG CONFIG_PATH
apply_env_alias COLLECTION_NAME DB_COLLECTION_NAME
apply_env_alias HTTP_REQUEST_VERBOSE DEBUG
# Redis env compatibility: keep consistent with DB's `MONGO_CONNECTION` handling.
apply_env_alias REDIS_CONNECTION_STRING REDIS_CONNECTION
apply_env_alias REDIS_CONNECTION_STRING REDIS_URL

# Derive SNOWFLAKE_WORKER_OFFSET from the Docker Swarm task slot when running
# multiple replicas. Swarm sets HOSTNAME to "<service>.<slot>.<task-id>" so
Expand Down Expand Up @@ -142,8 +145,10 @@ fi
# Redis summary
if [ -n "${REDIS_CONNECTION_STRING:-}" ]; then
redis_conn_source="env:REDIS_CONNECTION_STRING"
if [ -z "$REDIS_CONNECTION_STRING_ORIG" ] && [ -n "${REDIS_CONNECTION:-}" ]; then
if [ -z "$REDIS_CONNECTION_STRING_ORIG" ] && [ -n "$REDIS_CONNECTION_ORIG" ]; then
redis_conn_source="env:REDIS_CONNECTION (aliased to REDIS_CONNECTION_STRING)"
elif [ -z "$REDIS_CONNECTION_STRING_ORIG" ] && [ -n "$REDIS_URL_ORIG" ]; then
redis_conn_source="env:REDIS_URL (aliased to REDIS_CONNECTION_STRING)"
fi
log_kv "Redis" "$(mask_redis_password "$REDIS_CONNECTION_STRING")" "$redis_conn_source"
else
Expand Down