tide-commander 1.86.0 → 1.87.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 (45) hide show
  1. package/dist/assets/{BossLogsModal-BB1vCM5J.js → BossLogsModal-S3Rke-8g.js} +1 -1
  2. package/dist/assets/{BossSpawnModal-wMLySpgn.js → BossSpawnModal-BjWGNCnz.js} +1 -1
  3. package/dist/assets/{ControlsModal-BklEN7j8.js → ControlsModal-6yfU0XjZ.js} +1 -1
  4. package/dist/assets/{DockerLogsModal-B49O9xcw.js → DockerLogsModal-CYq0hNz6.js} +1 -1
  5. package/dist/assets/{EmbeddedEditor-BX9aBsiv.js → EmbeddedEditor-ZBdqRDqm.js} +1 -1
  6. package/dist/assets/GmailOAuthSetup-BcV5jAse.js +388 -0
  7. package/dist/assets/{GoogleOAuthSetup-4nw4iS8V.js → GoogleOAuthSetup-DyUW_STE.js} +2 -2
  8. package/dist/assets/{IframeModal-DGXje5GK.js → IframeModal-D9A3dUUc.js} +1 -1
  9. package/dist/assets/{IntegrationsPanel-CjtCtu7-.js → IntegrationsPanel-CHaNJBJW.js} +2 -2
  10. package/dist/assets/{LogViewerModal-CnOV3Ai5.js → LogViewerModal-BWkbY7wa.js} +1 -1
  11. package/dist/assets/{MonitoringModal-VwORpB9L.js → MonitoringModal-AZzokZAZ.js} +1 -1
  12. package/dist/assets/{PM2LogsModal-8bnD0XcQ.js → PM2LogsModal-q98eiBfq.js} +1 -1
  13. package/dist/assets/{RestoreArchivedAreaModal-BhwpPzFY.js → RestoreArchivedAreaModal-CTxRP2qE.js} +1 -1
  14. package/dist/assets/{Scene2DCanvas-DaRWkux-.js → Scene2DCanvas-C11dztp1.js} +1 -1
  15. package/dist/assets/{SceneManager-CKz-v1rU.js → SceneManager-CsW9MYrD.js} +1 -1
  16. package/dist/assets/{SkillsPanel-l0DKbOtc.js → SkillsPanel-BeZr9w6E.js} +1 -1
  17. package/dist/assets/{SpawnModal-DEAh9hdW.js → SpawnModal-DY_KM6lX.js} +1 -1
  18. package/dist/assets/{SubordinateAssignmentModal-CczaDW9_.js → SubordinateAssignmentModal-D6RvjGX9.js} +1 -1
  19. package/dist/assets/{TriggerManagerPanel-aczhr8bG.js → TriggerManagerPanel-BmqjXv9T.js} +1 -1
  20. package/dist/assets/{WorkflowEditorPanel-Pp473-yC.js → WorkflowEditorPanel-Rd5ZjJmt.js} +1 -1
  21. package/dist/assets/{index-BrntsiFl.js → index-BOr_tbLK.js} +1 -1
  22. package/dist/assets/{index-Da106QNu.js → index-BYVHgVEo.js} +1 -1
  23. package/dist/assets/{index-VOQ8B_0s.js → index-BoORE9Q1.js} +1 -1
  24. package/dist/assets/{index-CsZl2cpZ.js → index-BtJyOo4p.js} +1 -1
  25. package/dist/assets/{index-DAsa2Q29.js → index-BxaEkSIx.js} +3 -3
  26. package/dist/assets/{index-CfEa34G0.js → index-Co7njQ0Q.js} +1 -1
  27. package/dist/assets/{index-DEEA8DzD.js → index-DHHRkTG1.js} +1 -1
  28. package/dist/assets/{index-bumMaJlF.js → index-DRGyDtmm.js} +1 -1
  29. package/dist/assets/{index-Bsu-4DMd.js → index-DSvJOrb7.js} +2 -2
  30. package/dist/assets/{main-D2EQVTAu.js → main-BrZe9Zbd.js} +4 -4
  31. package/dist/assets/{web-DlbC6v-L.js → web-D3zCwsS9.js} +1 -1
  32. package/dist/assets/{web-CZQVhlQA.js → web-DEq3Te_H.js} +1 -1
  33. package/dist/assets/{web-Bdm2kdQ5.js → web-DS0FHmg8.js} +1 -1
  34. package/dist/index.html +1 -1
  35. package/dist/src/packages/server/integrations/gmail/gmail-client.js +15 -2
  36. package/dist/src/packages/server/integrations/gmail/gmail-config.js +8 -0
  37. package/dist/src/packages/server/integrations/gmail/index.js +12 -2
  38. package/dist/src/packages/server/integrations/google-calendar/calendar-client.js +10 -2
  39. package/dist/src/packages/server/integrations/google-calendar/calendar-config.js +13 -0
  40. package/dist/src/packages/server/integrations/google-calendar/index.js +7 -0
  41. package/dist/src/packages/server/integrations/google-drive/drive-client.js +10 -2
  42. package/dist/src/packages/server/integrations/google-drive/drive-config.js +13 -0
  43. package/dist/src/packages/server/integrations/google-drive/index.js +7 -0
  44. package/package.json +1 -1
  45. package/dist/assets/GmailOAuthSetup-BzB4QGx5.js +0 -388
