fix(api): require access codes and limit sizes on helper routes

- Shared checkAccessCode for validate-diagram, validate-model, parse-url, verify-access-code
- parse-url: 5 MB streamed body limit; validate-diagram: 5 MB image limit
- validate-model refuses redirects when private URLs are blocked
- Admin settings state shared across module instances via globalThis
- Server model ids: unique slugs (non-ASCII names encoded), duplicates rejected
- Panel Bedrock credentials stored as ADMIN_AWS_* so the DynamoDB client keeps its own
- Locale redirect keeps basePath and query; EdgeOne function drops open CORS and checks the access code
- Providers payload reports whether .env sets a default model
This commit is contained in:
dayuan.jiang
2026-10-03 17:45:41 +09:00
parent 366480426d
commit 528b6e54c8
16 changed files with 622 additions and 93 deletions
+34 -19
View File
@@ -10,13 +10,27 @@ interface SettingsFile {
values: Record<string, string>
}
// Original env values snapshotted before the first overlay, so removing a
// key from the settings file restores the env default. null = was unset.
const originalEnv: Record<string, string | null> = {}
// Keys currently overlaid, so we can restore ones removed from the file.
let overlaidKeys = new Set<string>()
interface SettingsState {
// Original env values snapshotted before the first overlay, so removing
// a key from the settings file restores the env default. null = was unset.
originalEnv: Record<string, string | null>
// Keys currently overlaid, so we can restore ones removed from the file.
overlaidKeys: Set<string>
cachedSettings: Record<string, string> | null
}
let cachedSettings: Record<string, string> | null = null
// Kept on globalThis because the build can load this module more than once
// (instrumentation.ts and the API routes get separate copies); per-module
// state would make a route forget what instrumentation overlaid at startup.
const globalState = globalThis as typeof globalThis & {
__adminSettingsState?: SettingsState
}
globalState.__adminSettingsState ??= {
originalEnv: {},
overlaidKeys: new Set(),
cachedSettings: null,
}
const state = globalState.__adminSettingsState
export function getSettingsPath(): string {
const custom = process.env.SETTINGS_FILE
@@ -25,7 +39,7 @@ export function getSettingsPath(): string {
}
export function loadSettings(): Record<string, string> {
if (cachedSettings) return cachedSettings
if (state.cachedSettings) return state.cachedSettings
try {
const raw = fs.readFileSync(getSettingsPath(), "utf8")
const parsed = JSON.parse(raw) as SettingsFile
@@ -43,21 +57,22 @@ export function loadSettings(): Record<string, string> {
for (const [key, value] of Object.entries(rawValues)) {
if (typeof value === "string") values[key] = value
}
cachedSettings = values
state.cachedSettings = values
} catch (err: any) {
if (err?.code !== "ENOENT") {
console.error("[admin-settings] Failed to read settings file:", err)
}
cachedSettings = {}
state.cachedSettings = {}
}
return cachedSettings
return state.cachedSettings
}
export function applyToEnv(): void {
const values = loadSettings()
const { originalEnv } = state
// Restore env for keys that were overlaid before but are now gone
for (const key of overlaidKeys) {
for (const key of state.overlaidKeys) {
if (!(key in values)) {
const original = originalEnv[key]
if (original === null) delete process.env[key]
@@ -72,12 +87,12 @@ export function applyToEnv(): void {
process.env[key] = value
}
overlaidKeys = new Set(Object.keys(values))
state.overlaidKeys = new Set(Object.keys(values))
}
// The effective env value if the file entry were removed (for fallback display)
export function getEnvFallback(key: string): string | null {
if (overlaidKeys.has(key)) return originalEnv[key] ?? null
if (state.overlaidKeys.has(key)) return state.originalEnv[key] ?? null
return process.env[key] ?? null
}
@@ -101,7 +116,7 @@ export function saveSettings(updates: Record<string, string | null>): void {
fs.writeFileSync(tmpPath, JSON.stringify(data, null, 2), { mode: 0o600 })
fs.renameSync(tmpPath, filePath)
cachedSettings = current
state.cachedSettings = current
applyToEnv()
}
@@ -122,13 +137,13 @@ export function isSettingsWritable(): boolean {
// Test-only: reset module state
export function _resetForTests(): void {
cachedSettings = null
state.cachedSettings = null
writableCache = null
for (const key of overlaidKeys) {
const original = originalEnv[key]
for (const key of state.overlaidKeys) {
const original = state.originalEnv[key]
if (original === null) delete process.env[key]
else if (original !== undefined) process.env[key] = original
}
overlaidKeys = new Set()
for (const key of Object.keys(originalEnv)) delete originalEnv[key]
state.overlaidKeys = new Set()
state.originalEnv = {}
}