feat: add OpenAI OAuth, strict model overrides, and Gmail pull mode

This commit is contained in:
William Valentin
2026-02-13 14:55:40 -08:00
parent 8f644d5e25
commit 955b9e28e0
50 changed files with 5955 additions and 160 deletions
+55 -2
View File
@@ -137,7 +137,8 @@ const checkModelConnectivity: Check = async (ctx) => {
// Check if API key is present for providers that need one
const needsKey = ['anthropic', 'openai', 'gemini', 'openrouter'];
if (needsKey.includes(model.provider) && !model.api_key && !model.auth_token) {
const openaiUsingOAuth = model.provider === 'openai' && Boolean((model as unknown as { use_oauth?: boolean }).use_oauth);
if (needsKey.includes(model.provider) && !openaiUsingOAuth && !model.api_key && !model.auth_token) {
const envVarMap: Record<string, string> = {
anthropic: 'ANTHROPIC_API_KEY',
openai: 'OPENAI_API_KEY',
@@ -256,12 +257,64 @@ const checkGmail: Check = async (ctx) => {
return { status: 'fail', label: 'Gmail configured', detail: `credentials file not found: ${credentialsPath}` };
}
let googleProjectId: string | undefined;
try {
const creds = JSON.parse(readFileSync(credentialsPath, 'utf-8')) as Record<string, unknown>;
const installed = (creds.installed as Record<string, unknown> | undefined) ?? (creds.web as Record<string, unknown> | undefined);
const projectId = installed?.project_id;
if (typeof projectId === 'string' && projectId.trim()) {
googleProjectId = projectId.trim();
}
} catch {
// Ignore JSON parse errors; doctor will still validate token and output.
}
const tokenPath = expandPath(gmail.token_file ?? '~/.config/flynn/gmail-token.json');
if (!existsSync(tokenPath)) {
return { status: 'warn', label: 'Gmail configured', detail: 'run `flynn gmail-auth` to authenticate' };
}
return { status: 'pass', label: 'Gmail configured', detail: `(output: ${gmail.output.channel}/${gmail.output.peer})` };
const modes: string[] = [];
const warnings: string[] = [];
const topicRaw = (gmail.pubsub_topic ?? process.env.FLYNN_GMAIL_PUBSUB_TOPIC ?? '').trim();
const pushEnabled = Boolean(topicRaw) && !gmail.disable_push;
if (pushEnabled) {
modes.push('push');
if (topicRaw.includes('/')) {
const ok = /^projects\/[^/]+\/topics\/[^/]+$/.test(topicRaw);
if (!ok) {
warnings.push('pubsub_topic format invalid (expected projects/<project>/topics/<topic>)');
}
} else if (!googleProjectId) {
warnings.push('pubsub_topic shorthand requires project_id in Gmail credentials');
}
if (ctx.config.server?.tailscale?.serve) {
warnings.push('push requires a public HTTPS endpoint; Tailscale Serve is typically tailnet-only');
}
} else if (gmail.disable_push && topicRaw) {
warnings.push('push disabled (disable_push=true)');
}
const subRaw = (gmail.pubsub_subscription_id ?? '').trim();
if (subRaw) {
modes.push('pull');
if (subRaw.includes('/')) {
const ok = /^projects\/[^/]+\/subscriptions\/[^/]+$/.test(subRaw);
if (!ok) {
warnings.push('pubsub_subscription_id format invalid (expected projects/<project>/subscriptions/<sub>)');
}
} else if (!googleProjectId) {
warnings.push('pubsub_subscription_id shorthand requires project_id in Gmail credentials');
}
}
modes.push('poll');
const detail = `(${modes.join(' + ')} -> ${gmail.output.channel}/${gmail.output.peer})`;
const withWarnings = warnings.length > 0 ? `${detail}${warnings.join('; ')}` : detail;
return { status: warnings.length > 0 ? 'warn' : 'pass', label: 'Gmail configured', detail: withWarnings };
};
const allChecks: Check[] = [