dexie-cloud-addon 4.3.0 → 4.3.2

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.2, Thu Jan 22 2026
12
12
  *
13
13
  * https://dexie.org
14
14
  *
@@ -545,15 +545,6 @@ function randomString$1(bytes) {
545
545
  }
546
546
  }
547
547
 
548
- /** Type guard to check if a message is an OAuthResultMessage */
549
- function isOAuthResultMessage(msg) {
550
- return (typeof msg === 'object' &&
551
- msg !== null &&
552
- msg.type === 'dexie:oauthResult' &&
553
- typeof msg.provider === 'string' &&
554
- typeof msg.state === 'string');
555
- }
556
-
557
548
  function assert(b) {
558
549
  if (!b)
559
550
  throw new Error('Assertion Failed');
@@ -1154,6 +1145,74 @@ class TokenErrorResponseError extends Error {
1154
1145
  }
1155
1146
  }
1156
1147
 
1148
+ /** Cache for fetched SVG content to avoid re-fetching */
1149
+ const svgCache = {};
1150
+ /** Default SVG icons for built-in OAuth providers */
1151
+ const ProviderIcons = {
1152
+ 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>`,
1153
+ 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>`,
1154
+ 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>`,
1155
+ 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>`,
1156
+ };
1157
+ /** Email/envelope icon for OTP option */
1158
+ 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>`;
1159
+ /**
1160
+ * Fetches SVG content from a URL and caches it.
1161
+ * Returns the SVG string or null if fetch fails.
1162
+ */
1163
+ function fetchSvgIcon(url) {
1164
+ return __awaiter(this, void 0, void 0, function* () {
1165
+ if (svgCache[url]) {
1166
+ return svgCache[url];
1167
+ }
1168
+ try {
1169
+ const res = yield fetch(url);
1170
+ if (res.ok) {
1171
+ const svg = yield res.text();
1172
+ // Validate it looks like SVG
1173
+ if (svg.includes('<svg')) {
1174
+ svgCache[url] = svg;
1175
+ return svg;
1176
+ }
1177
+ }
1178
+ }
1179
+ catch (_a) {
1180
+ // Silently fail - will show no icon
1181
+ }
1182
+ return null;
1183
+ });
1184
+ }
1185
+ /**
1186
+ * Converts an OAuthProviderInfo to a generic DXCOption.
1187
+ * Fetches SVG icons from URLs if needed.
1188
+ */
1189
+ function providerToOption(provider) {
1190
+ return __awaiter(this, void 0, void 0, function* () {
1191
+ var _a;
1192
+ let iconSvg;
1193
+ // First check for built-in icons
1194
+ if (ProviderIcons[provider.type]) {
1195
+ iconSvg = ProviderIcons[provider.type];
1196
+ }
1197
+ // If provider has iconUrl pointing to SVG, fetch and inline it
1198
+ else if ((_a = provider.iconUrl) === null || _a === void 0 ? void 0 : _a.toLowerCase().endsWith('.svg')) {
1199
+ const fetched = yield fetchSvgIcon(provider.iconUrl);
1200
+ if (fetched) {
1201
+ iconSvg = fetched;
1202
+ }
1203
+ }
1204
+ return {
1205
+ name: 'provider',
1206
+ value: provider.name,
1207
+ displayName: `Continue with ${provider.displayName}`,
1208
+ iconSvg,
1209
+ // If iconUrl is not SVG, pass it through for img tag rendering
1210
+ iconUrl: (!iconSvg && provider.iconUrl) ? provider.iconUrl : undefined,
1211
+ // Use provider type as style hint for branding
1212
+ styleHint: provider.type,
1213
+ };
1214
+ });
1215
+ }
1157
1216
  function interactWithUser(userInteraction, req) {
1158
1217
  return new Promise((resolve, reject) => {
1159
1218
  const interactionProps = Object.assign(Object.assign({ submitLabel: 'Submit', cancelLabel: 'Cancel' }, req), { onSubmit: (res) => {
@@ -1291,6 +1350,9 @@ function confirmLogout(userInteraction, currentUserId, numUnsyncedChanges) {
1291
1350
  /**
1292
1351
  * Prompts the user to select an authentication method (OAuth provider or OTP).
1293
1352
  *
1353
+ * This function converts OAuth providers and OTP option into generic DXCOption[]
1354
+ * for the DXCSelect interaction, handling icon fetching and style hints.
1355
+ *
1294
1356
  * @param userInteraction - The user interaction BehaviorSubject
1295
1357
  * @param providers - Available OAuth providers
1296
1358
  * @param otpEnabled - Whether OTP is available
@@ -1298,31 +1360,52 @@ function confirmLogout(userInteraction, currentUserId, numUnsyncedChanges) {
1298
1360
  * @param alerts - Optional alerts to display
1299
1361
  * @returns Promise resolving to the user's selection
1300
1362
  */
1301
- function promptForProvider(userInteraction, providers, otpEnabled, title = 'Choose login method', alerts = []) {
1302
- return new Promise((resolve, reject) => {
1303
- const interactionProps = {
1304
- type: 'provider-selection',
1305
- title,
1306
- alerts,
1307
- providers,
1308
- otpEnabled,
1309
- fields: {},
1310
- submitLabel: undefined,
1311
- cancelLabel: 'Cancel',
1312
- onSelectProvider: (providerName) => {
1313
- userInteraction.next(undefined);
1314
- resolve({ type: 'provider', provider: providerName });
1315
- },
1316
- onSelectOtp: () => {
1317
- userInteraction.next(undefined);
1318
- resolve({ type: 'otp' });
1319
- },
1320
- onCancel: () => {
1321
- userInteraction.next(undefined);
1322
- reject(new Dexie.AbortError('User cancelled'));
1323
- },
1324
- };
1325
- userInteraction.next(interactionProps);
1363
+ function promptForProvider(userInteraction_1, providers_1, otpEnabled_1) {
1364
+ return __awaiter(this, arguments, void 0, function* (userInteraction, providers, otpEnabled, title = 'Choose login method', alerts = []) {
1365
+ // Convert providers to generic options (with icon fetching)
1366
+ const providerOptions = yield Promise.all(providers.map(providerToOption));
1367
+ // Build the options array
1368
+ const options = [...providerOptions];
1369
+ // Add OTP option if enabled
1370
+ if (otpEnabled) {
1371
+ options.push({
1372
+ name: 'otp',
1373
+ value: 'email',
1374
+ displayName: 'Continue with email',
1375
+ iconSvg: EmailIcon,
1376
+ styleHint: 'otp',
1377
+ });
1378
+ }
1379
+ return new Promise((resolve, reject) => {
1380
+ const interactionProps = {
1381
+ type: 'generic',
1382
+ title,
1383
+ alerts,
1384
+ options,
1385
+ fields: {},
1386
+ submitLabel: '', // No submit button - just options
1387
+ cancelLabel: 'Cancel',
1388
+ onSubmit: (params) => {
1389
+ userInteraction.next(undefined);
1390
+ // Check which option was selected
1391
+ if ('otp' in params) {
1392
+ resolve({ type: 'otp' });
1393
+ }
1394
+ else if ('provider' in params) {
1395
+ resolve({ type: 'provider', provider: params.provider });
1396
+ }
1397
+ else {
1398
+ // Unknown - default to OTP
1399
+ resolve({ type: 'otp' });
1400
+ }
1401
+ },
1402
+ onCancel: () => {
1403
+ userInteraction.next(undefined);
1404
+ reject(new Dexie.AbortError('User cancelled'));
1405
+ },
1406
+ };
1407
+ userInteraction.next(interactionProps);
1408
+ });
1326
1409
  });
1327
1410
  }
1328
1411
 
@@ -3590,8 +3673,6 @@ function _logout(db_1) {
3590
3673
 
3591
3674
  /** User-friendly messages for OAuth error codes */
3592
3675
  const ERROR_MESSAGES = {
3593
- popup_blocked: 'The login popup was blocked by your browser. Please allow popups for this site and try again.',
3594
- popup_closed: 'The login popup was closed before completing authentication.',
3595
3676
  access_denied: 'Access was denied by the authentication provider.',
3596
3677
  invalid_state: 'The authentication response could not be verified. Please try again.',
3597
3678
  email_not_verified: 'Your email address must be verified before you can log in.',
@@ -3733,144 +3814,46 @@ function fetchAuthProviders(databaseUrl_1) {
3733
3814
  });
3734
3815
  }
3735
3816
 
3736
- /** Generate a random state string for CSRF protection */
3737
- function generateState() {
3738
- const array = new Uint8Array(32);
3739
- crypto.getRandomValues(array);
3740
- return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
3741
- }
3742
3817
  /** Build the OAuth login URL */
3743
- function buildOAuthLoginUrl(options, state) {
3818
+ function buildOAuthLoginUrl(options) {
3744
3819
  const url = new URL(`${options.databaseUrl}/oauth/login/${options.provider}`);
3745
- url.searchParams.set('state', state);
3746
- // Set the redirect URI for postMessage or custom scheme
3820
+ // Set the redirect URI - defaults to current page URL for web SPAs
3747
3821
  const redirectUri = options.redirectUri ||
3748
- (typeof window !== 'undefined' ? window.location.origin : '');
3822
+ (typeof window !== 'undefined' ? window.location.href : '');
3749
3823
  if (redirectUri) {
3750
3824
  url.searchParams.set('redirect_uri', redirectUri);
3751
3825
  }
3752
3826
  return url.toString();
3753
3827
  }
3754
- /** Calculate centered popup position */
3755
- function getPopupPosition(width, height) {
3756
- var _a, _b, _c, _d, _e, _f;
3757
- const screenLeft = (_a = window.screenLeft) !== null && _a !== void 0 ? _a : window.screenX;
3758
- const screenTop = (_b = window.screenTop) !== null && _b !== void 0 ? _b : window.screenY;
3759
- const screenWidth = (_d = (_c = window.innerWidth) !== null && _c !== void 0 ? _c : document.documentElement.clientWidth) !== null && _d !== void 0 ? _d : screen.width;
3760
- const screenHeight = (_f = (_e = window.innerHeight) !== null && _e !== void 0 ? _e : document.documentElement.clientHeight) !== null && _f !== void 0 ? _f : screen.height;
3761
- const left = screenLeft + (screenWidth - width) / 2;
3762
- const top = screenTop + (screenHeight - height) / 2;
3763
- return { left: Math.max(0, left), top: Math.max(0, top) };
3764
- }
3765
3828
  /**
3766
- * Initiates OAuth login flow using a popup window.
3829
+ * Initiates OAuth login via full page redirect.
3830
+ *
3831
+ * The page will navigate to the OAuth provider. After authentication,
3832
+ * the user is redirected back to the app with a `dxc-auth` query parameter
3833
+ * containing base64url-encoded JSON with the authorization code.
3767
3834
  *
3768
- * Opens a popup to the OAuth provider, listens for postMessage with the result,
3769
- * and returns the Dexie Cloud authorization code.
3835
+ * The dexie-cloud-addon automatically detects and processes this parameter
3836
+ * when db.cloud.configure() is called on page load.
3770
3837
  *
3771
- * @param options - OAuth login options
3772
- * @returns Promise resolving to OAuthLoginResult
3773
- * @throws OAuthError on failure
3838
+ * @param options - OAuth redirect options
3839
+ *
3840
+ * @example
3841
+ * ```typescript
3842
+ * // Initiate OAuth login
3843
+ * startOAuthRedirect({
3844
+ * databaseUrl: 'https://mydb.dexie.cloud',
3845
+ * provider: 'google'
3846
+ * });
3847
+ * // Page navigates away, user authenticates, then returns with auth code
3848
+ * ```
3774
3849
  */
3775
- function oauthLogin(options) {
3776
- return __awaiter(this, void 0, void 0, function* () {
3777
- const { databaseUrl, provider, usePopup = true } = options;
3778
- if (!usePopup) {
3779
- // For redirect flows, we can't return a promise - the page will navigate away
3780
- throw new Error('Non-popup OAuth flow requires handleOAuthCallback after redirect');
3781
- }
3782
- const state = generateState();
3783
- const loginUrl = buildOAuthLoginUrl(options, state);
3784
- // Calculate popup dimensions and position
3785
- const width = 500;
3786
- const height = 600;
3787
- const { left, top } = getPopupPosition(width, height);
3788
- // Open popup window
3789
- const popup = window.open(loginUrl, 'dexie-cloud-oauth', `width=${width},height=${height},left=${left},top=${top},menubar=no,toolbar=no,location=yes,status=no`);
3790
- if (!popup) {
3791
- throw new OAuthError('popup_blocked', provider);
3792
- }
3793
- return new Promise((resolve, reject) => {
3794
- let resolved = false;
3795
- // Listen for postMessage from the popup
3796
- const handleMessage = (event) => {
3797
- // Validate origin - must be from the Dexie Cloud server
3798
- const expectedOrigin = new URL(databaseUrl).origin;
3799
- if (event.origin !== expectedOrigin) {
3800
- return; // Ignore messages from other origins
3801
- }
3802
- // Check if this is our OAuth result message
3803
- if (!isOAuthResultMessage(event.data)) {
3804
- return;
3805
- }
3806
- const message = event.data;
3807
- // Validate state to prevent CSRF
3808
- if (message.state !== state) {
3809
- console.warn('[dexie-cloud] OAuth state mismatch, ignoring message');
3810
- return;
3811
- }
3812
- // Clean up
3813
- cleanup();
3814
- resolved = true;
3815
- // Handle error from OAuth flow
3816
- if (message.error) {
3817
- const errorCode = mapOAuthError(message.error);
3818
- reject(new OAuthError(errorCode, provider, message.error));
3819
- return;
3820
- }
3821
- // Success - return the authorization code
3822
- if (message.code) {
3823
- resolve({
3824
- code: message.code,
3825
- provider: message.provider,
3826
- state: message.state,
3827
- });
3828
- }
3829
- else {
3830
- reject(new OAuthError('provider_error', provider, 'No authorization code received'));
3831
- }
3832
- };
3833
- // Check if popup was closed without completing
3834
- const checkPopupClosed = setInterval(() => {
3835
- if (popup.closed && !resolved) {
3836
- cleanup();
3837
- reject(new OAuthError('popup_closed', provider));
3838
- }
3839
- }, 500);
3840
- // Cleanup function
3841
- const cleanup = () => {
3842
- window.removeEventListener('message', handleMessage);
3843
- clearInterval(checkPopupClosed);
3844
- try {
3845
- if (!popup.closed) {
3846
- popup.close();
3847
- }
3848
- }
3849
- catch (_a) {
3850
- // Ignore errors when closing popup
3851
- }
3852
- };
3853
- // Start listening for messages
3854
- window.addEventListener('message', handleMessage);
3855
- });
3856
- });
3857
- }
3858
- /** Map OAuth error strings to error codes */
3859
- function mapOAuthError(error) {
3860
- const lowerError = error.toLowerCase();
3861
- if (lowerError.includes('access_denied') || lowerError.includes('access denied')) {
3862
- return 'access_denied';
3863
- }
3864
- if (lowerError.includes('email') && lowerError.includes('verif')) {
3865
- return 'email_not_verified';
3866
- }
3867
- if (lowerError.includes('expired')) {
3868
- return 'expired_code';
3850
+ function startOAuthRedirect(options) {
3851
+ // Store provider in sessionStorage for reference on callback
3852
+ if (typeof sessionStorage !== 'undefined') {
3853
+ sessionStorage.setItem('dexie-cloud-oauth-provider', options.provider);
3869
3854
  }
3870
- if (lowerError.includes('state')) {
3871
- return 'invalid_state';
3872
- }
3873
- return 'provider_error';
3855
+ const loginUrl = buildOAuthLoginUrl(options);
3856
+ window.location.href = loginUrl;
3874
3857
  }
3875
3858
 
3876
3859
  function otpFetchTokenCallback(db) {
@@ -3891,9 +3874,11 @@ function otpFetchTokenCallback(db) {
3891
3874
  scopes: ['ACCESS_DB'],
3892
3875
  });
3893
3876
  }
3894
- // Handle OAuth provider login (popup flow)
3877
+ // Handle OAuth provider login via redirect
3895
3878
  if (hints === null || hints === void 0 ? void 0 : hints.provider) {
3896
- return yield handleOAuthFlow(db, public_key, hints.provider);
3879
+ initiateOAuthRedirect(db, hints.provider);
3880
+ // This function never returns - page navigates away
3881
+ throw new Error('OAuth redirect initiated');
3897
3882
  }
3898
3883
  if ((hints === null || hints === void 0 ? void 0 : hints.grant_type) === 'demo') {
3899
3884
  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));
@@ -3925,8 +3910,10 @@ function otpFetchTokenCallback(db) {
3925
3910
  if (authProviders.providers.length > 0) {
3926
3911
  const selection = yield promptForProvider(userInteraction, authProviders.providers, authProviders.otpEnabled, 'Sign in');
3927
3912
  if (selection.type === 'provider') {
3928
- // User selected an OAuth provider
3929
- return yield handleOAuthFlow(db, public_key, selection.provider);
3913
+ // User selected an OAuth provider - initiate redirect
3914
+ initiateOAuthRedirect(db, selection.provider);
3915
+ // This function never returns - page navigates away
3916
+ throw new Error('OAuth redirect initiated');
3930
3917
  }
3931
3918
  // User chose OTP - continue with email prompt below
3932
3919
  }
@@ -4008,46 +3995,24 @@ function otpFetchTokenCallback(db) {
4008
3995
  };
4009
3996
  }
4010
3997
  /**
4011
- * Handles the OAuth popup flow and token exchange.
3998
+ * Initiates OAuth login via full page redirect.
3999
+ *
4000
+ * The page will navigate away to the OAuth provider. After authentication,
4001
+ * the user is redirected back with a dxc-auth query parameter that is
4002
+ * automatically detected by db.cloud.configure().
4012
4003
  */
4013
- function handleOAuthFlow(db, publicKey, provider) {
4014
- return __awaiter(this, void 0, void 0, function* () {
4015
- var _a, _b, _c;
4016
- const url = (_a = db.cloud.options) === null || _a === void 0 ? void 0 : _a.databaseUrl;
4017
- if (!url)
4018
- throw new Error(`No database URL given.`);
4019
- const { userInteraction } = db.cloud;
4020
- const usePopup = ((_b = db.cloud.options) === null || _b === void 0 ? void 0 : _b.oauthPopup) !== false;
4021
- const redirectUri = ((_c = db.cloud.options) === null || _c === void 0 ? void 0 : _c.oauthRedirectUri) ||
4022
- (typeof window !== 'undefined' ? window.location.origin : undefined);
4023
- try {
4024
- // Start OAuth popup flow
4025
- const result = yield oauthLogin({
4026
- databaseUrl: url,
4027
- provider,
4028
- redirectUri,
4029
- usePopup,
4030
- });
4031
- // Exchange the auth code for tokens
4032
- return yield exchangeOAuthCode({
4033
- databaseUrl: url,
4034
- code: result.code,
4035
- publicKey,
4036
- scopes: ['ACCESS_DB'],
4037
- });
4038
- }
4039
- catch (error) {
4040
- if (error instanceof OAuthError) {
4041
- // Show user-friendly error message
4042
- yield alertUser(userInteraction, 'Authentication Failed', {
4043
- type: 'error',
4044
- messageCode: 'GENERIC_ERROR',
4045
- message: error.userMessage,
4046
- messageParams: {},
4047
- }).catch(() => { });
4048
- }
4049
- throw error;
4050
- }
4004
+ function initiateOAuthRedirect(db, provider) {
4005
+ var _a, _b;
4006
+ const url = (_a = db.cloud.options) === null || _a === void 0 ? void 0 : _a.databaseUrl;
4007
+ if (!url)
4008
+ throw new Error(`No database URL given.`);
4009
+ const redirectUri = ((_b = db.cloud.options) === null || _b === void 0 ? void 0 : _b.oauthRedirectUri) ||
4010
+ (typeof window !== 'undefined' ? window.location.href : undefined);
4011
+ // Start OAuth redirect flow - page navigates away
4012
+ startOAuthRedirect({
4013
+ databaseUrl: url,
4014
+ provider,
4015
+ redirectUri,
4051
4016
  });
4052
4017
  }
4053
4018
 
@@ -5809,7 +5774,10 @@ const Styles = {
5809
5774
  ProviderButtonIcon: {
5810
5775
  width: "20px",
5811
5776
  height: "20px",
5812
- flexShrink: 0
5777
+ flexShrink: 0,
5778
+ display: "flex",
5779
+ alignItems: "center",
5780
+ justifyContent: "center"
5813
5781
  },
5814
5782
  ProviderButtonText: {
5815
5783
  flex: 1,
@@ -5874,14 +5842,7 @@ const Styles = {
5874
5842
  color: "#374151",
5875
5843
  transition: "all 0.2s ease",
5876
5844
  gap: "12px"
5877
- },
5878
- // Cancel button for provider selection
5879
- CancelButtonRow: {
5880
- display: "flex",
5881
- justifyContent: "center",
5882
- marginTop: "16px"
5883
- }
5884
- };
5845
+ }};
5885
5846
 
5886
5847
  function Dialog({ children, className }) {
5887
5848
  return (_$1("div", { className: `dexie-dialog ${className || ''}` },
@@ -5910,19 +5871,126 @@ function resolveText({ message, messageCode, messageParams }) {
5910
5871
  return message.replace(/\{\w+\}/ig, n => messageParams[n.substring(1, n.length - 1)]);
5911
5872
  }
5912
5873
 
5874
+ /** Get style based on styleHint (for provider branding, etc.) */
5875
+ function getOptionStyle(styleHint) {
5876
+ const baseStyle = Object.assign({}, Styles.ProviderButton);
5877
+ if (!styleHint) {
5878
+ return baseStyle;
5879
+ }
5880
+ switch (styleHint) {
5881
+ case 'google':
5882
+ return Object.assign(Object.assign({}, baseStyle), Styles.ProviderGoogle);
5883
+ case 'github':
5884
+ return Object.assign(Object.assign({}, baseStyle), Styles.ProviderGitHub);
5885
+ case 'microsoft':
5886
+ return Object.assign(Object.assign({}, baseStyle), Styles.ProviderMicrosoft);
5887
+ case 'apple':
5888
+ return Object.assign(Object.assign({}, baseStyle), Styles.ProviderApple);
5889
+ case 'otp':
5890
+ return Object.assign({}, Styles.OtpButton);
5891
+ case 'custom-oauth2':
5892
+ return Object.assign(Object.assign({}, baseStyle), Styles.ProviderCustom);
5893
+ default:
5894
+ return baseStyle;
5895
+ }
5896
+ }
5897
+ /**
5898
+ * Generic button component for selectable options.
5899
+ * Displays the option's icon and display name.
5900
+ *
5901
+ * The icon can be:
5902
+ * - Inline SVG (iconSvg) - rendered directly with dangerouslySetInnerHTML
5903
+ * - Image URL (iconUrl) - rendered as an img tag
5904
+ *
5905
+ * Style is determined by the styleHint property for branding purposes.
5906
+ */
5907
+ function OptionButton({ option, onClick }) {
5908
+ const { displayName, iconUrl, iconSvg, styleHint, value } = option;
5909
+ const style = getOptionStyle(styleHint);
5910
+ // Get the text color from the button style for SVG fill processing
5911
+ const textColor = style.color || '#000000';
5912
+ // Process SVG to replace currentColor with actual text color
5913
+ const processedSvg = iconSvg
5914
+ ? iconSvg
5915
+ .replace(/fill="currentColor"/gi, `fill="${textColor}"`)
5916
+ .replace(/fill='currentColor'/gi, `fill='${textColor}'`)
5917
+ .replace(/stroke="currentColor"/gi, `stroke="${textColor}"`)
5918
+ .replace(/stroke='currentColor'/gi, `stroke='${textColor}'`)
5919
+ : null;
5920
+ // Render the appropriate icon
5921
+ const renderIcon = () => {
5922
+ // Inline SVG
5923
+ if (processedSvg) {
5924
+ return (_$1("span", { style: Styles.ProviderButtonIcon, "aria-hidden": "true", dangerouslySetInnerHTML: { __html: processedSvg } }));
5925
+ }
5926
+ // Image URL
5927
+ if (iconUrl) {
5928
+ return (_$1("img", { src: iconUrl, alt: "", style: Styles.ProviderButtonIcon, "aria-hidden": "true" }));
5929
+ }
5930
+ return null;
5931
+ };
5932
+ return (_$1("button", { type: "button", style: style, onClick: onClick, class: `dxc-option-btn${styleHint ? ` dxc-option-${styleHint}` : ''}`, "aria-label": displayName },
5933
+ renderIcon(),
5934
+ _$1("span", { style: Styles.ProviderButtonText }, displayName)));
5935
+ }
5936
+ /**
5937
+ * Visual divider with "or" text.
5938
+ */
5939
+ function Divider() {
5940
+ return (_$1("div", { style: Styles.Divider },
5941
+ _$1("div", { style: Styles.DividerLine }),
5942
+ _$1("span", { style: Styles.DividerText }, "or"),
5943
+ _$1("div", { style: Styles.DividerLine })));
5944
+ }
5945
+
5913
5946
  const OTP_LENGTH = 8;
5914
- function LoginDialog({ title, type, alerts, fields, submitLabel, cancelLabel, onCancel, onSubmit, }) {
5947
+ /**
5948
+ * Generic dialog that can render:
5949
+ * - Form fields (text inputs)
5950
+ * - Selectable options (buttons)
5951
+ * - Or both together
5952
+ *
5953
+ * When an option is clicked, calls onSubmit({ [option.name]: option.value }).
5954
+ * This unified approach means the same callback handles both form submission
5955
+ * and option selection.
5956
+ */
5957
+ function LoginDialog({ title, alerts, fields, options, submitLabel, cancelLabel, onCancel, onSubmit, }) {
5915
5958
  const [params, setParams] = d({});
5916
5959
  const firstFieldRef = A(null);
5917
5960
  _(() => { var _a; return (_a = firstFieldRef.current) === null || _a === void 0 ? void 0 : _a.focus(); }, []);
5961
+ const fieldEntries = Object.entries(fields || {});
5962
+ const hasFields = fieldEntries.length > 0;
5963
+ const hasOptions = options && options.length > 0;
5964
+ // Group options by name to detect if we have multiple groups
5965
+ const optionGroups = new Map();
5966
+ if (options) {
5967
+ for (const option of options) {
5968
+ const group = optionGroups.get(option.name) || [];
5969
+ group.push(option);
5970
+ optionGroups.set(option.name, group);
5971
+ }
5972
+ }
5973
+ const hasMultipleGroups = optionGroups.size > 1;
5974
+ // Handler for option clicks - calls onSubmit with { [option.name]: option.value }
5975
+ const handleOptionClick = (option) => {
5976
+ onSubmit({ [option.name]: option.value });
5977
+ };
5918
5978
  return (_$1(Dialog, { className: "dxc-login-dlg" },
5919
5979
  _$1(k$1, null,
5920
5980
  _$1("h3", { style: Styles.WindowHeader }, title),
5921
- alerts.map((alert) => (_$1("p", { style: Styles.Alert[alert.type] }, resolveText(alert)))),
5922
- _$1("form", { onSubmit: (ev) => {
5981
+ alerts.map((alert, idx) => (_$1("p", { key: idx, style: Styles.Alert[alert.type] }, resolveText(alert)))),
5982
+ hasOptions && (_$1("div", { class: "dxc-options" }, hasMultipleGroups ? (
5983
+ // Render with dividers between groups
5984
+ Array.from(optionGroups.entries()).map(([groupName, groupOptions], groupIdx) => (_$1(k$1, { key: groupName },
5985
+ groupIdx > 0 && _$1(Divider, null),
5986
+ groupOptions.map((option) => (_$1(OptionButton, { key: `${option.name}-${option.value}`, option: option, onClick: () => handleOptionClick(option) }))))))) : (
5987
+ // Simple case: all options in one group
5988
+ options.map((option) => (_$1(OptionButton, { key: `${option.name}-${option.value}`, option: option, onClick: () => handleOptionClick(option) })))))),
5989
+ hasOptions && hasFields && _$1(Divider, null),
5990
+ hasFields && (_$1("form", { onSubmit: (ev) => {
5923
5991
  ev.preventDefault();
5924
5992
  onSubmit(params);
5925
- } }, Object.entries(fields).map(([fieldName, { type, label, placeholder }], idx) => (_$1("label", { style: Styles.Label, key: idx },
5993
+ } }, fieldEntries.map(([fieldName, { type, label, placeholder }], idx) => (_$1("label", { style: Styles.Label, key: idx },
5926
5994
  label ? `${label}: ` : '',
5927
5995
  _$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
5996
  var _a;
@@ -5933,10 +6001,10 @@ function LoginDialog({ title, type, alerts, fields, submitLabel, cancelLabel, on
5933
6001
  // Auto-submit when OTP is filled in.
5934
6002
  onSubmit(updatedParams);
5935
6003
  }
5936
- } })))))),
6004
+ } }))))))),
5937
6005
  _$1("div", { style: Styles.ButtonsDiv },
5938
6006
  _$1(k$1, null,
5939
- _$1("button", { type: "submit", style: Styles.PrimaryButton, onClick: () => onSubmit(params) }, submitLabel),
6007
+ hasFields && submitLabel && (_$1("button", { type: "submit", style: Styles.PrimaryButton, onClick: () => onSubmit(params) }, submitLabel)),
5940
6008
  cancelLabel && (_$1("button", { style: Styles.Button, onClick: onCancel }, cancelLabel))))));
5941
6009
  }
5942
6010
  function valueTransformer(type, value) {
@@ -5950,82 +6018,6 @@ function valueTransformer(type, value) {
5950
6018
  }
5951
6019
  }
5952
6020
 
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
6021
  class LoginGui extends x {
6030
6022
  constructor(props) {
6031
6023
  super(props);
@@ -6044,11 +6036,8 @@ class LoginGui extends x {
6044
6036
  render(props, { userInteraction }) {
6045
6037
  if (!userInteraction)
6046
6038
  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
6039
+ // LoginDialog handles all interaction types uniformly
6040
+ // (forms with fields, options, or both)
6052
6041
  return _$1(LoginDialog, Object.assign({}, userInteraction));
6053
6042
  }
6054
6043
  }
@@ -6607,6 +6596,83 @@ function createAwareness(db, doc, provider) {
6607
6596
  return awareness;
6608
6597
  }
6609
6598
 
6599
+ /**
6600
+ * Decodes a base64url-encoded string to a regular string.
6601
+ * Base64url uses - instead of + and _ instead of /, and may omit padding.
6602
+ */
6603
+ function decodeBase64Url(encoded) {
6604
+ // Add padding if needed
6605
+ const padded = encoded + '='.repeat((4 - (encoded.length % 4)) % 4);
6606
+ // Convert base64url to base64
6607
+ const base64 = padded.replace(/-/g, '+').replace(/_/g, '/');
6608
+ return atob(base64);
6609
+ }
6610
+ /**
6611
+ * Parses OAuth callback parameters from the dxc-auth query parameter.
6612
+ *
6613
+ * The dxc-auth parameter contains base64url-encoded JSON with the following structure:
6614
+ * - On success: { "code": "...", "provider": "...", "state": "..." }
6615
+ * - On error: { "error": "...", "provider": "...", "state": "..." }
6616
+ *
6617
+ * @param url - The URL to parse (defaults to window.location.href)
6618
+ * @returns OAuthCallbackParams if valid callback, null otherwise
6619
+ * @throws OAuthError if there's an error in the callback
6620
+ */
6621
+ function parseOAuthCallback(url) {
6622
+ const targetUrl = (typeof window !== 'undefined' ? window.location.href : '');
6623
+ if (!targetUrl) {
6624
+ return null;
6625
+ }
6626
+ const parsed = new URL(targetUrl);
6627
+ const encoded = parsed.searchParams.get('dxc-auth');
6628
+ if (!encoded) {
6629
+ return null; // Not an OAuth callback URL
6630
+ }
6631
+ let payload;
6632
+ try {
6633
+ const json = decodeBase64Url(encoded);
6634
+ payload = JSON.parse(json);
6635
+ }
6636
+ catch (e) {
6637
+ console.warn('[dexie-cloud] Failed to parse dxc-auth parameter:', e);
6638
+ return null;
6639
+ }
6640
+ const { code, provider, state, error } = payload;
6641
+ // Check for error first
6642
+ if (error) {
6643
+ if (error.toLowerCase().includes('access_denied') || error.toLowerCase().includes('access denied')) {
6644
+ throw new OAuthError('access_denied', provider, error);
6645
+ }
6646
+ if (error.toLowerCase().includes('email') && error.toLowerCase().includes('verif')) {
6647
+ throw new OAuthError('email_not_verified', provider, error);
6648
+ }
6649
+ throw new OAuthError('provider_error', provider, error);
6650
+ }
6651
+ // Validate required fields for success case
6652
+ if (!code || !provider || !state) {
6653
+ console.warn('[dexie-cloud] Invalid dxc-auth payload: missing required fields');
6654
+ return null;
6655
+ }
6656
+ return { code, provider, state };
6657
+ }
6658
+ /**
6659
+ * Cleans up the dxc-auth query parameter from the URL.
6660
+ * Call this after successfully handling the callback to clean up the browser URL.
6661
+ */
6662
+ function cleanupOAuthUrl() {
6663
+ var _a;
6664
+ if (typeof window === 'undefined' || !((_a = window.history) === null || _a === void 0 ? void 0 : _a.replaceState)) {
6665
+ return;
6666
+ }
6667
+ const url = new URL(window.location.href);
6668
+ if (!url.searchParams.has('dxc-auth')) {
6669
+ return;
6670
+ }
6671
+ url.searchParams.delete('dxc-auth');
6672
+ const cleanUrl = url.pathname + (url.searchParams.toString() ? `?${url.searchParams.toString()}` : '') + url.hash;
6673
+ window.history.replaceState(null, '', cleanUrl);
6674
+ }
6675
+
6610
6676
  const DEFAULT_OPTIONS = {
6611
6677
  nameSuffix: true,
6612
6678
  };
@@ -6618,6 +6684,8 @@ function dexieCloud(dexie) {
6618
6684
  const currentUserEmitter = getCurrentUserEmitter(dexie);
6619
6685
  const subscriptions = [];
6620
6686
  let configuredProgramatically = false;
6687
+ // Pending OAuth auth code from dxc-auth redirect (detected in configure())
6688
+ let pendingOAuthCode = null;
6621
6689
  // local sync worker - used when there's no service worker.
6622
6690
  let localSyncWorker = null;
6623
6691
  dexie.on('ready', (dexie) => __awaiter(this, void 0, void 0, function* () {
@@ -6647,7 +6715,7 @@ function dexieCloud(dexie) {
6647
6715
  const syncComplete = new Subject();
6648
6716
  dexie.cloud = {
6649
6717
  // @ts-ignore
6650
- version: "4.3.0",
6718
+ version: "4.3.2",
6651
6719
  options: Object.assign({}, DEFAULT_OPTIONS),
6652
6720
  schema: null,
6653
6721
  get currentUserId() {
@@ -6682,6 +6750,26 @@ function dexieCloud(dexie) {
6682
6750
  DexieCloudDB(dexie).reconfigure(); // Update observable from new dexie.name
6683
6751
  }
6684
6752
  updateSchemaFromOptions(dexie.cloud.schema, dexie.cloud.options);
6753
+ // Check for OAuth callback (dxc-auth query parameter)
6754
+ // Only check in DOM environment, not workers
6755
+ if (typeof window !== 'undefined' && window.location) {
6756
+ try {
6757
+ const callback = parseOAuthCallback();
6758
+ if (callback) {
6759
+ // Clean up URL immediately (remove dxc-auth param)
6760
+ cleanupOAuthUrl();
6761
+ // Store the pending auth code for processing when db is ready
6762
+ pendingOAuthCode = { code: callback.code, provider: callback.provider };
6763
+ console.debug('[dexie-cloud] OAuth callback detected, auth code stored for processing');
6764
+ }
6765
+ }
6766
+ catch (error) {
6767
+ // parseOAuthCallback throws OAuthError on error callbacks
6768
+ // Store null for code but log the error
6769
+ console.warn('[dexie-cloud] OAuth callback error:', error);
6770
+ cleanupOAuthUrl();
6771
+ }
6772
+ }
6685
6773
  },
6686
6774
  logout() {
6687
6775
  return __awaiter(this, arguments, void 0, function* ({ force } = {}) {
@@ -6876,6 +6964,19 @@ function dexieCloud(dexie) {
6876
6964
  // HERE: If requireAuth, do athentication now.
6877
6965
  let changedUser = false;
6878
6966
  const user = yield db.getCurrentUser();
6967
+ // Process pending OAuth callback if present (from dxc-auth redirect)
6968
+ if (pendingOAuthCode && !db.cloud.isServiceWorkerDB) {
6969
+ const { code, provider } = pendingOAuthCode;
6970
+ pendingOAuthCode = null; // Clear pending code
6971
+ console.debug('[dexie-cloud] Processing OAuth callback, provider:', provider);
6972
+ try {
6973
+ changedUser = yield login(db, { oauthCode: code, provider });
6974
+ }
6975
+ catch (error) {
6976
+ console.error('[dexie-cloud] OAuth login failed:', error);
6977
+ // Continue with normal flow - user can try again
6978
+ }
6979
+ }
6879
6980
  const requireAuth = (_b = db.cloud.options) === null || _b === void 0 ? void 0 : _b.requireAuth;
6880
6981
  if (requireAuth) {
6881
6982
  if (db.cloud.isServiceWorkerDB) {
@@ -6964,7 +7065,7 @@ function dexieCloud(dexie) {
6964
7065
  }
6965
7066
  }
6966
7067
  // @ts-ignore
6967
- dexieCloud.version = "4.3.0";
7068
+ dexieCloud.version = "4.3.2";
6968
7069
  Dexie.Cloud = dexieCloud;
6969
7070
 
6970
7071
  // In case the SW lives for a while, let it reuse already opened connections: