feat(google-auth): centralize oauth token store and service checks
This commit is contained in:
+15
-58
@@ -1,13 +1,12 @@
|
||||
import { google, type Auth } from 'googleapis';
|
||||
import { readFileSync, writeFileSync, existsSync, mkdirSync, chmodSync } from 'fs';
|
||||
import { dirname, resolve } from 'path';
|
||||
import { homedir } from 'os';
|
||||
import { readFileSync, existsSync } from 'fs';
|
||||
import type { v1 } from '@google-cloud/pubsub';
|
||||
import type { GmailConfig } from '../config/schema.js';
|
||||
import type { ChannelAdapter, ChannelStatus, InboundMessage, OutboundMessage } from '../channels/types.js';
|
||||
import { parseInterval } from './heartbeat.js';
|
||||
import { sanitizeHtml } from '../utils/html.js';
|
||||
import { auditLogger } from '../audit/index.js';
|
||||
import { createGoogleOAuth2Client, expandPath } from '../google/oauth.js';
|
||||
|
||||
/** Minimal interface for the parts of ChannelRegistry we need. */
|
||||
interface ChannelLookup {
|
||||
@@ -178,6 +177,13 @@ export class GmailWatcher implements ChannelAdapter {
|
||||
this.messageHandler = handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Backward-compatible path helper used by tests and callers.
|
||||
*/
|
||||
expandPath(p: string): string {
|
||||
return expandPath(p);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a Pub/Sub push notification from Google.
|
||||
* Called by the gateway when POST /gmail/push is received.
|
||||
@@ -218,45 +224,22 @@ export class GmailWatcher implements ChannelAdapter {
|
||||
throw new Error('No credentials_file configured. Set automation.gmail.credentials_file in config.');
|
||||
}
|
||||
|
||||
const expandedCredsPath = this.expandPath(credentialsPath);
|
||||
const expandedCredsPath = expandPath(credentialsPath);
|
||||
if (!existsSync(expandedCredsPath)) {
|
||||
throw new Error(`Credentials file not found: ${expandedCredsPath}`);
|
||||
}
|
||||
|
||||
const credentials = JSON.parse(readFileSync(expandedCredsPath, 'utf-8'));
|
||||
const { client_id, client_secret, redirect_uris, project_id } = credentials.installed ?? credentials.web ?? {};
|
||||
const { project_id } = credentials.installed ?? credentials.web ?? {};
|
||||
if (project_id && typeof project_id === 'string') {
|
||||
this.googleProjectId = project_id;
|
||||
}
|
||||
|
||||
if (!client_id || !client_secret) {
|
||||
throw new Error('Invalid credentials file — missing client_id or client_secret');
|
||||
}
|
||||
|
||||
const oauth2Client = new google.auth.OAuth2(
|
||||
client_id,
|
||||
client_secret,
|
||||
redirect_uris?.[0] ?? 'http://localhost',
|
||||
);
|
||||
|
||||
// Load stored token
|
||||
const tokenPath = this.expandPath(this.config.token_file ?? '~/.config/flynn/gmail-token.json');
|
||||
if (!existsSync(tokenPath)) {
|
||||
throw new Error(
|
||||
`Token file not found: ${tokenPath}. Run "flynn gmail-auth" to authenticate.`,
|
||||
);
|
||||
}
|
||||
|
||||
const token = JSON.parse(readFileSync(tokenPath, 'utf-8'));
|
||||
oauth2Client.setCredentials(token);
|
||||
|
||||
// Auto-save refreshed tokens
|
||||
oauth2Client.on('tokens', (newTokens) => {
|
||||
const merged = { ...token, ...newTokens };
|
||||
this.saveToken(merged);
|
||||
return createGoogleOAuth2Client({
|
||||
service: 'gmail',
|
||||
credentialsFile: this.config.credentials_file,
|
||||
tokenFile: this.config.token_file,
|
||||
});
|
||||
|
||||
return oauth2Client;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -608,30 +591,4 @@ export class GmailWatcher implements ChannelAdapter {
|
||||
.replace(/\{\{labels\}\}/g, email.labels.join(', '));
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand ~ to the user's home directory.
|
||||
*/
|
||||
expandPath(p: string): string {
|
||||
if (p.startsWith('~/') || p === '~') {
|
||||
return resolve(homedir(), p.slice(2));
|
||||
}
|
||||
return resolve(p);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save token to disk with restrictive permissions (0o600).
|
||||
*/
|
||||
private saveToken(token: unknown): void {
|
||||
const tokenPath = this.expandPath(this.config.token_file ?? '~/.config/flynn/gmail-token.json');
|
||||
const dir = dirname(tokenPath);
|
||||
if (!existsSync(dir)) {
|
||||
mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
writeFileSync(tokenPath, JSON.stringify(token, null, 2), 'utf-8');
|
||||
try {
|
||||
chmodSync(tokenPath, 0o600);
|
||||
} catch {
|
||||
// chmod may fail on some filesystems — not critical
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user