fix(admin): address follow-up Copilot findings on the prior fixes

- loadAdminProviders now validates against a stored-shape schema where
  secrets are plain strings, so a hand-edited ADMIN_PROVIDERS holding an
  {isSet} marker is dropped instead of later crashing maskSecret().
- loadSettings guards against array values (typeof [] === 'object'),
  which would otherwise overlay numeric keys onto process.env.
- Admin SecretInput uses the bare id so the shared component's
  <Label htmlFor> stays associated (only one ProviderDetail mounts).
- Add tests: marker-secret rejection, array-values guard, bedrock
  multi-secret round-trip.
This commit is contained in:
dayuan.jiang
2026-06-15 00:16:01 +09:00
parent ee4613c388
commit 2cc64bbe5f
5 changed files with 61 additions and 6 deletions

View File

@@ -45,6 +45,16 @@ export const AdminProviderSchema = z.object({
export const AdminProvidersSchema = z.array(AdminProviderSchema)
// Stored shape: secrets are plain strings (never {isSet} markers, which
// only exist in transit). Used to validate ADMIN_PROVIDERS on load so a
// hand-edited/corrupted value can't slip a marker object past maskSecret.
const StoredAdminProviderSchema = AdminProviderSchema.extend({
apiKey: z.string().optional(),
awsAccessKeyId: z.string().optional(),
awsSecretAccessKey: z.string().optional(),
vertexApiKey: z.string().optional(),
})
export type AdminProviderInput = z.infer<typeof AdminProviderSchema>
// Stored form: secrets are plain strings
@@ -95,7 +105,7 @@ export function loadAdminProviders(): StoredAdminProvider[] {
// Validate each entry's shape — a malformed/hand-edited value must
// not reach runtime code that assumes provider/models exist.
return parsed.flatMap((entry) => {
const result = AdminProviderSchema.safeParse(entry)
const result = StoredAdminProviderSchema.safeParse(entry)
return result.success ? [result.data as StoredAdminProvider] : []
})
} catch {

View File

@@ -33,11 +33,14 @@ export function loadSettings(): Record<string, string> {
// hold null/arrays/numbers that would otherwise be overlaid onto
// process.env and coerce to junk like "[object Object]".
const values: Record<string, string> = {}
const raw_values =
parsed && typeof parsed.values === "object" && parsed.values
const rawValues =
parsed &&
typeof parsed.values === "object" &&
parsed.values &&
!Array.isArray(parsed.values)
? parsed.values
: {}
for (const [key, value] of Object.entries(raw_values)) {
for (const [key, value] of Object.entries(rawValues)) {
if (typeof value === "string") values[key] = value
}
cachedSettings = values