dexie-cloud-addon 4.3.0 → 4.3.3

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.
@@ -8,7 +8,7 @@
8
8
  *
9
9
  * ==========================================================================
10
10
  *
11
- * Version 4.3.0, Tue Jan 20 2026
11
+ * Version 4.3.3, Thu Jan 22 2026
12
12
  *
13
13
  * https://dexie.org
14
14
  *
@@ -542,15 +542,6 @@
542
542
  }
543
543
  }
544
544
 
545
- /** Type guard to check if a message is an OAuthResultMessage */
546
- function isOAuthResultMessage(msg) {
547
- return (typeof msg === 'object' &&
548
- msg !== null &&
549
- msg.type === 'dexie:oauthResult' &&
550
- typeof msg.provider === 'string' &&
551
- typeof msg.state === 'string');
552
- }
553
-
554
545
  function assert(b) {
555
546
  if (!b)
556
547
  throw new Error('Assertion Failed');
@@ -2454,6 +2445,74 @@
2454
2445
  }
2455
2446
  }
2456
2447
 
2448
+ /** Cache for fetched SVG content to avoid re-fetching */
2449
+ const svgCache = {};
2450
+ /** Default SVG icons for built-in OAuth providers */
2451
+ const ProviderIcons = {
2452
+ google: `<svg viewBox="0 0 24 24" width="20" height="20"><path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/><path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/><path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/><path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/></svg>`,
2453
+ github: `<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z"/></svg>`,
2454
+ microsoft: `<svg viewBox="0 0 24 24" width="20" height="20"><rect fill="#F25022" x="1" y="1" width="10" height="10"/><rect fill="#00A4EF" x="1" y="13" width="10" height="10"/><rect fill="#7FBA00" x="13" y="1" width="10" height="10"/><rect fill="#FFB900" x="13" y="13" width="10" height="10"/></svg>`,
2455
+ apple: `<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.81-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z"/></svg>`,
2456
+ };
2457
+ /** Email/envelope icon for OTP option */
2458
+ const EmailIcon = `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="M22 6L12 13 2 6"/></svg>`;
2459
+ /**
2460
+ * Fetches SVG content from a URL and caches it.
2461
+ * Returns the SVG string or null if fetch fails.
2462
+ */
2463
+ function fetchSvgIcon(url) {
2464
+ return __awaiter(this, void 0, void 0, function* () {
2465
+ if (svgCache[url]) {
2466
+ return svgCache[url];
2467
+ }
2468
+ try {
2469
+ const res = yield fetch(url);
2470
+ if (res.ok) {
2471
+ const svg = yield res.text();
2472
+ // Validate it looks like SVG
2473
+ if (svg.includes('<svg')) {
2474
+ svgCache[url] = svg;
2475
+ return svg;
2476
+ }
2477
+ }
2478
+ }
2479
+ catch (_a) {
2480
+ // Silently fail - will show no icon
2481
+ }
2482
+ return null;
2483
+ });
2484
+ }
2485
+ /**
2486
+ * Converts an OAuthProviderInfo to a generic DXCOption.
2487
+ * Fetches SVG icons from URLs if needed.
2488
+ */
2489
+ function providerToOption(provider) {
2490
+ return __awaiter(this, void 0, void 0, function* () {
2491
+ var _a;
2492
+ let iconSvg;
2493
+ // First check for built-in icons
2494
+ if (ProviderIcons[provider.type]) {
2495
+ iconSvg = ProviderIcons[provider.type];
2496
+ }
2497
+ // If provider has iconUrl pointing to SVG, fetch and inline it
2498
+ else if ((_a = provider.iconUrl) === null || _a === void 0 ? void 0 : _a.toLowerCase().endsWith('.svg')) {
2499
+ const fetched = yield fetchSvgIcon(provider.iconUrl);
2500
+ if (fetched) {
2501
+ iconSvg = fetched;
2502
+ }
2503
+ }
2504
+ return {
2505
+ name: 'provider',
2506
+ value: provider.name,
2507
+ displayName: `Continue with ${provider.displayName}`,
2508
+ iconSvg,
2509
+ // If iconUrl is not SVG, pass it through for img tag rendering
2510
+ iconUrl: (!iconSvg && provider.iconUrl) ? provider.iconUrl : undefined,
2511
+ // Use provider type as style hint for branding
2512
+ styleHint: provider.type,
2513
+ };
2514
+ });
2515
+ }
2457
2516
  function interactWithUser(userInteraction, req) {
2458
2517
  return new Promise((resolve, reject) => {
2459
2518
  const interactionProps = Object.assign(Object.assign({ submitLabel: 'Submit', cancelLabel: 'Cancel' }, req), { onSubmit: (res) => {
@@ -2591,6 +2650,9 @@
2591
2650
  /**
2592
2651
  * Prompts the user to select an authentication method (OAuth provider or OTP).
2593
2652
  *
2653
+ * This function converts OAuth providers and OTP option into generic DXCOption[]
2654
+ * for the DXCSelect interaction, handling icon fetching and style hints.
2655
+ *
2594
2656
  * @param userInteraction - The user interaction BehaviorSubject
2595
2657
  * @param providers - Available OAuth providers
2596
2658
  * @param otpEnabled - Whether OTP is available
@@ -2598,34 +2660,70 @@
2598
2660
  * @param alerts - Optional alerts to display
2599
2661
  * @returns Promise resolving to the user's selection
2600
2662
  */
2601
- function promptForProvider(userInteraction, providers, otpEnabled, title = 'Choose login method', alerts = []) {
2602
- return new Promise((resolve, reject) => {
2603
- const interactionProps = {
2604
- type: 'provider-selection',
2605
- title,
2606
- alerts,
2607
- providers,
2608
- otpEnabled,
2609
- fields: {},
2610
- submitLabel: undefined,
2611
- cancelLabel: 'Cancel',
2612
- onSelectProvider: (providerName) => {
2613
- userInteraction.next(undefined);
2614
- resolve({ type: 'provider', provider: providerName });
2615
- },
2616
- onSelectOtp: () => {
2617
- userInteraction.next(undefined);
2618
- resolve({ type: 'otp' });
2619
- },
2620
- onCancel: () => {
2621
- userInteraction.next(undefined);
2622
- reject(new Dexie.AbortError('User cancelled'));
2623
- },
2624
- };
2625
- userInteraction.next(interactionProps);
2663
+ function promptForProvider(userInteraction_1, providers_1, otpEnabled_1) {
2664
+ return __awaiter(this, arguments, void 0, function* (userInteraction, providers, otpEnabled, title = 'Choose login method', alerts = []) {
2665
+ // Convert providers to generic options (with icon fetching)
2666
+ const providerOptions = yield Promise.all(providers.map(providerToOption));
2667
+ // Build the options array
2668
+ const options = [...providerOptions];
2669
+ // Add OTP option if enabled
2670
+ if (otpEnabled) {
2671
+ options.push({
2672
+ name: 'otp',
2673
+ value: 'email',
2674
+ displayName: 'Continue with email',
2675
+ iconSvg: EmailIcon,
2676
+ styleHint: 'otp',
2677
+ });
2678
+ }
2679
+ return new Promise((resolve, reject) => {
2680
+ const interactionProps = {
2681
+ type: 'generic',
2682
+ title,
2683
+ alerts,
2684
+ options,
2685
+ fields: {},
2686
+ submitLabel: '', // No submit button - just options
2687
+ cancelLabel: 'Cancel',
2688
+ onSubmit: (params) => {
2689
+ userInteraction.next(undefined);
2690
+ // Check which option was selected
2691
+ if ('otp' in params) {
2692
+ resolve({ type: 'otp' });
2693
+ }
2694
+ else if ('provider' in params) {
2695
+ resolve({ type: 'provider', provider: params.provider });
2696
+ }
2697
+ else {
2698
+ // Unknown - default to OTP
2699
+ resolve({ type: 'otp' });
2700
+ }
2701
+ },
2702
+ onCancel: () => {
2703
+ userInteraction.next(undefined);
2704
+ reject(new Dexie.AbortError('User cancelled'));
2705
+ },
2706
+ };
2707
+ userInteraction.next(interactionProps);
2708
+ });
2626
2709
  });
2627
2710
  }
2628
2711
 
2712
+ /**
2713
+ * Error thrown when initiating an OAuth redirect.
2714
+ *
2715
+ * This is not a real error - it's used to signal that the page is
2716
+ * navigating away to an OAuth provider. It should be caught and
2717
+ * silently ignored at the appropriate level.
2718
+ */
2719
+ class OAuthRedirectError extends Error {
2720
+ constructor(provider) {
2721
+ super(`OAuth redirect initiated for provider: ${provider}`);
2722
+ this.name = 'OAuthRedirectError';
2723
+ this.provider = provider;
2724
+ }
2725
+ }
2726
+
2629
2727
  function loadAccessToken(db) {
2630
2728
  return __awaiter(this, void 0, void 0, function* () {
2631
2729
  var _a, _b, _c;
@@ -2793,6 +2891,10 @@
2793
2891
  return context;
2794
2892
  }
2795
2893
  catch (error) {
2894
+ // OAuth redirect is not an error - page is navigating away
2895
+ if (error instanceof OAuthRedirectError || (error === null || error === void 0 ? void 0 : error.name) === 'OAuthRedirectError') {
2896
+ throw error; // Re-throw without logging
2897
+ }
2796
2898
  if (error instanceof TokenErrorResponseError) {
2797
2899
  yield alertUser(userInteraction, error.title, {
2798
2900
  type: 'error',
@@ -14558,8 +14660,6 @@
14558
14660
 
14559
14661
  /** User-friendly messages for OAuth error codes */
14560
14662
  const ERROR_MESSAGES = {
14561
- popup_blocked: 'The login popup was blocked by your browser. Please allow popups for this site and try again.',
14562
- popup_closed: 'The login popup was closed before completing authentication.',
14563
14663
  access_denied: 'Access was denied by the authentication provider.',
14564
14664
  invalid_state: 'The authentication response could not be verified. Please try again.',
14565
14665
  email_not_verified: 'Your email address must be verified before you can log in.',
@@ -14701,144 +14801,46 @@
14701
14801
  });
14702
14802
  }
14703
14803
 
14704
- /** Generate a random state string for CSRF protection */
14705
- function generateState() {
14706
- const array = new Uint8Array(32);
14707
- crypto.getRandomValues(array);
14708
- return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
14709
- }
14710
14804
  /** Build the OAuth login URL */
14711
- function buildOAuthLoginUrl(options, state) {
14805
+ function buildOAuthLoginUrl(options) {
14712
14806
  const url = new URL(`${options.databaseUrl}/oauth/login/${options.provider}`);
14713
- url.searchParams.set('state', state);
14714
- // Set the redirect URI for postMessage or custom scheme
14807
+ // Set the redirect URI - defaults to current page URL for web SPAs
14715
14808
  const redirectUri = options.redirectUri ||
14716
- (typeof window !== 'undefined' ? window.location.origin : '');
14809
+ (typeof window !== 'undefined' ? window.location.href : '');
14717
14810
  if (redirectUri) {
14718
14811
  url.searchParams.set('redirect_uri', redirectUri);
14719
14812
  }
14720
14813
  return url.toString();
14721
14814
  }
14722
- /** Calculate centered popup position */
14723
- function getPopupPosition(width, height) {
14724
- var _a, _b, _c, _d, _e, _f;
14725
- const screenLeft = (_a = window.screenLeft) !== null && _a !== void 0 ? _a : window.screenX;
14726
- const screenTop = (_b = window.screenTop) !== null && _b !== void 0 ? _b : window.screenY;
14727
- const screenWidth = (_d = (_c = window.innerWidth) !== null && _c !== void 0 ? _c : document.documentElement.clientWidth) !== null && _d !== void 0 ? _d : screen.width;
14728
- const screenHeight = (_f = (_e = window.innerHeight) !== null && _e !== void 0 ? _e : document.documentElement.clientHeight) !== null && _f !== void 0 ? _f : screen.height;
14729
- const left = screenLeft + (screenWidth - width) / 2;
14730
- const top = screenTop + (screenHeight - height) / 2;
14731
- return { left: Math.max(0, left), top: Math.max(0, top) };
14732
- }
14733
14815
  /**
14734
- * Initiates OAuth login flow using a popup window.
14816
+ * Initiates OAuth login via full page redirect.
14817
+ *
14818
+ * The page will navigate to the OAuth provider. After authentication,
14819
+ * the user is redirected back to the app with a `dxc-auth` query parameter
14820
+ * containing base64url-encoded JSON with the authorization code.
14735
14821
  *
14736
- * Opens a popup to the OAuth provider, listens for postMessage with the result,
14737
- * and returns the Dexie Cloud authorization code.
14822
+ * The dexie-cloud-addon automatically detects and processes this parameter
14823
+ * when db.cloud.configure() is called on page load.
14738
14824
  *
14739
- * @param options - OAuth login options
14740
- * @returns Promise resolving to OAuthLoginResult
14741
- * @throws OAuthError on failure
14825
+ * @param options - OAuth redirect options
14826
+ *
14827
+ * @example
14828
+ * ```typescript
14829
+ * // Initiate OAuth login
14830
+ * startOAuthRedirect({
14831
+ * databaseUrl: 'https://mydb.dexie.cloud',
14832
+ * provider: 'google'
14833
+ * });
14834
+ * // Page navigates away, user authenticates, then returns with auth code
14835
+ * ```
14742
14836
  */
14743
- function oauthLogin(options) {
14744
- return __awaiter(this, void 0, void 0, function* () {
14745
- const { databaseUrl, provider, usePopup = true } = options;
14746
- if (!usePopup) {
14747
- // For redirect flows, we can't return a promise - the page will navigate away
14748
- throw new Error('Non-popup OAuth flow requires handleOAuthCallback after redirect');
14749
- }
14750
- const state = generateState();
14751
- const loginUrl = buildOAuthLoginUrl(options, state);
14752
- // Calculate popup dimensions and position
14753
- const width = 500;
14754
- const height = 600;
14755
- const { left, top } = getPopupPosition(width, height);
14756
- // Open popup window
14757
- const popup = window.open(loginUrl, 'dexie-cloud-oauth', `width=${width},height=${height},left=${left},top=${top},menubar=no,toolbar=no,location=yes,status=no`);
14758
- if (!popup) {
14759
- throw new OAuthError('popup_blocked', provider);
14760
- }
14761
- return new Promise((resolve, reject) => {
14762
- let resolved = false;
14763
- // Listen for postMessage from the popup
14764
- const handleMessage = (event) => {
14765
- // Validate origin - must be from the Dexie Cloud server
14766
- const expectedOrigin = new URL(databaseUrl).origin;
14767
- if (event.origin !== expectedOrigin) {
14768
- return; // Ignore messages from other origins
14769
- }
14770
- // Check if this is our OAuth result message
14771
- if (!isOAuthResultMessage(event.data)) {
14772
- return;
14773
- }
14774
- const message = event.data;
14775
- // Validate state to prevent CSRF
14776
- if (message.state !== state) {
14777
- console.warn('[dexie-cloud] OAuth state mismatch, ignoring message');
14778
- return;
14779
- }
14780
- // Clean up
14781
- cleanup();
14782
- resolved = true;
14783
- // Handle error from OAuth flow
14784
- if (message.error) {
14785
- const errorCode = mapOAuthError(message.error);
14786
- reject(new OAuthError(errorCode, provider, message.error));
14787
- return;
14788
- }
14789
- // Success - return the authorization code
14790
- if (message.code) {
14791
- resolve({
14792
- code: message.code,
14793
- provider: message.provider,
14794
- state: message.state,
14795
- });
14796
- }
14797
- else {
14798
- reject(new OAuthError('provider_error', provider, 'No authorization code received'));
14799
- }
14800
- };
14801
- // Check if popup was closed without completing
14802
- const checkPopupClosed = setInterval(() => {
14803
- if (popup.closed && !resolved) {
14804
- cleanup();
14805
- reject(new OAuthError('popup_closed', provider));
14806
- }
14807
- }, 500);
14808
- // Cleanup function
14809
- const cleanup = () => {
14810
- window.removeEventListener('message', handleMessage);
14811
- clearInterval(checkPopupClosed);
14812
- try {
14813
- if (!popup.closed) {
14814
- popup.close();
14815
- }
14816
- }
14817
- catch (_a) {
14818
- // Ignore errors when closing popup
14819
- }
14820
- };
14821
- // Start listening for messages
14822
- window.addEventListener('message', handleMessage);
14823
- });
14824
- });
14825
- }
14826
- /** Map OAuth error strings to error codes */
14827
- function mapOAuthError(error) {
14828
- const lowerError = error.toLowerCase();
14829
- if (lowerError.includes('access_denied') || lowerError.includes('access denied')) {
14830
- return 'access_denied';
14831
- }
14832
- if (lowerError.includes('email') && lowerError.includes('verif')) {
14833
- return 'email_not_verified';
14834
- }
14835
- if (lowerError.includes('expired')) {
14836
- return 'expired_code';
14837
- }
14838
- if (lowerError.includes('state')) {
14839
- return 'invalid_state';
14837
+ function startOAuthRedirect(options) {
14838
+ // Store provider in sessionStorage for reference on callback
14839
+ if (typeof sessionStorage !== 'undefined') {
14840
+ sessionStorage.setItem('dexie-cloud-oauth-provider', options.provider);
14840
14841
  }
14841
- return 'provider_error';
14842
+ const loginUrl = buildOAuthLoginUrl(options);
14843
+ window.location.href = loginUrl;
14842
14844
  }
14843
14845
 
14844
14846
  function otpFetchTokenCallback(db) {
@@ -14859,9 +14861,11 @@
14859
14861
  scopes: ['ACCESS_DB'],
14860
14862
  });
14861
14863
  }
14862
- // Handle OAuth provider login (popup flow)
14864
+ // Handle OAuth provider login via redirect
14863
14865
  if (hints === null || hints === void 0 ? void 0 : hints.provider) {
14864
- return yield handleOAuthFlow(db, public_key, hints.provider);
14866
+ initiateOAuthRedirect(db, hints.provider);
14867
+ // This function never returns - page navigates away
14868
+ throw new OAuthRedirectError(hints.provider);
14865
14869
  }
14866
14870
  if ((hints === null || hints === void 0 ? void 0 : hints.grant_type) === 'demo') {
14867
14871
  const demo_user = yield promptForEmail(userInteraction, 'Enter a demo user email', (hints === null || hints === void 0 ? void 0 : hints.email) || (hints === null || hints === void 0 ? void 0 : hints.userId));
@@ -14893,8 +14897,10 @@
14893
14897
  if (authProviders.providers.length > 0) {
14894
14898
  const selection = yield promptForProvider(userInteraction, authProviders.providers, authProviders.otpEnabled, 'Sign in');
14895
14899
  if (selection.type === 'provider') {
14896
- // User selected an OAuth provider
14897
- return yield handleOAuthFlow(db, public_key, selection.provider);
14900
+ // User selected an OAuth provider - initiate redirect
14901
+ initiateOAuthRedirect(db, selection.provider);
14902
+ // This function never returns - page navigates away
14903
+ throw new OAuthRedirectError(selection.provider);
14898
14904
  }
14899
14905
  // User chose OTP - continue with email prompt below
14900
14906
  }
@@ -14976,46 +14982,24 @@
14976
14982
  };
14977
14983
  }
14978
14984
  /**
14979
- * Handles the OAuth popup flow and token exchange.
14985
+ * Initiates OAuth login via full page redirect.
14986
+ *
14987
+ * The page will navigate away to the OAuth provider. After authentication,
14988
+ * the user is redirected back with a dxc-auth query parameter that is
14989
+ * automatically detected by db.cloud.configure().
14980
14990
  */
14981
- function handleOAuthFlow(db, publicKey, provider) {
14982
- return __awaiter(this, void 0, void 0, function* () {
14983
- var _a, _b, _c;
14984
- const url = (_a = db.cloud.options) === null || _a === void 0 ? void 0 : _a.databaseUrl;
14985
- if (!url)
14986
- throw new Error(`No database URL given.`);
14987
- const { userInteraction } = db.cloud;
14988
- const usePopup = ((_b = db.cloud.options) === null || _b === void 0 ? void 0 : _b.oauthPopup) !== false;
14989
- const redirectUri = ((_c = db.cloud.options) === null || _c === void 0 ? void 0 : _c.oauthRedirectUri) ||
14990
- (typeof window !== 'undefined' ? window.location.origin : undefined);
14991
- try {
14992
- // Start OAuth popup flow
14993
- const result = yield oauthLogin({
14994
- databaseUrl: url,
14995
- provider,
14996
- redirectUri,
14997
- usePopup,
14998
- });
14999
- // Exchange the auth code for tokens
15000
- return yield exchangeOAuthCode({
15001
- databaseUrl: url,
15002
- code: result.code,
15003
- publicKey,
15004
- scopes: ['ACCESS_DB'],
15005
- });
15006
- }
15007
- catch (error) {
15008
- if (error instanceof OAuthError) {
15009
- // Show user-friendly error message
15010
- yield alertUser(userInteraction, 'Authentication Failed', {
15011
- type: 'error',
15012
- messageCode: 'GENERIC_ERROR',
15013
- message: error.userMessage,
15014
- messageParams: {},
15015
- }).catch(() => { });
15016
- }
15017
- throw error;
15018
- }
14991
+ function initiateOAuthRedirect(db, provider) {
14992
+ var _a, _b;
14993
+ const url = (_a = db.cloud.options) === null || _a === void 0 ? void 0 : _a.databaseUrl;
14994
+ if (!url)
14995
+ throw new Error(`No database URL given.`);
14996
+ const redirectUri = ((_b = db.cloud.options) === null || _b === void 0 ? void 0 : _b.oauthRedirectUri) ||
14997
+ (typeof window !== 'undefined' ? window.location.href : undefined);
14998
+ // Start OAuth redirect flow - page navigates away
14999
+ startOAuthRedirect({
15000
+ databaseUrl: url,
15001
+ provider,
15002
+ redirectUri,
15019
15003
  });
15020
15004
  }
15021
15005
 
@@ -15105,7 +15089,15 @@
15105
15089
  claims: {},
15106
15090
  lastLogin: new Date(0),
15107
15091
  });
15108
- yield authenticate(db.cloud.options.databaseUrl, context, db.cloud.options.fetchTokens || otpFetchTokenCallback(db), db.cloud.userInteraction, hints);
15092
+ try {
15093
+ yield authenticate(db.cloud.options.databaseUrl, context, db.cloud.options.fetchTokens || otpFetchTokenCallback(db), db.cloud.userInteraction, hints);
15094
+ }
15095
+ catch (err) {
15096
+ if (err.name === 'OAuthRedirectError') {
15097
+ return false; // Page is redirecting for OAuth login
15098
+ }
15099
+ throw err;
15100
+ }
15109
15101
  if (origUserId !== UNAUTHORIZED_USER.userId &&
15110
15102
  context.userId !== origUserId) {
15111
15103
  // User was logged in before, but now logged in as another user.
@@ -17039,7 +17031,10 @@
17039
17031
  ProviderButtonIcon: {
17040
17032
  width: "20px",
17041
17033
  height: "20px",
17042
- flexShrink: 0
17034
+ flexShrink: 0,
17035
+ display: "flex",
17036
+ alignItems: "center",
17037
+ justifyContent: "center"
17043
17038
  },
17044
17039
  ProviderButtonText: {
17045
17040
  flex: 1,
@@ -17104,14 +17099,7 @@
17104
17099
  color: "#374151",
17105
17100
  transition: "all 0.2s ease",
17106
17101
  gap: "12px"
17107
- },
17108
- // Cancel button for provider selection
17109
- CancelButtonRow: {
17110
- display: "flex",
17111
- justifyContent: "center",
17112
- marginTop: "16px"
17113
- }
17114
- };
17102
+ }};
17115
17103
 
17116
17104
  function Dialog({ children, className }) {
17117
17105
  return (_$1("div", { className: `dexie-dialog ${className || ''}` },
@@ -17140,19 +17128,126 @@
17140
17128
  return message.replace(/\{\w+\}/ig, n => messageParams[n.substring(1, n.length - 1)]);
17141
17129
  }
17142
17130
 
17131
+ /** Get style based on styleHint (for provider branding, etc.) */
17132
+ function getOptionStyle(styleHint) {
17133
+ const baseStyle = Object.assign({}, Styles.ProviderButton);
17134
+ if (!styleHint) {
17135
+ return baseStyle;
17136
+ }
17137
+ switch (styleHint) {
17138
+ case 'google':
17139
+ return Object.assign(Object.assign({}, baseStyle), Styles.ProviderGoogle);
17140
+ case 'github':
17141
+ return Object.assign(Object.assign({}, baseStyle), Styles.ProviderGitHub);
17142
+ case 'microsoft':
17143
+ return Object.assign(Object.assign({}, baseStyle), Styles.ProviderMicrosoft);
17144
+ case 'apple':
17145
+ return Object.assign(Object.assign({}, baseStyle), Styles.ProviderApple);
17146
+ case 'otp':
17147
+ return Object.assign({}, Styles.OtpButton);
17148
+ case 'custom-oauth2':
17149
+ return Object.assign(Object.assign({}, baseStyle), Styles.ProviderCustom);
17150
+ default:
17151
+ return baseStyle;
17152
+ }
17153
+ }
17154
+ /**
17155
+ * Generic button component for selectable options.
17156
+ * Displays the option's icon and display name.
17157
+ *
17158
+ * The icon can be:
17159
+ * - Inline SVG (iconSvg) - rendered directly with dangerouslySetInnerHTML
17160
+ * - Image URL (iconUrl) - rendered as an img tag
17161
+ *
17162
+ * Style is determined by the styleHint property for branding purposes.
17163
+ */
17164
+ function OptionButton({ option, onClick }) {
17165
+ const { displayName, iconUrl, iconSvg, styleHint, value } = option;
17166
+ const style = getOptionStyle(styleHint);
17167
+ // Get the text color from the button style for SVG fill processing
17168
+ const textColor = style.color || '#000000';
17169
+ // Process SVG to replace currentColor with actual text color
17170
+ const processedSvg = iconSvg
17171
+ ? iconSvg
17172
+ .replace(/fill="currentColor"/gi, `fill="${textColor}"`)
17173
+ .replace(/fill='currentColor'/gi, `fill='${textColor}'`)
17174
+ .replace(/stroke="currentColor"/gi, `stroke="${textColor}"`)
17175
+ .replace(/stroke='currentColor'/gi, `stroke='${textColor}'`)
17176
+ : null;
17177
+ // Render the appropriate icon
17178
+ const renderIcon = () => {
17179
+ // Inline SVG
17180
+ if (processedSvg) {
17181
+ return (_$1("span", { style: Styles.ProviderButtonIcon, "aria-hidden": "true", dangerouslySetInnerHTML: { __html: processedSvg } }));
17182
+ }
17183
+ // Image URL
17184
+ if (iconUrl) {
17185
+ return (_$1("img", { src: iconUrl, alt: "", style: Styles.ProviderButtonIcon, "aria-hidden": "true" }));
17186
+ }
17187
+ return null;
17188
+ };
17189
+ return (_$1("button", { type: "button", style: style, onClick: onClick, class: `dxc-option-btn${styleHint ? ` dxc-option-${styleHint}` : ''}`, "aria-label": displayName },
17190
+ renderIcon(),
17191
+ _$1("span", { style: Styles.ProviderButtonText }, displayName)));
17192
+ }
17193
+ /**
17194
+ * Visual divider with "or" text.
17195
+ */
17196
+ function Divider() {
17197
+ return (_$1("div", { style: Styles.Divider },
17198
+ _$1("div", { style: Styles.DividerLine }),
17199
+ _$1("span", { style: Styles.DividerText }, "or"),
17200
+ _$1("div", { style: Styles.DividerLine })));
17201
+ }
17202
+
17143
17203
  const OTP_LENGTH = 8;
17144
- function LoginDialog({ title, type, alerts, fields, submitLabel, cancelLabel, onCancel, onSubmit, }) {
17204
+ /**
17205
+ * Generic dialog that can render:
17206
+ * - Form fields (text inputs)
17207
+ * - Selectable options (buttons)
17208
+ * - Or both together
17209
+ *
17210
+ * When an option is clicked, calls onSubmit({ [option.name]: option.value }).
17211
+ * This unified approach means the same callback handles both form submission
17212
+ * and option selection.
17213
+ */
17214
+ function LoginDialog({ title, alerts, fields, options, submitLabel, cancelLabel, onCancel, onSubmit, }) {
17145
17215
  const [params, setParams] = d({});
17146
17216
  const firstFieldRef = A(null);
17147
17217
  _(() => { var _a; return (_a = firstFieldRef.current) === null || _a === void 0 ? void 0 : _a.focus(); }, []);
17218
+ const fieldEntries = Object.entries(fields || {});
17219
+ const hasFields = fieldEntries.length > 0;
17220
+ const hasOptions = options && options.length > 0;
17221
+ // Group options by name to detect if we have multiple groups
17222
+ const optionGroups = new Map();
17223
+ if (options) {
17224
+ for (const option of options) {
17225
+ const group = optionGroups.get(option.name) || [];
17226
+ group.push(option);
17227
+ optionGroups.set(option.name, group);
17228
+ }
17229
+ }
17230
+ const hasMultipleGroups = optionGroups.size > 1;
17231
+ // Handler for option clicks - calls onSubmit with { [option.name]: option.value }
17232
+ const handleOptionClick = (option) => {
17233
+ onSubmit({ [option.name]: option.value });
17234
+ };
17148
17235
  return (_$1(Dialog, { className: "dxc-login-dlg" },
17149
17236
  _$1(k$1, null,
17150
17237
  _$1("h3", { style: Styles.WindowHeader }, title),
17151
- alerts.map((alert) => (_$1("p", { style: Styles.Alert[alert.type] }, resolveText(alert)))),
17152
- _$1("form", { onSubmit: (ev) => {
17238
+ alerts.map((alert, idx) => (_$1("p", { key: idx, style: Styles.Alert[alert.type] }, resolveText(alert)))),
17239
+ hasOptions && (_$1("div", { class: "dxc-options" }, hasMultipleGroups ? (
17240
+ // Render with dividers between groups
17241
+ Array.from(optionGroups.entries()).map(([groupName, groupOptions], groupIdx) => (_$1(k$1, { key: groupName },
17242
+ groupIdx > 0 && _$1(Divider, null),
17243
+ groupOptions.map((option) => (_$1(OptionButton, { key: `${option.name}-${option.value}`, option: option, onClick: () => handleOptionClick(option) }))))))) : (
17244
+ // Simple case: all options in one group
17245
+ options.map((option) => (_$1(OptionButton, { key: `${option.name}-${option.value}`, option: option, onClick: () => handleOptionClick(option) })))))),
17246
+ hasOptions && hasFields && _$1(Divider, null),
17247
+ hasFields && (_$1("form", { onSubmit: (ev) => {
17153
17248
  ev.preventDefault();
17154
17249
  onSubmit(params);
17155
- } }, Object.entries(fields).map(([fieldName, { type, label, placeholder }], idx) => (_$1("label", { style: Styles.Label, key: idx },
17250
+ } }, fieldEntries.map(([fieldName, { type, label, placeholder }], idx) => (_$1("label", { style: Styles.Label, key: idx },
17156
17251
  label ? `${label}: ` : '',
17157
17252
  _$1("input", { ref: idx === 0 ? firstFieldRef : undefined, type: type, name: fieldName, autoComplete: "on", style: Styles.Input, autoFocus: true, placeholder: placeholder, value: params[fieldName] || '', onInput: (ev) => {
17158
17253
  var _a;
@@ -17163,10 +17258,10 @@
17163
17258
  // Auto-submit when OTP is filled in.
17164
17259
  onSubmit(updatedParams);
17165
17260
  }
17166
- } })))))),
17261
+ } }))))))),
17167
17262
  _$1("div", { style: Styles.ButtonsDiv },
17168
17263
  _$1(k$1, null,
17169
- _$1("button", { type: "submit", style: Styles.PrimaryButton, onClick: () => onSubmit(params) }, submitLabel),
17264
+ submitLabel && (hasFields || (!hasOptions && !hasFields)) && (_$1("button", { type: "submit", style: Styles.PrimaryButton, onClick: () => onSubmit(params) }, submitLabel)),
17170
17265
  cancelLabel && (_$1("button", { style: Styles.Button, onClick: onCancel }, cancelLabel))))));
17171
17266
  }
17172
17267
  function valueTransformer(type, value) {
@@ -17180,82 +17275,6 @@
17180
17275
  }
17181
17276
  }
17182
17277
 
17183
- /** Default SVG icons for built-in providers */
17184
- const ProviderIcons = {
17185
- google: `<svg viewBox="0 0 24 24" width="20" height="20"><path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/><path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/><path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/><path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/></svg>`,
17186
- github: `<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z"/></svg>`,
17187
- microsoft: `<svg viewBox="0 0 24 24" width="20" height="20"><rect fill="#F25022" x="1" y="1" width="10" height="10"/><rect fill="#00A4EF" x="1" y="13" width="10" height="10"/><rect fill="#7FBA00" x="13" y="1" width="10" height="10"/><rect fill="#FFB900" x="13" y="13" width="10" height="10"/></svg>`,
17188
- apple: `<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.81-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z"/></svg>`,
17189
- };
17190
- /** Get provider-specific button styles */
17191
- function getProviderStyle(providerType) {
17192
- const baseStyle = Object.assign({}, Styles.ProviderButton);
17193
- switch (providerType) {
17194
- case 'google':
17195
- return Object.assign(Object.assign({}, baseStyle), Styles.ProviderGoogle);
17196
- case 'github':
17197
- return Object.assign(Object.assign({}, baseStyle), Styles.ProviderGitHub);
17198
- case 'microsoft':
17199
- return Object.assign(Object.assign({}, baseStyle), Styles.ProviderMicrosoft);
17200
- case 'apple':
17201
- return Object.assign(Object.assign({}, baseStyle), Styles.ProviderApple);
17202
- default:
17203
- return Object.assign(Object.assign({}, baseStyle), Styles.ProviderCustom);
17204
- }
17205
- }
17206
- /**
17207
- * Button component for OAuth provider login.
17208
- * Displays the provider's icon and name following provider branding guidelines.
17209
- */
17210
- function AuthProviderButton({ provider, onClick }) {
17211
- const { type, name, displayName, iconUrl } = provider;
17212
- const style = getProviderStyle(type);
17213
- // Determine button text
17214
- const buttonText = `Continue with ${displayName}`;
17215
- // Get icon - use custom iconUrl if provided, otherwise use built-in SVG
17216
- const iconSvg = ProviderIcons[type] || '';
17217
- return (_$1("button", { type: "button", style: style, onClick: onClick, class: `dxc-provider-btn dxc-provider-${type}`, "aria-label": buttonText },
17218
- iconUrl ? (_$1("img", { src: iconUrl, alt: "", style: Styles.ProviderButtonIcon, "aria-hidden": "true" })) : iconSvg ? (_$1("span", { style: Styles.ProviderButtonIcon, "aria-hidden": "true", dangerouslySetInnerHTML: { __html: iconSvg } })) : null,
17219
- _$1("span", { style: Styles.ProviderButtonText }, buttonText)));
17220
- }
17221
- /** Email/envelope icon for OTP button */
17222
- const EmailIcon = `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="M22 6L12 13 2 6"/></svg>`;
17223
- /**
17224
- * Button for email/OTP authentication option.
17225
- */
17226
- function OtpButton({ onClick }) {
17227
- return (_$1("button", { type: "button", style: Styles.OtpButton, onClick: onClick, class: "dxc-otp-btn", "aria-label": "Continue with email" },
17228
- _$1("span", { style: Styles.ProviderButtonIcon, "aria-hidden": "true", dangerouslySetInnerHTML: { __html: EmailIcon } }),
17229
- _$1("span", { style: Styles.ProviderButtonText }, "Continue with email")));
17230
- }
17231
- /**
17232
- * Visual divider with "or" text.
17233
- */
17234
- function Divider() {
17235
- return (_$1("div", { style: Styles.Divider },
17236
- _$1("div", { style: Styles.DividerLine }),
17237
- _$1("span", { style: Styles.DividerText }, "or"),
17238
- _$1("div", { style: Styles.DividerLine })));
17239
- }
17240
-
17241
- /**
17242
- * Dialog component for OAuth provider selection.
17243
- * Displays available OAuth providers as buttons and optionally an email/OTP option.
17244
- */
17245
- function ProviderSelectionDialog({ title, alerts, providers, otpEnabled, cancelLabel, onSelectProvider, onSelectOtp, onCancel, }) {
17246
- return (_$1(Dialog, { className: "dxc-provider-selection-dlg" },
17247
- _$1(k$1, null,
17248
- _$1("h3", { style: Styles.WindowHeader }, title),
17249
- alerts.map((alert, idx) => (_$1("p", { key: idx, style: Styles.Alert[alert.type] }, resolveText(alert)))),
17250
- _$1("div", { class: "dxc-providers" }, providers.map((provider) => (_$1(AuthProviderButton, { key: provider.name, provider: provider, onClick: () => onSelectProvider(provider.name) })))),
17251
- otpEnabled && providers.length > 0 && (_$1(k$1, null,
17252
- _$1(Divider, null),
17253
- _$1(OtpButton, { onClick: onSelectOtp }))),
17254
- otpEnabled && providers.length === 0 && (_$1(OtpButton, { onClick: onSelectOtp })),
17255
- cancelLabel && (_$1("div", { style: Styles.CancelButtonRow },
17256
- _$1("button", { type: "button", style: Styles.Button, onClick: onCancel }, cancelLabel))))));
17257
- }
17258
-
17259
17278
  class LoginGui extends x {
17260
17279
  constructor(props) {
17261
17280
  super(props);
@@ -17274,11 +17293,8 @@
17274
17293
  render(props, { userInteraction }) {
17275
17294
  if (!userInteraction)
17276
17295
  return null;
17277
- // Render appropriate dialog based on interaction type
17278
- if (userInteraction.type === 'provider-selection') {
17279
- return _$1(ProviderSelectionDialog, Object.assign({}, userInteraction));
17280
- }
17281
- // Default to LoginDialog for other interaction types
17296
+ // LoginDialog handles all interaction types uniformly
17297
+ // (forms with fields, options, or both)
17282
17298
  return _$1(LoginDialog, Object.assign({}, userInteraction));
17283
17299
  }
17284
17300
  }
@@ -17837,6 +17853,83 @@
17837
17853
  return awareness;
17838
17854
  }
17839
17855
 
17856
+ /**
17857
+ * Decodes a base64url-encoded string to a regular string.
17858
+ * Base64url uses - instead of + and _ instead of /, and may omit padding.
17859
+ */
17860
+ function decodeBase64Url(encoded) {
17861
+ // Add padding if needed
17862
+ const padded = encoded + '='.repeat((4 - (encoded.length % 4)) % 4);
17863
+ // Convert base64url to base64
17864
+ const base64 = padded.replace(/-/g, '+').replace(/_/g, '/');
17865
+ return atob(base64);
17866
+ }
17867
+ /**
17868
+ * Parses OAuth callback parameters from the dxc-auth query parameter.
17869
+ *
17870
+ * The dxc-auth parameter contains base64url-encoded JSON with the following structure:
17871
+ * - On success: { "code": "...", "provider": "...", "state": "..." }
17872
+ * - On error: { "error": "...", "provider": "...", "state": "..." }
17873
+ *
17874
+ * @param url - The URL to parse (defaults to window.location.href)
17875
+ * @returns OAuthCallbackParams if valid callback, null otherwise
17876
+ * @throws OAuthError if there's an error in the callback
17877
+ */
17878
+ function parseOAuthCallback(url) {
17879
+ const targetUrl = (typeof window !== 'undefined' ? window.location.href : '');
17880
+ if (!targetUrl) {
17881
+ return null;
17882
+ }
17883
+ const parsed = new URL(targetUrl);
17884
+ const encoded = parsed.searchParams.get('dxc-auth');
17885
+ if (!encoded) {
17886
+ return null; // Not an OAuth callback URL
17887
+ }
17888
+ let payload;
17889
+ try {
17890
+ const json = decodeBase64Url(encoded);
17891
+ payload = JSON.parse(json);
17892
+ }
17893
+ catch (e) {
17894
+ console.warn('[dexie-cloud] Failed to parse dxc-auth parameter:', e);
17895
+ return null;
17896
+ }
17897
+ const { code, provider, state, error } = payload;
17898
+ // Check for error first
17899
+ if (error) {
17900
+ if (error.toLowerCase().includes('access_denied') || error.toLowerCase().includes('access denied')) {
17901
+ throw new OAuthError('access_denied', provider, error);
17902
+ }
17903
+ if (error.toLowerCase().includes('email') && error.toLowerCase().includes('verif')) {
17904
+ throw new OAuthError('email_not_verified', provider, error);
17905
+ }
17906
+ throw new OAuthError('provider_error', provider, error);
17907
+ }
17908
+ // Validate required fields for success case
17909
+ if (!code || !provider || !state) {
17910
+ console.warn('[dexie-cloud] Invalid dxc-auth payload: missing required fields');
17911
+ return null;
17912
+ }
17913
+ return { code, provider, state };
17914
+ }
17915
+ /**
17916
+ * Cleans up the dxc-auth query parameter from the URL.
17917
+ * Call this after successfully handling the callback to clean up the browser URL.
17918
+ */
17919
+ function cleanupOAuthUrl() {
17920
+ var _a;
17921
+ if (typeof window === 'undefined' || !((_a = window.history) === null || _a === void 0 ? void 0 : _a.replaceState)) {
17922
+ return;
17923
+ }
17924
+ const url = new URL(window.location.href);
17925
+ if (!url.searchParams.has('dxc-auth')) {
17926
+ return;
17927
+ }
17928
+ url.searchParams.delete('dxc-auth');
17929
+ const cleanUrl = url.pathname + (url.searchParams.toString() ? `?${url.searchParams.toString()}` : '') + url.hash;
17930
+ window.history.replaceState(null, '', cleanUrl);
17931
+ }
17932
+
17840
17933
  const DEFAULT_OPTIONS = {
17841
17934
  nameSuffix: true,
17842
17935
  };
@@ -17848,6 +17941,10 @@
17848
17941
  const currentUserEmitter = getCurrentUserEmitter(dexie);
17849
17942
  const subscriptions = [];
17850
17943
  let configuredProgramatically = false;
17944
+ // Pending OAuth auth code from dxc-auth redirect (detected in configure())
17945
+ let pendingOAuthCode = null;
17946
+ // Pending OAuth error from dxc-auth redirect (detected in configure())
17947
+ let pendingOAuthError = null;
17851
17948
  // local sync worker - used when there's no service worker.
17852
17949
  let localSyncWorker = null;
17853
17950
  dexie.on('ready', (dexie) => __awaiter(this, void 0, void 0, function* () {
@@ -17877,7 +17974,7 @@
17877
17974
  const syncComplete = new rxjs.Subject();
17878
17975
  dexie.cloud = {
17879
17976
  // @ts-ignore
17880
- version: "4.3.0",
17977
+ version: "4.3.3",
17881
17978
  options: Object.assign({}, DEFAULT_OPTIONS),
17882
17979
  schema: null,
17883
17980
  get currentUserId() {
@@ -17912,6 +18009,31 @@
17912
18009
  DexieCloudDB(dexie).reconfigure(); // Update observable from new dexie.name
17913
18010
  }
17914
18011
  updateSchemaFromOptions(dexie.cloud.schema, dexie.cloud.options);
18012
+ // Check for OAuth callback (dxc-auth query parameter)
18013
+ // Only check in DOM environment, not workers
18014
+ if (typeof window !== 'undefined' && window.location) {
18015
+ try {
18016
+ const callback = parseOAuthCallback();
18017
+ if (callback) {
18018
+ // Clean up URL immediately (remove dxc-auth param)
18019
+ cleanupOAuthUrl();
18020
+ // Store the pending auth code for processing when db is ready
18021
+ pendingOAuthCode = { code: callback.code, provider: callback.provider };
18022
+ console.debug('[dexie-cloud] OAuth callback detected, auth code stored for processing');
18023
+ }
18024
+ }
18025
+ catch (error) {
18026
+ // parseOAuthCallback throws OAuthError on error callbacks
18027
+ cleanupOAuthUrl();
18028
+ if (error instanceof OAuthError) {
18029
+ pendingOAuthError = error;
18030
+ console.error('[dexie-cloud] OAuth callback error:', error.message);
18031
+ }
18032
+ else {
18033
+ console.error('[dexie-cloud] OAuth callback error:', error);
18034
+ }
18035
+ }
18036
+ }
17915
18037
  },
17916
18038
  logout() {
17917
18039
  return __awaiter(this, arguments, void 0, function* ({ force } = {}) {
@@ -18105,7 +18227,34 @@
18105
18227
  }
18106
18228
  // HERE: If requireAuth, do athentication now.
18107
18229
  let changedUser = false;
18108
- const user = yield db.getCurrentUser();
18230
+ let user = yield db.getCurrentUser();
18231
+ // Show pending OAuth error if present (from dxc-auth redirect)
18232
+ if (pendingOAuthError && !db.cloud.isServiceWorkerDB) {
18233
+ const error = pendingOAuthError;
18234
+ pendingOAuthError = null; // Clear pending error
18235
+ console.debug('[dexie-cloud] Showing OAuth error:', error.message);
18236
+ // Show alert to user about the OAuth error
18237
+ yield alertUser(db.cloud.userInteraction, 'Authentication Error', {
18238
+ type: 'error',
18239
+ messageCode: 'GENERIC_ERROR',
18240
+ message: error.message,
18241
+ messageParams: { provider: error.provider || 'unknown' }
18242
+ });
18243
+ }
18244
+ // Process pending OAuth callback if present (from dxc-auth redirect)
18245
+ if (pendingOAuthCode && !db.cloud.isServiceWorkerDB) {
18246
+ const { code, provider } = pendingOAuthCode;
18247
+ pendingOAuthCode = null; // Clear pending code
18248
+ console.debug('[dexie-cloud] Processing OAuth callback, provider:', provider);
18249
+ try {
18250
+ changedUser = yield login(db, { oauthCode: code, provider });
18251
+ user = yield db.getCurrentUser();
18252
+ }
18253
+ catch (error) {
18254
+ console.error('[dexie-cloud] OAuth login failed:', error);
18255
+ // Continue with normal flow - user can try again
18256
+ }
18257
+ }
18109
18258
  const requireAuth = (_b = db.cloud.options) === null || _b === void 0 ? void 0 : _b.requireAuth;
18110
18259
  if (requireAuth) {
18111
18260
  if (db.cloud.isServiceWorkerDB) {
@@ -18194,7 +18343,7 @@
18194
18343
  }
18195
18344
  }
18196
18345
  // @ts-ignore
18197
- dexieCloud.version = "4.3.0";
18346
+ dexieCloud.version = "4.3.3";
18198
18347
  Dexie.Cloud = dexieCloud;
18199
18348
 
18200
18349
  // In case the SW lives for a while, let it reuse already opened connections: