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
|
*
|
|
@@ -103,15 +103,6 @@
|
|
|
103
103
|
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
104
104
|
};
|
|
105
105
|
|
|
106
|
-
/** Type guard to check if a message is an OAuthResultMessage */
|
|
107
|
-
function isOAuthResultMessage(msg) {
|
|
108
|
-
return (typeof msg === 'object' &&
|
|
109
|
-
msg !== null &&
|
|
110
|
-
msg.type === 'dexie:oauthResult' &&
|
|
111
|
-
typeof msg.provider === 'string' &&
|
|
112
|
-
typeof msg.state === 'string');
|
|
113
|
-
}
|
|
114
|
-
|
|
115
106
|
function assert(b) {
|
|
116
107
|
if (!b)
|
|
117
108
|
throw new Error('Assertion Failed');
|
|
@@ -2070,6 +2061,74 @@
|
|
|
2070
2061
|
}
|
|
2071
2062
|
}
|
|
2072
2063
|
|
|
2064
|
+
/** Cache for fetched SVG content to avoid re-fetching */
|
|
2065
|
+
const svgCache = {};
|
|
2066
|
+
/** Default SVG icons for built-in OAuth providers */
|
|
2067
|
+
const ProviderIcons = {
|
|
2068
|
+
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>`,
|
|
2069
|
+
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>`,
|
|
2070
|
+
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>`,
|
|
2071
|
+
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>`,
|
|
2072
|
+
};
|
|
2073
|
+
/** Email/envelope icon for OTP option */
|
|
2074
|
+
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>`;
|
|
2075
|
+
/**
|
|
2076
|
+
* Fetches SVG content from a URL and caches it.
|
|
2077
|
+
* Returns the SVG string or null if fetch fails.
|
|
2078
|
+
*/
|
|
2079
|
+
function fetchSvgIcon(url) {
|
|
2080
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
2081
|
+
if (svgCache[url]) {
|
|
2082
|
+
return svgCache[url];
|
|
2083
|
+
}
|
|
2084
|
+
try {
|
|
2085
|
+
const res = yield fetch(url);
|
|
2086
|
+
if (res.ok) {
|
|
2087
|
+
const svg = yield res.text();
|
|
2088
|
+
// Validate it looks like SVG
|
|
2089
|
+
if (svg.includes('<svg')) {
|
|
2090
|
+
svgCache[url] = svg;
|
|
2091
|
+
return svg;
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
catch (_a) {
|
|
2096
|
+
// Silently fail - will show no icon
|
|
2097
|
+
}
|
|
2098
|
+
return null;
|
|
2099
|
+
});
|
|
2100
|
+
}
|
|
2101
|
+
/**
|
|
2102
|
+
* Converts an OAuthProviderInfo to a generic DXCOption.
|
|
2103
|
+
* Fetches SVG icons from URLs if needed.
|
|
2104
|
+
*/
|
|
2105
|
+
function providerToOption(provider) {
|
|
2106
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
2107
|
+
var _a;
|
|
2108
|
+
let iconSvg;
|
|
2109
|
+
// First check for built-in icons
|
|
2110
|
+
if (ProviderIcons[provider.type]) {
|
|
2111
|
+
iconSvg = ProviderIcons[provider.type];
|
|
2112
|
+
}
|
|
2113
|
+
// If provider has iconUrl pointing to SVG, fetch and inline it
|
|
2114
|
+
else if ((_a = provider.iconUrl) === null || _a === void 0 ? void 0 : _a.toLowerCase().endsWith('.svg')) {
|
|
2115
|
+
const fetched = yield fetchSvgIcon(provider.iconUrl);
|
|
2116
|
+
if (fetched) {
|
|
2117
|
+
iconSvg = fetched;
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
return {
|
|
2121
|
+
name: 'provider',
|
|
2122
|
+
value: provider.name,
|
|
2123
|
+
displayName: `Continue with ${provider.displayName}`,
|
|
2124
|
+
iconSvg,
|
|
2125
|
+
// If iconUrl is not SVG, pass it through for img tag rendering
|
|
2126
|
+
iconUrl: (!iconSvg && provider.iconUrl) ? provider.iconUrl : undefined,
|
|
2127
|
+
// Use provider type as style hint for branding
|
|
2128
|
+
styleHint: provider.type,
|
|
2129
|
+
};
|
|
2130
|
+
});
|
|
2131
|
+
}
|
|
2073
2132
|
function interactWithUser(userInteraction, req) {
|
|
2074
2133
|
return new Promise((resolve, reject) => {
|
|
2075
2134
|
const interactionProps = Object.assign(Object.assign({ submitLabel: 'Submit', cancelLabel: 'Cancel' }, req), { onSubmit: (res) => {
|
|
@@ -2207,6 +2266,9 @@
|
|
|
2207
2266
|
/**
|
|
2208
2267
|
* Prompts the user to select an authentication method (OAuth provider or OTP).
|
|
2209
2268
|
*
|
|
2269
|
+
* This function converts OAuth providers and OTP option into generic DXCOption[]
|
|
2270
|
+
* for the DXCSelect interaction, handling icon fetching and style hints.
|
|
2271
|
+
*
|
|
2210
2272
|
* @param userInteraction - The user interaction BehaviorSubject
|
|
2211
2273
|
* @param providers - Available OAuth providers
|
|
2212
2274
|
* @param otpEnabled - Whether OTP is available
|
|
@@ -2214,34 +2276,70 @@
|
|
|
2214
2276
|
* @param alerts - Optional alerts to display
|
|
2215
2277
|
* @returns Promise resolving to the user's selection
|
|
2216
2278
|
*/
|
|
2217
|
-
function promptForProvider(
|
|
2218
|
-
return
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
}
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2279
|
+
function promptForProvider(userInteraction_1, providers_1, otpEnabled_1) {
|
|
2280
|
+
return __awaiter(this, arguments, void 0, function* (userInteraction, providers, otpEnabled, title = 'Choose login method', alerts = []) {
|
|
2281
|
+
// Convert providers to generic options (with icon fetching)
|
|
2282
|
+
const providerOptions = yield Promise.all(providers.map(providerToOption));
|
|
2283
|
+
// Build the options array
|
|
2284
|
+
const options = [...providerOptions];
|
|
2285
|
+
// Add OTP option if enabled
|
|
2286
|
+
if (otpEnabled) {
|
|
2287
|
+
options.push({
|
|
2288
|
+
name: 'otp',
|
|
2289
|
+
value: 'email',
|
|
2290
|
+
displayName: 'Continue with email',
|
|
2291
|
+
iconSvg: EmailIcon,
|
|
2292
|
+
styleHint: 'otp',
|
|
2293
|
+
});
|
|
2294
|
+
}
|
|
2295
|
+
return new Promise((resolve, reject) => {
|
|
2296
|
+
const interactionProps = {
|
|
2297
|
+
type: 'generic',
|
|
2298
|
+
title,
|
|
2299
|
+
alerts,
|
|
2300
|
+
options,
|
|
2301
|
+
fields: {},
|
|
2302
|
+
submitLabel: '', // No submit button - just options
|
|
2303
|
+
cancelLabel: 'Cancel',
|
|
2304
|
+
onSubmit: (params) => {
|
|
2305
|
+
userInteraction.next(undefined);
|
|
2306
|
+
// Check which option was selected
|
|
2307
|
+
if ('otp' in params) {
|
|
2308
|
+
resolve({ type: 'otp' });
|
|
2309
|
+
}
|
|
2310
|
+
else if ('provider' in params) {
|
|
2311
|
+
resolve({ type: 'provider', provider: params.provider });
|
|
2312
|
+
}
|
|
2313
|
+
else {
|
|
2314
|
+
// Unknown - default to OTP
|
|
2315
|
+
resolve({ type: 'otp' });
|
|
2316
|
+
}
|
|
2317
|
+
},
|
|
2318
|
+
onCancel: () => {
|
|
2319
|
+
userInteraction.next(undefined);
|
|
2320
|
+
reject(new Dexie.AbortError('User cancelled'));
|
|
2321
|
+
},
|
|
2322
|
+
};
|
|
2323
|
+
userInteraction.next(interactionProps);
|
|
2324
|
+
});
|
|
2242
2325
|
});
|
|
2243
2326
|
}
|
|
2244
2327
|
|
|
2328
|
+
/**
|
|
2329
|
+
* Error thrown when initiating an OAuth redirect.
|
|
2330
|
+
*
|
|
2331
|
+
* This is not a real error - it's used to signal that the page is
|
|
2332
|
+
* navigating away to an OAuth provider. It should be caught and
|
|
2333
|
+
* silently ignored at the appropriate level.
|
|
2334
|
+
*/
|
|
2335
|
+
class OAuthRedirectError extends Error {
|
|
2336
|
+
constructor(provider) {
|
|
2337
|
+
super(`OAuth redirect initiated for provider: ${provider}`);
|
|
2338
|
+
this.name = 'OAuthRedirectError';
|
|
2339
|
+
this.provider = provider;
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
|
|
2245
2343
|
function loadAccessToken(db) {
|
|
2246
2344
|
return __awaiter(this, void 0, void 0, function* () {
|
|
2247
2345
|
var _a, _b, _c;
|
|
@@ -2409,6 +2507,10 @@
|
|
|
2409
2507
|
return context;
|
|
2410
2508
|
}
|
|
2411
2509
|
catch (error) {
|
|
2510
|
+
// OAuth redirect is not an error - page is navigating away
|
|
2511
|
+
if (error instanceof OAuthRedirectError || (error === null || error === void 0 ? void 0 : error.name) === 'OAuthRedirectError') {
|
|
2512
|
+
throw error; // Re-throw without logging
|
|
2513
|
+
}
|
|
2412
2514
|
if (error instanceof TokenErrorResponseError) {
|
|
2413
2515
|
yield alertUser(userInteraction, error.title, {
|
|
2414
2516
|
type: 'error',
|
|
@@ -2566,8 +2668,6 @@
|
|
|
2566
2668
|
|
|
2567
2669
|
/** User-friendly messages for OAuth error codes */
|
|
2568
2670
|
const ERROR_MESSAGES = {
|
|
2569
|
-
popup_blocked: 'The login popup was blocked by your browser. Please allow popups for this site and try again.',
|
|
2570
|
-
popup_closed: 'The login popup was closed before completing authentication.',
|
|
2571
2671
|
access_denied: 'Access was denied by the authentication provider.',
|
|
2572
2672
|
invalid_state: 'The authentication response could not be verified. Please try again.',
|
|
2573
2673
|
email_not_verified: 'Your email address must be verified before you can log in.',
|
|
@@ -2709,144 +2809,46 @@
|
|
|
2709
2809
|
});
|
|
2710
2810
|
}
|
|
2711
2811
|
|
|
2712
|
-
/** Generate a random state string for CSRF protection */
|
|
2713
|
-
function generateState() {
|
|
2714
|
-
const array = new Uint8Array(32);
|
|
2715
|
-
crypto.getRandomValues(array);
|
|
2716
|
-
return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
|
|
2717
|
-
}
|
|
2718
2812
|
/** Build the OAuth login URL */
|
|
2719
|
-
function buildOAuthLoginUrl(options
|
|
2813
|
+
function buildOAuthLoginUrl(options) {
|
|
2720
2814
|
const url = new URL(`${options.databaseUrl}/oauth/login/${options.provider}`);
|
|
2721
|
-
|
|
2722
|
-
// Set the redirect URI for postMessage or custom scheme
|
|
2815
|
+
// Set the redirect URI - defaults to current page URL for web SPAs
|
|
2723
2816
|
const redirectUri = options.redirectUri ||
|
|
2724
|
-
(typeof window !== 'undefined' ? window.location.
|
|
2817
|
+
(typeof window !== 'undefined' ? window.location.href : '');
|
|
2725
2818
|
if (redirectUri) {
|
|
2726
2819
|
url.searchParams.set('redirect_uri', redirectUri);
|
|
2727
2820
|
}
|
|
2728
2821
|
return url.toString();
|
|
2729
2822
|
}
|
|
2730
|
-
/** Calculate centered popup position */
|
|
2731
|
-
function getPopupPosition(width, height) {
|
|
2732
|
-
var _a, _b, _c, _d, _e, _f;
|
|
2733
|
-
const screenLeft = (_a = window.screenLeft) !== null && _a !== void 0 ? _a : window.screenX;
|
|
2734
|
-
const screenTop = (_b = window.screenTop) !== null && _b !== void 0 ? _b : window.screenY;
|
|
2735
|
-
const screenWidth = (_d = (_c = window.innerWidth) !== null && _c !== void 0 ? _c : document.documentElement.clientWidth) !== null && _d !== void 0 ? _d : screen.width;
|
|
2736
|
-
const screenHeight = (_f = (_e = window.innerHeight) !== null && _e !== void 0 ? _e : document.documentElement.clientHeight) !== null && _f !== void 0 ? _f : screen.height;
|
|
2737
|
-
const left = screenLeft + (screenWidth - width) / 2;
|
|
2738
|
-
const top = screenTop + (screenHeight - height) / 2;
|
|
2739
|
-
return { left: Math.max(0, left), top: Math.max(0, top) };
|
|
2740
|
-
}
|
|
2741
2823
|
/**
|
|
2742
|
-
* Initiates OAuth login
|
|
2824
|
+
* Initiates OAuth login via full page redirect.
|
|
2825
|
+
*
|
|
2826
|
+
* The page will navigate to the OAuth provider. After authentication,
|
|
2827
|
+
* the user is redirected back to the app with a `dxc-auth` query parameter
|
|
2828
|
+
* containing base64url-encoded JSON with the authorization code.
|
|
2743
2829
|
*
|
|
2744
|
-
*
|
|
2745
|
-
*
|
|
2830
|
+
* The dexie-cloud-addon automatically detects and processes this parameter
|
|
2831
|
+
* when db.cloud.configure() is called on page load.
|
|
2746
2832
|
*
|
|
2747
|
-
* @param options - OAuth
|
|
2748
|
-
*
|
|
2749
|
-
* @
|
|
2833
|
+
* @param options - OAuth redirect options
|
|
2834
|
+
*
|
|
2835
|
+
* @example
|
|
2836
|
+
* ```typescript
|
|
2837
|
+
* // Initiate OAuth login
|
|
2838
|
+
* startOAuthRedirect({
|
|
2839
|
+
* databaseUrl: 'https://mydb.dexie.cloud',
|
|
2840
|
+
* provider: 'google'
|
|
2841
|
+
* });
|
|
2842
|
+
* // Page navigates away, user authenticates, then returns with auth code
|
|
2843
|
+
* ```
|
|
2750
2844
|
*/
|
|
2751
|
-
function
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
// For redirect flows, we can't return a promise - the page will navigate away
|
|
2756
|
-
throw new Error('Non-popup OAuth flow requires handleOAuthCallback after redirect');
|
|
2757
|
-
}
|
|
2758
|
-
const state = generateState();
|
|
2759
|
-
const loginUrl = buildOAuthLoginUrl(options, state);
|
|
2760
|
-
// Calculate popup dimensions and position
|
|
2761
|
-
const width = 500;
|
|
2762
|
-
const height = 600;
|
|
2763
|
-
const { left, top } = getPopupPosition(width, height);
|
|
2764
|
-
// Open popup window
|
|
2765
|
-
const popup = window.open(loginUrl, 'dexie-cloud-oauth', `width=${width},height=${height},left=${left},top=${top},menubar=no,toolbar=no,location=yes,status=no`);
|
|
2766
|
-
if (!popup) {
|
|
2767
|
-
throw new OAuthError('popup_blocked', provider);
|
|
2768
|
-
}
|
|
2769
|
-
return new Promise((resolve, reject) => {
|
|
2770
|
-
let resolved = false;
|
|
2771
|
-
// Listen for postMessage from the popup
|
|
2772
|
-
const handleMessage = (event) => {
|
|
2773
|
-
// Validate origin - must be from the Dexie Cloud server
|
|
2774
|
-
const expectedOrigin = new URL(databaseUrl).origin;
|
|
2775
|
-
if (event.origin !== expectedOrigin) {
|
|
2776
|
-
return; // Ignore messages from other origins
|
|
2777
|
-
}
|
|
2778
|
-
// Check if this is our OAuth result message
|
|
2779
|
-
if (!isOAuthResultMessage(event.data)) {
|
|
2780
|
-
return;
|
|
2781
|
-
}
|
|
2782
|
-
const message = event.data;
|
|
2783
|
-
// Validate state to prevent CSRF
|
|
2784
|
-
if (message.state !== state) {
|
|
2785
|
-
console.warn('[dexie-cloud] OAuth state mismatch, ignoring message');
|
|
2786
|
-
return;
|
|
2787
|
-
}
|
|
2788
|
-
// Clean up
|
|
2789
|
-
cleanup();
|
|
2790
|
-
resolved = true;
|
|
2791
|
-
// Handle error from OAuth flow
|
|
2792
|
-
if (message.error) {
|
|
2793
|
-
const errorCode = mapOAuthError(message.error);
|
|
2794
|
-
reject(new OAuthError(errorCode, provider, message.error));
|
|
2795
|
-
return;
|
|
2796
|
-
}
|
|
2797
|
-
// Success - return the authorization code
|
|
2798
|
-
if (message.code) {
|
|
2799
|
-
resolve({
|
|
2800
|
-
code: message.code,
|
|
2801
|
-
provider: message.provider,
|
|
2802
|
-
state: message.state,
|
|
2803
|
-
});
|
|
2804
|
-
}
|
|
2805
|
-
else {
|
|
2806
|
-
reject(new OAuthError('provider_error', provider, 'No authorization code received'));
|
|
2807
|
-
}
|
|
2808
|
-
};
|
|
2809
|
-
// Check if popup was closed without completing
|
|
2810
|
-
const checkPopupClosed = setInterval(() => {
|
|
2811
|
-
if (popup.closed && !resolved) {
|
|
2812
|
-
cleanup();
|
|
2813
|
-
reject(new OAuthError('popup_closed', provider));
|
|
2814
|
-
}
|
|
2815
|
-
}, 500);
|
|
2816
|
-
// Cleanup function
|
|
2817
|
-
const cleanup = () => {
|
|
2818
|
-
window.removeEventListener('message', handleMessage);
|
|
2819
|
-
clearInterval(checkPopupClosed);
|
|
2820
|
-
try {
|
|
2821
|
-
if (!popup.closed) {
|
|
2822
|
-
popup.close();
|
|
2823
|
-
}
|
|
2824
|
-
}
|
|
2825
|
-
catch (_a) {
|
|
2826
|
-
// Ignore errors when closing popup
|
|
2827
|
-
}
|
|
2828
|
-
};
|
|
2829
|
-
// Start listening for messages
|
|
2830
|
-
window.addEventListener('message', handleMessage);
|
|
2831
|
-
});
|
|
2832
|
-
});
|
|
2833
|
-
}
|
|
2834
|
-
/** Map OAuth error strings to error codes */
|
|
2835
|
-
function mapOAuthError(error) {
|
|
2836
|
-
const lowerError = error.toLowerCase();
|
|
2837
|
-
if (lowerError.includes('access_denied') || lowerError.includes('access denied')) {
|
|
2838
|
-
return 'access_denied';
|
|
2839
|
-
}
|
|
2840
|
-
if (lowerError.includes('email') && lowerError.includes('verif')) {
|
|
2841
|
-
return 'email_not_verified';
|
|
2842
|
-
}
|
|
2843
|
-
if (lowerError.includes('expired')) {
|
|
2844
|
-
return 'expired_code';
|
|
2845
|
-
}
|
|
2846
|
-
if (lowerError.includes('state')) {
|
|
2847
|
-
return 'invalid_state';
|
|
2845
|
+
function startOAuthRedirect(options) {
|
|
2846
|
+
// Store provider in sessionStorage for reference on callback
|
|
2847
|
+
if (typeof sessionStorage !== 'undefined') {
|
|
2848
|
+
sessionStorage.setItem('dexie-cloud-oauth-provider', options.provider);
|
|
2848
2849
|
}
|
|
2849
|
-
|
|
2850
|
+
const loginUrl = buildOAuthLoginUrl(options);
|
|
2851
|
+
window.location.href = loginUrl;
|
|
2850
2852
|
}
|
|
2851
2853
|
|
|
2852
2854
|
function otpFetchTokenCallback(db) {
|
|
@@ -2867,9 +2869,11 @@
|
|
|
2867
2869
|
scopes: ['ACCESS_DB'],
|
|
2868
2870
|
});
|
|
2869
2871
|
}
|
|
2870
|
-
// Handle OAuth provider login
|
|
2872
|
+
// Handle OAuth provider login via redirect
|
|
2871
2873
|
if (hints === null || hints === void 0 ? void 0 : hints.provider) {
|
|
2872
|
-
|
|
2874
|
+
initiateOAuthRedirect(db, hints.provider);
|
|
2875
|
+
// This function never returns - page navigates away
|
|
2876
|
+
throw new OAuthRedirectError(hints.provider);
|
|
2873
2877
|
}
|
|
2874
2878
|
if ((hints === null || hints === void 0 ? void 0 : hints.grant_type) === 'demo') {
|
|
2875
2879
|
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));
|
|
@@ -2901,8 +2905,10 @@
|
|
|
2901
2905
|
if (authProviders.providers.length > 0) {
|
|
2902
2906
|
const selection = yield promptForProvider(userInteraction, authProviders.providers, authProviders.otpEnabled, 'Sign in');
|
|
2903
2907
|
if (selection.type === 'provider') {
|
|
2904
|
-
// User selected an OAuth provider
|
|
2905
|
-
|
|
2908
|
+
// User selected an OAuth provider - initiate redirect
|
|
2909
|
+
initiateOAuthRedirect(db, selection.provider);
|
|
2910
|
+
// This function never returns - page navigates away
|
|
2911
|
+
throw new OAuthRedirectError(selection.provider);
|
|
2906
2912
|
}
|
|
2907
2913
|
// User chose OTP - continue with email prompt below
|
|
2908
2914
|
}
|
|
@@ -2984,46 +2990,24 @@
|
|
|
2984
2990
|
};
|
|
2985
2991
|
}
|
|
2986
2992
|
/**
|
|
2987
|
-
*
|
|
2993
|
+
* Initiates OAuth login via full page redirect.
|
|
2994
|
+
*
|
|
2995
|
+
* The page will navigate away to the OAuth provider. After authentication,
|
|
2996
|
+
* the user is redirected back with a dxc-auth query parameter that is
|
|
2997
|
+
* automatically detected by db.cloud.configure().
|
|
2988
2998
|
*/
|
|
2989
|
-
function
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
const result = yield oauthLogin({
|
|
3002
|
-
databaseUrl: url,
|
|
3003
|
-
provider,
|
|
3004
|
-
redirectUri,
|
|
3005
|
-
usePopup,
|
|
3006
|
-
});
|
|
3007
|
-
// Exchange the auth code for tokens
|
|
3008
|
-
return yield exchangeOAuthCode({
|
|
3009
|
-
databaseUrl: url,
|
|
3010
|
-
code: result.code,
|
|
3011
|
-
publicKey,
|
|
3012
|
-
scopes: ['ACCESS_DB'],
|
|
3013
|
-
});
|
|
3014
|
-
}
|
|
3015
|
-
catch (error) {
|
|
3016
|
-
if (error instanceof OAuthError) {
|
|
3017
|
-
// Show user-friendly error message
|
|
3018
|
-
yield alertUser(userInteraction, 'Authentication Failed', {
|
|
3019
|
-
type: 'error',
|
|
3020
|
-
messageCode: 'GENERIC_ERROR',
|
|
3021
|
-
message: error.userMessage,
|
|
3022
|
-
messageParams: {},
|
|
3023
|
-
}).catch(() => { });
|
|
3024
|
-
}
|
|
3025
|
-
throw error;
|
|
3026
|
-
}
|
|
2999
|
+
function initiateOAuthRedirect(db, provider) {
|
|
3000
|
+
var _a, _b;
|
|
3001
|
+
const url = (_a = db.cloud.options) === null || _a === void 0 ? void 0 : _a.databaseUrl;
|
|
3002
|
+
if (!url)
|
|
3003
|
+
throw new Error(`No database URL given.`);
|
|
3004
|
+
const redirectUri = ((_b = db.cloud.options) === null || _b === void 0 ? void 0 : _b.oauthRedirectUri) ||
|
|
3005
|
+
(typeof window !== 'undefined' ? window.location.href : undefined);
|
|
3006
|
+
// Start OAuth redirect flow - page navigates away
|
|
3007
|
+
startOAuthRedirect({
|
|
3008
|
+
databaseUrl: url,
|
|
3009
|
+
provider,
|
|
3010
|
+
redirectUri,
|
|
3027
3011
|
});
|
|
3028
3012
|
}
|
|
3029
3013
|
|
|
@@ -3113,7 +3097,15 @@
|
|
|
3113
3097
|
claims: {},
|
|
3114
3098
|
lastLogin: new Date(0),
|
|
3115
3099
|
});
|
|
3116
|
-
|
|
3100
|
+
try {
|
|
3101
|
+
yield authenticate(db.cloud.options.databaseUrl, context, db.cloud.options.fetchTokens || otpFetchTokenCallback(db), db.cloud.userInteraction, hints);
|
|
3102
|
+
}
|
|
3103
|
+
catch (err) {
|
|
3104
|
+
if (err.name === 'OAuthRedirectError') {
|
|
3105
|
+
return false; // Page is redirecting for OAuth login
|
|
3106
|
+
}
|
|
3107
|
+
throw err;
|
|
3108
|
+
}
|
|
3117
3109
|
if (origUserId !== UNAUTHORIZED_USER.userId &&
|
|
3118
3110
|
context.userId !== origUserId) {
|
|
3119
3111
|
// User was logged in before, but now logged in as another user.
|
|
@@ -17039,7 +17031,10 @@
|
|
|
17039
17031
|
ProviderButtonIcon: {
|
|
17040
17032
|
width: "20px",
|
|
17041
17033
|
height: "20px",
|
|
17042
|
-
flexShrink: 0
|
|
17034
|
+
flexShrink: 0,
|
|
17035
|
+
display: "flex",
|
|
17036
|
+
alignItems: "center",
|
|
17037
|
+
justifyContent: "center"
|
|
17043
17038
|
},
|
|
17044
17039
|
ProviderButtonText: {
|
|
17045
17040
|
flex: 1,
|
|
@@ -17104,14 +17099,7 @@
|
|
|
17104
17099
|
color: "#374151",
|
|
17105
17100
|
transition: "all 0.2s ease",
|
|
17106
17101
|
gap: "12px"
|
|
17107
|
-
}
|
|
17108
|
-
// Cancel button for provider selection
|
|
17109
|
-
CancelButtonRow: {
|
|
17110
|
-
display: "flex",
|
|
17111
|
-
justifyContent: "center",
|
|
17112
|
-
marginTop: "16px"
|
|
17113
|
-
}
|
|
17114
|
-
};
|
|
17102
|
+
}};
|
|
17115
17103
|
|
|
17116
17104
|
function Dialog({ children, className }) {
|
|
17117
17105
|
return (_$1("div", { className: `dexie-dialog ${className || ''}` },
|
|
@@ -17140,19 +17128,126 @@
|
|
|
17140
17128
|
return message.replace(/\{\w+\}/ig, n => messageParams[n.substring(1, n.length - 1)]);
|
|
17141
17129
|
}
|
|
17142
17130
|
|
|
17131
|
+
/** Get style based on styleHint (for provider branding, etc.) */
|
|
17132
|
+
function getOptionStyle(styleHint) {
|
|
17133
|
+
const baseStyle = Object.assign({}, Styles.ProviderButton);
|
|
17134
|
+
if (!styleHint) {
|
|
17135
|
+
return baseStyle;
|
|
17136
|
+
}
|
|
17137
|
+
switch (styleHint) {
|
|
17138
|
+
case 'google':
|
|
17139
|
+
return Object.assign(Object.assign({}, baseStyle), Styles.ProviderGoogle);
|
|
17140
|
+
case 'github':
|
|
17141
|
+
return Object.assign(Object.assign({}, baseStyle), Styles.ProviderGitHub);
|
|
17142
|
+
case 'microsoft':
|
|
17143
|
+
return Object.assign(Object.assign({}, baseStyle), Styles.ProviderMicrosoft);
|
|
17144
|
+
case 'apple':
|
|
17145
|
+
return Object.assign(Object.assign({}, baseStyle), Styles.ProviderApple);
|
|
17146
|
+
case 'otp':
|
|
17147
|
+
return Object.assign({}, Styles.OtpButton);
|
|
17148
|
+
case 'custom-oauth2':
|
|
17149
|
+
return Object.assign(Object.assign({}, baseStyle), Styles.ProviderCustom);
|
|
17150
|
+
default:
|
|
17151
|
+
return baseStyle;
|
|
17152
|
+
}
|
|
17153
|
+
}
|
|
17154
|
+
/**
|
|
17155
|
+
* Generic button component for selectable options.
|
|
17156
|
+
* Displays the option's icon and display name.
|
|
17157
|
+
*
|
|
17158
|
+
* The icon can be:
|
|
17159
|
+
* - Inline SVG (iconSvg) - rendered directly with dangerouslySetInnerHTML
|
|
17160
|
+
* - Image URL (iconUrl) - rendered as an img tag
|
|
17161
|
+
*
|
|
17162
|
+
* Style is determined by the styleHint property for branding purposes.
|
|
17163
|
+
*/
|
|
17164
|
+
function OptionButton({ option, onClick }) {
|
|
17165
|
+
const { displayName, iconUrl, iconSvg, styleHint, value } = option;
|
|
17166
|
+
const style = getOptionStyle(styleHint);
|
|
17167
|
+
// Get the text color from the button style for SVG fill processing
|
|
17168
|
+
const textColor = style.color || '#000000';
|
|
17169
|
+
// Process SVG to replace currentColor with actual text color
|
|
17170
|
+
const processedSvg = iconSvg
|
|
17171
|
+
? iconSvg
|
|
17172
|
+
.replace(/fill="currentColor"/gi, `fill="${textColor}"`)
|
|
17173
|
+
.replace(/fill='currentColor'/gi, `fill='${textColor}'`)
|
|
17174
|
+
.replace(/stroke="currentColor"/gi, `stroke="${textColor}"`)
|
|
17175
|
+
.replace(/stroke='currentColor'/gi, `stroke='${textColor}'`)
|
|
17176
|
+
: null;
|
|
17177
|
+
// Render the appropriate icon
|
|
17178
|
+
const renderIcon = () => {
|
|
17179
|
+
// Inline SVG
|
|
17180
|
+
if (processedSvg) {
|
|
17181
|
+
return (_$1("span", { style: Styles.ProviderButtonIcon, "aria-hidden": "true", dangerouslySetInnerHTML: { __html: processedSvg } }));
|
|
17182
|
+
}
|
|
17183
|
+
// Image URL
|
|
17184
|
+
if (iconUrl) {
|
|
17185
|
+
return (_$1("img", { src: iconUrl, alt: "", style: Styles.ProviderButtonIcon, "aria-hidden": "true" }));
|
|
17186
|
+
}
|
|
17187
|
+
return null;
|
|
17188
|
+
};
|
|
17189
|
+
return (_$1("button", { type: "button", style: style, onClick: onClick, class: `dxc-option-btn${styleHint ? ` dxc-option-${styleHint}` : ''}`, "aria-label": displayName },
|
|
17190
|
+
renderIcon(),
|
|
17191
|
+
_$1("span", { style: Styles.ProviderButtonText }, displayName)));
|
|
17192
|
+
}
|
|
17193
|
+
/**
|
|
17194
|
+
* Visual divider with "or" text.
|
|
17195
|
+
*/
|
|
17196
|
+
function Divider() {
|
|
17197
|
+
return (_$1("div", { style: Styles.Divider },
|
|
17198
|
+
_$1("div", { style: Styles.DividerLine }),
|
|
17199
|
+
_$1("span", { style: Styles.DividerText }, "or"),
|
|
17200
|
+
_$1("div", { style: Styles.DividerLine })));
|
|
17201
|
+
}
|
|
17202
|
+
|
|
17143
17203
|
const OTP_LENGTH = 8;
|
|
17144
|
-
|
|
17204
|
+
/**
|
|
17205
|
+
* Generic dialog that can render:
|
|
17206
|
+
* - Form fields (text inputs)
|
|
17207
|
+
* - Selectable options (buttons)
|
|
17208
|
+
* - Or both together
|
|
17209
|
+
*
|
|
17210
|
+
* When an option is clicked, calls onSubmit({ [option.name]: option.value }).
|
|
17211
|
+
* This unified approach means the same callback handles both form submission
|
|
17212
|
+
* and option selection.
|
|
17213
|
+
*/
|
|
17214
|
+
function LoginDialog({ title, alerts, fields, options, submitLabel, cancelLabel, onCancel, onSubmit, }) {
|
|
17145
17215
|
const [params, setParams] = d({});
|
|
17146
17216
|
const firstFieldRef = A(null);
|
|
17147
17217
|
_(() => { var _a; return (_a = firstFieldRef.current) === null || _a === void 0 ? void 0 : _a.focus(); }, []);
|
|
17218
|
+
const fieldEntries = Object.entries(fields || {});
|
|
17219
|
+
const hasFields = fieldEntries.length > 0;
|
|
17220
|
+
const hasOptions = options && options.length > 0;
|
|
17221
|
+
// Group options by name to detect if we have multiple groups
|
|
17222
|
+
const optionGroups = new Map();
|
|
17223
|
+
if (options) {
|
|
17224
|
+
for (const option of options) {
|
|
17225
|
+
const group = optionGroups.get(option.name) || [];
|
|
17226
|
+
group.push(option);
|
|
17227
|
+
optionGroups.set(option.name, group);
|
|
17228
|
+
}
|
|
17229
|
+
}
|
|
17230
|
+
const hasMultipleGroups = optionGroups.size > 1;
|
|
17231
|
+
// Handler for option clicks - calls onSubmit with { [option.name]: option.value }
|
|
17232
|
+
const handleOptionClick = (option) => {
|
|
17233
|
+
onSubmit({ [option.name]: option.value });
|
|
17234
|
+
};
|
|
17148
17235
|
return (_$1(Dialog, { className: "dxc-login-dlg" },
|
|
17149
17236
|
_$1(k$1, null,
|
|
17150
17237
|
_$1("h3", { style: Styles.WindowHeader }, title),
|
|
17151
|
-
alerts.map((alert) => (_$1("p", { style: Styles.Alert[alert.type] }, resolveText(alert)))),
|
|
17152
|
-
_$1("
|
|
17238
|
+
alerts.map((alert, idx) => (_$1("p", { key: idx, style: Styles.Alert[alert.type] }, resolveText(alert)))),
|
|
17239
|
+
hasOptions && (_$1("div", { class: "dxc-options" }, hasMultipleGroups ? (
|
|
17240
|
+
// Render with dividers between groups
|
|
17241
|
+
Array.from(optionGroups.entries()).map(([groupName, groupOptions], groupIdx) => (_$1(k$1, { key: groupName },
|
|
17242
|
+
groupIdx > 0 && _$1(Divider, null),
|
|
17243
|
+
groupOptions.map((option) => (_$1(OptionButton, { key: `${option.name}-${option.value}`, option: option, onClick: () => handleOptionClick(option) }))))))) : (
|
|
17244
|
+
// Simple case: all options in one group
|
|
17245
|
+
options.map((option) => (_$1(OptionButton, { key: `${option.name}-${option.value}`, option: option, onClick: () => handleOptionClick(option) })))))),
|
|
17246
|
+
hasOptions && hasFields && _$1(Divider, null),
|
|
17247
|
+
hasFields && (_$1("form", { onSubmit: (ev) => {
|
|
17153
17248
|
ev.preventDefault();
|
|
17154
17249
|
onSubmit(params);
|
|
17155
|
-
} },
|
|
17250
|
+
} }, fieldEntries.map(([fieldName, { type, label, placeholder }], idx) => (_$1("label", { style: Styles.Label, key: idx },
|
|
17156
17251
|
label ? `${label}: ` : '',
|
|
17157
17252
|
_$1("input", { ref: idx === 0 ? firstFieldRef : undefined, type: type, name: fieldName, autoComplete: "on", style: Styles.Input, autoFocus: true, placeholder: placeholder, value: params[fieldName] || '', onInput: (ev) => {
|
|
17158
17253
|
var _a;
|
|
@@ -17163,10 +17258,10 @@
|
|
|
17163
17258
|
// Auto-submit when OTP is filled in.
|
|
17164
17259
|
onSubmit(updatedParams);
|
|
17165
17260
|
}
|
|
17166
|
-
} })))))),
|
|
17261
|
+
} }))))))),
|
|
17167
17262
|
_$1("div", { style: Styles.ButtonsDiv },
|
|
17168
17263
|
_$1(k$1, null,
|
|
17169
|
-
_$1("button", { type: "submit", style: Styles.PrimaryButton, onClick: () => onSubmit(params) }, submitLabel),
|
|
17264
|
+
submitLabel && (hasFields || (!hasOptions && !hasFields)) && (_$1("button", { type: "submit", style: Styles.PrimaryButton, onClick: () => onSubmit(params) }, submitLabel)),
|
|
17170
17265
|
cancelLabel && (_$1("button", { style: Styles.Button, onClick: onCancel }, cancelLabel))))));
|
|
17171
17266
|
}
|
|
17172
17267
|
function valueTransformer(type, value) {
|
|
@@ -17180,82 +17275,6 @@
|
|
|
17180
17275
|
}
|
|
17181
17276
|
}
|
|
17182
17277
|
|
|
17183
|
-
/** Default SVG icons for built-in providers */
|
|
17184
|
-
const ProviderIcons = {
|
|
17185
|
-
google: `<svg viewBox="0 0 24 24" width="20" height="20"><path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/><path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/><path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/><path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/></svg>`,
|
|
17186
|
-
github: `<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z"/></svg>`,
|
|
17187
|
-
microsoft: `<svg viewBox="0 0 24 24" width="20" height="20"><rect fill="#F25022" x="1" y="1" width="10" height="10"/><rect fill="#00A4EF" x="1" y="13" width="10" height="10"/><rect fill="#7FBA00" x="13" y="1" width="10" height="10"/><rect fill="#FFB900" x="13" y="13" width="10" height="10"/></svg>`,
|
|
17188
|
-
apple: `<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.81-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z"/></svg>`,
|
|
17189
|
-
};
|
|
17190
|
-
/** Get provider-specific button styles */
|
|
17191
|
-
function getProviderStyle(providerType) {
|
|
17192
|
-
const baseStyle = Object.assign({}, Styles.ProviderButton);
|
|
17193
|
-
switch (providerType) {
|
|
17194
|
-
case 'google':
|
|
17195
|
-
return Object.assign(Object.assign({}, baseStyle), Styles.ProviderGoogle);
|
|
17196
|
-
case 'github':
|
|
17197
|
-
return Object.assign(Object.assign({}, baseStyle), Styles.ProviderGitHub);
|
|
17198
|
-
case 'microsoft':
|
|
17199
|
-
return Object.assign(Object.assign({}, baseStyle), Styles.ProviderMicrosoft);
|
|
17200
|
-
case 'apple':
|
|
17201
|
-
return Object.assign(Object.assign({}, baseStyle), Styles.ProviderApple);
|
|
17202
|
-
default:
|
|
17203
|
-
return Object.assign(Object.assign({}, baseStyle), Styles.ProviderCustom);
|
|
17204
|
-
}
|
|
17205
|
-
}
|
|
17206
|
-
/**
|
|
17207
|
-
* Button component for OAuth provider login.
|
|
17208
|
-
* Displays the provider's icon and name following provider branding guidelines.
|
|
17209
|
-
*/
|
|
17210
|
-
function AuthProviderButton({ provider, onClick }) {
|
|
17211
|
-
const { type, name, displayName, iconUrl } = provider;
|
|
17212
|
-
const style = getProviderStyle(type);
|
|
17213
|
-
// Determine button text
|
|
17214
|
-
const buttonText = `Continue with ${displayName}`;
|
|
17215
|
-
// Get icon - use custom iconUrl if provided, otherwise use built-in SVG
|
|
17216
|
-
const iconSvg = ProviderIcons[type] || '';
|
|
17217
|
-
return (_$1("button", { type: "button", style: style, onClick: onClick, class: `dxc-provider-btn dxc-provider-${type}`, "aria-label": buttonText },
|
|
17218
|
-
iconUrl ? (_$1("img", { src: iconUrl, alt: "", style: Styles.ProviderButtonIcon, "aria-hidden": "true" })) : iconSvg ? (_$1("span", { style: Styles.ProviderButtonIcon, "aria-hidden": "true", dangerouslySetInnerHTML: { __html: iconSvg } })) : null,
|
|
17219
|
-
_$1("span", { style: Styles.ProviderButtonText }, buttonText)));
|
|
17220
|
-
}
|
|
17221
|
-
/** Email/envelope icon for OTP button */
|
|
17222
|
-
const EmailIcon = `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="M22 6L12 13 2 6"/></svg>`;
|
|
17223
|
-
/**
|
|
17224
|
-
* Button for email/OTP authentication option.
|
|
17225
|
-
*/
|
|
17226
|
-
function OtpButton({ onClick }) {
|
|
17227
|
-
return (_$1("button", { type: "button", style: Styles.OtpButton, onClick: onClick, class: "dxc-otp-btn", "aria-label": "Continue with email" },
|
|
17228
|
-
_$1("span", { style: Styles.ProviderButtonIcon, "aria-hidden": "true", dangerouslySetInnerHTML: { __html: EmailIcon } }),
|
|
17229
|
-
_$1("span", { style: Styles.ProviderButtonText }, "Continue with email")));
|
|
17230
|
-
}
|
|
17231
|
-
/**
|
|
17232
|
-
* Visual divider with "or" text.
|
|
17233
|
-
*/
|
|
17234
|
-
function Divider() {
|
|
17235
|
-
return (_$1("div", { style: Styles.Divider },
|
|
17236
|
-
_$1("div", { style: Styles.DividerLine }),
|
|
17237
|
-
_$1("span", { style: Styles.DividerText }, "or"),
|
|
17238
|
-
_$1("div", { style: Styles.DividerLine })));
|
|
17239
|
-
}
|
|
17240
|
-
|
|
17241
|
-
/**
|
|
17242
|
-
* Dialog component for OAuth provider selection.
|
|
17243
|
-
* Displays available OAuth providers as buttons and optionally an email/OTP option.
|
|
17244
|
-
*/
|
|
17245
|
-
function ProviderSelectionDialog({ title, alerts, providers, otpEnabled, cancelLabel, onSelectProvider, onSelectOtp, onCancel, }) {
|
|
17246
|
-
return (_$1(Dialog, { className: "dxc-provider-selection-dlg" },
|
|
17247
|
-
_$1(k$1, null,
|
|
17248
|
-
_$1("h3", { style: Styles.WindowHeader }, title),
|
|
17249
|
-
alerts.map((alert, idx) => (_$1("p", { key: idx, style: Styles.Alert[alert.type] }, resolveText(alert)))),
|
|
17250
|
-
_$1("div", { class: "dxc-providers" }, providers.map((provider) => (_$1(AuthProviderButton, { key: provider.name, provider: provider, onClick: () => onSelectProvider(provider.name) })))),
|
|
17251
|
-
otpEnabled && providers.length > 0 && (_$1(k$1, null,
|
|
17252
|
-
_$1(Divider, null),
|
|
17253
|
-
_$1(OtpButton, { onClick: onSelectOtp }))),
|
|
17254
|
-
otpEnabled && providers.length === 0 && (_$1(OtpButton, { onClick: onSelectOtp })),
|
|
17255
|
-
cancelLabel && (_$1("div", { style: Styles.CancelButtonRow },
|
|
17256
|
-
_$1("button", { type: "button", style: Styles.Button, onClick: onCancel }, cancelLabel))))));
|
|
17257
|
-
}
|
|
17258
|
-
|
|
17259
17278
|
class LoginGui extends x {
|
|
17260
17279
|
constructor(props) {
|
|
17261
17280
|
super(props);
|
|
@@ -17274,11 +17293,8 @@
|
|
|
17274
17293
|
render(props, { userInteraction }) {
|
|
17275
17294
|
if (!userInteraction)
|
|
17276
17295
|
return null;
|
|
17277
|
-
//
|
|
17278
|
-
|
|
17279
|
-
return _$1(ProviderSelectionDialog, Object.assign({}, userInteraction));
|
|
17280
|
-
}
|
|
17281
|
-
// Default to LoginDialog for other interaction types
|
|
17296
|
+
// LoginDialog handles all interaction types uniformly
|
|
17297
|
+
// (forms with fields, options, or both)
|
|
17282
17298
|
return _$1(LoginDialog, Object.assign({}, userInteraction));
|
|
17283
17299
|
}
|
|
17284
17300
|
}
|
|
@@ -17837,6 +17853,83 @@
|
|
|
17837
17853
|
return awareness;
|
|
17838
17854
|
}
|
|
17839
17855
|
|
|
17856
|
+
/**
|
|
17857
|
+
* Decodes a base64url-encoded string to a regular string.
|
|
17858
|
+
* Base64url uses - instead of + and _ instead of /, and may omit padding.
|
|
17859
|
+
*/
|
|
17860
|
+
function decodeBase64Url(encoded) {
|
|
17861
|
+
// Add padding if needed
|
|
17862
|
+
const padded = encoded + '='.repeat((4 - (encoded.length % 4)) % 4);
|
|
17863
|
+
// Convert base64url to base64
|
|
17864
|
+
const base64 = padded.replace(/-/g, '+').replace(/_/g, '/');
|
|
17865
|
+
return atob(base64);
|
|
17866
|
+
}
|
|
17867
|
+
/**
|
|
17868
|
+
* Parses OAuth callback parameters from the dxc-auth query parameter.
|
|
17869
|
+
*
|
|
17870
|
+
* The dxc-auth parameter contains base64url-encoded JSON with the following structure:
|
|
17871
|
+
* - On success: { "code": "...", "provider": "...", "state": "..." }
|
|
17872
|
+
* - On error: { "error": "...", "provider": "...", "state": "..." }
|
|
17873
|
+
*
|
|
17874
|
+
* @param url - The URL to parse (defaults to window.location.href)
|
|
17875
|
+
* @returns OAuthCallbackParams if valid callback, null otherwise
|
|
17876
|
+
* @throws OAuthError if there's an error in the callback
|
|
17877
|
+
*/
|
|
17878
|
+
function parseOAuthCallback(url) {
|
|
17879
|
+
const targetUrl = (typeof window !== 'undefined' ? window.location.href : '');
|
|
17880
|
+
if (!targetUrl) {
|
|
17881
|
+
return null;
|
|
17882
|
+
}
|
|
17883
|
+
const parsed = new URL(targetUrl);
|
|
17884
|
+
const encoded = parsed.searchParams.get('dxc-auth');
|
|
17885
|
+
if (!encoded) {
|
|
17886
|
+
return null; // Not an OAuth callback URL
|
|
17887
|
+
}
|
|
17888
|
+
let payload;
|
|
17889
|
+
try {
|
|
17890
|
+
const json = decodeBase64Url(encoded);
|
|
17891
|
+
payload = JSON.parse(json);
|
|
17892
|
+
}
|
|
17893
|
+
catch (e) {
|
|
17894
|
+
console.warn('[dexie-cloud] Failed to parse dxc-auth parameter:', e);
|
|
17895
|
+
return null;
|
|
17896
|
+
}
|
|
17897
|
+
const { code, provider, state, error } = payload;
|
|
17898
|
+
// Check for error first
|
|
17899
|
+
if (error) {
|
|
17900
|
+
if (error.toLowerCase().includes('access_denied') || error.toLowerCase().includes('access denied')) {
|
|
17901
|
+
throw new OAuthError('access_denied', provider, error);
|
|
17902
|
+
}
|
|
17903
|
+
if (error.toLowerCase().includes('email') && error.toLowerCase().includes('verif')) {
|
|
17904
|
+
throw new OAuthError('email_not_verified', provider, error);
|
|
17905
|
+
}
|
|
17906
|
+
throw new OAuthError('provider_error', provider, error);
|
|
17907
|
+
}
|
|
17908
|
+
// Validate required fields for success case
|
|
17909
|
+
if (!code || !provider || !state) {
|
|
17910
|
+
console.warn('[dexie-cloud] Invalid dxc-auth payload: missing required fields');
|
|
17911
|
+
return null;
|
|
17912
|
+
}
|
|
17913
|
+
return { code, provider, state };
|
|
17914
|
+
}
|
|
17915
|
+
/**
|
|
17916
|
+
* Cleans up the dxc-auth query parameter from the URL.
|
|
17917
|
+
* Call this after successfully handling the callback to clean up the browser URL.
|
|
17918
|
+
*/
|
|
17919
|
+
function cleanupOAuthUrl() {
|
|
17920
|
+
var _a;
|
|
17921
|
+
if (typeof window === 'undefined' || !((_a = window.history) === null || _a === void 0 ? void 0 : _a.replaceState)) {
|
|
17922
|
+
return;
|
|
17923
|
+
}
|
|
17924
|
+
const url = new URL(window.location.href);
|
|
17925
|
+
if (!url.searchParams.has('dxc-auth')) {
|
|
17926
|
+
return;
|
|
17927
|
+
}
|
|
17928
|
+
url.searchParams.delete('dxc-auth');
|
|
17929
|
+
const cleanUrl = url.pathname + (url.searchParams.toString() ? `?${url.searchParams.toString()}` : '') + url.hash;
|
|
17930
|
+
window.history.replaceState(null, '', cleanUrl);
|
|
17931
|
+
}
|
|
17932
|
+
|
|
17840
17933
|
function getTiedRealmId(objectId) {
|
|
17841
17934
|
return 'rlm~' + objectId;
|
|
17842
17935
|
}
|
|
@@ -18019,6 +18112,10 @@
|
|
|
18019
18112
|
const currentUserEmitter = getCurrentUserEmitter(dexie);
|
|
18020
18113
|
const subscriptions = [];
|
|
18021
18114
|
let configuredProgramatically = false;
|
|
18115
|
+
// Pending OAuth auth code from dxc-auth redirect (detected in configure())
|
|
18116
|
+
let pendingOAuthCode = null;
|
|
18117
|
+
// Pending OAuth error from dxc-auth redirect (detected in configure())
|
|
18118
|
+
let pendingOAuthError = null;
|
|
18022
18119
|
// local sync worker - used when there's no service worker.
|
|
18023
18120
|
let localSyncWorker = null;
|
|
18024
18121
|
dexie.on('ready', (dexie) => __awaiter(this, void 0, void 0, function* () {
|
|
@@ -18048,7 +18145,7 @@
|
|
|
18048
18145
|
const syncComplete = new rxjs.Subject();
|
|
18049
18146
|
dexie.cloud = {
|
|
18050
18147
|
// @ts-ignore
|
|
18051
|
-
version: "4.3.
|
|
18148
|
+
version: "4.3.3",
|
|
18052
18149
|
options: Object.assign({}, DEFAULT_OPTIONS),
|
|
18053
18150
|
schema: null,
|
|
18054
18151
|
get currentUserId() {
|
|
@@ -18083,6 +18180,31 @@
|
|
|
18083
18180
|
DexieCloudDB(dexie).reconfigure(); // Update observable from new dexie.name
|
|
18084
18181
|
}
|
|
18085
18182
|
updateSchemaFromOptions(dexie.cloud.schema, dexie.cloud.options);
|
|
18183
|
+
// Check for OAuth callback (dxc-auth query parameter)
|
|
18184
|
+
// Only check in DOM environment, not workers
|
|
18185
|
+
if (typeof window !== 'undefined' && window.location) {
|
|
18186
|
+
try {
|
|
18187
|
+
const callback = parseOAuthCallback();
|
|
18188
|
+
if (callback) {
|
|
18189
|
+
// Clean up URL immediately (remove dxc-auth param)
|
|
18190
|
+
cleanupOAuthUrl();
|
|
18191
|
+
// Store the pending auth code for processing when db is ready
|
|
18192
|
+
pendingOAuthCode = { code: callback.code, provider: callback.provider };
|
|
18193
|
+
console.debug('[dexie-cloud] OAuth callback detected, auth code stored for processing');
|
|
18194
|
+
}
|
|
18195
|
+
}
|
|
18196
|
+
catch (error) {
|
|
18197
|
+
// parseOAuthCallback throws OAuthError on error callbacks
|
|
18198
|
+
cleanupOAuthUrl();
|
|
18199
|
+
if (error instanceof OAuthError) {
|
|
18200
|
+
pendingOAuthError = error;
|
|
18201
|
+
console.error('[dexie-cloud] OAuth callback error:', error.message);
|
|
18202
|
+
}
|
|
18203
|
+
else {
|
|
18204
|
+
console.error('[dexie-cloud] OAuth callback error:', error);
|
|
18205
|
+
}
|
|
18206
|
+
}
|
|
18207
|
+
}
|
|
18086
18208
|
},
|
|
18087
18209
|
logout() {
|
|
18088
18210
|
return __awaiter(this, arguments, void 0, function* ({ force } = {}) {
|
|
@@ -18276,7 +18398,34 @@
|
|
|
18276
18398
|
}
|
|
18277
18399
|
// HERE: If requireAuth, do athentication now.
|
|
18278
18400
|
let changedUser = false;
|
|
18279
|
-
|
|
18401
|
+
let user = yield db.getCurrentUser();
|
|
18402
|
+
// Show pending OAuth error if present (from dxc-auth redirect)
|
|
18403
|
+
if (pendingOAuthError && !db.cloud.isServiceWorkerDB) {
|
|
18404
|
+
const error = pendingOAuthError;
|
|
18405
|
+
pendingOAuthError = null; // Clear pending error
|
|
18406
|
+
console.debug('[dexie-cloud] Showing OAuth error:', error.message);
|
|
18407
|
+
// Show alert to user about the OAuth error
|
|
18408
|
+
yield alertUser(db.cloud.userInteraction, 'Authentication Error', {
|
|
18409
|
+
type: 'error',
|
|
18410
|
+
messageCode: 'GENERIC_ERROR',
|
|
18411
|
+
message: error.message,
|
|
18412
|
+
messageParams: { provider: error.provider || 'unknown' }
|
|
18413
|
+
});
|
|
18414
|
+
}
|
|
18415
|
+
// Process pending OAuth callback if present (from dxc-auth redirect)
|
|
18416
|
+
if (pendingOAuthCode && !db.cloud.isServiceWorkerDB) {
|
|
18417
|
+
const { code, provider } = pendingOAuthCode;
|
|
18418
|
+
pendingOAuthCode = null; // Clear pending code
|
|
18419
|
+
console.debug('[dexie-cloud] Processing OAuth callback, provider:', provider);
|
|
18420
|
+
try {
|
|
18421
|
+
changedUser = yield login(db, { oauthCode: code, provider });
|
|
18422
|
+
user = yield db.getCurrentUser();
|
|
18423
|
+
}
|
|
18424
|
+
catch (error) {
|
|
18425
|
+
console.error('[dexie-cloud] OAuth login failed:', error);
|
|
18426
|
+
// Continue with normal flow - user can try again
|
|
18427
|
+
}
|
|
18428
|
+
}
|
|
18280
18429
|
const requireAuth = (_b = db.cloud.options) === null || _b === void 0 ? void 0 : _b.requireAuth;
|
|
18281
18430
|
if (requireAuth) {
|
|
18282
18431
|
if (db.cloud.isServiceWorkerDB) {
|
|
@@ -18365,7 +18514,7 @@
|
|
|
18365
18514
|
}
|
|
18366
18515
|
}
|
|
18367
18516
|
// @ts-ignore
|
|
18368
|
-
dexieCloud.version = "4.3.
|
|
18517
|
+
dexieCloud.version = "4.3.3";
|
|
18369
18518
|
Dexie.Cloud = dexieCloud;
|
|
18370
18519
|
|
|
18371
18520
|
exports.default = dexieCloud;
|