Conversation
oauthLoopback.ts: a provider-agnostic PKCE (RFC 7636) authorization-code flow for a public client - starts a temporary HTTP server on a free loopback port, opens the identity provider's consent screen in the system browser via shell.openExternal, and resolves once it redirects back with an authorization code (rejecting on an error response, a state mismatch, or a timeout). Deliberately not OneDrive-specific: Cloud: Phase 5's Google Drive OAuth (#99) is the same shape and can reuse this directly. oneDriveAuth.ts: builds on that for Microsoft's identity platform specifically - the authorize/token endpoints, PKCE challenge generation, and the offline_access/Files.ReadWrite/User.Read scopes OneDriveStorageProvider (#97) and the connect UI (#98) will need. CLIENT_ID is a placeholder pending an actual Azure AD app registration (not a secret, so hardcoding the real value once known is fine - see the TODO comment for the exact registration steps: public client, "Mobile and desktop applications" platform, http://localhost redirect URI, multi-tenant + personal accounts). Wired up end to end: native.ts's new "maktaba:connect-onedrive" IPC handler, exposed via preload.ts/maktaba.d.ts as window.maktaba.connectOneDrive() - returns the token set to the renderer the same way S3's credential fields do, so the connect UI (#98) can hand them to the backend and to saveCloudCredential exactly like S3ConnectModal already does, rather than introducing a second credential-handling pattern. Token refresh deliberately isn't handled here - OneDriveStorageProvider (#97) will do that directly from the .NET backend with a plain HTTPS call (its own copy of the client id/token endpoint), the same way S3StorageProvider talks to AWS directly rather than routing every request through Electron. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Second concrete IStorageProvider, following S3StorageProvider's exact shape: a local cache mirror (ICloudCacheManager) every method downloads into first, so everything above the provider layer keeps working with plain local paths. Addresses items by path under the configured folder (OneDriveProviderOptions.Folder) via Microsoft.Graph's ItemWithPath - GetLocalPathAsync/NotifyWrittenAsync/CreateDirectoryAsync/DeleteAsync/ MoveAsync/ExistsAsync/ExistsRemoteAsync/EnumerateAsync/Pull+PushDatabaseAsync all implemented, matching IStorageProvider's full surface (including ExistsRemoteAsync, added during Phase 3's migration-resumability bugfix). Notable differences from S3, since OneDrive isn't a flat keyspace: - CreateDirectoryAsync/NotifyWrittenAsync ensure the parent folder chain actually exists (creating missing segments, tolerating 409 Conflict for ones that already do) before writing - S3 needs no such step. - NotifyWrittenAsync uses Graph's simple PUT .../content for files under its 4 MiB ceiling and a manually-driven upload session (chunked PUT against the pre-authenticated session URL) above it, since ebook files routinely exceed that ceiling. - MoveAsync uses Graph's PATCH-with-ParentReference rename/move, not an S3-style copy+delete. OneDriveTokenManager refreshes the access token in-memory as needed (60s-early, SemaphoreSlim-guarded against concurrent refreshes), duplicating oneDriveAuth.ts's CLIENT_ID/token endpoint rather than inventing a backend-calls-Electron channel - see that file's comment. A rotated refresh token is kept for the process's lifetime but not relayed back to Electron's encrypted storage yet - a documented v1 gap the existing Reconnect action (Settings -> Libraries) already covers as a recovery path, matching S3's own documented v1 caching limitations. StorageProviderFactory: generalized the per-S3 provider cache into a provider-type-agnostic one (keyed by libraryId+providerType+credential hash) and added the "onedrive" branch to both Current and CreateForProvider - no other change needed there, or in the /libraries/ cloud connect endpoint, or the migration wizard's backend, since both were already provider-type-agnostic strings. Verified against the actual installed Microsoft.Graph 6.6.0 SDK surface via reflection (RootRequestBuilder has no Children of its own - listing the drive's own root needs Items["root"].Children instead) rather than guessing API shapes; NOT live-tested against a real OneDrive account yet (pending an Azure AD app registration - see oneDriveAuth.ts). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…nly)
Maktaba only targets OneDrive Personal, not OneDrive for Business - no
reason to register the app as multi-tenant ("any organizational
directory and personal Microsoft accounts") when "Personal Microsoft
accounts only" is simpler for both the Azure AD registration itself and
the end-user consent screen. That account type requires the /consumers/
endpoint rather than /common/ - using /common/ against a
consumers-only registration is a documented source of "application not
found in directory" errors.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…vider # Conflicts: # apps/desktop/src/native.ts # apps/desktop/src/oauthLoopback.ts # apps/desktop/src/preload.ts # apps/frontend/src/maktaba.d.ts # backend/Maktaba.Data/Services/StorageProviderFactory.cs
The phase-4-onedrive-provider branch had OneDriveStorageProvider and the Electron-side OAuth loopback flow built and build-verified, but never got the actual Settings -> Libraries "Connect OneDrive..." UI, translations, or docs that would make it reachable - Google Drive (phase 5, merged after this branch was parked) got all of that instead. Adds: - OneDriveConnectModal in LibrariesSettings.tsx (name + optional folder + "Sign in with Microsoft"), mirroring GoogleDriveConnectModal exactly - an interactive sign-in rather than typed credentials. - ReconnectModal generalized from an isGoogleDrive branch to isOAuthProvider (Google Drive or OneDrive), dispatching to the right window.maktaba.connect*/cancel*Connect pair per providerType. - OneDriveCredential type in api.ts, alongside GoogleDriveCredential. - EN/UR translations for the new form's labels/buttons. - docs/en+ur/libraries.md: "Connecting a OneDrive library" section, and OneDrive mentions alongside Google Drive in the cloud-libraries overview and "if a cloud library won't reconnect" sections. - CLAUDE.md: OneDrive added to the ProviderType list and provider summary, an "OneDrive specifics" paragraph (path-based Graph addressing vs. Drive's id-based addressing, PKCE-only public client with no client_secret), and a full Azure AD app registration walkthrough mirroring the existing Google Cloud project one. Also fixes a compile gap from the merge with main (Cloud: Phase 5) still to catch: added OneDriveStorageProvider.GetRemoteDatabaseLastModifiedAsync, implementing IStorageProvider's newer member (Graph's DriveItem LastModifiedDateTime) - the interface gained this after the OneDrive branch was originally written, for ActivateAsync's "don't unconditionally overwrite local edits" pull/push comparison. CLIENT_ID in oneDriveAuth.ts/OneDriveTokenManager is still the 00000000-... placeholder - OneDrive sign-in will fail until a real Azure AD app registration exists (see CLAUDE.md's walkthrough). Build- verified (dotnet build, frontend/desktop/docs builds); not live-tested, same as the rest of this phase's original work. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2 of 3 tasks
The cloud storage epic always documented a known gap: "don't have the same cloud library open on two devices at once (an accidental double-open silently loses whichever side pushes second)" - and Maktaba.Core/Sync/LibraryLockInfo.cs already existed as a scaffolded data model (parse/serialize/staleness rules) for fixing this, but nothing actually read or wrote a lock anywhere. This wires it up. IStorageProvider gains ReadLockAsync/WriteLockAsync/DeleteLockAsync, implemented for S3 and Google Drive (OneDrive's implementation lands on that still-open branch separately, see phase-4-onedrive-provider) - each reads/writes a plain ".maktaba-lock" text object directly against the remote store, always bypassing the local cache mirror, since the whole point is seeing what a *different* device just wrote. A no-op for LocalFileSystemProvider. LibraryService.ActivateAsync now checks the lock before pulling/ pushing a cloud library's database: a non-stale lock (LibraryLockInfo. IsStale, 2 minutes) held by a different device throws a clear error (surfaced to the frontend the same way any other failed-open error already is) instead of silently racing another device's edits. CloudSyncLifecycleService runs a second, 60s-interval timer loop refreshing the lock for as long as this process holds the library open (comfortably under the 2-minute staleness window), and releases it - best-effort, alongside the existing DB push - both when switching/ removing the active library and on clean app shutdown, so another device doesn't have to wait out the full staleness window after a normal close. Updated CLAUDE.md's "Cloud storage" section and docs/en+ur/libraries.md to describe the new behavior in place of the old "known sharp edge" wording. Build-verified (dotnet build, docs build); not live-tested against a real two-device scenario. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
3 tasks done
Merges in phase-cloud-library-locking (#126), which added IStorageProvider.ReadLockAsync/WriteLockAsync/DeleteLockAsync for S3 and Google Drive but not OneDrive (OneDriveStorageProvider.cs doesn't exist on main yet), and implements the same three methods here so this branch stays compilable regardless of which of the two PRs merges first. Same approach as GetRemoteDatabaseLastModifiedAsync just above it: talks to Graph directly against the ".maktaba-lock" path, bypassing the local cache mirror entirely (the whole point is seeing what a different device just wrote). WriteLockAsync reuses EnsureParentFolderExistsAsync the same way NotifyWrittenAsync does, in case the library's configured Folder doesn't exist yet. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This was referenced Sep 14, 2026
umerfaruk
added a commit
that referenced
this pull request
Sep 14, 2026
* Implement OAuth2 loopback flow for OneDrive sign-in (#96) oauthLoopback.ts: a provider-agnostic PKCE (RFC 7636) authorization-code flow for a public client - starts a temporary HTTP server on a free loopback port, opens the identity provider's consent screen in the system browser via shell.openExternal, and resolves once it redirects back with an authorization code (rejecting on an error response, a state mismatch, or a timeout). Deliberately not OneDrive-specific: Cloud: Phase 5's Google Drive OAuth (#99) is the same shape and can reuse this directly. oneDriveAuth.ts: builds on that for Microsoft's identity platform specifically - the authorize/token endpoints, PKCE challenge generation, and the offline_access/Files.ReadWrite/User.Read scopes OneDriveStorageProvider (#97) and the connect UI (#98) will need. CLIENT_ID is a placeholder pending an actual Azure AD app registration (not a secret, so hardcoding the real value once known is fine - see the TODO comment for the exact registration steps: public client, "Mobile and desktop applications" platform, http://localhost redirect URI, multi-tenant + personal accounts). Wired up end to end: native.ts's new "maktaba:connect-onedrive" IPC handler, exposed via preload.ts/maktaba.d.ts as window.maktaba.connectOneDrive() - returns the token set to the renderer the same way S3's credential fields do, so the connect UI (#98) can hand them to the backend and to saveCloudCredential exactly like S3ConnectModal already does, rather than introducing a second credential-handling pattern. Token refresh deliberately isn't handled here - OneDriveStorageProvider (#97) will do that directly from the .NET backend with a plain HTTPS call (its own copy of the client id/token endpoint), the same way S3StorageProvider talks to AWS directly rather than routing every request through Electron. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * Implement OneDriveStorageProvider via the Microsoft Graph SDK (#97) Second concrete IStorageProvider, following S3StorageProvider's exact shape: a local cache mirror (ICloudCacheManager) every method downloads into first, so everything above the provider layer keeps working with plain local paths. Addresses items by path under the configured folder (OneDriveProviderOptions.Folder) via Microsoft.Graph's ItemWithPath - GetLocalPathAsync/NotifyWrittenAsync/CreateDirectoryAsync/DeleteAsync/ MoveAsync/ExistsAsync/ExistsRemoteAsync/EnumerateAsync/Pull+PushDatabaseAsync all implemented, matching IStorageProvider's full surface (including ExistsRemoteAsync, added during Phase 3's migration-resumability bugfix). Notable differences from S3, since OneDrive isn't a flat keyspace: - CreateDirectoryAsync/NotifyWrittenAsync ensure the parent folder chain actually exists (creating missing segments, tolerating 409 Conflict for ones that already do) before writing - S3 needs no such step. - NotifyWrittenAsync uses Graph's simple PUT .../content for files under its 4 MiB ceiling and a manually-driven upload session (chunked PUT against the pre-authenticated session URL) above it, since ebook files routinely exceed that ceiling. - MoveAsync uses Graph's PATCH-with-ParentReference rename/move, not an S3-style copy+delete. OneDriveTokenManager refreshes the access token in-memory as needed (60s-early, SemaphoreSlim-guarded against concurrent refreshes), duplicating oneDriveAuth.ts's CLIENT_ID/token endpoint rather than inventing a backend-calls-Electron channel - see that file's comment. A rotated refresh token is kept for the process's lifetime but not relayed back to Electron's encrypted storage yet - a documented v1 gap the existing Reconnect action (Settings -> Libraries) already covers as a recovery path, matching S3's own documented v1 caching limitations. StorageProviderFactory: generalized the per-S3 provider cache into a provider-type-agnostic one (keyed by libraryId+providerType+credential hash) and added the "onedrive" branch to both Current and CreateForProvider - no other change needed there, or in the /libraries/ cloud connect endpoint, or the migration wizard's backend, since both were already provider-type-agnostic strings. Verified against the actual installed Microsoft.Graph 6.6.0 SDK surface via reflection (RootRequestBuilder has no Children of its own - listing the drive's own root needs Items["root"].Children instead) rather than guessing API shapes; NOT live-tested against a real OneDrive account yet (pending an Azure AD app registration - see oneDriveAuth.ts). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * Switch OneDrive auth to the /consumers/ endpoint (personal accounts only) Maktaba only targets OneDrive Personal, not OneDrive for Business - no reason to register the app as multi-tenant ("any organizational directory and personal Microsoft accounts") when "Personal Microsoft accounts only" is simpler for both the Azure AD registration itself and the end-user consent screen. That account type requires the /consumers/ endpoint rather than /common/ - using /common/ against a consumers-only registration is a documented source of "application not found in directory" errors. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * Bring OneDrive support to parity with Google Drive's connect UI The phase-4-onedrive-provider branch had OneDriveStorageProvider and the Electron-side OAuth loopback flow built and build-verified, but never got the actual Settings -> Libraries "Connect OneDrive..." UI, translations, or docs that would make it reachable - Google Drive (phase 5, merged after this branch was parked) got all of that instead. Adds: - OneDriveConnectModal in LibrariesSettings.tsx (name + optional folder + "Sign in with Microsoft"), mirroring GoogleDriveConnectModal exactly - an interactive sign-in rather than typed credentials. - ReconnectModal generalized from an isGoogleDrive branch to isOAuthProvider (Google Drive or OneDrive), dispatching to the right window.maktaba.connect*/cancel*Connect pair per providerType. - OneDriveCredential type in api.ts, alongside GoogleDriveCredential. - EN/UR translations for the new form's labels/buttons. - docs/en+ur/libraries.md: "Connecting a OneDrive library" section, and OneDrive mentions alongside Google Drive in the cloud-libraries overview and "if a cloud library won't reconnect" sections. - CLAUDE.md: OneDrive added to the ProviderType list and provider summary, an "OneDrive specifics" paragraph (path-based Graph addressing vs. Drive's id-based addressing, PKCE-only public client with no client_secret), and a full Azure AD app registration walkthrough mirroring the existing Google Cloud project one. Also fixes a compile gap from the merge with main (Cloud: Phase 5) still to catch: added OneDriveStorageProvider.GetRemoteDatabaseLastModifiedAsync, implementing IStorageProvider's newer member (Graph's DriveItem LastModifiedDateTime) - the interface gained this after the OneDrive branch was originally written, for ActivateAsync's "don't unconditionally overwrite local edits" pull/push comparison. CLIENT_ID in oneDriveAuth.ts/OneDriveTokenManager is still the 00000000-... placeholder - OneDrive sign-in will fail until a real Azure AD app registration exists (see CLAUDE.md's walkthrough). Build- verified (dotnet build, frontend/desktop/docs builds); not live-tested, same as the rest of this phase's original work. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * Let the migration wizard target Google Drive, not just S3 The Migrate-to-cloud wizard's Target step was hardcoded to S3, even though the backend side (StorageProviderFactory.CreateForProvider, the /migrate/start endpoint, MigrationTarget) was already fully provider-agnostic - migrating straight to Google Drive only needed frontend wiring, unblocked by any external setup (unlike OneDrive, which still needs a real Azure AD app registration - see CLAUDE.md). Adds a SegmentedControl to MigrationWizard's Target step to pick S3 or Google Drive, then shows that provider's own fields: S3CredentialFields + Test connection for S3 (unchanged), or a folder field + "Sign in with Google" button for Google Drive (mirroring GoogleDriveConnectModal's own flow, including cancelling a pending sign-in on step change/close). `startMigration` is now generic over the credential shape, same as `connectCloudLibrary`/`reopenCloudLibrary` already are. Exported `PROVIDER_LABELS` from LibrariesSettings.tsx so the picker's labels match the Connect buttons' wording exactly instead of duplicating it. OneDrive isn't offered as a migration target yet, same reasoning as its own Connect form not being reachable until a real Azure AD app registration exists - adding it here later is the same pattern Google Drive followed. Updated docs/en+ur/libraries.md's migration wizard section and CLAUDE.md's "Migration wizard" writeup to describe the provider picker. Build-verified (dotnet build, frontend build); not live-tested - same caveat as every other cloud provider change, see CLAUDE.md's "Build & verification" section on what this sandbox can and can't check. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * Implement real cloud library locking (single-writer enforcement) The cloud storage epic always documented a known gap: "don't have the same cloud library open on two devices at once (an accidental double-open silently loses whichever side pushes second)" - and Maktaba.Core/Sync/LibraryLockInfo.cs already existed as a scaffolded data model (parse/serialize/staleness rules) for fixing this, but nothing actually read or wrote a lock anywhere. This wires it up. IStorageProvider gains ReadLockAsync/WriteLockAsync/DeleteLockAsync, implemented for S3 and Google Drive (OneDrive's implementation lands on that still-open branch separately, see phase-4-onedrive-provider) - each reads/writes a plain ".maktaba-lock" text object directly against the remote store, always bypassing the local cache mirror, since the whole point is seeing what a *different* device just wrote. A no-op for LocalFileSystemProvider. LibraryService.ActivateAsync now checks the lock before pulling/ pushing a cloud library's database: a non-stale lock (LibraryLockInfo. IsStale, 2 minutes) held by a different device throws a clear error (surfaced to the frontend the same way any other failed-open error already is) instead of silently racing another device's edits. CloudSyncLifecycleService runs a second, 60s-interval timer loop refreshing the lock for as long as this process holds the library open (comfortably under the 2-minute staleness window), and releases it - best-effort, alongside the existing DB push - both when switching/ removing the active library and on clean app shutdown, so another device doesn't have to wait out the full staleness window after a normal close. Updated CLAUDE.md's "Cloud storage" section and docs/en+ur/libraries.md to describe the new behavior in place of the old "known sharp edge" wording. Build-verified (dotnet build, docs build); not live-tested against a real two-device scenario. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * Implement OneDrive's cloud library lock methods Merges in phase-cloud-library-locking (#126), which added IStorageProvider.ReadLockAsync/WriteLockAsync/DeleteLockAsync for S3 and Google Drive but not OneDrive (OneDriveStorageProvider.cs doesn't exist on main yet), and implements the same three methods here so this branch stays compilable regardless of which of the two PRs merges first. Same approach as GetRemoteDatabaseLastModifiedAsync just above it: talks to Graph directly against the ".maktaba-lock" path, bypassing the local cache mirror entirely (the whole point is seeing what a different device just wrote). WriteLockAsync reuses EnsureParentFolderExistsAsync the same way NotifyWrittenAsync does, in case the library's configured Folder doesn't exist yet. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * Fix: deleting a book/periodical from a cloud library never deleted it remotely Issue #102 ("Reveal/Open/Trash equivalents for cloud-backed libraries") flagged this: BookRemovalService/PeriodicalService only ever removed the DB rows and left the actual file deletion to the frontend's window.maktaba.trashPath - a pure local-disk OS-trash call with zero cloud awareness. For a cloud library this meant deleting a book only ever removed its DB row; the book's files (every issue's, for a periodical) stayed orphaned in the remote store forever, since trashPath was either a no-op (nothing cached locally to trash) or, at best, only removed the local cache mirror copy while leaving the real remote object/folder untouched. BookRemovalResult/PeriodicalDeleteResult gain RequiresLocalTrash (true only for "local"). For a cloud library, the backend now calls IStorageProvider.DeleteAsync(folderPath, recursive: true) itself before returning (best-effort - a failed remote delete logs a warning rather than blocking DB row removal, same philosophy as PushCurrentLibraryIfCloudAsync's own swallowed exceptions elsewhere). Every frontend call site that deletes a book/periodical now checks this flag before calling trashPath at all: BookDetailPanel, DeleteBooksConfirmDialog, MergeConfirmDialog's source-book cleanup, PeriodicalDetailView (issue + periodical delete), PeriodicalsView. Local libraries are completely unaffected - same OS-trash flow as before, RequiresLocalTrash is just always true there. Per-file deletion (BookEditService's file replace/remove paths) was never affected by this bug - those already called Storage.DeleteAsync directly instead of routing through the frontend's OS-trash flow. Also confirmed (no code change needed): opening/revealing an individual ebook file already ensures it's cached first, since every endpoint that returns a file's absolutePath already calls IStorageProvider.GetLocalPathAsync per-file server-side (which downloads-into-cache if missing) before responding - issue #102's "View in {Provider}" web-link action for a plain OS-reveal remains open as future polish, not addressed here. Build-verified (dotnet build, frontend build); not live-tested against a real cloud provider. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * Add a dedicated Cloud Storage help topic (issue #105) The "Cloud libraries" content (connecting each provider, syncing, migrating an existing local library, reconnecting, cloud-aware delete) had grown into more than half of the "Choosing & Switching Libraries" topic, with no dedicated entry point of its own in the Help sidebar/topic list - issue #105 asked for it to be split into its own registered docs/topics.cjs topic instead. New docs/en/cloud-storage.md + docs/ur/cloud-storage.md carry all of that content (S3, Google Drive, OneDrive, sync + the new cloud library locking behavior, the migration wizard's provider picker, and the cloud-aware delete fix), registered right after "libraries" in topics.cjs so it shows up in both the VitePress site and the in-app Help window automatically. docs/en+ur/libraries.md's own "Cloud libraries" section is trimmed to a short pointer (its #cloud-libraries anchor, referenced by the Relocate note further down the same page, still resolves - the heading just moved to a shorter section, not away). docs/en+ur/index.md's "Where to go next" list links the new topic too. Urdu content is Claude-authored/unreviewed, same convention as every other Urdu string added this project. This branch merges in the four other in-flight cloud storage PRs (#124 OneDrive, #125 migration wizard Google Drive target, #126 library locking, #127 cloud-aware delete) since the topic needed to document all of their accumulated content - the diff will shrink to just the docs/topics.cjs changes once those merge into main; this one can be merged last, or rebased onto an updated main first. Build-verified (docs build, frontend build). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
OneDriveStorageProvider(Microsoft Graph SDK, path-based item addressing) and the Electron-side OAuth2+PKCE loopback sign-in flow were already built on this branch; this PR merges inmain(picking up Google Drive support and the cover-loading fixes) and adds the missing end-to-end wiring: Settings → Libraries "Connect OneDrive…" form, a Reconnect modal generalized across S3/Google Drive/OneDrive, translations (EN+UR), user docs, and a CLAUDE.md walkthrough for the one-time Azure AD app registration.IStorageProvider.GetRemoteDatabaseLastModifiedAsyncfor OneDrive (a member the interface gained after this branch was originally written), so the "don't unconditionally overwrite local edits on open/switch" logic works for OneDrive too.Known gap
oneDriveAuth.ts/OneDriveTokenManager'sCLIENT_IDis still a00000000-...placeholder — OneDrive sign-in will fail until a real Azure AD app registration is created and its Application (client) ID is pasted into both files. Steps are in CLAUDE.md's new "Setting up the Azure AD app registration" section (personal Microsoft account, no Azure subscription/cost needed).Test plan
dotnet build backend/Maktaba.slnnpm run build:frontendnpm run build:desktopnpm run docs:build🤖 Generated with Claude Code