dexie-cloud-addon 4.3.5 → 4.3.7

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.
@@ -9,7 +9,7 @@ This feature adds support for OAuth 2.0 social login providers (Google, GitHub,
9
9
  ### Related Files
10
10
 
11
11
  - **Detailed flow diagram**: [oauth_flow.md](oauth_flow.md) - Sequence diagrams and detailed protocol description
12
- - **Server implementation**: `/Users/daw/repos/dexie-cloud/libs/dexie-cloud-server`
12
+ - **Server implementation**: See `dexie-cloud-server` repository
13
13
  - `src/api/oauth/registerOAuthEndpoints.ts` - OAuth endpoints
14
14
  - `src/api/oauth/oauth-helpers.ts` - Provider exchange logic
15
15
  - `src/api/registerTokenEndpoint.ts` - Token endpoint (authorization_code grant)
@@ -21,6 +21,8 @@ export interface LoginHints {
21
21
  provider?: string;
22
22
  /** Dexie Cloud authorization code received from OAuth callback */
23
23
  oauthCode?: string;
24
+ /** Optional redirect path (relative or absolute) to use for OAuth redirect URI. */
25
+ redirectPath?: string;
24
26
  }
25
27
  export interface DexieCloudAPI {
26
28
  version: string;
@@ -26,10 +26,6 @@ export declare function parseOAuthCallback(url?: string): OAuthCallbackParams |
26
26
  * @returns true if valid, false otherwise
27
27
  */
28
28
  export declare function validateOAuthState(receivedState: string): boolean;
29
- /**
30
- * Gets the OAuth provider from sessionStorage (for redirect flows).
31
- */
32
- export declare function getStoredOAuthProvider(): string | null;
33
29
  /**
34
30
  * Cleans up the dxc-auth query parameter from the URL.
35
31
  * Call this after successfully handling the callback to clean up the browser URL.
@@ -8,10 +8,6 @@ export interface OptionButtonProps {
8
8
  * Generic button component for selectable options.
9
9
  * Displays the option's icon and display name.
10
10
  *
11
- * The icon can be:
12
- * - Inline SVG (iconSvg) - rendered directly with dangerouslySetInnerHTML
13
- * - Image URL (iconUrl) - rendered as an img tag
14
- *
15
11
  * Style is determined by the styleHint property for branding purposes.
16
12
  */
17
13
  export declare function OptionButton({ option, onClick }: OptionButtonProps): h.JSX.Element;
@@ -8,7 +8,7 @@
8
8
  *
9
9
  * ==========================================================================
10
10
  *
11
- * Version 4.3.5, Fri Jan 23 2026
11
+ * Version 4.3.7, Wed Jan 28 2026
12
12
  *
13
13
  * https://dexie.org
14
14
  *
@@ -761,73 +761,19 @@ class TokenErrorResponseError extends Error {
761
761
  }
762
762
  }
763
763
 
764
- /** Cache for fetched SVG content to avoid re-fetching */
765
- const svgCache = {};
766
- /** Default SVG icons for built-in OAuth providers */
767
- const ProviderIcons = {
768
- google: `<svg viewBox="0 0 24 24" width="20" height="20"><path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/><path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/><path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/><path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/></svg>`,
769
- github: `<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z"/></svg>`,
770
- microsoft: `<svg viewBox="0 0 24 24" width="20" height="20"><rect fill="#F25022" x="1" y="1" width="10" height="10"/><rect fill="#00A4EF" x="1" y="13" width="10" height="10"/><rect fill="#7FBA00" x="13" y="1" width="10" height="10"/><rect fill="#FFB900" x="13" y="13" width="10" height="10"/></svg>`,
771
- apple: `<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.81-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z"/></svg>`,
772
- };
773
- /** Email/envelope icon for OTP option */
774
- const EmailIcon = `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="M22 6L12 13 2 6"/></svg>`;
775
- /**
776
- * Fetches SVG content from a URL and caches it.
777
- * Returns the SVG string or null if fetch fails.
778
- */
779
- function fetchSvgIcon(url) {
780
- return __awaiter(this, void 0, void 0, function* () {
781
- if (svgCache[url]) {
782
- return svgCache[url];
783
- }
784
- try {
785
- const res = yield fetch(url);
786
- if (res.ok) {
787
- const svg = yield res.text();
788
- // Validate it looks like SVG
789
- if (svg.includes('<svg')) {
790
- svgCache[url] = svg;
791
- return svg;
792
- }
793
- }
794
- }
795
- catch (_a) {
796
- // Silently fail - will show no icon
797
- }
798
- return null;
799
- });
800
- }
764
+ /** Email/envelope icon data URL for OTP option */
765
+ const EmailIcon = `data:image/svg+xml;base64,${btoa('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="#666666" stroke-width="2"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="M22 6L12 13 2 6"/></svg>')}`;
801
766
  /**
802
767
  * Converts an OAuthProviderInfo to a generic DXCOption.
803
- * Fetches SVG icons from URLs if needed.
804
768
  */
805
769
  function providerToOption(provider) {
806
- return __awaiter(this, void 0, void 0, function* () {
807
- var _a;
808
- let iconSvg;
809
- // First check for built-in icons
810
- if (ProviderIcons[provider.type]) {
811
- iconSvg = ProviderIcons[provider.type];
812
- }
813
- // If provider has iconUrl pointing to SVG, fetch and inline it
814
- else if ((_a = provider.iconUrl) === null || _a === void 0 ? void 0 : _a.toLowerCase().endsWith('.svg')) {
815
- const fetched = yield fetchSvgIcon(provider.iconUrl);
816
- if (fetched) {
817
- iconSvg = fetched;
818
- }
819
- }
820
- return {
821
- name: 'provider',
822
- value: provider.name,
823
- displayName: `Continue with ${provider.displayName}`,
824
- iconSvg,
825
- // If iconUrl is not SVG, pass it through for img tag rendering
826
- iconUrl: (!iconSvg && provider.iconUrl) ? provider.iconUrl : undefined,
827
- // Use provider type as style hint for branding
828
- styleHint: provider.type,
829
- };
830
- });
770
+ return {
771
+ name: 'provider',
772
+ value: provider.name,
773
+ displayName: `Continue with ${provider.displayName}`,
774
+ iconUrl: provider.iconUrl,
775
+ styleHint: provider.type,
776
+ };
831
777
  }
832
778
  function interactWithUser(userInteraction, req) {
833
779
  return new Promise((resolve, reject) => {
@@ -978,8 +924,8 @@ function confirmLogout(userInteraction, currentUserId, numUnsyncedChanges) {
978
924
  */
979
925
  function promptForProvider(userInteraction_1, providers_1, otpEnabled_1) {
980
926
  return __awaiter(this, arguments, void 0, function* (userInteraction, providers, otpEnabled, title = 'Choose login method', alerts = []) {
981
- // Convert providers to generic options (with icon fetching)
982
- const providerOptions = yield Promise.all(providers.map(providerToOption));
927
+ // Convert providers to generic options
928
+ const providerOptions = providers.map(providerToOption);
983
929
  // Build the options array
984
930
  const options = [...providerOptions];
985
931
  // Add OTP option if enabled
@@ -988,7 +934,7 @@ function promptForProvider(userInteraction_1, providers_1, otpEnabled_1) {
988
934
  name: 'otp',
989
935
  value: 'email',
990
936
  displayName: 'Continue with email',
991
- iconSvg: EmailIcon,
937
+ iconUrl: EmailIcon,
992
938
  styleHint: 'otp',
993
939
  });
994
940
  }
@@ -1416,10 +1362,12 @@ function exchangeOAuthCode(options) {
1416
1362
  mode: 'cors',
1417
1363
  });
1418
1364
  if (!res.ok) {
1365
+ // Read body once as text to avoid stream consumption issues
1366
+ const bodyText = yield res.text().catch(() => res.statusText);
1419
1367
  if (res.status === 400 || res.status === 401) {
1420
- // Try to parse error response
1368
+ // Try to parse error response as JSON
1421
1369
  try {
1422
- const errorResponse = yield res.json();
1370
+ const errorResponse = JSON.parse(bodyText);
1423
1371
  if (errorResponse.type === 'error') {
1424
1372
  // Check for specific error codes
1425
1373
  if (errorResponse.messageCode === 'INVALID_OTP') {
@@ -1436,8 +1384,7 @@ function exchangeOAuthCode(options) {
1436
1384
  // Fall through to generic error
1437
1385
  }
1438
1386
  }
1439
- const errorText = yield res.text().catch(() => res.statusText);
1440
- throw new OAuthError('provider_error', undefined, `Token exchange failed: ${res.status} ${errorText}`);
1387
+ throw new OAuthError('provider_error', undefined, `Token exchange failed: ${res.status} ${bodyText}`);
1441
1388
  }
1442
1389
  const response = yield res.json();
1443
1390
  if (response.type === 'error') {
@@ -1543,9 +1490,8 @@ function buildOAuthLoginUrl(options) {
1543
1490
  * ```
1544
1491
  */
1545
1492
  function startOAuthRedirect(options) {
1546
- // Store provider in sessionStorage for reference on callback
1547
- if (typeof sessionStorage !== 'undefined') {
1548
- sessionStorage.setItem('dexie-cloud-oauth-provider', options.provider);
1493
+ if (typeof window === 'undefined') {
1494
+ throw new Error('OAuth redirect requires a browser environment');
1549
1495
  }
1550
1496
  const loginUrl = buildOAuthLoginUrl(options);
1551
1497
  window.location.href = loginUrl;
@@ -1571,7 +1517,21 @@ function otpFetchTokenCallback(db) {
1571
1517
  }
1572
1518
  // Handle OAuth provider login via redirect
1573
1519
  if (hints === null || hints === void 0 ? void 0 : hints.provider) {
1574
- initiateOAuthRedirect(db, hints.provider);
1520
+ let resolvedRedirectUri = undefined;
1521
+ if (hints.redirectPath) {
1522
+ // If redirectPath is absolute, use as is. If relative, resolve against current location
1523
+ if (/^https?:\/\//i.test(hints.redirectPath)) {
1524
+ resolvedRedirectUri = hints.redirectPath;
1525
+ }
1526
+ else if (typeof window !== 'undefined' && window.location) {
1527
+ // Use URL constructor to resolve relative path
1528
+ resolvedRedirectUri = new URL(hints.redirectPath, window.location.href).toString();
1529
+ }
1530
+ else if (typeof location !== 'undefined' && location.href) {
1531
+ resolvedRedirectUri = new URL(hints.redirectPath, location.href).toString();
1532
+ }
1533
+ }
1534
+ initiateOAuthRedirect(db, hints.provider, resolvedRedirectUri);
1575
1535
  // This function never returns - page navigates away
1576
1536
  throw new OAuthRedirectError(hints.provider);
1577
1537
  }
@@ -1651,7 +1611,8 @@ function otpFetchTokenCallback(db) {
1651
1611
  const res1 = yield fetch(`${url}/token`, {
1652
1612
  body: JSON.stringify(tokenRequest),
1653
1613
  method: 'post',
1654
- headers: { 'Content-Type': 'application/json', mode: 'cors' },
1614
+ headers: { 'Content-Type': 'application/json' },
1615
+ mode: 'cors',
1655
1616
  });
1656
1617
  if (res1.status !== 200) {
1657
1618
  const errMsg = yield res1.text();
@@ -1715,13 +1676,18 @@ function otpFetchTokenCallback(db) {
1715
1676
  * the user is redirected back with a dxc-auth query parameter that is
1716
1677
  * automatically detected by db.cloud.configure().
1717
1678
  */
1718
- function initiateOAuthRedirect(db, provider) {
1679
+ function initiateOAuthRedirect(db, provider, redirectUriOverride) {
1719
1680
  var _a, _b;
1720
1681
  const url = (_a = db.cloud.options) === null || _a === void 0 ? void 0 : _a.databaseUrl;
1721
1682
  if (!url)
1722
1683
  throw new Error(`No database URL given.`);
1723
- const redirectUri = ((_b = db.cloud.options) === null || _b === void 0 ? void 0 : _b.oauthRedirectUri) ||
1724
- (typeof window !== 'undefined' ? window.location.href : undefined);
1684
+ const redirectUri = redirectUriOverride ||
1685
+ ((_b = db.cloud.options) === null || _b === void 0 ? void 0 : _b.oauthRedirectUri) ||
1686
+ (typeof location !== 'undefined' ? location.href : undefined);
1687
+ // CodeRabbit suggested to fail fast here, but the only situation where
1688
+ // redirectUri would be undefined is in non-browser environments, and
1689
+ // in those environments OAuth redirect does not make sense anyway
1690
+ // and will fail fast in startOAuthRedirect().
1725
1691
  // Start OAuth redirect flow - page navigates away
1726
1692
  startOAuthRedirect({
1727
1693
  databaseUrl: url,
@@ -5836,13 +5802,13 @@ const Styles = {
5836
5802
  color: "#3c4043"
5837
5803
  },
5838
5804
  ProviderGitHub: {
5839
- backgroundColor: "#24292e",
5840
- border: "1px solid #24292e",
5841
- color: "#ffffff"
5805
+ backgroundColor: "#ffffff",
5806
+ border: "1px solid #dadce0",
5807
+ color: "#181717"
5842
5808
  },
5843
5809
  ProviderMicrosoft: {
5844
5810
  backgroundColor: "#ffffff",
5845
- border: "1px solid #8c8c8c",
5811
+ border: "1px solid #dadce0",
5846
5812
  color: "#5e5e5e"
5847
5813
  },
5848
5814
  ProviderApple: {
@@ -5851,9 +5817,9 @@ const Styles = {
5851
5817
  color: "#ffffff"
5852
5818
  },
5853
5819
  ProviderCustom: {
5854
- backgroundColor: "#4f46e5",
5855
- border: "1px solid #4f46e5",
5856
- color: "#ffffff"
5820
+ backgroundColor: "#ffffff",
5821
+ border: "1px solid #dadce0",
5822
+ color: "#181717"
5857
5823
  },
5858
5824
  // Divider styles
5859
5825
  Divider: {
@@ -5944,39 +5910,13 @@ function getOptionStyle(styleHint) {
5944
5910
  * Generic button component for selectable options.
5945
5911
  * Displays the option's icon and display name.
5946
5912
  *
5947
- * The icon can be:
5948
- * - Inline SVG (iconSvg) - rendered directly with dangerouslySetInnerHTML
5949
- * - Image URL (iconUrl) - rendered as an img tag
5950
- *
5951
5913
  * Style is determined by the styleHint property for branding purposes.
5952
5914
  */
5953
5915
  function OptionButton({ option, onClick }) {
5954
- const { displayName, iconUrl, iconSvg, styleHint, value } = option;
5916
+ const { displayName, iconUrl, styleHint } = option;
5955
5917
  const style = getOptionStyle(styleHint);
5956
- // Get the text color from the button style for SVG fill processing
5957
- const textColor = style.color || '#000000';
5958
- // Process SVG to replace currentColor with actual text color
5959
- const processedSvg = iconSvg
5960
- ? iconSvg
5961
- .replace(/fill="currentColor"/gi, `fill="${textColor}"`)
5962
- .replace(/fill='currentColor'/gi, `fill='${textColor}'`)
5963
- .replace(/stroke="currentColor"/gi, `stroke="${textColor}"`)
5964
- .replace(/stroke='currentColor'/gi, `stroke='${textColor}'`)
5965
- : null;
5966
- // Render the appropriate icon
5967
- const renderIcon = () => {
5968
- // Inline SVG
5969
- if (processedSvg) {
5970
- return (_$1("span", { style: Styles.ProviderButtonIcon, "aria-hidden": "true", dangerouslySetInnerHTML: { __html: processedSvg } }));
5971
- }
5972
- // Image URL
5973
- if (iconUrl) {
5974
- return (_$1("img", { src: iconUrl, alt: "", style: Styles.ProviderButtonIcon, "aria-hidden": "true" }));
5975
- }
5976
- return null;
5977
- };
5978
5918
  return (_$1("button", { type: "button", style: style, onClick: onClick, class: `dxc-option-btn${styleHint ? ` dxc-option-${styleHint}` : ''}`, "aria-label": displayName },
5979
- renderIcon(),
5919
+ iconUrl && (_$1("img", { src: iconUrl, alt: "", style: Styles.ProviderButtonIcon, "aria-hidden": "true" })),
5980
5920
  _$1("span", { style: Styles.ProviderButtonText }, displayName)));
5981
5921
  }
5982
5922
  /**
@@ -6934,7 +6874,7 @@ function dexieCloud(dexie) {
6934
6874
  const syncComplete = new Subject();
6935
6875
  dexie.cloud = {
6936
6876
  // @ts-ignore
6937
- version: "4.3.5",
6877
+ version: "4.3.7",
6938
6878
  options: Object.assign({}, DEFAULT_OPTIONS),
6939
6879
  schema: null,
6940
6880
  get currentUserId() {
@@ -7204,12 +7144,18 @@ function dexieCloud(dexie) {
7204
7144
  pendingOAuthError = null; // Clear pending error
7205
7145
  console.debug('[dexie-cloud] Showing OAuth error:', error.message);
7206
7146
  // Show alert to user about the OAuth error
7207
- yield alertUser(db.cloud.userInteraction, 'Authentication Error', {
7208
- type: 'error',
7209
- messageCode: 'GENERIC_ERROR',
7210
- message: error.message,
7211
- messageParams: { provider: error.provider || 'unknown' }
7212
- });
7147
+ // Guard so UI errors don't abort initialization
7148
+ try {
7149
+ yield alertUser(db.cloud.userInteraction, 'Authentication Error', {
7150
+ type: 'error',
7151
+ messageCode: 'GENERIC_ERROR',
7152
+ message: error.message,
7153
+ messageParams: { provider: error.provider || 'unknown' }
7154
+ });
7155
+ }
7156
+ catch (uiError) {
7157
+ console.error('[dexie-cloud] Failed to show OAuth error alert:', uiError);
7158
+ }
7213
7159
  }
7214
7160
  // Process pending OAuth callback if present (from dxc-auth redirect)
7215
7161
  if (pendingOAuthCode && !db.cloud.isServiceWorkerDB) {
@@ -7313,7 +7259,7 @@ function dexieCloud(dexie) {
7313
7259
  }
7314
7260
  }
7315
7261
  // @ts-ignore
7316
- dexieCloud.version = "4.3.5";
7262
+ dexieCloud.version = "4.3.7";
7317
7263
  Dexie.Cloud = dexieCloud;
7318
7264
 
7319
7265
  export { dexieCloud as default, defineYDocTrigger, dexieCloud, getTiedObjectId, getTiedRealmId, resolveText };