super-productivity/electron/shared-with-frontend/ipc-events.const.ts
Harbinger 9910d30fca
feat(sync): add OneDrive sync provider with PKCE auth (#7523)
* feat(sync): integrate onedrive with pkce and operation log sync

* fix(sync): add oauth state validation and token refresh concurrency control

* fix(sync): refactor onedrive oauth cleanup and reduce download API calls

- Replace setInterval-based OAuth state pruning with on-demand
  _pruneExpiredOAuthStates() to avoid background timer overhead
- Optimize downloadFile to read ETag from content response headers
  first, falling back to a metadata request only when needed (reduces
  API calls from 2 to 1 in the common case)
- Narrow OneDrive class interface from SyncProviderServiceInterface to
  FileSyncProvider for stronger type guarantees
- Accept optional devPath constructor parameter for non-production
  folder namespacing
- Extract isOneDriveClientIdRequired to a helper function in
  sync-form.const.ts for readability
- Change default OneDrive sync folder name to 'Super Productivity'

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(sync): address PR #7523 review feedback for OneDrive sync

Critical fixes:
- Detect invalid_grant in token refresh responses and clear credentials
- Reset _tokenRefreshInFlightPromise in clearAuthCredentials to prevent
  stale refresh results from overwriting cleared state
- Re-read config before persisting refreshed tokens to avoid race with
  concurrent clearAuthCredentials calls

Important fixes:
- Extract OAuth state management to shared oauth-state.util.ts to fix
  circular dependency between OneDrive provider and callback handler
- Add 401 retry pattern (_is401Retry) matching Dropbox's approach:
  on 401, proactively refresh token and retry, only clear credentials
  if retry fails
- Remove unused auth status translation keys (AUTH_SUCCESS,
  BTN_AUTHENTICATE, BTN_REAUTHENTICATE, REAUTH_SUCCESS,
  STATUS_CONFIGURED, STATUS_NOT_CONFIGURED) and form props
- Remove unused requireAuth parameter from _cfgOrError

Minor fixes:
- Replace path-exposing log messages with structured logging
- Remove unused _formatHttpErrorDetails helper
- Return empty ETag fallback in getFileRev instead of throwing
- Add folder cache invalidation comment for path changes
- Add afterEach to restore global fetch in tests
- Add PKCE defence-in-depth comment in auth code dialog

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(sync): use eslint-disable-next-line for @microsoft.graph.conflictBehavior

Replace spread+computed-property workaround with a plain property plus
an explicit eslint-disable-next-line comment. Clearer intent: the
naming violation is a Graph API requirement, not accidental.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(sync): pre-save OneDrive form config in reauth() to prevent MissingCredentialsSPError

Re-auth was failing silently on Linux because getAuthHelper() reads
clientId from IndexedDB (privateCfg), but the form values were never
written before calling configuredAuthForSyncProviderIfNecessary().

- Extract shared BTN_REAUTHENTICATE and REAUTH_SUCCESS translation keys
- Add OneDrive to OAUTH_SYNC_PROVIDERS
- Pre-save form config in reauth() matching the existing save() flow

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(sync): adapt TooManyRequestsAPIError to new constructor signature after rebase

* refactor(sync): migrate OneDrive provider to packages/sync-providers

Move OneDrive core logic into the shared sync-providers package,
matching the pattern used by Dropbox, WebDAV, and other providers.
The app-side code is now a thin adapter with a createOneDriveProvider()
factory function.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(sync): re-throw MissingRefreshTokenAPIError from _requestOAuthToken catch block

The catch block in _requestOAuthToken was swallowing the
MissingRefreshTokenAPIError thrown after clearing credentials on
invalid_grant, causing it to fall through to a generic HttpNotOkAPIError
instead. Now re-throws MissingRefreshTokenAPIError explicitly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(sync): address PR #7523 second review critical and important items

- Replace _is401Retry instance field with per-call parameter to prevent
  concurrent 401 races (Critical #1)
- Fix _requestOAuthToken catch block to re-throw MissingRefreshTokenAPIError
  instead of swallowing it (Critical #2)
- Fix _parseOAuthCallback defaulting unknown provider to 'unknown' instead
  of 'dropbox' to prevent misrouting (Critical #3 + Important #6)
- Change conflictBehavior from 'replace' to 'fail' to prevent data loss
  if a file exists with the same name as the folder (Critical #4)
- Await in-flight token refresh promise in clearAuthCredentials before
  clearing, to close the refresh-during-clear race (Important #5)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(sync): address PR #7523 review important and minor items

- Extract duplicate OneDrive pre-auth save block into
  _persistOneDriveFormCfgBeforeAuth() method (Important #7)
- Add GET probe in _ensureSyncFolderExists to skip per-segment
  creation when folder already exists (Important #9)
- Redact sensitive params (code, code_verifier, refresh_token,
  access_token) from token endpoint and Graph error bodies
  (Important #11)
- Remove targetPath from _mapAndThrow error messages to prevent
  logging user content (Important #12)
- Add OAuth state validation in manual paste flow for OneDrive
  (Important #13)
- Fix unknown provider routing in _parseOAuthCallback (Important #14)
- Guard Electron IPC listener against post-destroy emissions
  (Important #15)
- Remove dead authStatus Formly field from sync-form.const.ts (Minor)
- Simplify Headers builder in _ensureSyncFolderExists (Minor)
- Fix spec afterEach to restore original fetch instead of undefined
  (Minor)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(sync): fix clearAuthCredentials race and add unit tests for OneDrive

Fix a race condition in clearAuthCredentials where _tokenRefreshInFlightPromise
was nulled AFTER awaiting, causing the invalid_grant handler's clearAuthCredentials
to wait on itself. Now captures the promise and nulls the field before awaiting.

Add 5 new unit tests covering critical code paths:
- 401 token refresh and retry
- 401 retry failure with credential clearing
- 400 invalid_grant credential clearing
- 412 precondition failed mapping
- Concurrent token refresh deduplication

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(sync): address PR #7523 third review items

- Simplify clearAuthCredentials: null _tokenRefreshInFlightPromise without
  awaiting to avoid deadlock when called from inside the refresh IIFE
- Add superproductivity:// scheme validation in Electron OAuth listener
- Fix invalid_grant test to assert MissingRefreshTokenAPIError
- Fix folder cache test to match GET-probe optimization
- Set setComplete default in beforeEach

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* test(sync): fix OAuthCallbackHandler spec to expect 'unknown' provider

The _parseOAuthCallback now returns 'unknown' instead of 'dropbox' for
URLs without an explicit provider path segment.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(sync): address PR #7523 fourth review items

- Handle generic 401 from token endpoint (clear creds + MissingRefreshTokenAPIError)
- Remove user-supplied paths from error constructors (rule 9 compliance)
- Soften clearAuthCredentials comment to reflect actual TOCTOU guarantee
- Tighten Electron scheme gate to match native path prefix
- Add scheme info to Electron rejection log
- Fix folder-cache test to assert GET probe count
- Remove redundant per-test setComplete stub

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(sync): address PR #7523 fifth review items

- Scope invalid_grant/401 credential clearing to refresh_token grant only
  (bad auth code exchange no longer wipes existing credentials)
- Redact OAuth code/state from Electron protocol handler logs
- Add @sp/sync-providers/onedrive TS path alias to tsconfig files
- Default undefined sync config fields instead of overwriting with undefined
- Handle OneDrive in provider-switch branch (preserve encryptKey)
- Update sync-config test expectations to match default values

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(sync): address PR #7523 sixth review items

- Use If-None-Match: * for null-rev uploads to prevent concurrent overwrite
- Guard in-flight refresh against stale credentials (match refreshToken/clientId/tenantId)
- Gate OneDrive provider option behind Electron/Native (web CORS unsupported)
- Don't clear credentials on transient token endpoint errors (429/5xx)
- Preserve null syncProvider instead of defaulting to WebDAV
- Hydrate full OneDrive privateCfg on provider switch
- Remove duplicate syncInterval/isManualSyncOnly Formly controls
- Revert non-English locale changes (zh.json, zh-tw.json)
- Redact URL fragments (#) in Electron protocol handler logs
- listFiles rethrows non-404 errors instead of swallowing all
- Update 401 test expectations for new rethrow behavior

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(sync): guard credential clearing against stale concurrent refresh

Prevent a stale in-flight refresh from wiping newer credentials after
a concurrent re-auth. The refresh IIFE now throws a plain Error (not
MissingRefreshTokenAPIError) when it detects config drift, and all
clearAuthCredentials() call sites now verify the stored config still
matches before clearing. Also fix listFiles() to handle 404 from
_requestJson (which throws HttpNotOkAPIError, not RemoteFileNotFoundAPIError).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(sync): address PR #7523 eighth review items

- Replace If-None-Match:* with @microsoft.graph.conflictBehavior=fail query param for upload create-only semantics
- Add identity change detection in _persistOneDriveFormCfgBeforeAuth to clear tokens when useCustomApp/clientId/tenantId change
- Extract _clearIfConfigMatches helper to guard credential clearing against stale concurrent refresh
- Restrict folder probe fallthrough to 404 only, propagate other errors
- Restore locale files from upstream master
- Centralize IS_ONEDRIVE_SUPPORTED in onedrive-auth-mode.const.ts, use in factory and form config

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(sync): address PR #7523 ninth review items

- Paginate listFiles via @odata.nextLink to avoid silent data loss
- Fix logger misuse (log -> normal with proper string message)
- Replace as any with OneDrivePrivateCfg in dialog-sync-cfg
- Throw NoRevAPIError when uploadFile response lacks eTag
- Omit undefined optional booleans from global config to prevent overwrite
- Move OneDrive dynamic import inside IS_ONEDRIVE_SUPPORTED gate
- Require state param for OneDrive full-URL paste (CSRF)
- Add id_token to sensitive keys for body redaction

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(sync): add OneDrive type alias to electron tsconfig paths

The @sp/sync-providers/onedrive path alias was missing from
electron/tsconfig.electron.json, causing the Electron build to fail
with "Cannot find module '@sp/sync-providers/onedrive'". The alias was
already present in tsconfig.base.json but electron uses its own paths.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 15:31:06 +02:00

113 lines
3.7 KiB
TypeScript

// TODO sort by direction
export enum IPC {
SHUTDOWN_NOW = 'SHUTDOWN_NOW',
EXIT = 'EXIT',
RELAUNCH = 'RELAUNCH',
JIRA_CB_EVENT = 'JIRA_RESPONSE',
JIRA_MAKE_REQUEST_EVENT = 'JIRA_MAKE_REQUEST_EVENT',
JIRA_SETUP_IMG_HEADERS = 'JIRA_SETUP_IMG_HEADERS',
REGISTER_GLOBAL_SHORTCUTS_EVENT = 'REGISTER_GLOBAL_SHORTCUTS',
IDLE_TIME = 'IDLE_TIME',
RESUME = 'RESUME',
SUSPEND = 'SUSPEND',
SHOW_OR_FOCUS = 'SHOW_OR_FOCUS',
LOCK_SCREEN = 'LOCK_SCREEN',
APP_READY = 'APP_READY',
ERROR = 'ELECTRON_ERROR',
CURRENT_TASK_UPDATED = 'CURRENT_TASK_UPDATED',
REQUEST_CURRENT_TASK_FOR_TASK_WIDGET = 'REQUEST_CURRENT_TASK_FOR_TASK_WIDGET',
TASK_TOGGLE_START = 'TASK_TOGGLE_START',
SHOW_ADD_TASK_BAR = 'SHOW_ADD_TASK_BAR',
ADD_TASK_FROM_APP_URI = 'ADD_TASK_FROM_APP_URI',
ADD_NOTE = 'ADD_NOTE',
TRANSFER_SETTINGS_REQUESTED = 'TRANSFER_SETTINGS_REQUESTED',
TRANSFER_SETTINGS_TO_ELECTRON = 'TRANSFER_SETTINGS_TO_ELECTRON',
EXEC = 'EXEC',
BACKUP = 'BACKUP',
BACKUP_IS_AVAILABLE = 'BACKUP_IS_AVAILABLE',
BACKUP_LOAD_DATA = 'BACKUP_LOAD_DATA',
SET_PROGRESS_BAR = 'SET_PROGRESS_BAR',
FLASH_FRAME = 'FLASH_FRAME',
NOTIFY_ON_CLOSE = 'NOTIFY_ON_CLOSE',
REGISTER_BEFORE_CLOSE = 'REGISTER_BEFORE_CLOSE',
UNREGISTER_BEFORE_CLOSE = 'UNREGISTER_BEFORE_CLOSE',
BEFORE_CLOSE_DONE = 'BEFORE_CLOSE_DONE',
FILE_SYNC_LOAD = 'FILE_SYNC_LOAD',
FILE_SYNC_SAVE = 'FILE_SYNC_SAVE',
FILE_SYNC_REMOVE = 'FILE_SYNC_REMOVE',
FILE_SYNC_LIST_FILES = 'FILE_SYNC_LIST_FILES',
FILE_SYNC_GET_REV_AND_CLIENT_UPDATE = 'FILE_SYNC_GET_REV_AND_CLIENT_UPDATE',
CHECK_DIR_EXISTS = 'CHECK_DIR_EXISTS',
PICK_DIRECTORY = 'PICK_DIRECTORY',
SHOW_OPEN_DIALOG = 'SHOW_OPEN_DIALOG',
READ_LOCAL_IMAGE_AS_DATA_URL = 'READ_LOCAL_IMAGE_AS_DATA_URL',
TO_FILE_URL = 'TO_FILE_URL',
ANY_FILE_DOWNLOADED = 'ANY_FILE_DOWNLOADED',
FULL_SCREEN_BLOCKER = 'FULL_SCREEN_BLOCKER',
ENTER_FULL_SCREEN = 'ENTER_FULL_SCREEN',
LEAVE_FULL_SCREEN = 'LEAVE_FULL_SCREEN',
GET_PATH = 'GET_PATH',
GET_BACKUP_PATH = 'GET_BACKUP_PATH',
RELOAD_MAIN_WIN = 'RELOAD_MAIN_WIN',
OPEN_DEV_TOOLS = 'OPEN_DEV_TOOLS',
SHOW_EMOJI_PANEL = 'SHOW_EMOJI_PANEL',
OPEN_PATH = 'OPEN_PATH',
OPEN_EXTERNAL = 'OPEN_EXTERNAL',
SAVE_FILE_DIALOG = 'SAVE_FILE_DIALOG',
SHARE_NATIVE = 'SHARE_NATIVE',
// Clipboard Images
CLIPBOARD_IMAGE_SAVE = 'CLIPBOARD_IMAGE_SAVE',
CLIPBOARD_IMAGE_LOAD = 'CLIPBOARD_IMAGE_LOAD',
CLIPBOARD_IMAGE_DELETE = 'CLIPBOARD_IMAGE_DELETE',
CLIPBOARD_IMAGE_LIST = 'CLIPBOARD_IMAGE_LIST',
CLIPBOARD_IMAGE_GET_PATH = 'CLIPBOARD_IMAGE_GET_PATH',
CLIPBOARD_GET_FILE_PATHS = 'CLIPBOARD_GET_FILE_PATHS',
CLIPBOARD_COPY_IMAGE_FILE = 'CLIPBOARD_COPY_IMAGE_FILE',
CLIPBOARD_READ_IMAGE = 'CLIPBOARD_READ_IMAGE',
// Plugin Node Execution
PLUGIN_EXEC_NODE_SCRIPT = 'PLUGIN_EXEC_NODE_SCRIPT',
// Plugin OAuth
PLUGIN_OAUTH_PREPARE = 'PLUGIN_OAUTH_PREPARE',
PLUGIN_OAUTH_START = 'PLUGIN_OAUTH_START',
PLUGIN_OAUTH_CB = 'PLUGIN_OAUTH_CB',
// Generic OAuth callback for app protocol redirects (e.g. OneDrive)
OAUTH_CALLBACK = 'OAUTH_CALLBACK',
LOCAL_REST_API_REQUEST = 'LOCAL_REST_API_REQUEST',
LOCAL_REST_API_RESPONSE = 'LOCAL_REST_API_RESPONSE',
UPDATE_SETTINGS = 'UPDATE_SETTINGS',
UPDATE_TASK_WIDGET_SETTINGS = 'UPDATE_TASK_WIDGET_SETTINGS',
UPDATE_TITLE_BAR_DARK_MODE = 'UPDATE_TITLE_BAR_DARK_MODE',
TODAY_TASKS_UPDATED = 'TODAY_TASKS_UPDATED',
SWITCH_TASK = 'SWITCH_TASK',
// maybe_UPDATE_CURRENT_TASK = 'UPDATE_CURRENT_TASK',
// maybe_IS_IDLE = 'IS_IDLE',
// maybe_IS_BUSY = 'IS_BUSY',
// POMODORO_UPDATE = 'POMODORO_UPDATE',
// maybe_PROJECT_CHANGED = 'PROJECT_CHANGED',
// maybe_COMPLETE_DATA_RELOAD = 'COMPLETE_DATA_RELOAD',
}
export type IPCEventValue = `${IPC}`;