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.
- package/TODO-SOCIALAUTH.md +129 -400
- package/dist/modern/DexieCloudOptions.d.ts +8 -11
- package/dist/modern/authentication/handleOAuthCallback.d.ts +20 -14
- package/dist/modern/authentication/interactWithUser.d.ts +3 -0
- package/dist/modern/authentication/oauthLogin.d.ts +28 -27
- package/dist/modern/default-ui/LoginDialog.d.ts +29 -4
- package/dist/modern/default-ui/OptionButton.d.ts +21 -0
- package/dist/modern/default-ui/SelectDialog.d.ts +10 -0
- package/dist/modern/dexie-cloud-addon.js +454 -305
- package/dist/modern/dexie-cloud-addon.js.map +1 -1
- package/dist/modern/dexie-cloud-addon.min.js +1 -1
- package/dist/modern/dexie-cloud-addon.min.js.map +1 -1
- package/dist/modern/errors/OAuthError.d.ts +1 -1
- package/dist/modern/errors/OAuthRedirectError.d.ts +11 -0
- package/dist/modern/service-worker.js +454 -305
- package/dist/modern/service-worker.js.map +1 -1
- package/dist/modern/service-worker.min.js +1 -1
- package/dist/modern/service-worker.min.js.map +1 -1
- package/dist/modern/types/DXCUserInteraction.d.ts +33 -25
- package/dist/umd/dexie-cloud-addon.js +454 -305
- package/dist/umd/dexie-cloud-addon.js.map +1 -1
- package/dist/umd/dexie-cloud-addon.min.js +1 -1
- package/dist/umd/dexie-cloud-addon.min.js.map +1 -1
- package/dist/umd/service-worker.js +454 -305
- package/dist/umd/service-worker.js.map +1 -1
- package/dist/umd/service-worker.min.js +1 -1
- package/dist/umd/service-worker.min.js.map +1 -1
- package/oauth_flow.md +84 -76
- package/package.json +3 -3
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* ==========================================================================
|
|
10
10
|
*
|
|
11
|
-
* Version 4.3.
|
|
11
|
+
* Version 4.3.3, 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,34 +1360,70 @@ 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(
|
|
1302
|
-
return
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
}
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
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
|
|
|
1412
|
+
/**
|
|
1413
|
+
* Error thrown when initiating an OAuth redirect.
|
|
1414
|
+
*
|
|
1415
|
+
* This is not a real error - it's used to signal that the page is
|
|
1416
|
+
* navigating away to an OAuth provider. It should be caught and
|
|
1417
|
+
* silently ignored at the appropriate level.
|
|
1418
|
+
*/
|
|
1419
|
+
class OAuthRedirectError extends Error {
|
|
1420
|
+
constructor(provider) {
|
|
1421
|
+
super(`OAuth redirect initiated for provider: ${provider}`);
|
|
1422
|
+
this.name = 'OAuthRedirectError';
|
|
1423
|
+
this.provider = provider;
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1329
1427
|
function loadAccessToken(db) {
|
|
1330
1428
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1331
1429
|
var _a, _b, _c;
|
|
@@ -1493,6 +1591,10 @@ function userAuthenticate(context, fetchToken, userInteraction, hints) {
|
|
|
1493
1591
|
return context;
|
|
1494
1592
|
}
|
|
1495
1593
|
catch (error) {
|
|
1594
|
+
// OAuth redirect is not an error - page is navigating away
|
|
1595
|
+
if (error instanceof OAuthRedirectError || (error === null || error === void 0 ? void 0 : error.name) === 'OAuthRedirectError') {
|
|
1596
|
+
throw error; // Re-throw without logging
|
|
1597
|
+
}
|
|
1496
1598
|
if (error instanceof TokenErrorResponseError) {
|
|
1497
1599
|
yield alertUser(userInteraction, error.title, {
|
|
1498
1600
|
type: 'error',
|
|
@@ -3590,8 +3692,6 @@ function _logout(db_1) {
|
|
|
3590
3692
|
|
|
3591
3693
|
/** User-friendly messages for OAuth error codes */
|
|
3592
3694
|
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
3695
|
access_denied: 'Access was denied by the authentication provider.',
|
|
3596
3696
|
invalid_state: 'The authentication response could not be verified. Please try again.',
|
|
3597
3697
|
email_not_verified: 'Your email address must be verified before you can log in.',
|
|
@@ -3733,144 +3833,46 @@ function fetchAuthProviders(databaseUrl_1) {
|
|
|
3733
3833
|
});
|
|
3734
3834
|
}
|
|
3735
3835
|
|
|
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
3836
|
/** Build the OAuth login URL */
|
|
3743
|
-
function buildOAuthLoginUrl(options
|
|
3837
|
+
function buildOAuthLoginUrl(options) {
|
|
3744
3838
|
const url = new URL(`${options.databaseUrl}/oauth/login/${options.provider}`);
|
|
3745
|
-
|
|
3746
|
-
// Set the redirect URI for postMessage or custom scheme
|
|
3839
|
+
// Set the redirect URI - defaults to current page URL for web SPAs
|
|
3747
3840
|
const redirectUri = options.redirectUri ||
|
|
3748
|
-
(typeof window !== 'undefined' ? window.location.
|
|
3841
|
+
(typeof window !== 'undefined' ? window.location.href : '');
|
|
3749
3842
|
if (redirectUri) {
|
|
3750
3843
|
url.searchParams.set('redirect_uri', redirectUri);
|
|
3751
3844
|
}
|
|
3752
3845
|
return url.toString();
|
|
3753
3846
|
}
|
|
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
3847
|
/**
|
|
3766
|
-
* Initiates OAuth login
|
|
3848
|
+
* Initiates OAuth login via full page redirect.
|
|
3767
3849
|
*
|
|
3768
|
-
*
|
|
3769
|
-
*
|
|
3850
|
+
* The page will navigate to the OAuth provider. After authentication,
|
|
3851
|
+
* the user is redirected back to the app with a `dxc-auth` query parameter
|
|
3852
|
+
* containing base64url-encoded JSON with the authorization code.
|
|
3770
3853
|
*
|
|
3771
|
-
*
|
|
3772
|
-
*
|
|
3773
|
-
*
|
|
3854
|
+
* The dexie-cloud-addon automatically detects and processes this parameter
|
|
3855
|
+
* when db.cloud.configure() is called on page load.
|
|
3856
|
+
*
|
|
3857
|
+
* @param options - OAuth redirect options
|
|
3858
|
+
*
|
|
3859
|
+
* @example
|
|
3860
|
+
* ```typescript
|
|
3861
|
+
* // Initiate OAuth login
|
|
3862
|
+
* startOAuthRedirect({
|
|
3863
|
+
* databaseUrl: 'https://mydb.dexie.cloud',
|
|
3864
|
+
* provider: 'google'
|
|
3865
|
+
* });
|
|
3866
|
+
* // Page navigates away, user authenticates, then returns with auth code
|
|
3867
|
+
* ```
|
|
3774
3868
|
*/
|
|
3775
|
-
function
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
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';
|
|
3869
|
+
function startOAuthRedirect(options) {
|
|
3870
|
+
// Store provider in sessionStorage for reference on callback
|
|
3871
|
+
if (typeof sessionStorage !== 'undefined') {
|
|
3872
|
+
sessionStorage.setItem('dexie-cloud-oauth-provider', options.provider);
|
|
3866
3873
|
}
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
}
|
|
3870
|
-
if (lowerError.includes('state')) {
|
|
3871
|
-
return 'invalid_state';
|
|
3872
|
-
}
|
|
3873
|
-
return 'provider_error';
|
|
3874
|
+
const loginUrl = buildOAuthLoginUrl(options);
|
|
3875
|
+
window.location.href = loginUrl;
|
|
3874
3876
|
}
|
|
3875
3877
|
|
|
3876
3878
|
function otpFetchTokenCallback(db) {
|
|
@@ -3891,9 +3893,11 @@ function otpFetchTokenCallback(db) {
|
|
|
3891
3893
|
scopes: ['ACCESS_DB'],
|
|
3892
3894
|
});
|
|
3893
3895
|
}
|
|
3894
|
-
// Handle OAuth provider login
|
|
3896
|
+
// Handle OAuth provider login via redirect
|
|
3895
3897
|
if (hints === null || hints === void 0 ? void 0 : hints.provider) {
|
|
3896
|
-
|
|
3898
|
+
initiateOAuthRedirect(db, hints.provider);
|
|
3899
|
+
// This function never returns - page navigates away
|
|
3900
|
+
throw new OAuthRedirectError(hints.provider);
|
|
3897
3901
|
}
|
|
3898
3902
|
if ((hints === null || hints === void 0 ? void 0 : hints.grant_type) === 'demo') {
|
|
3899
3903
|
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 +3929,10 @@ function otpFetchTokenCallback(db) {
|
|
|
3925
3929
|
if (authProviders.providers.length > 0) {
|
|
3926
3930
|
const selection = yield promptForProvider(userInteraction, authProviders.providers, authProviders.otpEnabled, 'Sign in');
|
|
3927
3931
|
if (selection.type === 'provider') {
|
|
3928
|
-
// User selected an OAuth provider
|
|
3929
|
-
|
|
3932
|
+
// User selected an OAuth provider - initiate redirect
|
|
3933
|
+
initiateOAuthRedirect(db, selection.provider);
|
|
3934
|
+
// This function never returns - page navigates away
|
|
3935
|
+
throw new OAuthRedirectError(selection.provider);
|
|
3930
3936
|
}
|
|
3931
3937
|
// User chose OTP - continue with email prompt below
|
|
3932
3938
|
}
|
|
@@ -4008,46 +4014,24 @@ function otpFetchTokenCallback(db) {
|
|
|
4008
4014
|
};
|
|
4009
4015
|
}
|
|
4010
4016
|
/**
|
|
4011
|
-
*
|
|
4017
|
+
* Initiates OAuth login via full page redirect.
|
|
4018
|
+
*
|
|
4019
|
+
* The page will navigate away to the OAuth provider. After authentication,
|
|
4020
|
+
* the user is redirected back with a dxc-auth query parameter that is
|
|
4021
|
+
* automatically detected by db.cloud.configure().
|
|
4012
4022
|
*/
|
|
4013
|
-
function
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
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
|
-
}
|
|
4023
|
+
function initiateOAuthRedirect(db, provider) {
|
|
4024
|
+
var _a, _b;
|
|
4025
|
+
const url = (_a = db.cloud.options) === null || _a === void 0 ? void 0 : _a.databaseUrl;
|
|
4026
|
+
if (!url)
|
|
4027
|
+
throw new Error(`No database URL given.`);
|
|
4028
|
+
const redirectUri = ((_b = db.cloud.options) === null || _b === void 0 ? void 0 : _b.oauthRedirectUri) ||
|
|
4029
|
+
(typeof window !== 'undefined' ? window.location.href : undefined);
|
|
4030
|
+
// Start OAuth redirect flow - page navigates away
|
|
4031
|
+
startOAuthRedirect({
|
|
4032
|
+
databaseUrl: url,
|
|
4033
|
+
provider,
|
|
4034
|
+
redirectUri,
|
|
4051
4035
|
});
|
|
4052
4036
|
}
|
|
4053
4037
|
|
|
@@ -4137,7 +4121,15 @@ function login(db, hints) {
|
|
|
4137
4121
|
claims: {},
|
|
4138
4122
|
lastLogin: new Date(0),
|
|
4139
4123
|
});
|
|
4140
|
-
|
|
4124
|
+
try {
|
|
4125
|
+
yield authenticate(db.cloud.options.databaseUrl, context, db.cloud.options.fetchTokens || otpFetchTokenCallback(db), db.cloud.userInteraction, hints);
|
|
4126
|
+
}
|
|
4127
|
+
catch (err) {
|
|
4128
|
+
if (err.name === 'OAuthRedirectError') {
|
|
4129
|
+
return false; // Page is redirecting for OAuth login
|
|
4130
|
+
}
|
|
4131
|
+
throw err;
|
|
4132
|
+
}
|
|
4141
4133
|
if (origUserId !== UNAUTHORIZED_USER.userId &&
|
|
4142
4134
|
context.userId !== origUserId) {
|
|
4143
4135
|
// 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
|
-
|
|
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("
|
|
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
|
-
} },
|
|
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
|
-
//
|
|
6048
|
-
|
|
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
|
const DEFAULT_OPTIONS = {
|
|
6611
6704
|
nameSuffix: true,
|
|
6612
6705
|
};
|
|
@@ -6618,6 +6711,10 @@ function dexieCloud(dexie) {
|
|
|
6618
6711
|
const currentUserEmitter = getCurrentUserEmitter(dexie);
|
|
6619
6712
|
const subscriptions = [];
|
|
6620
6713
|
let configuredProgramatically = false;
|
|
6714
|
+
// Pending OAuth auth code from dxc-auth redirect (detected in configure())
|
|
6715
|
+
let pendingOAuthCode = null;
|
|
6716
|
+
// Pending OAuth error from dxc-auth redirect (detected in configure())
|
|
6717
|
+
let pendingOAuthError = null;
|
|
6621
6718
|
// local sync worker - used when there's no service worker.
|
|
6622
6719
|
let localSyncWorker = null;
|
|
6623
6720
|
dexie.on('ready', (dexie) => __awaiter(this, void 0, void 0, function* () {
|
|
@@ -6647,7 +6744,7 @@ function dexieCloud(dexie) {
|
|
|
6647
6744
|
const syncComplete = new Subject();
|
|
6648
6745
|
dexie.cloud = {
|
|
6649
6746
|
// @ts-ignore
|
|
6650
|
-
version: "4.3.
|
|
6747
|
+
version: "4.3.3",
|
|
6651
6748
|
options: Object.assign({}, DEFAULT_OPTIONS),
|
|
6652
6749
|
schema: null,
|
|
6653
6750
|
get currentUserId() {
|
|
@@ -6682,6 +6779,31 @@ function dexieCloud(dexie) {
|
|
|
6682
6779
|
DexieCloudDB(dexie).reconfigure(); // Update observable from new dexie.name
|
|
6683
6780
|
}
|
|
6684
6781
|
updateSchemaFromOptions(dexie.cloud.schema, dexie.cloud.options);
|
|
6782
|
+
// Check for OAuth callback (dxc-auth query parameter)
|
|
6783
|
+
// Only check in DOM environment, not workers
|
|
6784
|
+
if (typeof window !== 'undefined' && window.location) {
|
|
6785
|
+
try {
|
|
6786
|
+
const callback = parseOAuthCallback();
|
|
6787
|
+
if (callback) {
|
|
6788
|
+
// Clean up URL immediately (remove dxc-auth param)
|
|
6789
|
+
cleanupOAuthUrl();
|
|
6790
|
+
// Store the pending auth code for processing when db is ready
|
|
6791
|
+
pendingOAuthCode = { code: callback.code, provider: callback.provider };
|
|
6792
|
+
console.debug('[dexie-cloud] OAuth callback detected, auth code stored for processing');
|
|
6793
|
+
}
|
|
6794
|
+
}
|
|
6795
|
+
catch (error) {
|
|
6796
|
+
// parseOAuthCallback throws OAuthError on error callbacks
|
|
6797
|
+
cleanupOAuthUrl();
|
|
6798
|
+
if (error instanceof OAuthError) {
|
|
6799
|
+
pendingOAuthError = error;
|
|
6800
|
+
console.error('[dexie-cloud] OAuth callback error:', error.message);
|
|
6801
|
+
}
|
|
6802
|
+
else {
|
|
6803
|
+
console.error('[dexie-cloud] OAuth callback error:', error);
|
|
6804
|
+
}
|
|
6805
|
+
}
|
|
6806
|
+
}
|
|
6685
6807
|
},
|
|
6686
6808
|
logout() {
|
|
6687
6809
|
return __awaiter(this, arguments, void 0, function* ({ force } = {}) {
|
|
@@ -6875,7 +6997,34 @@ function dexieCloud(dexie) {
|
|
|
6875
6997
|
}
|
|
6876
6998
|
// HERE: If requireAuth, do athentication now.
|
|
6877
6999
|
let changedUser = false;
|
|
6878
|
-
|
|
7000
|
+
let user = yield db.getCurrentUser();
|
|
7001
|
+
// Show pending OAuth error if present (from dxc-auth redirect)
|
|
7002
|
+
if (pendingOAuthError && !db.cloud.isServiceWorkerDB) {
|
|
7003
|
+
const error = pendingOAuthError;
|
|
7004
|
+
pendingOAuthError = null; // Clear pending error
|
|
7005
|
+
console.debug('[dexie-cloud] Showing OAuth error:', error.message);
|
|
7006
|
+
// Show alert to user about the OAuth error
|
|
7007
|
+
yield alertUser(db.cloud.userInteraction, 'Authentication Error', {
|
|
7008
|
+
type: 'error',
|
|
7009
|
+
messageCode: 'GENERIC_ERROR',
|
|
7010
|
+
message: error.message,
|
|
7011
|
+
messageParams: { provider: error.provider || 'unknown' }
|
|
7012
|
+
});
|
|
7013
|
+
}
|
|
7014
|
+
// Process pending OAuth callback if present (from dxc-auth redirect)
|
|
7015
|
+
if (pendingOAuthCode && !db.cloud.isServiceWorkerDB) {
|
|
7016
|
+
const { code, provider } = pendingOAuthCode;
|
|
7017
|
+
pendingOAuthCode = null; // Clear pending code
|
|
7018
|
+
console.debug('[dexie-cloud] Processing OAuth callback, provider:', provider);
|
|
7019
|
+
try {
|
|
7020
|
+
changedUser = yield login(db, { oauthCode: code, provider });
|
|
7021
|
+
user = yield db.getCurrentUser();
|
|
7022
|
+
}
|
|
7023
|
+
catch (error) {
|
|
7024
|
+
console.error('[dexie-cloud] OAuth login failed:', error);
|
|
7025
|
+
// Continue with normal flow - user can try again
|
|
7026
|
+
}
|
|
7027
|
+
}
|
|
6879
7028
|
const requireAuth = (_b = db.cloud.options) === null || _b === void 0 ? void 0 : _b.requireAuth;
|
|
6880
7029
|
if (requireAuth) {
|
|
6881
7030
|
if (db.cloud.isServiceWorkerDB) {
|
|
@@ -6964,7 +7113,7 @@ function dexieCloud(dexie) {
|
|
|
6964
7113
|
}
|
|
6965
7114
|
}
|
|
6966
7115
|
// @ts-ignore
|
|
6967
|
-
dexieCloud.version = "4.3.
|
|
7116
|
+
dexieCloud.version = "4.3.3";
|
|
6968
7117
|
Dexie.Cloud = dexieCloud;
|
|
6969
7118
|
|
|
6970
7119
|
// In case the SW lives for a while, let it reuse already opened connections:
|