7e78f80e14
* feat(sync): add Google Drive file-sync provider core Second FileSyncProvider for the merged provider-agnostic file-sync engine, behind the provider seam. This is the CI-testable core only: no settings UI and no platform OAuth runners yet (those land in later phases). - GoogleDriveProvider over the Drive v3 REST API: id-addressed path resolution with a per-instance id cache, create-then-name uploads, real idempotent ensureDir, files.list pagination, Retry-After-aware 429/5xx backoff, per-path folder-creation locks with deterministic duplicate collapse, stale-id eviction, and FileSyncError mapping (403 split into rate-limit vs permission). - DI OAuth layer: pkce, parseRedirect (redirect-target + CSRF state), reverseDnsRedirect, tokenStore (iOS client, no secret), oauthFlow. - PersistedDriveAuth with single-flight token refresh; keychain-backed token store with no ephemeral fallback for the refresh token; account label via about.get. - providerRegistry (backend kind to provider) and buildGoogleDriveProvider assembly. - Shared transport-agnostic provider semantic contract, run against both WebDAV and Drive. - Keyed secure-KV bridge contract (set/get/clear_secure_item); the native keychain implementation lands with the desktop OAuth slice that first exercises it. Adapted from ratatabananana-bit/Readest-google-drive-mod-patcher (AGPL-3.0) with the author's explicit permission. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sync): multi-provider file-sync settings + sync-state foundation PR2 foundation for a second file-sync backend (Google Drive). The behaviour-sensitive reader-hook and Sync-now form generalization land in PR3 alongside OAuth, where Drive actually connects and the multi-provider paths can be exercised and live-verified (and the extracted form gets its second consumer, avoiding a single-use abstraction). - GoogleDriveSettings type (mirrors WebDAVSettings minus URL/credentials/ rootPath, plus accountLabel) wired into SystemSettings, with DEFAULT_GOOGLE_DRIVE_SETTINGS in the defaults. - googleDrive.deviceId + googleDrive.lastSyncedAt added to the backup blacklist so device-local sync identity / cursors never restore onto another device. Covered by the existing backup-settings test. - Generalize webdavSyncStore into fileSyncStore: per-backend progress keyed by provider kind, plus a global library-sync mutex (beginSync returns false when another backend already holds the lock) since every backend's syncLibrary mutates the same local library. Migrate WebDAVForm and IntegrationsPanel to the keyed API; WebDAV behaviour is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(native-bridge): add keyed secure key-value store commands A generic, keyed secret store over the same OS keychain backends as the sync passphrase (set/get/clear_secure_item), so secrets that aren't the single sync passphrase get the same XSS-free cross-launch persistence without each needing its own native command. The Google Drive OAuth token store (PR1's KeychainTokenPersistence) is the first consumer; a future cloud provider's refresh token reuses it. - Desktop (macOS/Windows/Linux): keyring-core, keyed by the item key as the entry account under the existing "Readest Safe Storage" service. - Android: EncryptedSharedPreferences (a dedicated readest_secure_items_v1 file, the item key as the pref key). - iOS: Security framework Keychain (kSecClassGenericPassword, dedicated service, the item key as kSecAttrAccount). Registered in the plugin invoke handler + build COMMANDS + default permission set (autogenerated permission files regenerated; the passphrase entries are preserved). The TS bridge wrappers shipped in PR1. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sync): desktop Google Drive OAuth runner + connect flow The desktop half of Drive sign-in: open consent in the system browser, capture the reverse-DNS redirect the OS routes back, and exchange the code for tokens. - oauthDesktop.ts: runDesktopDeepLinkOAuth wires the DI OAuth flow to the desktop mechanics (open default browser, capture via single-instance / onOpenUrl, cold-browser fallback after a grace period, hard deadline). Fully headless-unit-tested via injected deps. - spawn_fresh_browser.rs (+ registration, Windows-only winreg dep): the cold browser the runner falls back to when the user's already-running browser snapshotted protocol associations before the scheme was registered (a Windows-specific failure). Resolves the default browser from the registry and spawns it cold with an isolated --user-data-dir; a no-op on macOS/Linux where the default-browser open already routes the redirect. Pure helpers unit-tested. - connectGoogleDrive.ts: run the platform OAuth runner, persist the token (fail-loud — Drive is not reported connected if the refresh token does not save), and resolve the account label via about.get (best-effort). OAuth runner adapted from ratatabananana-bit/Readest-google-drive-mod-patcher (AGPL-3.0) with the author's permission. Scheme registration + the ingress redirect filter + the Drive connect UI land in the following commits; live desktop verification follows once the official Google client id is provisioned. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sync): filter Google OAuth redirects out of the deep-link ingress The reverse-DNS OAuth redirect (com.googleusercontent.apps.<id>:/oauthredirect) is delivered through the same single-instance / onOpenUrl channels as book-file deep links. Without a filter the book-import consumer would treat the redirect URL as a file path to open. Drop it at the ingress source (useAppUrlIngress) before the app-incoming-url broadcast, so no consumer ever sees it; the Drive sign-in runner still captures it via its own listeners. isGoogleOAuthRedirectUrl matches the scheme prefix (not a specific client id), so it stays correct regardless of which client is baked into the build. Note: registering the scheme in tauri.conf.json (so the OS routes it back to the app) needs the official Google client id, which is a provisioning prerequisite. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sync): bake the official Google Drive OAuth client id + redirect scheme Provisioned the Readest Google Cloud OAuth client (iOS application type, no secret, drive.file scope). Bake the client id as the default in getGoogleClientId (overridable via NEXT_PUBLIC_GOOGLE_CLIENT_ID for forkers, who must also regenerate the manifest schemes) and register the derived reverse-DNS redirect scheme com.googleusercontent.apps.<id> in tauri.conf.json (desktop + mobile deep-link) so the OS routes the OAuth redirect back to the app. The client id is a public client identifier, not a secret. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sync): Google Drive connect UI + shared FileSyncForm Make Drive usable from Settings, and extract the now-two-consumer sync controls. - FileSyncForm: the provider-agnostic sync controls (sub-toggles, conflict strategy, manual "Sync now" with progress + result toast), parameterised by backend kind and building the provider through the registry. Extracted from WebDAVForm now that a second consumer exists. WebDAVForm keeps its URL/credentials connect panel + browse pane and renders FileSyncForm for the sync section; behaviour is unchanged (WebDAV "Sync now" goes through the same provider via the registry). - GoogleDriveForm: an OAuth connect panel (Connect -> runGoogleDriveConnect -> store token in keychain -> "Connected as <email>"; Disconnect) + FileSyncForm. - googleDriveConnect.ts: assemble the env client id + keychain + desktop runner into connectGoogleDrive/disconnectGoogleDrive for the UI. - IntegrationsPanel: a "Google Drive" row + sub-page, shown only on desktop (mobile OAuth runners land in later phases). Reader-side auto-sync (generalizing useWebDAVSync) is a follow-up; manual "Sync now" already exercises the full Drive stack. Full suite 6412 green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(settings): unified Third-party Cloud Sync section (exclusive provider) Group WebDAV + Google Drive into a new "Third-party Cloud Sync" section and make them mutually exclusive — only one cloud provider syncs the library at a time. - New unified "Cloud Sync" sub-page (CloudSyncForm): a provider picker (radio, the AIPanel mutually-exclusive pattern) on top, the shared FileSyncForm sync options below for whichever provider is active. Google Drive is offered only on desktop; on mobile the page is WebDAV only and the picker is hidden. - withActiveCloudProvider helper: enabling one provider disables the other in one save. Both panels' connect/activate paths use it. Unit-tested. - WebDAVForm / GoogleDriveForm refactored into embeddable panels (the unified page owns the header). Drive gains a "configured but inactive" state so switching back re-activates it without a fresh sign-in; explicit Disconnect clears the keychain token. - IntegrationsPanel: remove the two separate WebDAV / Google Drive rows from "Reading Sync" (now KOReader Sync / Readwise / Hardcover only); add the Third-party Cloud Sync section with one Cloud Sync row (status = active provider). Old webdav/gdrive deep-links route to the unified page. Also removes the temporary Drive concurrency probe (the upload already runs at the intended concurrency 4; the probe confirmed it). Full suite 6416 green; lint + format clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(reader): auto-sync the active cloud provider while reading Generalize the reader sync hook from useWebDAVSync to useFileSync so the active third-party cloud provider (WebDAV OR Google Drive) syncs per-book while reading — pull-on-open, debounced push on progress/booknote changes, cover/file upload — not just via the manual "Sync now" in settings. Since the providers are mutually exclusive, the hook drives exactly the one enabled backend, built through the provider registry. The build is async (the Google Drive provider probes the OS keychain), so the engine lives in state and the pull-on-open waits for it; switching providers mid-session resets the per-book locks. The engine is keyed on connection-relevant settings so a lastSyncedAt write doesn't re-probe the keychain. deviceId / lastSyncedAt now write the active provider's settings slice; the auth-failed toast is provider-neutral; the per-book events are renamed *-file-sync. WebDAV reader-sync behaviour is unchanged. Full suite 6416 green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(settings): surface cloud providers in the section with inline switch Show WebDAV + Google Drive as separate rows in the Third-party Cloud Sync section (instead of one "Cloud Sync" row), so both providers are visible and the active one can be switched right there. - CloudProviderRow: a trailing radio makes a provider the single active sync target inline (enabled only when it's already configured — WebDAV creds / a Drive token); the row body / chevron opens its config sub-page (connect, sync options, disconnect). Status reads Active / Configured / Not connected, with a Syncing… indicator. - Each provider drills into its own sub-page again (WebDAV / Google Drive), rendering the embeddable panel under a SubPageHeader; the brief unified CloudSyncForm picker page is removed (its old deep-link maps to Google Drive). - Switching stays exclusive via withActiveCloudProvider; an inline switch trusts the stored credentials/token (no re-validate / re-OAuth). Full suite 6416 green; lint + format clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sync): gate third-party cloud sync behind a premium plan WebDAV + Google Drive sync is now a premium feature: available on any paid plan (Plus, Pro, or Lifetime), not on free. - isCloudSyncInPlan(plan) helper (mirrors isEmailInPlan; plus/pro/purchase). - IntegrationsPanel: free users see the Third-party Cloud Sync section with an upgrade row ("Available on Plus, Pro, or Lifetime") that opens the plans page instead of the provider rows; the cloud-sync deep-links are gated too (waiting for the plan to load before deciding). - useFileSync: the reader's auto-sync only runs on a paid plan, so a downgraded user's sync stops even if a provider's enabled flag lingers. Full suite 6418 green; lint + format clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(sync): escape backslashes in Drive query literals (CodeQL) escapeDriveLiteral escaped single quotes but not the backslash escape character, so a file name containing a backslash (or ending in one) could break out of the single-quoted Drive `files.list` query literal and malform the query. Escape backslashes first, then single quotes, so the backslashes added for the quotes are not doubled. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
189 lines
5.7 KiB
JSON
189 lines
5.7 KiB
JSON
{
|
|
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
|
"productName": "Readest",
|
|
"mainBinaryName": "readest",
|
|
"identifier": "com.bilingify.readest",
|
|
"version": "../package.json",
|
|
"build": {
|
|
"frontendDist": "../out",
|
|
"devUrl": "http://localhost:3000",
|
|
"beforeDevCommand": "pnpm dev",
|
|
"beforeBuildCommand": "pnpm build"
|
|
},
|
|
"app": {
|
|
"windows": [],
|
|
"security": {
|
|
"csp": {
|
|
"default-src": "'self' 'unsafe-inline' blob: data: customprotocol: asset: http://asset.localhost http://rangefile.localhost ipc: http://ipc.localhost",
|
|
"connect-src": "'self' blob: data: asset: http://asset.localhost http://rangefile.localhost ipc: http://ipc.localhost http://*:* https://*:* https://*.sentry.io https://*.posthog.com https://*.deepl.com https://*.wikipedia.org https://*.wiktionary.org https://*.supabase.co https://*.readest.com wss://speech.platform.bing.com https://*.cloudflarestorage.com https://translate.googleapis.com https://translate.toil.cc https://*.microsofttranslator.com https://edge.microsoft.com https://*.googleusercontent.com",
|
|
"img-src": "'self' blob: data: asset: http://asset.localhost https://* https://*:* http://* http://*:*",
|
|
"style-src": "'self' 'unsafe-inline' blob: asset: http://asset.localhost https://cdn.jsdelivr.net https://fonts.googleapis.com https://cdnjs.cloudflare.com https://storage.readest.com",
|
|
"font-src": "'self' blob: data: asset: http://asset.localhost tauri: https://db.onlinewebfonts.com https://cdn.jsdelivr.net https://fonts.gstatic.com https://cdnjs.cloudflare.com https://storage.readest.com",
|
|
"frame-src": "'self' blob: asset: http://asset.localhost https://*.stripe.com",
|
|
"script-src": "'self' 'unsafe-inline' 'unsafe-eval' data: blob: asset: http://asset.localhost https://*.sentry.io https://*.posthog.com https://*.stripe.com"
|
|
},
|
|
"assetProtocol": {
|
|
"enable": true,
|
|
"scope": {
|
|
"allow": [
|
|
"$RESOURCE/**",
|
|
"$APPDATA/**/*",
|
|
"$APPCACHE/**/*",
|
|
"$TEMP/**/*",
|
|
"**/Readest/**/*",
|
|
"/private/var/mobile/Containers/Data/Application/**/*"
|
|
],
|
|
"deny": []
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"bundle": {
|
|
"active": true,
|
|
"targets": "all",
|
|
"category": "Productivity",
|
|
"icon": [
|
|
"icons/32x32.png",
|
|
"icons/128x128.png",
|
|
"icons/128x128@2x.png",
|
|
"icons/icon.icns",
|
|
"icons/icon.ico"
|
|
],
|
|
"resources": [],
|
|
"windows": {
|
|
"webviewInstallMode": {
|
|
"type": "embedBootstrapper"
|
|
},
|
|
"nsis": {
|
|
"installerHooks": "nsis/installer-hooks.nsh",
|
|
"installMode": "both"
|
|
}
|
|
},
|
|
"macOS": {
|
|
"minimumSystemVersion": "12.0"
|
|
},
|
|
"linux": {
|
|
"deb": {
|
|
"section": "text"
|
|
}
|
|
},
|
|
"android": {
|
|
"minSdkVersion": 26
|
|
},
|
|
"iOS": {
|
|
"developmentTeam": "J5W48D69VR",
|
|
"infoPlist": "./Info-ios.plist",
|
|
"minimumSystemVersion": "15.0"
|
|
},
|
|
"fileAssociations": [
|
|
{
|
|
"name": "epub",
|
|
"ext": ["epub"],
|
|
"description": "EPUB file",
|
|
"mimeType": "application/epub+zip",
|
|
"role": "Viewer"
|
|
},
|
|
{
|
|
"name": "mobi",
|
|
"ext": ["mobi"],
|
|
"description": "MOBI file",
|
|
"mimeType": "application/x-mobipocket-ebook",
|
|
"role": "Viewer"
|
|
},
|
|
{
|
|
"name": "azw",
|
|
"ext": ["azw"],
|
|
"description": "AZW file",
|
|
"mimeType": "application/vnd.amazon.ebook",
|
|
"role": "Viewer"
|
|
},
|
|
{
|
|
"name": "azw3",
|
|
"ext": ["azw3"],
|
|
"description": "AZW3 file",
|
|
"mimeType": "application/vnd.amazon.mobi8-ebook",
|
|
"role": "Viewer"
|
|
},
|
|
{
|
|
"name": "fb2",
|
|
"ext": ["fb2"],
|
|
"description": "FB2 file",
|
|
"mimeType": "application/x-fictionbook+xml",
|
|
"role": "Viewer"
|
|
},
|
|
{
|
|
"name": "cbz",
|
|
"ext": ["cbz"],
|
|
"description": "CBZ file",
|
|
"mimeType": "application/vnd.comicbook+zip",
|
|
"role": "Viewer"
|
|
},
|
|
{
|
|
"name": "pdf",
|
|
"ext": ["pdf"],
|
|
"description": "PDF file",
|
|
"mimeType": "application/pdf",
|
|
"role": "Viewer"
|
|
}
|
|
],
|
|
"createUpdaterArtifacts": true
|
|
},
|
|
"plugins": {
|
|
"fs": {
|
|
"requireLiteralLeadingDot": false
|
|
},
|
|
"cli": {
|
|
"description": "Readest CLI",
|
|
"args": [
|
|
{
|
|
"name": "file1",
|
|
"index": 1,
|
|
"takesValue": true
|
|
},
|
|
{
|
|
"name": "file2",
|
|
"index": 2,
|
|
"takesValue": true
|
|
},
|
|
{
|
|
"name": "file3",
|
|
"index": 3,
|
|
"takesValue": true
|
|
},
|
|
{
|
|
"name": "file4",
|
|
"index": 4,
|
|
"takesValue": true
|
|
}
|
|
]
|
|
},
|
|
"deep-link": {
|
|
"mobile": [
|
|
{ "scheme": ["https"], "host": "web.readest.com", "appLink": true },
|
|
{ "scheme": ["readest"], "appLink": false },
|
|
{
|
|
"scheme": ["com.googleusercontent.apps.209390247301-ctpmep68ppfa56r1b8tr35e4qi4p60kq"],
|
|
"appLink": false
|
|
}
|
|
],
|
|
"desktop": {
|
|
"schemes": [
|
|
"readest",
|
|
"com.googleusercontent.apps.209390247301-ctpmep68ppfa56r1b8tr35e4qi4p60kq"
|
|
]
|
|
}
|
|
},
|
|
"updater": {
|
|
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEJFMEQ1QjE2OEU1NEIzNTEKUldSUnMxU09GbHNOdmpEaWFMT1crRFpEV2VORzQ2MklxaFc0M1R0ci9xY2c1bENXS0xhM1R1L2sK",
|
|
"endpoints": [
|
|
"https://download.readest.com/releases/latest.json",
|
|
"https://github.com/readest/readest/releases/latest/download/latest.json"
|
|
]
|
|
},
|
|
"webview-upgrade": {
|
|
"minUpgradeMajor": 121,
|
|
"minSupportedMajor": 92
|
|
}
|
|
}
|
|
}
|