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
  *
@@ -106,15 +106,6 @@ typeof SuppressedError === "function" ? SuppressedError : function (error, suppr
106
106
  return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
107
107
  };
108
108
 
109
- /** Type guard to check if a message is an OAuthResultMessage */
110
- function isOAuthResultMessage(msg) {
111
- return (typeof msg === 'object' &&
112
- msg !== null &&
113
- msg.type === 'dexie:oauthResult' &&
114
- typeof msg.provider === 'string' &&
115
- typeof msg.state === 'string');
116
- }
117
-
118
109
  function assert(b) {
119
110
  if (!b)
120
111
  throw new Error('Assertion Failed');
@@ -770,6 +761,74 @@ class TokenErrorResponseError extends Error {
770
761
  }
771
762
  }
772
763
 
764
+ /** Cache for fetched SVG content to avoid re-fetching */
765
+ const svgCache = {};
766
+ /** Default SVG icons for built-in OAuth providers */
767
+ const ProviderIcons = {
768
+ 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>`,
769
+ 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>`,
770
+ 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>`,
771
+ 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>`,
772
+ };
773
+ /** Email/envelope icon for OTP option */
774
+ 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>`;
775
+ /**
776
+ * Fetches SVG content from a URL and caches it.
777
+ * Returns the SVG string or null if fetch fails.
778
+ */
779
+ function fetchSvgIcon(url) {
780
+ return __awaiter(this, void 0, void 0, function* () {
781
+ if (svgCache[url]) {
782
+ return svgCache[url];
783
+ }
784
+ try {
785
+ const res = yield fetch(url);
786
+ if (res.ok) {
787
+ const svg = yield res.text();
788
+ // Validate it looks like SVG
789
+ if (svg.includes('<svg')) {
790
+ svgCache[url] = svg;
791
+ return svg;
792
+ }
793
+ }
794
+ }
795
+ catch (_a) {
796
+ // Silently fail - will show no icon
797
+ }
798
+ return null;
799
+ });
800
+ }
801
+ /**
802
+ * Converts an OAuthProviderInfo to a generic DXCOption.
803
+ * Fetches SVG icons from URLs if needed.
804
+ */
805
+ function providerToOption(provider) {
806
+ return __awaiter(this, void 0, void 0, function* () {
807
+ var _a;
808
+ let iconSvg;
809
+ // First check for built-in icons
810
+ if (ProviderIcons[provider.type]) {
811
+ iconSvg = ProviderIcons[provider.type];
812
+ }
813
+ // If provider has iconUrl pointing to SVG, fetch and inline it
814
+ else if ((_a = provider.iconUrl) === null || _a === void 0 ? void 0 : _a.toLowerCase().endsWith('.svg')) {
815
+ const fetched = yield fetchSvgIcon(provider.iconUrl);
816
+ if (fetched) {
817
+ iconSvg = fetched;
818
+ }
819
+ }
820
+ return {
821
+ name: 'provider',
822
+ value: provider.name,
823
+ displayName: `Continue with ${provider.displayName}`,
824
+ iconSvg,
825
+ // If iconUrl is not SVG, pass it through for img tag rendering
826
+ iconUrl: (!iconSvg && provider.iconUrl) ? provider.iconUrl : undefined,
827
+ // Use provider type as style hint for branding
828
+ styleHint: provider.type,
829
+ };
830
+ });
831
+ }
773
832
  function interactWithUser(userInteraction, req) {
774
833
  return new Promise((resolve, reject) => {
775
834
  const interactionProps = Object.assign(Object.assign({ submitLabel: 'Submit', cancelLabel: 'Cancel' }, req), { onSubmit: (res) => {
@@ -907,6 +966,9 @@ function confirmLogout(userInteraction, currentUserId, numUnsyncedChanges) {
907
966
  /**
908
967
  * Prompts the user to select an authentication method (OAuth provider or OTP).
909
968
  *
969
+ * This function converts OAuth providers and OTP option into generic DXCOption[]
970
+ * for the DXCSelect interaction, handling icon fetching and style hints.
971
+ *
910
972
  * @param userInteraction - The user interaction BehaviorSubject
911
973
  * @param providers - Available OAuth providers
912
974
  * @param otpEnabled - Whether OTP is available
@@ -914,34 +976,70 @@ function confirmLogout(userInteraction, currentUserId, numUnsyncedChanges) {
914
976
  * @param alerts - Optional alerts to display
915
977
  * @returns Promise resolving to the user's selection
916
978
  */
917
- function promptForProvider(userInteraction, providers, otpEnabled, title = 'Choose login method', alerts = []) {
918
- return new Promise((resolve, reject) => {
919
- const interactionProps = {
920
- type: 'provider-selection',
921
- title,
922
- alerts,
923
- providers,
924
- otpEnabled,
925
- fields: {},
926
- submitLabel: undefined,
927
- cancelLabel: 'Cancel',
928
- onSelectProvider: (providerName) => {
929
- userInteraction.next(undefined);
930
- resolve({ type: 'provider', provider: providerName });
931
- },
932
- onSelectOtp: () => {
933
- userInteraction.next(undefined);
934
- resolve({ type: 'otp' });
935
- },
936
- onCancel: () => {
937
- userInteraction.next(undefined);
938
- reject(new Dexie.AbortError('User cancelled'));
939
- },
940
- };
941
- userInteraction.next(interactionProps);
979
+ function promptForProvider(userInteraction_1, providers_1, otpEnabled_1) {
980
+ return __awaiter(this, arguments, void 0, function* (userInteraction, providers, otpEnabled, title = 'Choose login method', alerts = []) {
981
+ // Convert providers to generic options (with icon fetching)
982
+ const providerOptions = yield Promise.all(providers.map(providerToOption));
983
+ // Build the options array
984
+ const options = [...providerOptions];
985
+ // Add OTP option if enabled
986
+ if (otpEnabled) {
987
+ options.push({
988
+ name: 'otp',
989
+ value: 'email',
990
+ displayName: 'Continue with email',
991
+ iconSvg: EmailIcon,
992
+ styleHint: 'otp',
993
+ });
994
+ }
995
+ return new Promise((resolve, reject) => {
996
+ const interactionProps = {
997
+ type: 'generic',
998
+ title,
999
+ alerts,
1000
+ options,
1001
+ fields: {},
1002
+ submitLabel: '', // No submit button - just options
1003
+ cancelLabel: 'Cancel',
1004
+ onSubmit: (params) => {
1005
+ userInteraction.next(undefined);
1006
+ // Check which option was selected
1007
+ if ('otp' in params) {
1008
+ resolve({ type: 'otp' });
1009
+ }
1010
+ else if ('provider' in params) {
1011
+ resolve({ type: 'provider', provider: params.provider });
1012
+ }
1013
+ else {
1014
+ // Unknown - default to OTP
1015
+ resolve({ type: 'otp' });
1016
+ }
1017
+ },
1018
+ onCancel: () => {
1019
+ userInteraction.next(undefined);
1020
+ reject(new Dexie.AbortError('User cancelled'));
1021
+ },
1022
+ };
1023
+ userInteraction.next(interactionProps);
1024
+ });
942
1025
  });
943
1026
  }
944
1027
 
1028
+ /**
1029
+ * Error thrown when initiating an OAuth redirect.
1030
+ *
1031
+ * This is not a real error - it's used to signal that the page is
1032
+ * navigating away to an OAuth provider. It should be caught and
1033
+ * silently ignored at the appropriate level.
1034
+ */
1035
+ class OAuthRedirectError extends Error {
1036
+ constructor(provider) {
1037
+ super(`OAuth redirect initiated for provider: ${provider}`);
1038
+ this.name = 'OAuthRedirectError';
1039
+ this.provider = provider;
1040
+ }
1041
+ }
1042
+
945
1043
  function loadAccessToken(db) {
946
1044
  return __awaiter(this, void 0, void 0, function* () {
947
1045
  var _a, _b, _c;
@@ -1109,6 +1207,10 @@ function userAuthenticate(context, fetchToken, userInteraction, hints) {
1109
1207
  return context;
1110
1208
  }
1111
1209
  catch (error) {
1210
+ // OAuth redirect is not an error - page is navigating away
1211
+ if (error instanceof OAuthRedirectError || (error === null || error === void 0 ? void 0 : error.name) === 'OAuthRedirectError') {
1212
+ throw error; // Re-throw without logging
1213
+ }
1112
1214
  if (error instanceof TokenErrorResponseError) {
1113
1215
  yield alertUser(userInteraction, error.title, {
1114
1216
  type: 'error',
@@ -1266,8 +1368,6 @@ class HttpError extends Error {
1266
1368
 
1267
1369
  /** User-friendly messages for OAuth error codes */
1268
1370
  const ERROR_MESSAGES = {
1269
- popup_blocked: 'The login popup was blocked by your browser. Please allow popups for this site and try again.',
1270
- popup_closed: 'The login popup was closed before completing authentication.',
1271
1371
  access_denied: 'Access was denied by the authentication provider.',
1272
1372
  invalid_state: 'The authentication response could not be verified. Please try again.',
1273
1373
  email_not_verified: 'Your email address must be verified before you can log in.',
@@ -1409,144 +1509,46 @@ function fetchAuthProviders(databaseUrl_1) {
1409
1509
  });
1410
1510
  }
1411
1511
 
1412
- /** Generate a random state string for CSRF protection */
1413
- function generateState() {
1414
- const array = new Uint8Array(32);
1415
- crypto.getRandomValues(array);
1416
- return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
1417
- }
1418
1512
  /** Build the OAuth login URL */
1419
- function buildOAuthLoginUrl(options, state) {
1513
+ function buildOAuthLoginUrl(options) {
1420
1514
  const url = new URL(`${options.databaseUrl}/oauth/login/${options.provider}`);
1421
- url.searchParams.set('state', state);
1422
- // Set the redirect URI for postMessage or custom scheme
1515
+ // Set the redirect URI - defaults to current page URL for web SPAs
1423
1516
  const redirectUri = options.redirectUri ||
1424
- (typeof window !== 'undefined' ? window.location.origin : '');
1517
+ (typeof window !== 'undefined' ? window.location.href : '');
1425
1518
  if (redirectUri) {
1426
1519
  url.searchParams.set('redirect_uri', redirectUri);
1427
1520
  }
1428
1521
  return url.toString();
1429
1522
  }
1430
- /** Calculate centered popup position */
1431
- function getPopupPosition(width, height) {
1432
- var _a, _b, _c, _d, _e, _f;
1433
- const screenLeft = (_a = window.screenLeft) !== null && _a !== void 0 ? _a : window.screenX;
1434
- const screenTop = (_b = window.screenTop) !== null && _b !== void 0 ? _b : window.screenY;
1435
- const screenWidth = (_d = (_c = window.innerWidth) !== null && _c !== void 0 ? _c : document.documentElement.clientWidth) !== null && _d !== void 0 ? _d : screen.width;
1436
- const screenHeight = (_f = (_e = window.innerHeight) !== null && _e !== void 0 ? _e : document.documentElement.clientHeight) !== null && _f !== void 0 ? _f : screen.height;
1437
- const left = screenLeft + (screenWidth - width) / 2;
1438
- const top = screenTop + (screenHeight - height) / 2;
1439
- return { left: Math.max(0, left), top: Math.max(0, top) };
1440
- }
1441
1523
  /**
1442
- * Initiates OAuth login flow using a popup window.
1524
+ * Initiates OAuth login via full page redirect.
1443
1525
  *
1444
- * Opens a popup to the OAuth provider, listens for postMessage with the result,
1445
- * and returns the Dexie Cloud authorization code.
1526
+ * The page will navigate to the OAuth provider. After authentication,
1527
+ * the user is redirected back to the app with a `dxc-auth` query parameter
1528
+ * containing base64url-encoded JSON with the authorization code.
1446
1529
  *
1447
- * @param options - OAuth login options
1448
- * @returns Promise resolving to OAuthLoginResult
1449
- * @throws OAuthError on failure
1530
+ * The dexie-cloud-addon automatically detects and processes this parameter
1531
+ * when db.cloud.configure() is called on page load.
1532
+ *
1533
+ * @param options - OAuth redirect options
1534
+ *
1535
+ * @example
1536
+ * ```typescript
1537
+ * // Initiate OAuth login
1538
+ * startOAuthRedirect({
1539
+ * databaseUrl: 'https://mydb.dexie.cloud',
1540
+ * provider: 'google'
1541
+ * });
1542
+ * // Page navigates away, user authenticates, then returns with auth code
1543
+ * ```
1450
1544
  */
1451
- function oauthLogin(options) {
1452
- return __awaiter(this, void 0, void 0, function* () {
1453
- const { databaseUrl, provider, usePopup = true } = options;
1454
- if (!usePopup) {
1455
- // For redirect flows, we can't return a promise - the page will navigate away
1456
- throw new Error('Non-popup OAuth flow requires handleOAuthCallback after redirect');
1457
- }
1458
- const state = generateState();
1459
- const loginUrl = buildOAuthLoginUrl(options, state);
1460
- // Calculate popup dimensions and position
1461
- const width = 500;
1462
- const height = 600;
1463
- const { left, top } = getPopupPosition(width, height);
1464
- // Open popup window
1465
- const popup = window.open(loginUrl, 'dexie-cloud-oauth', `width=${width},height=${height},left=${left},top=${top},menubar=no,toolbar=no,location=yes,status=no`);
1466
- if (!popup) {
1467
- throw new OAuthError('popup_blocked', provider);
1468
- }
1469
- return new Promise((resolve, reject) => {
1470
- let resolved = false;
1471
- // Listen for postMessage from the popup
1472
- const handleMessage = (event) => {
1473
- // Validate origin - must be from the Dexie Cloud server
1474
- const expectedOrigin = new URL(databaseUrl).origin;
1475
- if (event.origin !== expectedOrigin) {
1476
- return; // Ignore messages from other origins
1477
- }
1478
- // Check if this is our OAuth result message
1479
- if (!isOAuthResultMessage(event.data)) {
1480
- return;
1481
- }
1482
- const message = event.data;
1483
- // Validate state to prevent CSRF
1484
- if (message.state !== state) {
1485
- console.warn('[dexie-cloud] OAuth state mismatch, ignoring message');
1486
- return;
1487
- }
1488
- // Clean up
1489
- cleanup();
1490
- resolved = true;
1491
- // Handle error from OAuth flow
1492
- if (message.error) {
1493
- const errorCode = mapOAuthError(message.error);
1494
- reject(new OAuthError(errorCode, provider, message.error));
1495
- return;
1496
- }
1497
- // Success - return the authorization code
1498
- if (message.code) {
1499
- resolve({
1500
- code: message.code,
1501
- provider: message.provider,
1502
- state: message.state,
1503
- });
1504
- }
1505
- else {
1506
- reject(new OAuthError('provider_error', provider, 'No authorization code received'));
1507
- }
1508
- };
1509
- // Check if popup was closed without completing
1510
- const checkPopupClosed = setInterval(() => {
1511
- if (popup.closed && !resolved) {
1512
- cleanup();
1513
- reject(new OAuthError('popup_closed', provider));
1514
- }
1515
- }, 500);
1516
- // Cleanup function
1517
- const cleanup = () => {
1518
- window.removeEventListener('message', handleMessage);
1519
- clearInterval(checkPopupClosed);
1520
- try {
1521
- if (!popup.closed) {
1522
- popup.close();
1523
- }
1524
- }
1525
- catch (_a) {
1526
- // Ignore errors when closing popup
1527
- }
1528
- };
1529
- // Start listening for messages
1530
- window.addEventListener('message', handleMessage);
1531
- });
1532
- });
1533
- }
1534
- /** Map OAuth error strings to error codes */
1535
- function mapOAuthError(error) {
1536
- const lowerError = error.toLowerCase();
1537
- if (lowerError.includes('access_denied') || lowerError.includes('access denied')) {
1538
- return 'access_denied';
1539
- }
1540
- if (lowerError.includes('email') && lowerError.includes('verif')) {
1541
- return 'email_not_verified';
1545
+ function startOAuthRedirect(options) {
1546
+ // Store provider in sessionStorage for reference on callback
1547
+ if (typeof sessionStorage !== 'undefined') {
1548
+ sessionStorage.setItem('dexie-cloud-oauth-provider', options.provider);
1542
1549
  }
1543
- if (lowerError.includes('expired')) {
1544
- return 'expired_code';
1545
- }
1546
- if (lowerError.includes('state')) {
1547
- return 'invalid_state';
1548
- }
1549
- return 'provider_error';
1550
+ const loginUrl = buildOAuthLoginUrl(options);
1551
+ window.location.href = loginUrl;
1550
1552
  }
1551
1553
 
1552
1554
  function otpFetchTokenCallback(db) {
@@ -1567,9 +1569,11 @@ function otpFetchTokenCallback(db) {
1567
1569
  scopes: ['ACCESS_DB'],
1568
1570
  });
1569
1571
  }
1570
- // Handle OAuth provider login (popup flow)
1572
+ // Handle OAuth provider login via redirect
1571
1573
  if (hints === null || hints === void 0 ? void 0 : hints.provider) {
1572
- return yield handleOAuthFlow(db, public_key, hints.provider);
1574
+ initiateOAuthRedirect(db, hints.provider);
1575
+ // This function never returns - page navigates away
1576
+ throw new OAuthRedirectError(hints.provider);
1573
1577
  }
1574
1578
  if ((hints === null || hints === void 0 ? void 0 : hints.grant_type) === 'demo') {
1575
1579
  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));
@@ -1601,8 +1605,10 @@ function otpFetchTokenCallback(db) {
1601
1605
  if (authProviders.providers.length > 0) {
1602
1606
  const selection = yield promptForProvider(userInteraction, authProviders.providers, authProviders.otpEnabled, 'Sign in');
1603
1607
  if (selection.type === 'provider') {
1604
- // User selected an OAuth provider
1605
- return yield handleOAuthFlow(db, public_key, selection.provider);
1608
+ // User selected an OAuth provider - initiate redirect
1609
+ initiateOAuthRedirect(db, selection.provider);
1610
+ // This function never returns - page navigates away
1611
+ throw new OAuthRedirectError(selection.provider);
1606
1612
  }
1607
1613
  // User chose OTP - continue with email prompt below
1608
1614
  }
@@ -1684,46 +1690,24 @@ function otpFetchTokenCallback(db) {
1684
1690
  };
1685
1691
  }
1686
1692
  /**
1687
- * Handles the OAuth popup flow and token exchange.
1693
+ * Initiates OAuth login via full page redirect.
1694
+ *
1695
+ * The page will navigate away to the OAuth provider. After authentication,
1696
+ * the user is redirected back with a dxc-auth query parameter that is
1697
+ * automatically detected by db.cloud.configure().
1688
1698
  */
1689
- function handleOAuthFlow(db, publicKey, provider) {
1690
- return __awaiter(this, void 0, void 0, function* () {
1691
- var _a, _b, _c;
1692
- const url = (_a = db.cloud.options) === null || _a === void 0 ? void 0 : _a.databaseUrl;
1693
- if (!url)
1694
- throw new Error(`No database URL given.`);
1695
- const { userInteraction } = db.cloud;
1696
- const usePopup = ((_b = db.cloud.options) === null || _b === void 0 ? void 0 : _b.oauthPopup) !== false;
1697
- const redirectUri = ((_c = db.cloud.options) === null || _c === void 0 ? void 0 : _c.oauthRedirectUri) ||
1698
- (typeof window !== 'undefined' ? window.location.origin : undefined);
1699
- try {
1700
- // Start OAuth popup flow
1701
- const result = yield oauthLogin({
1702
- databaseUrl: url,
1703
- provider,
1704
- redirectUri,
1705
- usePopup,
1706
- });
1707
- // Exchange the auth code for tokens
1708
- return yield exchangeOAuthCode({
1709
- databaseUrl: url,
1710
- code: result.code,
1711
- publicKey,
1712
- scopes: ['ACCESS_DB'],
1713
- });
1714
- }
1715
- catch (error) {
1716
- if (error instanceof OAuthError) {
1717
- // Show user-friendly error message
1718
- yield alertUser(userInteraction, 'Authentication Failed', {
1719
- type: 'error',
1720
- messageCode: 'GENERIC_ERROR',
1721
- message: error.userMessage,
1722
- messageParams: {},
1723
- }).catch(() => { });
1724
- }
1725
- throw error;
1726
- }
1699
+ function initiateOAuthRedirect(db, provider) {
1700
+ var _a, _b;
1701
+ const url = (_a = db.cloud.options) === null || _a === void 0 ? void 0 : _a.databaseUrl;
1702
+ if (!url)
1703
+ throw new Error(`No database URL given.`);
1704
+ const redirectUri = ((_b = db.cloud.options) === null || _b === void 0 ? void 0 : _b.oauthRedirectUri) ||
1705
+ (typeof window !== 'undefined' ? window.location.href : undefined);
1706
+ // Start OAuth redirect flow - page navigates away
1707
+ startOAuthRedirect({
1708
+ databaseUrl: url,
1709
+ provider,
1710
+ redirectUri,
1727
1711
  });
1728
1712
  }
1729
1713
 
@@ -1813,7 +1797,15 @@ function login(db, hints) {
1813
1797
  claims: {},
1814
1798
  lastLogin: new Date(0),
1815
1799
  });
1816
- yield authenticate(db.cloud.options.databaseUrl, context, db.cloud.options.fetchTokens || otpFetchTokenCallback(db), db.cloud.userInteraction, hints);
1800
+ try {
1801
+ yield authenticate(db.cloud.options.databaseUrl, context, db.cloud.options.fetchTokens || otpFetchTokenCallback(db), db.cloud.userInteraction, hints);
1802
+ }
1803
+ catch (err) {
1804
+ if (err.name === 'OAuthRedirectError') {
1805
+ return false; // Page is redirecting for OAuth login
1806
+ }
1807
+ throw err;
1808
+ }
1817
1809
  if (origUserId !== UNAUTHORIZED_USER.userId &&
1818
1810
  context.userId !== origUserId) {
1819
1811
  // User was logged in before, but now logged in as another user.
@@ -5809,7 +5801,10 @@ const Styles = {
5809
5801
  ProviderButtonIcon: {
5810
5802
  width: "20px",
5811
5803
  height: "20px",
5812
- flexShrink: 0
5804
+ flexShrink: 0,
5805
+ display: "flex",
5806
+ alignItems: "center",
5807
+ justifyContent: "center"
5813
5808
  },
5814
5809
  ProviderButtonText: {
5815
5810
  flex: 1,
@@ -5874,14 +5869,7 @@ const Styles = {
5874
5869
  color: "#374151",
5875
5870
  transition: "all 0.2s ease",
5876
5871
  gap: "12px"
5877
- },
5878
- // Cancel button for provider selection
5879
- CancelButtonRow: {
5880
- display: "flex",
5881
- justifyContent: "center",
5882
- marginTop: "16px"
5883
- }
5884
- };
5872
+ }};
5885
5873
 
5886
5874
  function Dialog({ children, className }) {
5887
5875
  return (_$1("div", { className: `dexie-dialog ${className || ''}` },
@@ -5910,19 +5898,126 @@ function resolveText({ message, messageCode, messageParams }) {
5910
5898
  return message.replace(/\{\w+\}/ig, n => messageParams[n.substring(1, n.length - 1)]);
5911
5899
  }
5912
5900
 
5901
+ /** Get style based on styleHint (for provider branding, etc.) */
5902
+ function getOptionStyle(styleHint) {
5903
+ const baseStyle = Object.assign({}, Styles.ProviderButton);
5904
+ if (!styleHint) {
5905
+ return baseStyle;
5906
+ }
5907
+ switch (styleHint) {
5908
+ case 'google':
5909
+ return Object.assign(Object.assign({}, baseStyle), Styles.ProviderGoogle);
5910
+ case 'github':
5911
+ return Object.assign(Object.assign({}, baseStyle), Styles.ProviderGitHub);
5912
+ case 'microsoft':
5913
+ return Object.assign(Object.assign({}, baseStyle), Styles.ProviderMicrosoft);
5914
+ case 'apple':
5915
+ return Object.assign(Object.assign({}, baseStyle), Styles.ProviderApple);
5916
+ case 'otp':
5917
+ return Object.assign({}, Styles.OtpButton);
5918
+ case 'custom-oauth2':
5919
+ return Object.assign(Object.assign({}, baseStyle), Styles.ProviderCustom);
5920
+ default:
5921
+ return baseStyle;
5922
+ }
5923
+ }
5924
+ /**
5925
+ * Generic button component for selectable options.
5926
+ * Displays the option's icon and display name.
5927
+ *
5928
+ * The icon can be:
5929
+ * - Inline SVG (iconSvg) - rendered directly with dangerouslySetInnerHTML
5930
+ * - Image URL (iconUrl) - rendered as an img tag
5931
+ *
5932
+ * Style is determined by the styleHint property for branding purposes.
5933
+ */
5934
+ function OptionButton({ option, onClick }) {
5935
+ const { displayName, iconUrl, iconSvg, styleHint, value } = option;
5936
+ const style = getOptionStyle(styleHint);
5937
+ // Get the text color from the button style for SVG fill processing
5938
+ const textColor = style.color || '#000000';
5939
+ // Process SVG to replace currentColor with actual text color
5940
+ const processedSvg = iconSvg
5941
+ ? iconSvg
5942
+ .replace(/fill="currentColor"/gi, `fill="${textColor}"`)
5943
+ .replace(/fill='currentColor'/gi, `fill='${textColor}'`)
5944
+ .replace(/stroke="currentColor"/gi, `stroke="${textColor}"`)
5945
+ .replace(/stroke='currentColor'/gi, `stroke='${textColor}'`)
5946
+ : null;
5947
+ // Render the appropriate icon
5948
+ const renderIcon = () => {
5949
+ // Inline SVG
5950
+ if (processedSvg) {
5951
+ return (_$1("span", { style: Styles.ProviderButtonIcon, "aria-hidden": "true", dangerouslySetInnerHTML: { __html: processedSvg } }));
5952
+ }
5953
+ // Image URL
5954
+ if (iconUrl) {
5955
+ return (_$1("img", { src: iconUrl, alt: "", style: Styles.ProviderButtonIcon, "aria-hidden": "true" }));
5956
+ }
5957
+ return null;
5958
+ };
5959
+ return (_$1("button", { type: "button", style: style, onClick: onClick, class: `dxc-option-btn${styleHint ? ` dxc-option-${styleHint}` : ''}`, "aria-label": displayName },
5960
+ renderIcon(),
5961
+ _$1("span", { style: Styles.ProviderButtonText }, displayName)));
5962
+ }
5963
+ /**
5964
+ * Visual divider with "or" text.
5965
+ */
5966
+ function Divider() {
5967
+ return (_$1("div", { style: Styles.Divider },
5968
+ _$1("div", { style: Styles.DividerLine }),
5969
+ _$1("span", { style: Styles.DividerText }, "or"),
5970
+ _$1("div", { style: Styles.DividerLine })));
5971
+ }
5972
+
5913
5973
  const OTP_LENGTH = 8;
5914
- function LoginDialog({ title, type, alerts, fields, submitLabel, cancelLabel, onCancel, onSubmit, }) {
5974
+ /**
5975
+ * Generic dialog that can render:
5976
+ * - Form fields (text inputs)
5977
+ * - Selectable options (buttons)
5978
+ * - Or both together
5979
+ *
5980
+ * When an option is clicked, calls onSubmit({ [option.name]: option.value }).
5981
+ * This unified approach means the same callback handles both form submission
5982
+ * and option selection.
5983
+ */
5984
+ function LoginDialog({ title, alerts, fields, options, submitLabel, cancelLabel, onCancel, onSubmit, }) {
5915
5985
  const [params, setParams] = d({});
5916
5986
  const firstFieldRef = A(null);
5917
5987
  _(() => { var _a; return (_a = firstFieldRef.current) === null || _a === void 0 ? void 0 : _a.focus(); }, []);
5988
+ const fieldEntries = Object.entries(fields || {});
5989
+ const hasFields = fieldEntries.length > 0;
5990
+ const hasOptions = options && options.length > 0;
5991
+ // Group options by name to detect if we have multiple groups
5992
+ const optionGroups = new Map();
5993
+ if (options) {
5994
+ for (const option of options) {
5995
+ const group = optionGroups.get(option.name) || [];
5996
+ group.push(option);
5997
+ optionGroups.set(option.name, group);
5998
+ }
5999
+ }
6000
+ const hasMultipleGroups = optionGroups.size > 1;
6001
+ // Handler for option clicks - calls onSubmit with { [option.name]: option.value }
6002
+ const handleOptionClick = (option) => {
6003
+ onSubmit({ [option.name]: option.value });
6004
+ };
5918
6005
  return (_$1(Dialog, { className: "dxc-login-dlg" },
5919
6006
  _$1(k$1, null,
5920
6007
  _$1("h3", { style: Styles.WindowHeader }, title),
5921
- alerts.map((alert) => (_$1("p", { style: Styles.Alert[alert.type] }, resolveText(alert)))),
5922
- _$1("form", { onSubmit: (ev) => {
6008
+ alerts.map((alert, idx) => (_$1("p", { key: idx, style: Styles.Alert[alert.type] }, resolveText(alert)))),
6009
+ hasOptions && (_$1("div", { class: "dxc-options" }, hasMultipleGroups ? (
6010
+ // Render with dividers between groups
6011
+ Array.from(optionGroups.entries()).map(([groupName, groupOptions], groupIdx) => (_$1(k$1, { key: groupName },
6012
+ groupIdx > 0 && _$1(Divider, null),
6013
+ groupOptions.map((option) => (_$1(OptionButton, { key: `${option.name}-${option.value}`, option: option, onClick: () => handleOptionClick(option) }))))))) : (
6014
+ // Simple case: all options in one group
6015
+ options.map((option) => (_$1(OptionButton, { key: `${option.name}-${option.value}`, option: option, onClick: () => handleOptionClick(option) })))))),
6016
+ hasOptions && hasFields && _$1(Divider, null),
6017
+ hasFields && (_$1("form", { onSubmit: (ev) => {
5923
6018
  ev.preventDefault();
5924
6019
  onSubmit(params);
5925
- } }, Object.entries(fields).map(([fieldName, { type, label, placeholder }], idx) => (_$1("label", { style: Styles.Label, key: idx },
6020
+ } }, fieldEntries.map(([fieldName, { type, label, placeholder }], idx) => (_$1("label", { style: Styles.Label, key: idx },
5926
6021
  label ? `${label}: ` : '',
5927
6022
  _$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) => {
5928
6023
  var _a;
@@ -5933,10 +6028,10 @@ function LoginDialog({ title, type, alerts, fields, submitLabel, cancelLabel, on
5933
6028
  // Auto-submit when OTP is filled in.
5934
6029
  onSubmit(updatedParams);
5935
6030
  }
5936
- } })))))),
6031
+ } }))))))),
5937
6032
  _$1("div", { style: Styles.ButtonsDiv },
5938
6033
  _$1(k$1, null,
5939
- _$1("button", { type: "submit", style: Styles.PrimaryButton, onClick: () => onSubmit(params) }, submitLabel),
6034
+ submitLabel && (hasFields || (!hasOptions && !hasFields)) && (_$1("button", { type: "submit", style: Styles.PrimaryButton, onClick: () => onSubmit(params) }, submitLabel)),
5940
6035
  cancelLabel && (_$1("button", { style: Styles.Button, onClick: onCancel }, cancelLabel))))));
5941
6036
  }
5942
6037
  function valueTransformer(type, value) {
@@ -5950,82 +6045,6 @@ function valueTransformer(type, value) {
5950
6045
  }
5951
6046
  }
5952
6047
 
5953
- /** Default SVG icons for built-in providers */
5954
- const ProviderIcons = {
5955
- 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>`,
5956
- 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>`,
5957
- 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>`,
5958
- 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>`,
5959
- };
5960
- /** Get provider-specific button styles */
5961
- function getProviderStyle(providerType) {
5962
- const baseStyle = Object.assign({}, Styles.ProviderButton);
5963
- switch (providerType) {
5964
- case 'google':
5965
- return Object.assign(Object.assign({}, baseStyle), Styles.ProviderGoogle);
5966
- case 'github':
5967
- return Object.assign(Object.assign({}, baseStyle), Styles.ProviderGitHub);
5968
- case 'microsoft':
5969
- return Object.assign(Object.assign({}, baseStyle), Styles.ProviderMicrosoft);
5970
- case 'apple':
5971
- return Object.assign(Object.assign({}, baseStyle), Styles.ProviderApple);
5972
- default:
5973
- return Object.assign(Object.assign({}, baseStyle), Styles.ProviderCustom);
5974
- }
5975
- }
5976
- /**
5977
- * Button component for OAuth provider login.
5978
- * Displays the provider's icon and name following provider branding guidelines.
5979
- */
5980
- function AuthProviderButton({ provider, onClick }) {
5981
- const { type, name, displayName, iconUrl } = provider;
5982
- const style = getProviderStyle(type);
5983
- // Determine button text
5984
- const buttonText = `Continue with ${displayName}`;
5985
- // Get icon - use custom iconUrl if provided, otherwise use built-in SVG
5986
- const iconSvg = ProviderIcons[type] || '';
5987
- return (_$1("button", { type: "button", style: style, onClick: onClick, class: `dxc-provider-btn dxc-provider-${type}`, "aria-label": buttonText },
5988
- 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,
5989
- _$1("span", { style: Styles.ProviderButtonText }, buttonText)));
5990
- }
5991
- /** Email/envelope icon for OTP button */
5992
- 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>`;
5993
- /**
5994
- * Button for email/OTP authentication option.
5995
- */
5996
- function OtpButton({ onClick }) {
5997
- return (_$1("button", { type: "button", style: Styles.OtpButton, onClick: onClick, class: "dxc-otp-btn", "aria-label": "Continue with email" },
5998
- _$1("span", { style: Styles.ProviderButtonIcon, "aria-hidden": "true", dangerouslySetInnerHTML: { __html: EmailIcon } }),
5999
- _$1("span", { style: Styles.ProviderButtonText }, "Continue with email")));
6000
- }
6001
- /**
6002
- * Visual divider with "or" text.
6003
- */
6004
- function Divider() {
6005
- return (_$1("div", { style: Styles.Divider },
6006
- _$1("div", { style: Styles.DividerLine }),
6007
- _$1("span", { style: Styles.DividerText }, "or"),
6008
- _$1("div", { style: Styles.DividerLine })));
6009
- }
6010
-
6011
- /**
6012
- * Dialog component for OAuth provider selection.
6013
- * Displays available OAuth providers as buttons and optionally an email/OTP option.
6014
- */
6015
- function ProviderSelectionDialog({ title, alerts, providers, otpEnabled, cancelLabel, onSelectProvider, onSelectOtp, onCancel, }) {
6016
- return (_$1(Dialog, { className: "dxc-provider-selection-dlg" },
6017
- _$1(k$1, null,
6018
- _$1("h3", { style: Styles.WindowHeader }, title),
6019
- alerts.map((alert, idx) => (_$1("p", { key: idx, style: Styles.Alert[alert.type] }, resolveText(alert)))),
6020
- _$1("div", { class: "dxc-providers" }, providers.map((provider) => (_$1(AuthProviderButton, { key: provider.name, provider: provider, onClick: () => onSelectProvider(provider.name) })))),
6021
- otpEnabled && providers.length > 0 && (_$1(k$1, null,
6022
- _$1(Divider, null),
6023
- _$1(OtpButton, { onClick: onSelectOtp }))),
6024
- otpEnabled && providers.length === 0 && (_$1(OtpButton, { onClick: onSelectOtp })),
6025
- cancelLabel && (_$1("div", { style: Styles.CancelButtonRow },
6026
- _$1("button", { type: "button", style: Styles.Button, onClick: onCancel }, cancelLabel))))));
6027
- }
6028
-
6029
6048
  class LoginGui extends x {
6030
6049
  constructor(props) {
6031
6050
  super(props);
@@ -6044,11 +6063,8 @@ class LoginGui extends x {
6044
6063
  render(props, { userInteraction }) {
6045
6064
  if (!userInteraction)
6046
6065
  return null;
6047
- // Render appropriate dialog based on interaction type
6048
- if (userInteraction.type === 'provider-selection') {
6049
- return _$1(ProviderSelectionDialog, Object.assign({}, userInteraction));
6050
- }
6051
- // Default to LoginDialog for other interaction types
6066
+ // LoginDialog handles all interaction types uniformly
6067
+ // (forms with fields, options, or both)
6052
6068
  return _$1(LoginDialog, Object.assign({}, userInteraction));
6053
6069
  }
6054
6070
  }
@@ -6607,6 +6623,83 @@ function createAwareness(db, doc, provider) {
6607
6623
  return awareness;
6608
6624
  }
6609
6625
 
6626
+ /**
6627
+ * Decodes a base64url-encoded string to a regular string.
6628
+ * Base64url uses - instead of + and _ instead of /, and may omit padding.
6629
+ */
6630
+ function decodeBase64Url(encoded) {
6631
+ // Add padding if needed
6632
+ const padded = encoded + '='.repeat((4 - (encoded.length % 4)) % 4);
6633
+ // Convert base64url to base64
6634
+ const base64 = padded.replace(/-/g, '+').replace(/_/g, '/');
6635
+ return atob(base64);
6636
+ }
6637
+ /**
6638
+ * Parses OAuth callback parameters from the dxc-auth query parameter.
6639
+ *
6640
+ * The dxc-auth parameter contains base64url-encoded JSON with the following structure:
6641
+ * - On success: { "code": "...", "provider": "...", "state": "..." }
6642
+ * - On error: { "error": "...", "provider": "...", "state": "..." }
6643
+ *
6644
+ * @param url - The URL to parse (defaults to window.location.href)
6645
+ * @returns OAuthCallbackParams if valid callback, null otherwise
6646
+ * @throws OAuthError if there's an error in the callback
6647
+ */
6648
+ function parseOAuthCallback(url) {
6649
+ const targetUrl = (typeof window !== 'undefined' ? window.location.href : '');
6650
+ if (!targetUrl) {
6651
+ return null;
6652
+ }
6653
+ const parsed = new URL(targetUrl);
6654
+ const encoded = parsed.searchParams.get('dxc-auth');
6655
+ if (!encoded) {
6656
+ return null; // Not an OAuth callback URL
6657
+ }
6658
+ let payload;
6659
+ try {
6660
+ const json = decodeBase64Url(encoded);
6661
+ payload = JSON.parse(json);
6662
+ }
6663
+ catch (e) {
6664
+ console.warn('[dexie-cloud] Failed to parse dxc-auth parameter:', e);
6665
+ return null;
6666
+ }
6667
+ const { code, provider, state, error } = payload;
6668
+ // Check for error first
6669
+ if (error) {
6670
+ if (error.toLowerCase().includes('access_denied') || error.toLowerCase().includes('access denied')) {
6671
+ throw new OAuthError('access_denied', provider, error);
6672
+ }
6673
+ if (error.toLowerCase().includes('email') && error.toLowerCase().includes('verif')) {
6674
+ throw new OAuthError('email_not_verified', provider, error);
6675
+ }
6676
+ throw new OAuthError('provider_error', provider, error);
6677
+ }
6678
+ // Validate required fields for success case
6679
+ if (!code || !provider || !state) {
6680
+ console.warn('[dexie-cloud] Invalid dxc-auth payload: missing required fields');
6681
+ return null;
6682
+ }
6683
+ return { code, provider, state };
6684
+ }
6685
+ /**
6686
+ * Cleans up the dxc-auth query parameter from the URL.
6687
+ * Call this after successfully handling the callback to clean up the browser URL.
6688
+ */
6689
+ function cleanupOAuthUrl() {
6690
+ var _a;
6691
+ if (typeof window === 'undefined' || !((_a = window.history) === null || _a === void 0 ? void 0 : _a.replaceState)) {
6692
+ return;
6693
+ }
6694
+ const url = new URL(window.location.href);
6695
+ if (!url.searchParams.has('dxc-auth')) {
6696
+ return;
6697
+ }
6698
+ url.searchParams.delete('dxc-auth');
6699
+ const cleanUrl = url.pathname + (url.searchParams.toString() ? `?${url.searchParams.toString()}` : '') + url.hash;
6700
+ window.history.replaceState(null, '', cleanUrl);
6701
+ }
6702
+
6610
6703
  function getTiedRealmId(objectId) {
6611
6704
  return 'rlm~' + objectId;
6612
6705
  }
@@ -6789,6 +6882,10 @@ function dexieCloud(dexie) {
6789
6882
  const currentUserEmitter = getCurrentUserEmitter(dexie);
6790
6883
  const subscriptions = [];
6791
6884
  let configuredProgramatically = false;
6885
+ // Pending OAuth auth code from dxc-auth redirect (detected in configure())
6886
+ let pendingOAuthCode = null;
6887
+ // Pending OAuth error from dxc-auth redirect (detected in configure())
6888
+ let pendingOAuthError = null;
6792
6889
  // local sync worker - used when there's no service worker.
6793
6890
  let localSyncWorker = null;
6794
6891
  dexie.on('ready', (dexie) => __awaiter(this, void 0, void 0, function* () {
@@ -6818,7 +6915,7 @@ function dexieCloud(dexie) {
6818
6915
  const syncComplete = new Subject();
6819
6916
  dexie.cloud = {
6820
6917
  // @ts-ignore
6821
- version: "4.3.0",
6918
+ version: "4.3.3",
6822
6919
  options: Object.assign({}, DEFAULT_OPTIONS),
6823
6920
  schema: null,
6824
6921
  get currentUserId() {
@@ -6853,6 +6950,31 @@ function dexieCloud(dexie) {
6853
6950
  DexieCloudDB(dexie).reconfigure(); // Update observable from new dexie.name
6854
6951
  }
6855
6952
  updateSchemaFromOptions(dexie.cloud.schema, dexie.cloud.options);
6953
+ // Check for OAuth callback (dxc-auth query parameter)
6954
+ // Only check in DOM environment, not workers
6955
+ if (typeof window !== 'undefined' && window.location) {
6956
+ try {
6957
+ const callback = parseOAuthCallback();
6958
+ if (callback) {
6959
+ // Clean up URL immediately (remove dxc-auth param)
6960
+ cleanupOAuthUrl();
6961
+ // Store the pending auth code for processing when db is ready
6962
+ pendingOAuthCode = { code: callback.code, provider: callback.provider };
6963
+ console.debug('[dexie-cloud] OAuth callback detected, auth code stored for processing');
6964
+ }
6965
+ }
6966
+ catch (error) {
6967
+ // parseOAuthCallback throws OAuthError on error callbacks
6968
+ cleanupOAuthUrl();
6969
+ if (error instanceof OAuthError) {
6970
+ pendingOAuthError = error;
6971
+ console.error('[dexie-cloud] OAuth callback error:', error.message);
6972
+ }
6973
+ else {
6974
+ console.error('[dexie-cloud] OAuth callback error:', error);
6975
+ }
6976
+ }
6977
+ }
6856
6978
  },
6857
6979
  logout() {
6858
6980
  return __awaiter(this, arguments, void 0, function* ({ force } = {}) {
@@ -7046,7 +7168,34 @@ function dexieCloud(dexie) {
7046
7168
  }
7047
7169
  // HERE: If requireAuth, do athentication now.
7048
7170
  let changedUser = false;
7049
- const user = yield db.getCurrentUser();
7171
+ let user = yield db.getCurrentUser();
7172
+ // Show pending OAuth error if present (from dxc-auth redirect)
7173
+ if (pendingOAuthError && !db.cloud.isServiceWorkerDB) {
7174
+ const error = pendingOAuthError;
7175
+ pendingOAuthError = null; // Clear pending error
7176
+ console.debug('[dexie-cloud] Showing OAuth error:', error.message);
7177
+ // Show alert to user about the OAuth error
7178
+ yield alertUser(db.cloud.userInteraction, 'Authentication Error', {
7179
+ type: 'error',
7180
+ messageCode: 'GENERIC_ERROR',
7181
+ message: error.message,
7182
+ messageParams: { provider: error.provider || 'unknown' }
7183
+ });
7184
+ }
7185
+ // Process pending OAuth callback if present (from dxc-auth redirect)
7186
+ if (pendingOAuthCode && !db.cloud.isServiceWorkerDB) {
7187
+ const { code, provider } = pendingOAuthCode;
7188
+ pendingOAuthCode = null; // Clear pending code
7189
+ console.debug('[dexie-cloud] Processing OAuth callback, provider:', provider);
7190
+ try {
7191
+ changedUser = yield login(db, { oauthCode: code, provider });
7192
+ user = yield db.getCurrentUser();
7193
+ }
7194
+ catch (error) {
7195
+ console.error('[dexie-cloud] OAuth login failed:', error);
7196
+ // Continue with normal flow - user can try again
7197
+ }
7198
+ }
7050
7199
  const requireAuth = (_b = db.cloud.options) === null || _b === void 0 ? void 0 : _b.requireAuth;
7051
7200
  if (requireAuth) {
7052
7201
  if (db.cloud.isServiceWorkerDB) {
@@ -7135,7 +7284,7 @@ function dexieCloud(dexie) {
7135
7284
  }
7136
7285
  }
7137
7286
  // @ts-ignore
7138
- dexieCloud.version = "4.3.0";
7287
+ dexieCloud.version = "4.3.3";
7139
7288
  Dexie.Cloud = dexieCloud;
7140
7289
 
7141
7290
  export { dexieCloud as default, defineYDocTrigger, dexieCloud, getTiedObjectId, getTiedRealmId, resolveText };