@@ -1 +1 @@
1
- import{ck as s}from"./main-D2EQVTAu.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-Chj50gSY.js";class l extends s{constructor(){super(...arguments),this.pending=[],this.deliveredNotifications=[],this.hasNotificationSupport=()=>{if(!("Notification"in window)||!Notification.requestPermission)return!1;if(Notification.permission!=="granted")try{new Notification("")}catch(i){if(i instanceof Error&&i.name==="TypeError")return!1}return!0}}async getDeliveredNotifications(){const i=[];for(const t of this.deliveredNotifications){const e={title:t.title,id:parseInt(t.tag),body:t.body};i.push(e)}return{notifications:i}}async removeDeliveredNotifications(i){for(const t of i.notifications){const e=this.deliveredNotifications.find(n=>n.tag===String(t.id));e==null||e.close(),this.deliveredNotifications=this.deliveredNotifications.filter(()=>!e)}}async removeAllDeliveredNotifications(){for(const i of this.deliveredNotifications)i.close();this.deliveredNotifications=[]}async createChannel(){throw this.unimplemented("Not implemented on web.")}async deleteChannel(){throw this.unimplemented("Not implemented on web.")}async listChannels(){throw this.unimplemented("Not implemented on web.")}async schedule(i){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");for(const t of i.notifications)this.sendNotification(t);return{notifications:i.notifications.map(t=>({id:t.id}))}}async getPending(){return{notifications:this.pending}}async registerActionTypes(){throw this.unimplemented("Not implemented on web.")}async cancel(i){this.pending=this.pending.filter(t=>!i.notifications.find(e=>e.id===t.id))}async areEnabled(){const{display:i}=await this.checkPermissions();return{value:i==="granted"}}async changeExactNotificationSetting(){throw this.unimplemented("Not implemented on web.")}async checkExactNotificationSetting(){throw this.unimplemented("Not implemented on web.")}async requestPermissions(){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");return{display:this.transformNotificationPermission(await Notification.requestPermission())}}async checkPermissions(){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");return{display:this.transformNotificationPermission(Notification.permission)}}transformNotificationPermission(i){switch(i){case"granted":return"granted";case"denied":return"denied";default:return"prompt"}}sendPending(){var i;const t=[],e=new Date().getTime();for(const n of this.pending)!((i=n.schedule)===null||i===void 0)&&i.at&&n.schedule.at.getTime()<=e&&(this.buildNotification(n),t.push(n));this.pending=this.pending.filter(n=>!t.find(o=>o===n))}sendNotification(i){var t;if(!((t=i.schedule)===null||t===void 0)&&t.at){const e=i.schedule.at.getTime()-new Date().getTime();this.pending.push(i),setTimeout(()=>{this.sendPending()},e);return}this.buildNotification(i)}buildNotification(i){const t=new Notification(i.title,{body:i.body,tag:String(i.id)});return t.addEventListener("click",this.onClick.bind(this,i),!1),t.addEventListener("show",this.onShow.bind(this,i),!1),t.addEventListener("close",()=>{this.deliveredNotifications=this.deliveredNotifications.filter(()=>!this)},!1),this.deliveredNotifications.push(t),t}onClick(i){const t={actionId:"tap",notification:i};this.notifyListeners("localNotificationActionPerformed",t)}onShow(i){this.notifyListeners("localNotificationReceived",i)}}export{l as LocalNotificationsWeb};
1
+ import{ck as s}from"./main-BrZe9Zbd.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-Chj50gSY.js";class l extends s{constructor(){super(...arguments),this.pending=[],this.deliveredNotifications=[],this.hasNotificationSupport=()=>{if(!("Notification"in window)||!Notification.requestPermission)return!1;if(Notification.permission!=="granted")try{new Notification("")}catch(i){if(i instanceof Error&&i.name==="TypeError")return!1}return!0}}async getDeliveredNotifications(){const i=[];for(const t of this.deliveredNotifications){const e={title:t.title,id:parseInt(t.tag),body:t.body};i.push(e)}return{notifications:i}}async removeDeliveredNotifications(i){for(const t of i.notifications){const e=this.deliveredNotifications.find(n=>n.tag===String(t.id));e==null||e.close(),this.deliveredNotifications=this.deliveredNotifications.filter(()=>!e)}}async removeAllDeliveredNotifications(){for(const i of this.deliveredNotifications)i.close();this.deliveredNotifications=[]}async createChannel(){throw this.unimplemented("Not implemented on web.")}async deleteChannel(){throw this.unimplemented("Not implemented on web.")}async listChannels(){throw this.unimplemented("Not implemented on web.")}async schedule(i){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");for(const t of i.notifications)this.sendNotification(t);return{notifications:i.notifications.map(t=>({id:t.id}))}}async getPending(){return{notifications:this.pending}}async registerActionTypes(){throw this.unimplemented("Not implemented on web.")}async cancel(i){this.pending=this.pending.filter(t=>!i.notifications.find(e=>e.id===t.id))}async areEnabled(){const{display:i}=await this.checkPermissions();return{value:i==="granted"}}async changeExactNotificationSetting(){throw this.unimplemented("Not implemented on web.")}async checkExactNotificationSetting(){throw this.unimplemented("Not implemented on web.")}async requestPermissions(){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");return{display:this.transformNotificationPermission(await Notification.requestPermission())}}async checkPermissions(){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");return{display:this.transformNotificationPermission(Notification.permission)}}transformNotificationPermission(i){switch(i){case"granted":return"granted";case"denied":return"denied";default:return"prompt"}}sendPending(){var i;const t=[],e=new Date().getTime();for(const n of this.pending)!((i=n.schedule)===null||i===void 0)&&i.at&&n.schedule.at.getTime()<=e&&(this.buildNotification(n),t.push(n));this.pending=this.pending.filter(n=>!t.find(o=>o===n))}sendNotification(i){var t;if(!((t=i.schedule)===null||t===void 0)&&t.at){const e=i.schedule.at.getTime()-new Date().getTime();this.pending.push(i),setTimeout(()=>{this.sendPending()},e);return}this.buildNotification(i)}buildNotification(i){const t=new Notification(i.title,{body:i.body,tag:String(i.id)});return t.addEventListener("click",this.onClick.bind(this,i),!1),t.addEventListener("show",this.onShow.bind(this,i),!1),t.addEventListener("close",()=>{this.deliveredNotifications=this.deliveredNotifications.filter(()=>!this)},!1),this.deliveredNotifications.push(t),t}onClick(i){const t={actionId:"tap",notification:i};this.notifyListeners("localNotificationActionPerformed",t)}onShow(i){this.notifyListeners("localNotificationReceived",i)}}export{l as LocalNotificationsWeb};
@@ -1 +1 @@
1
- import{ck as t}from"./main-D2EQVTAu.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-Chj50gSY.js";class o extends t{constructor(){super(),this.handleVisibilityChange=()=>{const e={isActive:document.hidden!==!0};this.notifyListeners("appStateChange",e),document.hidden?this.notifyListeners("pause",null):this.notifyListeners("resume",null)},document.addEventListener("visibilitychange",this.handleVisibilityChange,!1)}exitApp(){throw this.unimplemented("Not implemented on web.")}async getInfo(){throw this.unimplemented("Not implemented on web.")}async getLaunchUrl(){return{url:""}}async getState(){return{isActive:document.hidden!==!0}}async minimizeApp(){throw this.unimplemented("Not implemented on web.")}async toggleBackButtonHandler(){throw this.unimplemented("Not implemented on web.")}async getAppLanguage(){return{value:navigator.language.split("-")[0].toLowerCase()}}}export{o as AppWeb};
1
+ import{ck as t}from"./main-BrZe9Zbd.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-Chj50gSY.js";class o extends t{constructor(){super(),this.handleVisibilityChange=()=>{const e={isActive:document.hidden!==!0};this.notifyListeners("appStateChange",e),document.hidden?this.notifyListeners("pause",null):this.notifyListeners("resume",null)},document.addEventListener("visibilitychange",this.handleVisibilityChange,!1)}exitApp(){throw this.unimplemented("Not implemented on web.")}async getInfo(){throw this.unimplemented("Not implemented on web.")}async getLaunchUrl(){return{url:""}}async getState(){return{isActive:document.hidden!==!0}}async minimizeApp(){throw this.unimplemented("Not implemented on web.")}async toggleBackButtonHandler(){throw this.unimplemented("Not implemented on web.")}async getAppLanguage(){return{value:navigator.language.split("-")[0].toLowerCase()}}}export{o as AppWeb};
@@ -1 +1 @@
1
- import{ck as a}from"./main-D2EQVTAu.js";import{ImpactStyle as i,NotificationType as r}from"./index-Bsu-4DMd.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-Chj50gSY.js";class h extends a{constructor(){super(...arguments),this.selectionStarted=!1}async impact(t){const e=this.patternForImpact(t==null?void 0:t.style);this.vibrateWithPattern(e)}async notification(t){const e=this.patternForNotification(t==null?void 0:t.type);this.vibrateWithPattern(e)}async vibrate(t){const e=(t==null?void 0:t.duration)||300;this.vibrateWithPattern([e])}async selectionStart(){this.selectionStarted=!0}async selectionChanged(){this.selectionStarted&&this.vibrateWithPattern([70])}async selectionEnd(){this.selectionStarted=!1}patternForImpact(t=i.Heavy){return t===i.Medium?[43]:t===i.Light?[20]:[61]}patternForNotification(t=r.Success){return t===r.Warning?[30,40,30,50,60]:t===r.Error?[27,45,50]:[35,65,21]}vibrateWithPattern(t){if(navigator.vibrate)navigator.vibrate(t);else throw this.unavailable("Browser does not support the vibrate API")}}export{h as HapticsWeb};
1
+ import{ck as a}from"./main-BrZe9Zbd.js";import{ImpactStyle as i,NotificationType as r}from"./index-DSvJOrb7.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-Chj50gSY.js";class h extends a{constructor(){super(...arguments),this.selectionStarted=!1}async impact(t){const e=this.patternForImpact(t==null?void 0:t.style);this.vibrateWithPattern(e)}async notification(t){const e=this.patternForNotification(t==null?void 0:t.type);this.vibrateWithPattern(e)}async vibrate(t){const e=(t==null?void 0:t.duration)||300;this.vibrateWithPattern([e])}async selectionStart(){this.selectionStarted=!0}async selectionChanged(){this.selectionStarted&&this.vibrateWithPattern([70])}async selectionEnd(){this.selectionStarted=!1}patternForImpact(t=i.Heavy){return t===i.Medium?[43]:t===i.Light?[20]:[61]}patternForNotification(t=r.Success){return t===r.Warning?[30,40,30,50,60]:t===r.Error?[27,45,50]:[35,65,21]}vibrateWithPattern(t){if(navigator.vibrate)navigator.vibrate(t);else throw this.unavailable("Browser does not support the vibrate API")}}export{h as HapticsWeb};
package/dist/index.html CHANGED
@@ -22,7 +22,7 @@
22
22
  <link rel="icon" type="image/png" sizes="16x16" href="/assets/icons/favicon-16x16.png" />
23
23
  <link rel="apple-touch-icon" sizes="180x180" href="/assets/icons/apple-touch-icon.png" />
24
24
  <title>Tide Commander</title>
25
- <script type="module" crossorigin src="/assets/main-D2EQVTAu.js"></script>
25
+ <script type="module" crossorigin src="/assets/main-BrZe9Zbd.js"></script>
26
26
  <link rel="modulepreload" crossorigin href="/assets/vendor-react--Eh9ivFN.js">
27
27
  <link rel="modulepreload" crossorigin href="/assets/vendor-three-Chj50gSY.js">
28
28
  <link rel="stylesheet" crossorigin href="/assets/main-kpU9m5LW.css">
@@ -71,7 +71,7 @@ export async function init(context) {
71
71
  ctx.log.info('Gmail not configured (missing OAuth credentials)');
72
72
  return;
73
73
  }
74
- oauth2Client = new google.auth.OAuth2(config.clientId, config.clientSecret, `${ctx.serverConfig.baseUrl}${REDIRECT_PATH}`);
74
+ oauth2Client = new google.auth.OAuth2(config.clientId, config.clientSecret, getRedirectUri());
75
75
  // Load refresh token from secrets
76
76
  const refreshToken = ctx.secrets.get('GOOGLE_REFRESH_TOKEN') || config.refreshToken;
77
77
  if (refreshToken) {
@@ -100,15 +100,23 @@ function loadConfig() {
100
100
  const refreshToken = ctx.secrets.get('GOOGLE_REFRESH_TOKEN');
101
101
  const serviceAccountJson = ctx.secrets.get('GOOGLE_SERVICE_ACCOUNT_JSON') || undefined;
102
102
  const impersonateEmail = ctx.secrets.get('GOOGLE_IMPERSONATE_EMAIL') || undefined;
103
+ const redirectBaseUrl = ctx.secrets.get('GOOGLE_REDIRECT_BASE_URL') || undefined;
103
104
  config = {
104
105
  ...config,
105
106
  clientId,
106
107
  clientSecret,
108
+ redirectBaseUrl,
107
109
  refreshToken: refreshToken || undefined,
108
110
  serviceAccountJson,
109
111
  impersonateEmail,
110
112
  };
111
113
  }
114
+ function getRedirectUri() {
115
+ if (!ctx)
116
+ throw new Error('Gmail not initialized');
117
+ const base = (config.redirectBaseUrl?.trim() || ctx.serverConfig.baseUrl).replace(/\/$/, '');
118
+ return `${base}${REDIRECT_PATH}`;
119
+ }
112
120
  export function updateConfig(updates) {
113
121
  if (updates.authMethod !== undefined) {
114
122
  config.authMethod = updates.authMethod;
@@ -121,6 +129,11 @@ export function updateConfig(updates) {
121
129
  config.clientSecret = updates.clientSecret;
122
130
  ctx?.secrets.set('GOOGLE_CLIENT_SECRET', updates.clientSecret);
123
131
  }
132
+ if (updates.redirectBaseUrl !== undefined) {
133
+ const trimmed = updates.redirectBaseUrl.trim();
134
+ config.redirectBaseUrl = trimmed || undefined;
135
+ ctx?.secrets.set('GOOGLE_REDIRECT_BASE_URL', trimmed);
136
+ }
124
137
  if (updates.serviceAccountJson !== undefined) {
125
138
  config.serviceAccountJson = updates.serviceAccountJson;
126
139
  ctx?.secrets.set('GOOGLE_SERVICE_ACCOUNT_JSON', updates.serviceAccountJson);
@@ -168,7 +181,7 @@ export function getAuthUrl() {
168
181
  if (!config.clientId || !config.clientSecret || !ctx) {
169
182
  throw new Error('Gmail OAuth not configured');
170
183
  }
171
- oauth2Client = new google.auth.OAuth2(config.clientId, config.clientSecret, `${ctx.serverConfig.baseUrl}${REDIRECT_PATH}`);
184
+ oauth2Client = new google.auth.OAuth2(config.clientId, config.clientSecret, getRedirectUri());
172
185
  }
173
186
  return oauth2Client.generateAuthUrl({
174
187
  access_type: 'offline',
@@ -34,6 +34,14 @@ export const gmailConfigSchema = [
34
34
  secret: true,
35
35
  group: 'Authentication',
36
36
  },
37
+ {
38
+ key: 'redirectBaseUrl',
39
+ label: 'OAuth Redirect Base URL',
40
+ type: 'text',
41
+ description: 'Override the base URL used for the OAuth redirect (e.g. http://commander.local:10003). Leave empty to use http://localhost:<port>. Google does not accept raw IPs — use a domain (you can map one in /etc/hosts).',
42
+ required: false,
43
+ group: 'Authentication',
44
+ },
37
45
  {
38
46
  key: 'serviceAccountJson',
39
47
  label: 'Service Account JSON',
@@ -42,6 +42,7 @@ export const gmailPlugin = {
42
42
  authMethod: 'oauth2',
43
43
  clientId: '',
44
44
  clientSecret: '',
45
+ redirectBaseUrl: '',
45
46
  serviceAccountJson: '',
46
47
  impersonateEmail: '',
47
48
  pollingIntervalMs: 30000,
@@ -54,6 +55,7 @@ export const gmailPlugin = {
54
55
  // Calendar and Drive integrations, which share these same secrets).
55
56
  clientId: integrationCtx.secrets.get('GOOGLE_CLIENT_ID') ? '********' : '',
56
57
  clientSecret: integrationCtx.secrets.get('GOOGLE_CLIENT_SECRET') ? '********' : '',
58
+ redirectBaseUrl: integrationCtx.secrets.get('GOOGLE_REDIRECT_BASE_URL') || '',
57
59
  serviceAccountJson: integrationCtx.secrets.get('GOOGLE_SERVICE_ACCOUNT_JSON') ? '********' : '',
58
60
  impersonateEmail: integrationCtx.secrets.get('GOOGLE_IMPERSONATE_EMAIL') || '',
59
61
  pollingIntervalMs: gmailClient.getConfig().pollingIntervalMs,
@@ -75,6 +77,11 @@ export const gmailPlugin = {
75
77
  updates.clientSecret = config.clientSecret;
76
78
  integrationCtx.secrets.set('GOOGLE_CLIENT_SECRET', config.clientSecret);
77
79
  }
80
+ if (config.redirectBaseUrl !== undefined) {
81
+ const value = String(config.redirectBaseUrl).trim();
82
+ updates.redirectBaseUrl = value;
83
+ integrationCtx.secrets.set('GOOGLE_REDIRECT_BASE_URL', value);
84
+ }
78
85
  if (config.serviceAccountJson && config.serviceAccountJson !== '********') {
79
86
  updates.serviceAccountJson = config.serviceAccountJson;
80
87
  integrationCtx.secrets.set('GOOGLE_SERVICE_ACCOUNT_JSON', config.serviceAccountJson);
@@ -94,8 +101,11 @@ export const gmailPlugin = {
94
101
  updates.defaultApprovalKeywords = keywords;
95
102
  }
96
103
  gmailClient.updateConfig(updates);
97
- // Re-initialize authentication when auth method or credentials change
98
- if (config.authMethod !== undefined || config.serviceAccountJson || config.clientId) {
104
+ // Re-initialize authentication when auth method, credentials, or redirect URL change
105
+ if (config.authMethod !== undefined
106
+ || config.serviceAccountJson
107
+ || config.clientId
108
+ || config.redirectBaseUrl !== undefined) {
99
109
  gmailClient.shutdown();
100
110
  await gmailClient.init(integrationCtx);
101
111
  }
@@ -24,13 +24,14 @@ export async function init(integrationCtx) {
24
24
  ctx.log.info('Google Calendar missing OAuth credentials, skipping init');
25
25
  return;
26
26
  }
27
- oauth2Client = new google.auth.OAuth2(clientId, clientSecret, `${ctx.serverConfig.baseUrl}${REDIRECT_PATH}`);
27
+ oauth2Client = new google.auth.OAuth2(clientId, clientSecret, getRedirectUri());
28
28
  oauth2Client.setCredentials({ refresh_token: refreshToken });
29
29
  calendarApi = google.calendar({ version: 'v3', auth: oauth2Client });
30
30
  ctx.log.info('Google Calendar initialized');
31
31
  }
32
32
  export async function shutdown() {
33
33
  calendarApi = null;
34
+ oauth2Client = null;
34
35
  }
35
36
  // ─── Status ───
36
37
  export function getStatus() {
@@ -208,6 +209,13 @@ const SCOPES = [
208
209
  ];
209
210
  const REDIRECT_PATH = '/api/calendar/auth/callback'; // Calendar's own callback
210
211
  let oauth2Client = null;
212
+ function getRedirectUri() {
213
+ if (!ctx)
214
+ throw new Error('Google Calendar not initialized');
215
+ const override = ctx.secrets.get('GOOGLE_REDIRECT_BASE_URL')?.trim();
216
+ const base = (override || ctx.serverConfig.baseUrl).replace(/\/$/, '');
217
+ return `${base}${REDIRECT_PATH}`;
218
+ }
211
219
  export function getAuthUrl() {
212
220
  if (!oauth2Client) {
213
221
  const clientId = ctx?.secrets.get('GOOGLE_CLIENT_ID');
@@ -215,7 +223,7 @@ export function getAuthUrl() {
215
223
  if (!clientId || !clientSecret || !ctx) {
216
224
  throw new Error('Google Calendar OAuth not configured');
217
225
  }
218
- oauth2Client = new google.auth.OAuth2(clientId, clientSecret, `${ctx.serverConfig.baseUrl}${REDIRECT_PATH}`);
226
+ oauth2Client = new google.auth.OAuth2(clientId, clientSecret, getRedirectUri());
219
227
  }
220
228
  return oauth2Client.generateAuthUrl({
221
229
  access_type: 'offline',
@@ -91,6 +91,14 @@ export const calendarConfigSchema = [
91
91
  secret: true,
92
92
  group: 'Authentication',
93
93
  },
94
+ {
95
+ key: 'GOOGLE_REDIRECT_BASE_URL',
96
+ label: 'OAuth Redirect Base URL',
97
+ type: 'text',
98
+ description: 'Override the base URL used for the OAuth redirect (e.g. http://commander.local:10003). Leave empty to use http://localhost:<port>. Shared with Gmail/Drive. Google does not accept raw IPs — use a domain (you can map one in /etc/hosts).',
99
+ required: false,
100
+ group: 'Authentication',
101
+ },
94
102
  {
95
103
  key: 'calendarId',
96
104
  label: 'Calendar ID',
@@ -128,6 +136,7 @@ export function getConfigValues(secrets) {
128
136
  GOOGLE_CLIENT_ID: secrets.get('GOOGLE_CLIENT_ID') ? '********' : '',
129
137
  GOOGLE_CLIENT_SECRET: secrets.get('GOOGLE_CLIENT_SECRET') ? '********' : '',
130
138
  GOOGLE_REFRESH_TOKEN: secrets.get('GOOGLE_REFRESH_TOKEN') ? '********' : '',
139
+ GOOGLE_REDIRECT_BASE_URL: secrets.get('GOOGLE_REDIRECT_BASE_URL') || '',
131
140
  };
132
141
  }
133
142
  export async function setConfigValues(values, secrets) {
@@ -138,6 +147,10 @@ export async function setConfigValues(values, secrets) {
138
147
  secrets.set(key, val);
139
148
  }
140
149
  }
150
+ // Redirect base URL is shared but not masked — write it whenever it's provided (empty string clears it).
151
+ if (typeof values.GOOGLE_REDIRECT_BASE_URL === 'string') {
152
+ secrets.set('GOOGLE_REDIRECT_BASE_URL', values.GOOGLE_REDIRECT_BASE_URL.trim());
153
+ }
141
154
  // Handle non-secret config
142
155
  const updates = {};
143
156
  if (typeof values.enabled === 'boolean')
@@ -55,6 +55,13 @@ export const googleCalendarPlugin = {
55
55
  if (!integrationCtx)
56
56
  throw new Error('Google Calendar not initialized');
57
57
  await setConfigValues(config, integrationCtx.secrets);
58
+ // Re-initialize so any cached oauth2Client picks up new credentials or redirect URL.
59
+ if (config.GOOGLE_CLIENT_ID !== undefined
60
+ || config.GOOGLE_CLIENT_SECRET !== undefined
61
+ || config.GOOGLE_REDIRECT_BASE_URL !== undefined) {
62
+ await calendarClient.shutdown();
63
+ await calendarClient.init(integrationCtx);
64
+ }
58
65
  },
59
66
  getCustomSettingsComponent() {
60
67
  return 'google-oauth';
@@ -22,6 +22,13 @@ const SCOPES = [
22
22
  ];
23
23
  const REDIRECT_PATH = '/api/drive/auth/callback';
24
24
  let oauth2Client = null;
25
+ function getRedirectUri() {
26
+ if (!ctx)
27
+ throw new Error('Google Drive not initialized');
28
+ const override = ctx.secrets.get('GOOGLE_REDIRECT_BASE_URL')?.trim();
29
+ const base = (override || ctx.serverConfig.baseUrl).replace(/\/$/, '');
30
+ return `${base}${REDIRECT_PATH}`;
31
+ }
25
32
  // ─── Init / Shutdown ───
26
33
  export async function init(integrationCtx) {
27
34
  ctx = integrationCtx;
@@ -37,7 +44,7 @@ export async function init(integrationCtx) {
37
44
  ctx.log.info('Google Drive missing OAuth credentials, skipping init');
38
45
  return;
39
46
  }
40
- oauth2Client = new google.auth.OAuth2(clientId, clientSecret, `${ctx.serverConfig.baseUrl}${REDIRECT_PATH}`);
47
+ oauth2Client = new google.auth.OAuth2(clientId, clientSecret, getRedirectUri());
41
48
  oauth2Client.setCredentials({ refresh_token: refreshToken });
42
49
  driveApi = google.drive({ version: 'v3', auth: oauth2Client });
43
50
  docsApi = google.docs({ version: 'v1', auth: oauth2Client });
@@ -46,6 +53,7 @@ export async function init(integrationCtx) {
46
53
  export async function shutdown() {
47
54
  driveApi = null;
48
55
  docsApi = null;
56
+ oauth2Client = null;
49
57
  }
50
58
  // ─── Status ───
51
59
  export function getStatus() {
@@ -71,7 +79,7 @@ export function getAuthUrl() {
71
79
  if (!clientId || !clientSecret || !ctx) {
72
80
  throw new Error('Google Drive OAuth not configured');
73
81
  }
74
- oauth2Client = new google.auth.OAuth2(clientId, clientSecret, `${ctx.serverConfig.baseUrl}${REDIRECT_PATH}`);
82
+ oauth2Client = new google.auth.OAuth2(clientId, clientSecret, getRedirectUri());
75
83
  }
76
84
  return oauth2Client.generateAuthUrl({
77
85
  access_type: 'offline',
@@ -89,6 +89,14 @@ export const driveConfigSchema = [
89
89
  secret: true,
90
90
  group: 'Authentication',
91
91
  },
92
+ {
93
+ key: 'GOOGLE_REDIRECT_BASE_URL',
94
+ label: 'OAuth Redirect Base URL',
95
+ type: 'text',
96
+ description: 'Override the base URL used for the OAuth redirect (e.g. http://commander.local:10003). Leave empty to use http://localhost:<port>. Shared with Gmail/Calendar. Google does not accept raw IPs — use a domain (you can map one in /etc/hosts).',
97
+ required: false,
98
+ group: 'Authentication',
99
+ },
92
100
  {
93
101
  key: 'defaultFolderId',
94
102
  label: 'Default Folder ID',
@@ -107,6 +115,7 @@ export function getConfigValues(secrets) {
107
115
  GOOGLE_CLIENT_ID: secrets.get('GOOGLE_CLIENT_ID') ? '********' : '',
108
116
  GOOGLE_CLIENT_SECRET: secrets.get('GOOGLE_CLIENT_SECRET') ? '********' : '',
109
117
  GOOGLE_REFRESH_TOKEN: secrets.get('GOOGLE_REFRESH_TOKEN') ? '********' : '',
118
+ GOOGLE_REDIRECT_BASE_URL: secrets.get('GOOGLE_REDIRECT_BASE_URL') || '',
110
119
  };
111
120
  }
112
121
  export async function setConfigValues(values, secrets) {
@@ -117,6 +126,10 @@ export async function setConfigValues(values, secrets) {
117
126
  secrets.set(key, val);
118
127
  }
119
128
  }
129
+ // Redirect base URL is shared but not masked — write it whenever it's provided (empty string clears it).
130
+ if (typeof values.GOOGLE_REDIRECT_BASE_URL === 'string') {
131
+ secrets.set('GOOGLE_REDIRECT_BASE_URL', values.GOOGLE_REDIRECT_BASE_URL.trim());
132
+ }
120
133
  // Handle non-secret config
121
134
  const updates = {};
122
135
  if (typeof values.enabled === 'boolean')
@@ -53,6 +53,13 @@ export const googleDrivePlugin = {
53
53
  if (!integrationCtx)
54
54
  throw new Error('Google Drive not initialized');
55
55
  await setConfigValues(config, integrationCtx.secrets);
56
+ // Re-initialize so any cached oauth2Client picks up new credentials or redirect URL.
57
+ if (config.GOOGLE_CLIENT_ID !== undefined
58
+ || config.GOOGLE_CLIENT_SECRET !== undefined
59
+ || config.GOOGLE_REDIRECT_BASE_URL !== undefined) {
60
+ await driveClient.shutdown();
61
+ await driveClient.init(integrationCtx);
62
+ }
56
63
  },
57
64
  getCustomSettingsComponent() {
58
65
  return 'google-oauth';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tide-commander",
3
- "version": "1.86.0",
3
+ "version": "1.87.0",
4
4
  "description": "Visual multi-agent orchestrator and manager for Claude Code with 3D/2D interface",
5
5
  "repository": {
6
6
  "type": "git",