Files
WeHub Mirror 6bf8bebf51
CI / Test and Build (push) Failing after 1s
CI / Migrate Dev DB (push) Has been skipped
CI / Migrate DB (push) Has been skipped
CodeQL / Analyze actions (push) Has been cancelled
CodeQL / Analyze javascript-typescript (push) Has been cancelled
CI / Detect Version (push) Has been cancelled
CI / Detect Desktop Changes (push) Has been cancelled
CI / Build AMD64 (blacksmith-2vcpu-ubuntu-2404, ./docker/cron.Dockerfile, ubuntu-latest, ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Build AMD64 (blacksmith-2vcpu-ubuntu-2404, ./docker/db.Dockerfile, ECR_MIGRATIONS, ubuntu-latest, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (blacksmith-4vcpu-ubuntu-2404, ./docker/pii.Dockerfile, ECR_PII, ubuntu-latest, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (blacksmith-4vcpu-ubuntu-2404, ./docker/realtime.Dockerfile, ECR_REALTIME, ubuntu-latest, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build AMD64 (blacksmith-8vcpu-ubuntu-2404, ./docker/app.Dockerfile, ECR_APP, linux-x64-8-core, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/cron.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/db.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/pii.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/realtime.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-8vcpu-ubuntu-2404-arm, ./docker/app.Dockerfile, linux-arm64-8-core, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
Helm Chart / Lint, test, and validate chart (push) Has been cancelled
Helm Chart / Chart version bumped (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Build Dev ECR (blacksmith-8vcpu-ubuntu-2404, ./docker/app.Dockerfile, ECR_APP, linux-x64-8-core) (push) Has been cancelled
CI / Promote Images (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-2vcpu-ubuntu-2404, ./docker/db.Dockerfile, ECR_MIGRATIONS, ubuntu-latest) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-4vcpu-ubuntu-2404, ./docker/pii.Dockerfile, ECR_PII, ubuntu-latest) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-4vcpu-ubuntu-2404, ./docker/realtime.Dockerfile, ECR_REALTIME, ubuntu-latest) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Check Desktop Signing Secrets (push) Has been cancelled
CI / Desktop Release (push) Has been cancelled
CI / Create Desktop Prerelease (push) Has been cancelled
CI / Desktop Prerelease Build (push) Has been cancelled
CI / Publish Desktop Prerelease (push) Has been cancelled
CI / Prune Desktop Prereleases (push) Has been cancelled
Helm Chart / Install on kind and run helm test (push) Has been cancelled
WeHub snapshot of cb28d14c6f2c081de7a0d8729a8c816c9adef67a
2026-08-10 11:17:50 +08:00

210 lines
7.2 KiB
TypeScript

import { existsSync, readFileSync, renameSync, writeFileSync } from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { generateRandomHex } from '@sim/utils/random'
export const ROOT = path.resolve(fileURLToPath(new URL('.', import.meta.url)), '../..')
export type EnvTarget = 'sim' | 'realtime' | 'db' | 'root'
export const ENV_PATHS: Record<EnvTarget, string> = {
sim: path.join(ROOT, 'apps/sim/.env'),
realtime: path.join(ROOT, 'apps/realtime/.env'),
db: path.join(ROOT, 'packages/db/.env'),
root: path.join(ROOT, '.env'),
}
const EXAMPLE_PATHS: Partial<Record<EnvTarget, string>> = {
sim: path.join(ROOT, 'apps/sim/.env.example'),
realtime: path.join(ROOT, 'apps/realtime/.env.example'),
db: path.join(ROOT, 'packages/db/.env.example'),
}
/** Keys that must be byte-identical between apps/sim/.env and apps/realtime/.env. */
export const SHARED_KEYS = [
'DATABASE_URL',
'BETTER_AUTH_SECRET',
'INTERNAL_API_SECRET',
'BETTER_AUTH_URL',
'NEXT_PUBLIC_APP_URL',
] as const
export const SECRET_KEYS = [
'BETTER_AUTH_SECRET',
'ENCRYPTION_KEY',
'INTERNAL_API_SECRET',
'API_ENCRYPTION_KEY',
// Authenticates the scheduler against the background job endpoints. Without
// it the cron service exits and no scheduled work runs, so setup generates it.
'CRON_SECRET',
] as const
const PLACEHOLDER_VALUES = new Set([
'your_password',
'your_secret_key',
'your_encryption_key',
'your_internal_api_secret',
'your_api_encryption_key',
'your_better_auth_secret_min_32_chars',
'dev-secret-at-least-32-characters-long',
'dev-encryption-key-at-least-32-chars',
'dev-internal-api-secret-min-32-chars',
])
export interface EnvFile {
target: EnvTarget
path: string
exists: boolean
content: string
vars: Map<string, string>
}
const LINE_RE = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/
function parseValue(raw: string): string {
const trimmed = raw.trim()
if (trimmed.startsWith('"') || trimmed.startsWith("'")) {
const quote = trimmed[0]
const end = trimmed.indexOf(quote, 1)
return end === -1 ? trimmed.slice(1) : trimmed.slice(1, end)
}
return trimmed.replace(/\s+#.*$/, '').trim()
}
export function parseEnv(content: string): Map<string, string> {
const vars = new Map<string, string>()
for (const line of content.split('\n')) {
const match = LINE_RE.exec(line)
if (match) vars.set(match[1], parseValue(match[2]))
}
return vars
}
export function readEnvFile(target: EnvTarget): EnvFile {
const filePath = ENV_PATHS[target]
const exists = existsSync(filePath)
const content = exists ? readFileSync(filePath, 'utf8') : ''
return { target, path: filePath, exists, content, vars: parseEnv(content) }
}
/**
* Sets a key in env-file content: replaces the active line, uncomments a
* commented-out line, or appends. Returns the new content.
*/
export function upsertEnv(content: string, key: string, value: string): string {
const lines = content.split('\n')
const activeRe = new RegExp(`^\\s*(?:export\\s+)?${key}\\s*=`)
const commentedRe = new RegExp(`^#\\s*${key}\\s*=`)
const activeIndexes = lines.flatMap((line, index) => (activeRe.test(line) ? [index] : []))
if (activeIndexes.length > 1) {
throw new Error(`Duplicate active ${key} entries found in environment file`)
}
const activeIdx = activeIndexes[0] ?? -1
const idx = activeIdx !== -1 ? activeIdx : lines.findIndex((l) => commentedRe.test(l))
const newLine = `${key}=${value}`
if (idx === -1) {
const trailing = lines.length > 0 && lines[lines.length - 1] === ''
if (trailing) lines.splice(lines.length - 1, 0, newLine)
else lines.push(newLine)
} else {
lines[idx] = newLine
}
return lines.join('\n')
}
/** Applies removals and replacements to one in-memory snapshot before it is written. */
export function reconcileEnvContent(
content: string,
remove: readonly string[],
values: Record<string, string>
): string {
const replacementKeys = new Set(Object.keys(values))
const removalKeys = new Set(remove.filter((key) => !replacementKeys.has(key)))
let reconciled = content
.split('\n')
.filter((line) => {
const match = LINE_RE.exec(line)
return !match || !removalKeys.has(match[1])
})
.join('\n')
for (const [key, value] of Object.entries(values)) {
reconciled = upsertEnv(reconciled, key, value)
}
return reconciled
}
/** Computes removals and replacements before writing the env file once. */
export function reconcileEnvValues(
target: EnvTarget,
remove: readonly string[],
values: Record<string, string>
): void {
const filePath = ENV_PATHS[target]
let content: string
if (existsSync(filePath)) {
content = readFileSync(filePath, 'utf8')
} else {
const example = EXAMPLE_PATHS[target]
content = example && existsSync(example) ? readFileSync(example, 'utf8') : ''
}
writeFileSync(filePath, reconcileEnvContent(content, remove, values))
}
/** Writes values into an env file, seeding a missing file from its .env.example. */
export function writeEnvValues(target: EnvTarget, values: Record<string, string>): void {
reconcileEnvValues(target, [], values)
}
export function archiveEnvFile(target: EnvTarget): string | null {
const filePath = ENV_PATHS[target]
if (!existsSync(filePath)) return null
const backup = `${filePath}.bak-${new Date().toISOString().replace(/[:.]/g, '-')}`
renameSync(filePath, backup)
return backup
}
export function generateSecret(): string {
return generateRandomHex(64)
}
/**
* `ENCRYPTION_KEY` and `API_ENCRYPTION_KEY` are read as raw AES-256 material,
* so the app requires exactly 64 hex characters and throws on anything else
* (`lib/core/security/encryption.ts`, `lib/api-key/crypto.ts`). A merely-long
* passphrase passes a length check and then fails every encryption path at
* runtime, so those two are validated on format rather than length.
*
* Lives here so setup (which replaces an unusable secret) and doctor (which
* reports one) apply the same rule — they disagreed while it was duplicated.
*/
const HEX_KEY_PATTERN = /^[0-9a-f]{64}$/i
const HEX_SECRET_KEYS = new Set<string>(['ENCRYPTION_KEY', 'API_ENCRYPTION_KEY'])
export function isUsableSecret(key: string, value: string): boolean {
if (isPlaceholder(value)) return false
return HEX_SECRET_KEYS.has(key) ? HEX_KEY_PATTERN.test(value) : value.length >= 32
}
/** Human-readable reason a secret is unusable, for doctor's finding message. */
export function secretRequirement(key: string): string {
return HEX_SECRET_KEYS.has(key)
? 'must be exactly 64 hex characters (32-byte AES key)'
: 'must be at least 32 characters'
}
export function isPlaceholder(value: string): boolean {
return PLACEHOLDER_VALUES.has(value) || value.startsWith('your_') || value.startsWith('your-')
}
/**
* Mirrors the app's `isTruthy` (apps/sim/lib/core/config/env.ts:633) exactly —
* `true` or `1` only. The app's separate `envBoolean` additionally accepts
* `yes`/`on`, but feature flags read through `isTruthy`, so accepting the wider
* set here made the wizard and doctor report a flag as on that the app treats
* as off.
*/
export function isTruthy(value: string | undefined): boolean {
if (value === undefined) return false
return value.toLowerCase() === 'true' || value === '1'
}