tide-commander 1.47.0 → 1.49.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/dist/assets/{BossLogsModal-BEZSJWBV.js → BossLogsModal-buht3LyW.js} +1 -1
  2. package/dist/assets/BossSpawnModal-D7M1JocE.js +1 -0
  3. package/dist/assets/{ControlsModal-BZKmtLz7.js → ControlsModal-C3-j1E6y.js} +1 -1
  4. package/dist/assets/{DockerLogsModal-DD8gRVu6.js → DockerLogsModal-CA2j24QX.js} +1 -1
  5. package/dist/assets/{EmbeddedEditor-DMcKiZ5q.js → EmbeddedEditor-CrLtAaIO.js} +1 -1
  6. package/dist/assets/GmailOAuthSetup-CG2BmCyg.js +303 -0
  7. package/dist/assets/GoogleOAuthSetup-DlaXAoyq.js +236 -0
  8. package/dist/assets/{IframeModal-DJn-dNqr.js → IframeModal-CvJ6LEIu.js} +1 -1
  9. package/dist/assets/IntegrationsPanel-iTByepHj.js +2 -0
  10. package/dist/assets/{LogViewerModal-BgIfdR-l.js → LogViewerModal-C75Yyse9.js} +1 -1
  11. package/dist/assets/{MonitoringModal-Dm8wbcHY.js → MonitoringModal-C3NapNrP.js} +1 -1
  12. package/dist/assets/{PM2LogsModal-CDULU4yk.js → PM2LogsModal-CaacRI13.js} +1 -1
  13. package/dist/assets/{RestoreArchivedAreaModal-DLuiZj6S.js → RestoreArchivedAreaModal-mwQqJ4qp.js} +1 -1
  14. package/dist/assets/{SaveSnapshotModal-CbfjHVJz.js → SaveSnapshotModal-Didvj3hf.js} +1 -1
  15. package/dist/assets/{Scene2DCanvas-Dw9o4Fk8.js → Scene2DCanvas-chSGLBlj.js} +1 -1
  16. package/dist/assets/{SceneManager-KAhHYpUf.js → SceneManager-G0H-cWU4.js} +1 -1
  17. package/dist/assets/{SkillsPanel-BYy3W8hd.js → SkillsPanel-nKzb0E59.js} +1 -1
  18. package/dist/assets/{SnapshotManager-LxlHIjaa.js → SnapshotManager-DoQRZCNb.js} +1 -1
  19. package/dist/assets/SpawnModal-CPTZGYNR.js +1 -0
  20. package/dist/assets/{SubordinateAssignmentModal-zuSTZAqj.js → SubordinateAssignmentModal-B2XP8wZU.js} +1 -1
  21. package/dist/assets/{SupervisorPanel-tTfMa-0x.js → SupervisorPanel-LlgMdGCQ.js} +1 -1
  22. package/dist/assets/{TriggerManagerPanel-BoHdbpqk.js → TriggerManagerPanel-DMiK0_y5.js} +1 -1
  23. package/dist/assets/{WorkflowEditorPanel-Cusd-51G.js → WorkflowEditorPanel-DPwmjV2f.js} +1 -1
  24. package/dist/assets/{index-BoFICHSl.js → index-BCuYxI5F.js} +2 -2
  25. package/dist/assets/{index-BPn1NjZ5.js → index-BIt0Cgpm.js} +1 -1
  26. package/dist/assets/{index-CeMGJMtZ.js → index-BM1s5vTS.js} +3 -3
  27. package/dist/assets/{index-ChZ1qMl3.js → index-CG-I_YeN.js} +1 -1
  28. package/dist/assets/{index-ifiZ65Cu.js → index-CxEHgPe3.js} +1 -1
  29. package/dist/assets/{index-C_3LXJYS.js → index-D33VRpYX.js} +1 -1
  30. package/dist/assets/{index-D3KwWKEv.js → index-DYwDeYnm.js} +1 -1
  31. package/dist/assets/{index-Cxx3kFoj.js → index-XXW1ctrV.js} +1 -1
  32. package/dist/assets/{main-BsOvZp5M.js → main-D-o_EGqJ.js} +35 -35
  33. package/dist/assets/{web-B5moyGVG.js → web-Blb26Kj7.js} +1 -1
  34. package/dist/assets/{web-BYzo_b4X.js → web-hHpxvpTK.js} +1 -1
  35. package/dist/index.html +1 -1
  36. package/dist/src/packages/server/codex/backend.js +5 -1
  37. package/dist/src/packages/server/data/builtin-skills/report-task-to-boss.js +1 -1
  38. package/dist/src/packages/server/data/event-queries.js +15 -0
  39. package/dist/src/packages/server/data/migrations/005_drive_actions.sql +19 -0
  40. package/dist/src/packages/server/index.js +1 -0
  41. package/dist/src/packages/server/integrations/gmail/index.js +8 -6
  42. package/dist/src/packages/server/integrations/google-drive/drive-client.js +605 -0
  43. package/dist/src/packages/server/integrations/google-drive/drive-config.js +127 -0
  44. package/dist/src/packages/server/integrations/google-drive/drive-routes.js +365 -0
  45. package/dist/src/packages/server/integrations/google-drive/drive-skill.js +248 -0
  46. package/dist/src/packages/server/integrations/google-drive/index.js +60 -0
  47. package/dist/src/packages/server/integrations/integration-registry.js +2 -0
  48. package/dist/src/packages/server/routes/agents.js +45 -17
  49. package/dist/src/packages/server/services/agent-service.js +7 -1
  50. package/dist/src/packages/server/services/llm-matcher-service.js +3 -1
  51. package/dist/src/packages/shared/agent-types.js +9 -1
  52. package/package.json +1 -1
  53. package/dist/assets/BossSpawnModal-BknVJp2f.js +0 -1
  54. package/dist/assets/GmailOAuthSetup-CO2LqLvU.js +0 -270
  55. package/dist/assets/GoogleOAuthSetup-OVzavssB.js +0 -203
  56. package/dist/assets/IntegrationsPanel-C0JSiDOw.js +0 -2
  57. package/dist/assets/SpawnModal-Ch7yKsdS.js +0 -1
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Google Drive Integration Configuration
3
+ * ConfigField[] schema + config persistence
4
+ *
5
+ * Shares OAuth2 credentials with Gmail/Calendar plugins via the shared secrets system.
6
+ * Secrets used: GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_REFRESH_TOKEN
7
+ */
8
+ import * as fs from 'fs';
9
+ import * as path from 'path';
10
+ import { getDataDir } from '../../data/index.js';
11
+ const DEFAULT_CONFIG = {
12
+ enabled: false,
13
+ defaultFolderId: '',
14
+ };
15
+ // ─── Config File Persistence ───
16
+ function getConfigPath() {
17
+ return path.join(getDataDir(), 'drive-config.json');
18
+ }
19
+ let cachedConfig = null;
20
+ export function loadConfig() {
21
+ if (cachedConfig)
22
+ return cachedConfig;
23
+ const configPath = getConfigPath();
24
+ try {
25
+ if (fs.existsSync(configPath)) {
26
+ const data = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
27
+ const result = { ...DEFAULT_CONFIG, ...data };
28
+ cachedConfig = result;
29
+ return result;
30
+ }
31
+ }
32
+ catch {
33
+ // Corrupted config, use defaults
34
+ }
35
+ const defaults = { ...DEFAULT_CONFIG };
36
+ cachedConfig = defaults;
37
+ return defaults;
38
+ }
39
+ export function saveConfig(config) {
40
+ cachedConfig = config;
41
+ const configPath = getConfigPath();
42
+ const dir = path.dirname(configPath);
43
+ if (!fs.existsSync(dir)) {
44
+ fs.mkdirSync(dir, { recursive: true });
45
+ }
46
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
47
+ }
48
+ export function updateConfig(updates) {
49
+ const config = loadConfig();
50
+ const updated = { ...config, ...updates };
51
+ saveConfig(updated);
52
+ return updated;
53
+ }
54
+ // ─── Config Schema (for generic settings UI) ───
55
+ export const driveConfigSchema = [
56
+ {
57
+ key: 'enabled',
58
+ label: 'Enable Google Drive Integration',
59
+ type: 'boolean',
60
+ description: 'Enable or disable the Google Drive connection',
61
+ defaultValue: false,
62
+ group: 'General',
63
+ },
64
+ {
65
+ key: 'GOOGLE_CLIENT_ID',
66
+ label: 'Google Client ID',
67
+ type: 'password',
68
+ description: 'OAuth2 Client ID (shared with Gmail/Calendar integrations)',
69
+ placeholder: 'xxxx.apps.googleusercontent.com',
70
+ required: true,
71
+ secret: true,
72
+ group: 'Authentication',
73
+ },
74
+ {
75
+ key: 'GOOGLE_CLIENT_SECRET',
76
+ label: 'Google Client Secret',
77
+ type: 'password',
78
+ description: 'OAuth2 Client Secret (shared with Gmail/Calendar integrations)',
79
+ required: true,
80
+ secret: true,
81
+ group: 'Authentication',
82
+ },
83
+ {
84
+ key: 'GOOGLE_REFRESH_TOKEN',
85
+ label: 'Google Refresh Token',
86
+ type: 'password',
87
+ description: 'OAuth2 Refresh Token (obtained via one-time auth flow, shared with Gmail/Calendar)',
88
+ required: true,
89
+ secret: true,
90
+ group: 'Authentication',
91
+ },
92
+ {
93
+ key: 'defaultFolderId',
94
+ label: 'Default Folder ID',
95
+ type: 'text',
96
+ description: 'Google Drive folder ID to use as default root (leave empty for My Drive root)',
97
+ placeholder: 'e.g. 1a2b3c4d5e6f...',
98
+ group: 'Defaults',
99
+ },
100
+ ];
101
+ // ─── Config Value Access ───
102
+ export function getConfigValues(secrets) {
103
+ const config = loadConfig();
104
+ return {
105
+ enabled: config.enabled,
106
+ defaultFolderId: config.defaultFolderId,
107
+ GOOGLE_CLIENT_ID: secrets.get('GOOGLE_CLIENT_ID') ? '********' : '',
108
+ GOOGLE_CLIENT_SECRET: secrets.get('GOOGLE_CLIENT_SECRET') ? '********' : '',
109
+ GOOGLE_REFRESH_TOKEN: secrets.get('GOOGLE_REFRESH_TOKEN') ? '********' : '',
110
+ };
111
+ }
112
+ export async function setConfigValues(values, secrets) {
113
+ // Handle secret fields (shared with Gmail/Calendar)
114
+ for (const key of ['GOOGLE_CLIENT_ID', 'GOOGLE_CLIENT_SECRET', 'GOOGLE_REFRESH_TOKEN']) {
115
+ const val = values[key];
116
+ if (typeof val === 'string' && val && val !== '********') {
117
+ secrets.set(key, val);
118
+ }
119
+ }
120
+ // Handle non-secret config
121
+ const updates = {};
122
+ if (typeof values.enabled === 'boolean')
123
+ updates.enabled = values.enabled;
124
+ if (typeof values.defaultFolderId === 'string')
125
+ updates.defaultFolderId = values.defaultFolderId;
126
+ updateConfig(updates);
127
+ }
@@ -0,0 +1,365 @@
1
+ /**
2
+ * Google Drive Routes
3
+ * Express Router with CRUD endpoints for Drive files and folders.
4
+ * Mounted at /api/drive/ by the integration registry.
5
+ */
6
+ import { Router } from 'express';
7
+ import * as driveClient from './drive-client.js';
8
+ import { createLogger } from '../../utils/logger.js';
9
+ const log = createLogger('DriveRoutes');
10
+ const router = Router();
11
+ // ─── Files ───
12
+ // GET /api/drive/files — List files
13
+ router.get('/files', async (req, res) => {
14
+ try {
15
+ const result = await driveClient.listFiles({
16
+ folderId: req.query.folderId,
17
+ query: req.query.query,
18
+ mimeType: req.query.mimeType,
19
+ maxResults: req.query.maxResults ? parseInt(req.query.maxResults, 10) : undefined,
20
+ pageToken: req.query.pageToken,
21
+ orderBy: req.query.orderBy,
22
+ trashed: req.query.trashed === 'true',
23
+ driveId: req.query.driveId,
24
+ includeItemsFromAllDrives: req.query.includeItemsFromAllDrives === 'true',
25
+ });
26
+ res.json(result);
27
+ }
28
+ catch (err) {
29
+ log.error(`Drive list files error: ${err}`);
30
+ res.status(500).json({ error: `Failed to list files: ${err instanceof Error ? err.message : err}` });
31
+ }
32
+ });
33
+ // GET /api/drive/files/:fileId — Get file metadata
34
+ router.get('/files/:fileId', async (req, res) => {
35
+ try {
36
+ const file = await driveClient.getFile(req.params.fileId);
37
+ res.json({ file });
38
+ }
39
+ catch (err) {
40
+ log.error(`Drive get file error: ${err}`);
41
+ res.status(500).json({ error: `Failed to get file: ${err instanceof Error ? err.message : err}` });
42
+ }
43
+ });
44
+ // GET /api/drive/files/:fileId/content — Read file content
45
+ router.get('/files/:fileId/content', async (req, res) => {
46
+ try {
47
+ const exportMimeType = req.query.exportAs;
48
+ const result = await driveClient.getFileContent(req.params.fileId, exportMimeType);
49
+ res.json(result);
50
+ }
51
+ catch (err) {
52
+ log.error(`Drive read file content error: ${err}`);
53
+ res.status(500).json({ error: `Failed to read file content: ${err instanceof Error ? err.message : err}` });
54
+ }
55
+ });
56
+ // POST /api/drive/files — Create a new file
57
+ router.post('/files', async (req, res) => {
58
+ try {
59
+ const { name, content, mimeType, folderId, description, agentId, workflowInstanceId } = req.body;
60
+ if (!name || content === undefined) {
61
+ res.status(400).json({ error: 'name and content are required' });
62
+ return;
63
+ }
64
+ const file = await driveClient.createFile({
65
+ name,
66
+ content,
67
+ mimeType,
68
+ folderId,
69
+ description,
70
+ agentId,
71
+ workflowInstanceId,
72
+ });
73
+ res.json({ file });
74
+ }
75
+ catch (err) {
76
+ log.error(`Drive create file error: ${err}`);
77
+ res.status(500).json({ error: `Failed to create file: ${err instanceof Error ? err.message : err}` });
78
+ }
79
+ });
80
+ // POST /api/drive/files/:fileId/copy — Copy a file (typically from a template)
81
+ router.post('/files/:fileId/copy', async (req, res) => {
82
+ try {
83
+ const { name, folderId, description, agentId, workflowInstanceId } = req.body || {};
84
+ const file = await driveClient.copyFile({
85
+ sourceFileId: req.params.fileId,
86
+ name,
87
+ folderId,
88
+ description,
89
+ agentId,
90
+ workflowInstanceId,
91
+ });
92
+ res.json({ file });
93
+ }
94
+ catch (err) {
95
+ log.error(`Drive copy file error: ${err}`);
96
+ res.status(500).json({ error: `Failed to copy file: ${err instanceof Error ? err.message : err}` });
97
+ }
98
+ });
99
+ // POST /api/drive/files/:fileId/move — Move a file to a different folder (cross-drive supported)
100
+ router.post('/files/:fileId/move', async (req, res) => {
101
+ try {
102
+ const { folderId, removeFromFolderIds, agentId, workflowInstanceId } = req.body || {};
103
+ if (!folderId) {
104
+ res.status(400).json({ error: 'folderId is required' });
105
+ return;
106
+ }
107
+ const file = await driveClient.moveFile(req.params.fileId, {
108
+ folderId,
109
+ removeFromFolderIds,
110
+ agentId,
111
+ workflowInstanceId,
112
+ });
113
+ res.json({ file });
114
+ }
115
+ catch (err) {
116
+ log.error(`Drive move file error: ${err}`);
117
+ res.status(500).json({ error: `Failed to move file: ${err instanceof Error ? err.message : err}` });
118
+ }
119
+ });
120
+ // POST /api/drive/files/:fileId/replace-text — Find/replace text inside a Google Doc (preserves formatting)
121
+ router.post('/files/:fileId/replace-text', async (req, res) => {
122
+ try {
123
+ const { replacements, appendText, agentId, workflowInstanceId } = req.body || {};
124
+ if (!Array.isArray(replacements) && !appendText) {
125
+ res.status(400).json({ error: 'replacements (array) or appendText (string) is required' });
126
+ return;
127
+ }
128
+ const result = await driveClient.replaceTextInDoc(req.params.fileId, {
129
+ replacements: Array.isArray(replacements) ? replacements : [],
130
+ appendText,
131
+ agentId,
132
+ workflowInstanceId,
133
+ });
134
+ res.json(result);
135
+ }
136
+ catch (err) {
137
+ log.error(`Drive replace-text error: ${err}`);
138
+ res.status(500).json({ error: `Failed to replace text in doc: ${err instanceof Error ? err.message : err}` });
139
+ }
140
+ });
141
+ // PATCH /api/drive/files/:fileId — Update a file (content, name, or both)
142
+ router.patch('/files/:fileId', async (req, res) => {
143
+ try {
144
+ const file = await driveClient.updateFile(req.params.fileId, req.body);
145
+ res.json({ file });
146
+ }
147
+ catch (err) {
148
+ log.error(`Drive update file error: ${err}`);
149
+ res.status(500).json({ error: `Failed to update file: ${err instanceof Error ? err.message : err}` });
150
+ }
151
+ });
152
+ // DELETE /api/drive/files/:fileId — Delete a file
153
+ router.delete('/files/:fileId', async (req, res) => {
154
+ try {
155
+ await driveClient.deleteFile(req.params.fileId, {
156
+ agentId: req.body?.agentId,
157
+ workflowInstanceId: req.body?.workflowInstanceId,
158
+ });
159
+ res.json({ success: true });
160
+ }
161
+ catch (err) {
162
+ log.error(`Drive delete file error: ${err}`);
163
+ res.status(500).json({ error: `Failed to delete file: ${err instanceof Error ? err.message : err}` });
164
+ }
165
+ });
166
+ // ─── Google Docs API ───
167
+ // Direct passthrough to the Docs API (documents.create / get / batchUpdate).
168
+ // Use these for native Google Docs when you need full-fidelity access (styles,
169
+ // structure, inline objects) that Drive's export can't give you.
170
+ // POST /api/drive/docs — Create a blank Google Doc with a title
171
+ router.post('/docs', async (req, res) => {
172
+ try {
173
+ const { title, agentId, workflowInstanceId } = req.body || {};
174
+ if (!title) {
175
+ res.status(400).json({ error: 'title is required' });
176
+ return;
177
+ }
178
+ const file = await driveClient.createDocument({ title, agentId, workflowInstanceId });
179
+ res.json({ file });
180
+ }
181
+ catch (err) {
182
+ log.error(`Docs create error: ${err}`);
183
+ res.status(500).json({ error: `Failed to create document: ${err instanceof Error ? err.message : err}` });
184
+ }
185
+ });
186
+ // GET /api/drive/docs/:docId — Get the full structured document
187
+ router.get('/docs/:docId', async (req, res) => {
188
+ try {
189
+ const document = await driveClient.getDocument(req.params.docId);
190
+ res.json({ document });
191
+ }
192
+ catch (err) {
193
+ log.error(`Docs get error: ${err}`);
194
+ res.status(500).json({ error: `Failed to get document: ${err instanceof Error ? err.message : err}` });
195
+ }
196
+ });
197
+ // POST /api/drive/docs/:docId/batch-update — Generic Docs batchUpdate passthrough
198
+ router.post('/docs/:docId/batch-update', async (req, res) => {
199
+ try {
200
+ const { requests, writeControl, agentId, workflowInstanceId } = req.body || {};
201
+ if (!Array.isArray(requests) || requests.length === 0) {
202
+ res.status(400).json({ error: 'requests (non-empty array) is required' });
203
+ return;
204
+ }
205
+ const result = await driveClient.batchUpdateDocument(req.params.docId, {
206
+ requests,
207
+ writeControl,
208
+ agentId,
209
+ workflowInstanceId,
210
+ });
211
+ res.json(result);
212
+ }
213
+ catch (err) {
214
+ log.error(`Docs batch-update error: ${err}`);
215
+ res.status(500).json({ error: `Failed to batch-update document: ${err instanceof Error ? err.message : err}` });
216
+ }
217
+ });
218
+ // ─── Folders ───
219
+ // POST /api/drive/folders — Create a folder
220
+ router.post('/folders', async (req, res) => {
221
+ try {
222
+ const { name, parentFolderId, agentId, workflowInstanceId } = req.body;
223
+ if (!name) {
224
+ res.status(400).json({ error: 'name is required' });
225
+ return;
226
+ }
227
+ const folder = await driveClient.createFolder(name, parentFolderId, {
228
+ agentId,
229
+ workflowInstanceId,
230
+ });
231
+ res.json({ folder });
232
+ }
233
+ catch (err) {
234
+ log.error(`Drive create folder error: ${err}`);
235
+ res.status(500).json({ error: `Failed to create folder: ${err instanceof Error ? err.message : err}` });
236
+ }
237
+ });
238
+ // ─── Shared Drives (Team Drives) ───
239
+ // GET /api/drive/drives — List Shared Drives the user has access to
240
+ router.get('/drives', async (req, res) => {
241
+ try {
242
+ const result = await driveClient.listSharedDrives({
243
+ maxResults: req.query.maxResults ? parseInt(req.query.maxResults, 10) : undefined,
244
+ pageToken: req.query.pageToken,
245
+ query: req.query.q,
246
+ useDomainAdminAccess: req.query.useDomainAdminAccess === 'true',
247
+ });
248
+ res.json(result);
249
+ }
250
+ catch (err) {
251
+ log.error(`Drive list shared drives error: ${err}`);
252
+ res.status(500).json({ error: `Failed to list shared drives: ${err instanceof Error ? err.message : err}` });
253
+ }
254
+ });
255
+ // GET /api/drive/drives/:driveId — Get metadata for a Shared Drive
256
+ router.get('/drives/:driveId', async (req, res) => {
257
+ try {
258
+ const drive = await driveClient.getSharedDrive(req.params.driveId);
259
+ res.json({ drive });
260
+ }
261
+ catch (err) {
262
+ log.error(`Drive get shared drive error: ${err}`);
263
+ res.status(500).json({ error: `Failed to get shared drive: ${err instanceof Error ? err.message : err}` });
264
+ }
265
+ });
266
+ // ─── Search ───
267
+ // GET /api/drive/search — Full-text search across files
268
+ router.get('/search', async (req, res) => {
269
+ try {
270
+ const queryText = req.query.q;
271
+ if (!queryText) {
272
+ res.status(400).json({ error: 'q (search query) is required' });
273
+ return;
274
+ }
275
+ const maxResults = req.query.maxResults ? parseInt(req.query.maxResults, 10) : undefined;
276
+ const files = await driveClient.searchFiles(queryText, maxResults);
277
+ res.json({ files });
278
+ }
279
+ catch (err) {
280
+ log.error(`Drive search error: ${err}`);
281
+ res.status(500).json({ error: `Failed to search files: ${err instanceof Error ? err.message : err}` });
282
+ }
283
+ });
284
+ // ─── Status & Auth ───
285
+ // GET /api/drive/status — Get Google Drive auth status
286
+ router.get('/status', (_req, res) => {
287
+ const status = driveClient.getStatus();
288
+ res.json(status);
289
+ });
290
+ // GET /api/drive/auth/url — Get OAuth authorization URL
291
+ router.get('/auth/url', (_req, res) => {
292
+ try {
293
+ const authUrl = driveClient.getAuthUrl();
294
+ res.json({ url: authUrl });
295
+ }
296
+ catch (err) {
297
+ log.error(`Failed to get drive auth URL: ${err}`);
298
+ res.status(500).json({ error: `Failed to get auth URL: ${err instanceof Error ? err.message : err}` });
299
+ }
300
+ });
301
+ // GET /api/drive/auth/callback — Handle OAuth callback from Google
302
+ router.get('/auth/callback', async (req, res) => {
303
+ try {
304
+ const code = req.query.code;
305
+ const error = req.query.error;
306
+ if (error) {
307
+ const errorDescription = req.query.error_description;
308
+ log.warn(`OAuth consent denied: ${error} - ${errorDescription}`);
309
+ const errorMsg = errorDescription || error;
310
+ res.send(`
311
+ <html>
312
+ <head><title>Drive Authorization Failed</title></head>
313
+ <body>
314
+ <h1>Authorization Denied</h1>
315
+ <p>You denied access to your Google Drive: ${errorMsg}</p>
316
+ <p><a href="/">Return to the app</a></p>
317
+ </body>
318
+ </html>
319
+ `);
320
+ return;
321
+ }
322
+ if (!code) {
323
+ res.status(400).send('Missing authorization code');
324
+ return;
325
+ }
326
+ log.log(`Google Drive OAuth callback received, processing code...`);
327
+ await driveClient.handleAuthCallback(code);
328
+ log.log('Google Drive OAuth callback completed successfully');
329
+ res.send(`
330
+ <html>
331
+ <head>
332
+ <title>Drive Authorization Successful</title>
333
+ <script>
334
+ if (window.opener) {
335
+ window.opener.postMessage({ type: 'drive-auth-success' }, '*');
336
+ setTimeout(() => window.close(), 1000);
337
+ } else {
338
+ setTimeout(() => { window.location.href = '/?drive-auth=success'; }, 2000);
339
+ }
340
+ </script>
341
+ </head>
342
+ <body style="font-family: sans-serif; padding: 40px; text-align: center;">
343
+ <h1>Drive Authorization Successful!</h1>
344
+ <p>Your Google Drive has been connected.</p>
345
+ <p>Closing this window or redirecting you to the app...</p>
346
+ </body>
347
+ </html>
348
+ `);
349
+ }
350
+ catch (err) {
351
+ const errorMsg = err instanceof Error ? err.message : String(err);
352
+ log.error(`Google Drive auth callback error: ${errorMsg}`, err);
353
+ res.status(500).send(`
354
+ <html>
355
+ <head><title>Drive Authorization Failed</title></head>
356
+ <body style="font-family: sans-serif; padding: 40px;">
357
+ <h1>Authorization Failed</h1>
358
+ <p><strong>Error:</strong> ${errorMsg}</p>
359
+ <p><a href="/">Return to the app and try again</a></p>
360
+ </body>
361
+ </html>
362
+ `);
363
+ }
364
+ });
365
+ export default router;