fix(staging): bootstrap database model key
This commit is contained in:
@@ -87,6 +87,7 @@ RECTIFICATION_PRICE_CREDITS=3
|
||||
|
||||
# Required to encrypt/decrypt model-provider API keys stored in the admin database.
|
||||
# Base64 decoding must produce exactly 32 random bytes; do not reuse other keys.
|
||||
# Staging migration/deploy creates this once when absent and removes legacy model API-key settings.
|
||||
MODEL_PROVIDER_CONFIG_ENCRYPTION_KEY=<independent-base64-encoded-32-byte-key>
|
||||
# OpenAI-compatible origins must be explicitly server-allowlisted.
|
||||
# MODEL_PROVIDER_BASE_URL_ALLOWLIST=https://api.deepseek.com
|
||||
|
||||
Executable
+39
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
set +x
|
||||
|
||||
ENV_FILE="${1:-.env.staging}"
|
||||
if [ ! -f "$ENV_FILE" ] || [ -L "$ENV_FILE" ]; then
|
||||
echo "staging environment file is missing or unsafe: $ENV_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
legacy_pattern='^(OPENAI_API_KEY|ANTHROPIC_API_KEY|DEEPSEEK_API_KEY|LLM_API_KEY|LLM_MODELS_JSON|LLM_BASE_URL|LLM_MODEL|LLM_DEFAULT_MODEL_ID|LLM_PROVIDER_ID|MASTRA_MODEL|MODEL_PROVIDER_[A-Z0-9_]+_API_KEY)='
|
||||
key_count="$(grep -Ec '^MODEL_PROVIDER_CONFIG_ENCRYPTION_KEY=' "$ENV_FILE" || true)"
|
||||
if [ "$key_count" -gt 1 ]; then
|
||||
echo "duplicate staging model provider encryption key" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if mode="$(stat -c '%a' "$ENV_FILE" 2>/dev/null)"; then
|
||||
owner="$(stat -c '%u' "$ENV_FILE")"
|
||||
group="$(stat -c '%g' "$ENV_FILE")"
|
||||
else
|
||||
mode="$(stat -f '%Lp' "$ENV_FILE")"
|
||||
owner="$(stat -f '%u' "$ENV_FILE")"
|
||||
group="$(stat -f '%g' "$ENV_FILE")"
|
||||
fi
|
||||
temporary="$(mktemp "${ENV_FILE}.tmp.XXXXXX")"
|
||||
trap 'rm -f -- "$temporary"' EXIT
|
||||
|
||||
grep -Ev "$legacy_pattern" "$ENV_FILE" > "$temporary" || true
|
||||
if [ "$key_count" -eq 0 ]; then
|
||||
key="$(openssl rand -base64 32 | tr -d '\n')"
|
||||
[[ "$key" =~ ^[A-Za-z0-9+/]{43}=$ ]] || exit 1
|
||||
printf 'MODEL_PROVIDER_CONFIG_ENCRYPTION_KEY=%s\n' "$key" >> "$temporary"
|
||||
fi
|
||||
|
||||
chmod "$mode" "$temporary"
|
||||
chown "$owner:$group" "$temporary"
|
||||
mv -f -- "$temporary" "$ENV_FILE"
|
||||
trap - EXIT
|
||||
@@ -126,6 +126,7 @@ EXPECTED_STAGING_ENV_OWNER_UID="$(stat -c '%u' "$DEPLOY_PATH" 2>/dev/null || sta
|
||||
exit 1
|
||||
}
|
||||
export EXPECTED_STAGING_ENV_OWNER_UID
|
||||
bash deploy/prepare-staging-model-provider-env.sh .env.staging
|
||||
bash deploy/validate-staging-env.sh \
|
||||
.env.staging staging.jyotisha.chat deploy/Caddyfile.staging
|
||||
bash deploy/validate-staging-database-env.sh .env.staging.database
|
||||
|
||||
@@ -78,6 +78,7 @@ EXPECTED_STAGING_ENV_OWNER_UID="$(stat -c '%u' "$DEPLOY_PATH" 2>/dev/null || sta
|
||||
exit 1
|
||||
}
|
||||
export EXPECTED_STAGING_ENV_OWNER_UID
|
||||
bash deploy/prepare-staging-model-provider-env.sh .env.staging
|
||||
bash deploy/validate-staging-env.sh \
|
||||
.env.staging staging.jyotisha.chat deploy/Caddyfile.staging
|
||||
bash deploy/validate-staging-database-env.sh .env.staging.database
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
mkdtempSync,
|
||||
readFileSync,
|
||||
rmSync,
|
||||
statSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
@@ -222,6 +223,33 @@ test("staging deploy consumes only the isolated staging environment and tested r
|
||||
assert.match(syncController, /--exclude='\/backups\/'/);
|
||||
});
|
||||
|
||||
test("staging model provider env preparation removes legacy settings and keeps one stable key", () => {
|
||||
const prepare = fileURLToPath(
|
||||
new URL("../../deploy/prepare-staging-model-provider-env.sh", import.meta.url),
|
||||
);
|
||||
const root = mkdtempSync(join(tmpdir(), "jyotisha-staging-model-env-"));
|
||||
const envFile = join(root, ".env.staging");
|
||||
try {
|
||||
writeFileSync(envFile, "APP_ENV_FILE=../.env.staging\nOPENAI_API_KEY=legacy\nLLM_MODEL=legacy\n");
|
||||
chmodSync(envFile, 0o600);
|
||||
const first = spawnSync("bash", [prepare, envFile], { encoding: "utf8" });
|
||||
assert.equal(first.status, 0, first.stderr);
|
||||
const prepared = readFileSync(envFile, "utf8");
|
||||
assert.match(prepared, /^APP_ENV_FILE=\.\.\/\.env\.staging$/m);
|
||||
assert.doesNotMatch(prepared, /^(OPENAI_API_KEY|LLM_MODEL)=/m);
|
||||
const key = prepared.match(/^MODEL_PROVIDER_CONFIG_ENCRYPTION_KEY=([A-Za-z0-9+/]{43}=)$/m)?.[1];
|
||||
assert.ok(key);
|
||||
assert.equal(statSync(envFile).mode & 0o777, 0o600);
|
||||
|
||||
const second = spawnSync("bash", [prepare, envFile], { encoding: "utf8" });
|
||||
assert.equal(second.status, 0, second.stderr);
|
||||
const secondKey = readFileSync(envFile, "utf8").match(/^MODEL_PROVIDER_CONFIG_ENCRYPTION_KEY=([A-Za-z0-9+/]{43}=)$/m)?.[1];
|
||||
assert.equal(secondKey, key);
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("staging env validator rejects selector drift, duplicates, and unsafe permissions", () => {
|
||||
const validator = fileURLToPath(
|
||||
new URL("../../deploy/validate-staging-env.sh", import.meta.url),
|
||||
|
||||
@@ -649,6 +649,7 @@ test("first immutable deployment rolls back to validated local image IDs", () =>
|
||||
writeFileSync(join(deploymentPath, ".state", "deployed-revision"), `${previousSha}\n`);
|
||||
for (const script of [
|
||||
join(incomingDeploy, "sync-staging-tree.sh"),
|
||||
join(liveDeploy, "prepare-staging-model-provider-env.sh"),
|
||||
join(liveDeploy, "validate-staging-env.sh"),
|
||||
join(liveDeploy, "validate-staging-database-env.sh"),
|
||||
]) {
|
||||
@@ -726,6 +727,7 @@ test("normal deployment checks migrations but never applies them", () => {
|
||||
assert.match(runner, /^#!\/usr\/bin\/env bash\nset -euo pipefail\nset \+x\n/);
|
||||
assert.match(runner, /-f deploy\/docker-compose\.staging\.yml/);
|
||||
assertOrder(runner, [
|
||||
"prepare-staging-model-provider-env.sh",
|
||||
"validate-staging-env.sh",
|
||||
"validate-staging-database-env.sh",
|
||||
"compose=(",
|
||||
|
||||
Reference in New Issue
Block a user