This commit is contained in:
+119
-4
@@ -26,6 +26,9 @@ const envBoolean = (defaultValue: boolean) =>
|
||||
return ['1', 'true', 'yes', 'on'].includes(value.trim().toLowerCase());
|
||||
});
|
||||
|
||||
const optionalEnvString = (value: unknown) =>
|
||||
typeof value === 'string' && value.trim() === '' ? undefined : value;
|
||||
|
||||
const schema = z.object({
|
||||
DATABASE_URL: z.string().min(1, 'DATABASE_URL is required.'),
|
||||
|
||||
@@ -45,16 +48,126 @@ const schema = z.object({
|
||||
PRIME_SYNC_ENABLED: envBoolean(false),
|
||||
PRIME_SYNC_INTERVAL_MINUTES: z.coerce.number().int().positive().default(30),
|
||||
|
||||
/** Base64-encoded 32-byte key. Secrets written in the admin UI require it. */
|
||||
PIG_SETTINGS_ENCRYPTION_KEY: z.string().optional(),
|
||||
|
||||
PIGGY_ENABLED: envBoolean(false),
|
||||
ANTHROPIC_API_KEY: z.string().optional(),
|
||||
PIGGY_MODEL: z.string().default('claude-sonnet-5'),
|
||||
PIGGY_MODEL: z.string().default('nvidia/nemotron-3-nano-30b-a3b'),
|
||||
PIGGY_INFERENCE_BASE: z.string().url().default('https://api.pinference.ai/api/v1'),
|
||||
PIGGY_LEASE_SECONDS: z.coerce.number().int().positive().default(300),
|
||||
PIGGY_INTERNAL_URL: z.preprocess(
|
||||
(value) => (value === '' ? undefined : value),
|
||||
z.string().url().optional(),
|
||||
),
|
||||
PIGGY_INTERNAL_TOKEN: z.preprocess(
|
||||
(value) => (value === '' ? undefined : value),
|
||||
z.string().min(32).optional(),
|
||||
),
|
||||
|
||||
SLACK_BOT_TOKEN: z.string().optional(),
|
||||
SLACK_SIGNING_SECRET: z.string().optional(),
|
||||
BUZZ_RELAY_URL: z.string().optional(),
|
||||
BUZZ_RELAY_URL: z.preprocess(optionalEnvString, z.string().url().optional()),
|
||||
BUZZ_PRIVATE_KEY: z.preprocess(optionalEnvString, z.string().min(1).optional()),
|
||||
BUZZ_AUTH_TAG: z.preprocess(optionalEnvString, z.string().min(1).optional()),
|
||||
|
||||
NOTION_CLIENT_ID: z.preprocess(optionalEnvString, z.string().min(1).optional()),
|
||||
NOTION_CLIENT_SECRET: z.preprocess(optionalEnvString, z.string().min(1).optional()),
|
||||
NOTION_REDIRECT_URI: z.preprocess(optionalEnvString, z.string().url().optional()),
|
||||
GOOGLE_CLIENT_ID: z.preprocess(optionalEnvString, z.string().min(1).optional()),
|
||||
GOOGLE_CLIENT_SECRET: z.preprocess(optionalEnvString, z.string().min(1).optional()),
|
||||
GOOGLE_REDIRECT_URI: z.preprocess(optionalEnvString, z.string().url().optional()),
|
||||
}).superRefine((value, context) => {
|
||||
const buzzConfigured = Boolean(
|
||||
value.BUZZ_RELAY_URL || value.BUZZ_PRIVATE_KEY || value.BUZZ_AUTH_TAG,
|
||||
);
|
||||
if (buzzConfigured && !value.BUZZ_RELAY_URL) {
|
||||
context.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ['BUZZ_RELAY_URL'],
|
||||
message: 'BUZZ_RELAY_URL is required when Buzz delivery is configured.',
|
||||
});
|
||||
}
|
||||
if (buzzConfigured && !value.BUZZ_PRIVATE_KEY) {
|
||||
context.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ['BUZZ_PRIVATE_KEY'],
|
||||
message: 'BUZZ_PRIVATE_KEY is required when Buzz delivery is configured.',
|
||||
});
|
||||
}
|
||||
|
||||
const notionConfigured = Boolean(
|
||||
value.NOTION_CLIENT_ID || value.NOTION_CLIENT_SECRET || value.NOTION_REDIRECT_URI,
|
||||
);
|
||||
for (const key of ['NOTION_CLIENT_ID', 'NOTION_CLIENT_SECRET', 'NOTION_REDIRECT_URI'] as const) {
|
||||
if (notionConfigured && !value[key]) {
|
||||
context.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: [key],
|
||||
message: `${key} is required when Notion import is configured.`,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (notionConfigured && !hasValidEncryptionKey(value.PIG_SETTINGS_ENCRYPTION_KEY)) {
|
||||
context.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ['PIG_SETTINGS_ENCRYPTION_KEY'],
|
||||
message: 'A base64-encoded 32-byte PIG_SETTINGS_ENCRYPTION_KEY is required for Notion OAuth.',
|
||||
});
|
||||
}
|
||||
|
||||
const googleConfigured = Boolean(
|
||||
value.GOOGLE_CLIENT_ID || value.GOOGLE_CLIENT_SECRET || value.GOOGLE_REDIRECT_URI,
|
||||
);
|
||||
for (const key of ['GOOGLE_CLIENT_ID', 'GOOGLE_CLIENT_SECRET', 'GOOGLE_REDIRECT_URI'] as const) {
|
||||
if (googleConfigured && !value[key]) {
|
||||
context.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: [key],
|
||||
message: `${key} is required when Google Sheets import is configured.`,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (googleConfigured && !hasValidEncryptionKey(value.PIG_SETTINGS_ENCRYPTION_KEY)) {
|
||||
context.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ['PIG_SETTINGS_ENCRYPTION_KEY'],
|
||||
message: 'A base64-encoded 32-byte PIG_SETTINGS_ENCRYPTION_KEY is required for Google OAuth.',
|
||||
});
|
||||
}
|
||||
if (value.GOOGLE_REDIRECT_URI) {
|
||||
try {
|
||||
const redirect = new URL(value.GOOGLE_REDIRECT_URI);
|
||||
const publicUrl = new URL(value.PIG_PUBLIC_URL);
|
||||
if (
|
||||
redirect.origin !== publicUrl.origin
|
||||
|| redirect.pathname !== '/oauth/google/callback'
|
||||
|| redirect.search
|
||||
|| redirect.hash
|
||||
) {
|
||||
context.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ['GOOGLE_REDIRECT_URI'],
|
||||
message: 'GOOGLE_REDIRECT_URI must use the PIG_PUBLIC_URL origin and exact /oauth/google/callback path.',
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
context.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ['GOOGLE_REDIRECT_URI'],
|
||||
message: 'GOOGLE_REDIRECT_URI must use the PIG_PUBLIC_URL origin and exact /oauth/google/callback path.',
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function hasValidEncryptionKey(value: string | undefined): boolean {
|
||||
if (!value) return false;
|
||||
const decoded = Buffer.from(value, 'base64');
|
||||
return decoded.length === 32
|
||||
&& decoded.toString('base64').replace(/=+$/, '') === value.replace(/=+$/, '');
|
||||
}
|
||||
|
||||
export type Config = z.infer<typeof schema> & {
|
||||
adminEmails: string[];
|
||||
isProduction: boolean;
|
||||
@@ -108,8 +221,10 @@ function warnOnFootguns(config: Config): void {
|
||||
warn('PRIME_SYNC_ENABLED is on but PRIME_API_KEY is unset — sync will not run.');
|
||||
}
|
||||
|
||||
if (config.PIGGY_ENABLED && !config.ANTHROPIC_API_KEY) {
|
||||
warn('PIGGY_ENABLED is on but ANTHROPIC_API_KEY is unset — the agent will idle.');
|
||||
if (config.PIGGY_ENABLED && (!config.PIGGY_INTERNAL_URL || !config.PIGGY_INTERNAL_TOKEN)) {
|
||||
warn(
|
||||
'PIGGY_ENABLED is on but the internal URL or token is unset — interactive chat will be unavailable.',
|
||||
);
|
||||
}
|
||||
|
||||
if (config.SUPABASE_SERVICE_KEY) {
|
||||
|
||||
Reference in New Issue
Block a user