dexie-cloud-addon 4.2.5 → 4.3.0

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.
Files changed (168) hide show
  1. package/TODO-SOCIALAUTH.md +545 -0
  2. package/dist/modern/DexieCloudAPI.d.ts +4 -0
  3. package/dist/modern/DexieCloudOptions.d.ts +20 -0
  4. package/dist/modern/authentication/exchangeOAuthCode.d.ts +23 -0
  5. package/dist/modern/authentication/fetchAuthProviders.d.ts +14 -0
  6. package/dist/modern/authentication/handleOAuthCallback.d.ts +57 -0
  7. package/dist/modern/authentication/interactWithUser.d.ts +19 -0
  8. package/dist/modern/authentication/oauthLogin.d.ts +37 -0
  9. package/dist/modern/default-ui/AuthProviderButton.d.ts +21 -0
  10. package/dist/modern/default-ui/LoginDialog.d.ts +5 -2
  11. package/dist/modern/default-ui/ProviderSelectionDialog.d.ts +7 -0
  12. package/dist/modern/dexie-cloud-addon.js +577 -5
  13. package/dist/modern/dexie-cloud-addon.js.map +1 -1
  14. package/dist/modern/dexie-cloud-addon.min.js +1 -1
  15. package/dist/modern/dexie-cloud-addon.min.js.gz +0 -0
  16. package/dist/modern/dexie-cloud-addon.min.js.map +1 -1
  17. package/dist/modern/errors/OAuthError.d.ts +10 -0
  18. package/dist/modern/service-worker.js +577 -5
  19. package/dist/modern/service-worker.js.map +1 -1
  20. package/dist/modern/service-worker.min.js +1 -1
  21. package/dist/modern/service-worker.min.js.map +1 -1
  22. package/dist/modern/types/DXCUserInteraction.d.ts +24 -1
  23. package/dist/umd/DISABLE_SERVICEWORKER_STRATEGY.d.ts +1 -0
  24. package/dist/umd/DXCWebSocketStatus.d.ts +1 -0
  25. package/dist/umd/DexieCloudAPI.d.ts +75 -0
  26. package/dist/umd/DexieCloudOptions.d.ts +27 -0
  27. package/dist/umd/DexieCloudSyncOptions.d.ts +4 -0
  28. package/dist/umd/DexieCloudTable.d.ts +18 -0
  29. package/dist/umd/InvalidLicenseError.d.ts +5 -0
  30. package/dist/umd/Invite.d.ts +8 -0
  31. package/dist/umd/PermissionChecker.d.ts +15 -0
  32. package/dist/umd/TSON.d.ts +17 -0
  33. package/dist/umd/WSObservable.d.ts +72 -0
  34. package/dist/umd/associate.d.ts +1 -0
  35. package/dist/umd/authentication/AuthPersistedContext.d.ts +9 -0
  36. package/dist/umd/authentication/TokenErrorResponseError.d.ts +10 -0
  37. package/dist/umd/authentication/TokenExpiredError.d.ts +3 -0
  38. package/dist/umd/authentication/UNAUTHORIZED_USER.d.ts +2 -0
  39. package/dist/umd/authentication/authenticate.d.ts +13 -0
  40. package/dist/umd/authentication/currentUserObservable.d.ts +1 -0
  41. package/dist/umd/authentication/interactWithUser.d.ts +21 -0
  42. package/dist/umd/authentication/login.d.ts +3 -0
  43. package/dist/umd/authentication/logout.d.ts +5 -0
  44. package/dist/umd/authentication/otpFetchTokenCallback.d.ts +3 -0
  45. package/dist/umd/authentication/setCurrentUser.d.ts +14 -0
  46. package/dist/umd/authentication/waitUntil.d.ts +3 -0
  47. package/dist/umd/computeSyncState.d.ts +4 -0
  48. package/dist/umd/createSharedValueObservable.d.ts +3 -0
  49. package/dist/umd/currentUserEmitter.d.ts +3 -0
  50. package/dist/umd/db/DexieCloudDB.d.ts +61 -0
  51. package/dist/umd/db/entities/BaseRevisionMapEntry.d.ts +5 -0
  52. package/dist/umd/db/entities/EntityCommon.d.ts +5 -0
  53. package/dist/umd/db/entities/GuardedJob.d.ts +5 -0
  54. package/dist/umd/db/entities/Member.d.ts +19 -0
  55. package/dist/umd/db/entities/PersistedSyncState.d.ts +22 -0
  56. package/dist/umd/db/entities/Realm.d.ts +14 -0
  57. package/dist/umd/db/entities/Role.d.ts +11 -0
  58. package/dist/umd/db/entities/UserLogin.d.ts +23 -0
  59. package/dist/umd/default-ui/Dialog.d.ts +5 -0
  60. package/dist/umd/default-ui/LoginDialog.d.ts +3 -0
  61. package/dist/umd/default-ui/Styles.d.ts +3 -0
  62. package/dist/umd/default-ui/index.d.ts +24 -0
  63. package/dist/umd/define-ydoc-trigger.d.ts +3 -0
  64. package/dist/umd/dexie-cloud-addon.d.ts +3 -0
  65. package/dist/umd/dexie-cloud-addon.js +578 -6
  66. package/dist/umd/dexie-cloud-addon.js.gz +0 -0
  67. package/dist/umd/dexie-cloud-addon.js.map +1 -1
  68. package/dist/umd/dexie-cloud-addon.min.js +1 -1
  69. package/dist/umd/dexie-cloud-addon.min.js.gz +0 -0
  70. package/dist/umd/dexie-cloud-addon.min.js.map +1 -1
  71. package/dist/umd/dexie-cloud-client.d.ts +23 -0
  72. package/dist/umd/errors/HttpError.d.ts +5 -0
  73. package/dist/umd/extend-dexie-interface.d.ts +23 -0
  74. package/dist/umd/getGlobalRolesObservable.d.ts +5 -0
  75. package/dist/umd/getInternalAccessControlObservable.d.ts +12 -0
  76. package/dist/umd/getInvitesObservable.d.ts +23 -0
  77. package/dist/umd/getPermissionsLookupObservable.d.ts +16 -0
  78. package/dist/umd/getTiedRealmId.d.ts +2 -0
  79. package/dist/umd/helpers/BroadcastedAndLocalEvent.d.ts +8 -0
  80. package/dist/umd/helpers/CancelToken.d.ts +4 -0
  81. package/dist/umd/helpers/IS_SERVICE_WORKER.d.ts +1 -0
  82. package/dist/umd/helpers/SWBroadcastChannel.d.ts +12 -0
  83. package/dist/umd/helpers/allSettled.d.ts +1 -0
  84. package/dist/umd/helpers/bulkUpdate.d.ts +4 -0
  85. package/dist/umd/helpers/computeRealmSetHash.d.ts +2 -0
  86. package/dist/umd/helpers/date-constants.d.ts +5 -0
  87. package/dist/umd/helpers/flatten.d.ts +1 -0
  88. package/dist/umd/helpers/getMutationTable.d.ts +1 -0
  89. package/dist/umd/helpers/getSyncableTables.d.ts +4 -0
  90. package/dist/umd/helpers/getTableFromMutationTable.d.ts +1 -0
  91. package/dist/umd/helpers/makeArray.d.ts +1 -0
  92. package/dist/umd/helpers/randomString.d.ts +1 -0
  93. package/dist/umd/helpers/resolveText.d.ts +16 -0
  94. package/dist/umd/helpers/throwVersionIncrementNeeded.d.ts +1 -0
  95. package/dist/umd/helpers/visibilityState.d.ts +1 -0
  96. package/dist/umd/isEagerSyncDisabled.d.ts +2 -0
  97. package/dist/umd/isFirefox.d.ts +1 -0
  98. package/dist/umd/isSafari.d.ts +2 -0
  99. package/dist/umd/mapValueObservable.d.ts +5 -0
  100. package/dist/umd/mergePermissions.d.ts +2 -0
  101. package/dist/umd/middleware-helpers/guardedTable.d.ts +11 -0
  102. package/dist/umd/middleware-helpers/idGenerationHelpers.d.ts +18 -0
  103. package/dist/umd/middlewares/createIdGenerationMiddleware.d.ts +3 -0
  104. package/dist/umd/middlewares/createImplicitPropSetterMiddleware.d.ts +3 -0
  105. package/dist/umd/middlewares/createMutationTrackingMiddleware.d.ts +17 -0
  106. package/dist/umd/middlewares/outstandingTransaction.d.ts +4 -0
  107. package/dist/umd/overrideParseStoresSpec.d.ts +4 -0
  108. package/dist/umd/performInitialSync.d.ts +4 -0
  109. package/dist/umd/permissions.d.ts +9 -0
  110. package/dist/umd/prodLog.d.ts +9 -0
  111. package/dist/umd/service-worker.d.ts +1 -0
  112. package/dist/umd/service-worker.js +578 -6
  113. package/dist/umd/service-worker.js.map +1 -1
  114. package/dist/umd/service-worker.min.js +1 -1
  115. package/dist/umd/service-worker.min.js.map +1 -1
  116. package/dist/umd/sync/DEXIE_CLOUD_SYNCER_ID.d.ts +1 -0
  117. package/dist/umd/sync/LocalSyncWorker.d.ts +7 -0
  118. package/dist/umd/sync/SyncRequiredError.d.ts +3 -0
  119. package/dist/umd/sync/applyServerChanges.d.ts +3 -0
  120. package/dist/umd/sync/connectWebSocket.d.ts +2 -0
  121. package/dist/umd/sync/encodeIdsForServer.d.ts +4 -0
  122. package/dist/umd/sync/extractRealm.d.ts +2 -0
  123. package/dist/umd/sync/getLatestRevisionsPerTable.d.ts +6 -0
  124. package/dist/umd/sync/getTablesToSyncify.d.ts +3 -0
  125. package/dist/umd/sync/isOnline.d.ts +1 -0
  126. package/dist/umd/sync/isSyncNeeded.d.ts +2 -0
  127. package/dist/umd/sync/listClientChanges.d.ts +9 -0
  128. package/dist/umd/sync/listSyncifiedChanges.d.ts +5 -0
  129. package/dist/umd/sync/messageConsumerIsReady.d.ts +2 -0
  130. package/dist/umd/sync/messagesFromServerQueue.d.ts +8 -0
  131. package/dist/umd/sync/modifyLocalObjectsWithNewUserId.d.ts +4 -0
  132. package/dist/umd/sync/myId.d.ts +1 -0
  133. package/dist/umd/sync/numUnsyncedMutations.d.ts +2 -0
  134. package/dist/umd/sync/old_startSyncingClientChanges.d.ts +39 -0
  135. package/dist/umd/sync/performGuardedJob.d.ts +2 -0
  136. package/dist/umd/sync/ratelimit.d.ts +3 -0
  137. package/dist/umd/sync/registerSyncEvent.d.ts +3 -0
  138. package/dist/umd/sync/sync.d.ts +15 -0
  139. package/dist/umd/sync/syncIfPossible.d.ts +5 -0
  140. package/dist/umd/sync/syncWithServer.d.ts +6 -0
  141. package/dist/umd/sync/triggerSync.d.ts +2 -0
  142. package/dist/umd/sync/updateBaseRevs.d.ts +5 -0
  143. package/dist/umd/types/DXCAlert.d.ts +25 -0
  144. package/dist/umd/types/DXCInputField.d.ts +11 -0
  145. package/dist/umd/types/DXCUserInteraction.d.ts +93 -0
  146. package/dist/umd/types/NewIdOptions.d.ts +3 -0
  147. package/dist/umd/types/SWMessageEvent.d.ts +3 -0
  148. package/dist/umd/types/SWSyncEvent.d.ts +4 -0
  149. package/dist/umd/types/SyncState.d.ts +9 -0
  150. package/dist/umd/types/TXExpandos.d.ts +11 -0
  151. package/dist/umd/updateSchemaFromOptions.d.ts +3 -0
  152. package/dist/umd/userIsActive.d.ts +7 -0
  153. package/dist/umd/verifyConfig.d.ts +2 -0
  154. package/dist/umd/verifySchema.d.ts +2 -0
  155. package/dist/umd/yjs/YDexieCloudSyncState.d.ts +3 -0
  156. package/dist/umd/yjs/YTable.d.ts +3 -0
  157. package/dist/umd/yjs/applyYMessages.d.ts +9 -0
  158. package/dist/umd/yjs/awareness.d.ts +3 -0
  159. package/dist/umd/yjs/createYClientUpdateObservable.d.ts +4 -0
  160. package/dist/umd/yjs/createYHandler.d.ts +2 -0
  161. package/dist/umd/yjs/downloadYDocsFromServer.d.ts +3 -0
  162. package/dist/umd/yjs/getUpdatesTable.d.ts +3 -0
  163. package/dist/umd/yjs/listUpdatesSince.d.ts +3 -0
  164. package/dist/umd/yjs/listYClientMessagesAndStateVector.d.ts +26 -0
  165. package/dist/umd/yjs/reopenDocSignal.d.ts +10 -0
  166. package/dist/umd/yjs/updateYSyncStates.d.ts +6 -0
  167. package/oauth_flow.md +299 -0
  168. package/package.json +3 -3
@@ -0,0 +1,10 @@
1
+ /** OAuth-specific error codes */
2
+ export type OAuthErrorCode = 'popup_blocked' | 'popup_closed' | 'access_denied' | 'invalid_state' | 'email_not_verified' | 'expired_code' | 'provider_error' | 'network_error';
3
+ /** Error class for OAuth-specific errors */
4
+ export declare class OAuthError extends Error {
5
+ readonly code: OAuthErrorCode;
6
+ readonly provider?: string;
7
+ constructor(code: OAuthErrorCode, provider?: string, customMessage?: string);
8
+ /** Get user-friendly message for this error */
9
+ get userMessage(): string;
10
+ }
@@ -8,7 +8,7 @@
8
8
  *
9
9
  * ==========================================================================
10
10
  *
11
- * Version 4.2.5, Sat Dec 20 2025
11
+ * Version 4.3.0, Tue Jan 20 2026
12
12
  *
13
13
  * https://dexie.org
14
14
  *
@@ -545,6 +545,15 @@ 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
+
548
557
  function assert(b) {
549
558
  if (!b)
550
559
  throw new Error('Assertion Failed');
@@ -1279,6 +1288,43 @@ function confirmLogout(userInteraction, currentUserId, numUnsyncedChanges) {
1279
1288
  .catch(() => false);
1280
1289
  });
1281
1290
  }
1291
+ /**
1292
+ * Prompts the user to select an authentication method (OAuth provider or OTP).
1293
+ *
1294
+ * @param userInteraction - The user interaction BehaviorSubject
1295
+ * @param providers - Available OAuth providers
1296
+ * @param otpEnabled - Whether OTP is available
1297
+ * @param title - Dialog title
1298
+ * @param alerts - Optional alerts to display
1299
+ * @returns Promise resolving to the user's selection
1300
+ */
1301
+ function promptForProvider(userInteraction, providers, otpEnabled, title = 'Choose login method', alerts = []) {
1302
+ return new Promise((resolve, reject) => {
1303
+ const interactionProps = {
1304
+ type: 'provider-selection',
1305
+ title,
1306
+ alerts,
1307
+ providers,
1308
+ otpEnabled,
1309
+ fields: {},
1310
+ submitLabel: undefined,
1311
+ cancelLabel: 'Cancel',
1312
+ onSelectProvider: (providerName) => {
1313
+ userInteraction.next(undefined);
1314
+ resolve({ type: 'provider', provider: providerName });
1315
+ },
1316
+ onSelectOtp: () => {
1317
+ userInteraction.next(undefined);
1318
+ resolve({ type: 'otp' });
1319
+ },
1320
+ onCancel: () => {
1321
+ userInteraction.next(undefined);
1322
+ reject(new Dexie.AbortError('User cancelled'));
1323
+ },
1324
+ };
1325
+ userInteraction.next(interactionProps);
1326
+ });
1327
+ }
1282
1328
 
1283
1329
  function loadAccessToken(db) {
1284
1330
  return __awaiter(this, void 0, void 0, function* () {
@@ -3542,15 +3588,313 @@ function _logout(db_1) {
3542
3588
  });
3543
3589
  }
3544
3590
 
3591
+ /** User-friendly messages for OAuth error codes */
3592
+ 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
+ access_denied: 'Access was denied by the authentication provider.',
3596
+ invalid_state: 'The authentication response could not be verified. Please try again.',
3597
+ email_not_verified: 'Your email address must be verified before you can log in.',
3598
+ expired_code: 'The authentication code has expired. Please try again.',
3599
+ provider_error: 'An error occurred with the authentication provider.',
3600
+ network_error: 'A network error occurred during authentication. Please check your connection and try again.',
3601
+ };
3602
+ /** Error class for OAuth-specific errors */
3603
+ class OAuthError extends Error {
3604
+ constructor(code, provider, customMessage) {
3605
+ super(customMessage || ERROR_MESSAGES[code]);
3606
+ this.name = 'OAuthError';
3607
+ this.code = code;
3608
+ this.provider = provider;
3609
+ }
3610
+ /** Get user-friendly message for this error */
3611
+ get userMessage() {
3612
+ return ERROR_MESSAGES[this.code] || this.message;
3613
+ }
3614
+ }
3615
+
3616
+ /**
3617
+ * Exchanges a Dexie Cloud authorization code for access and refresh tokens.
3618
+ *
3619
+ * This is called after the OAuth callback delivers the authorization code
3620
+ * via postMessage (popup flow) or redirect.
3621
+ *
3622
+ * @param options - Exchange options
3623
+ * @returns Promise resolving to TokenFinalResponse
3624
+ * @throws OAuthError or TokenErrorResponseError on failure
3625
+ */
3626
+ function exchangeOAuthCode(options) {
3627
+ return __awaiter(this, void 0, void 0, function* () {
3628
+ const { databaseUrl, code, publicKey, scopes = ['ACCESS_DB'] } = options;
3629
+ const tokenRequest = {
3630
+ grant_type: 'authorization_code',
3631
+ code,
3632
+ public_key: publicKey,
3633
+ scopes,
3634
+ };
3635
+ try {
3636
+ const res = yield fetch(`${databaseUrl}/token`, {
3637
+ method: 'POST',
3638
+ headers: { 'Content-Type': 'application/json' },
3639
+ body: JSON.stringify(tokenRequest),
3640
+ mode: 'cors',
3641
+ });
3642
+ if (!res.ok) {
3643
+ if (res.status === 400 || res.status === 401) {
3644
+ // Try to parse error response
3645
+ try {
3646
+ const errorResponse = yield res.json();
3647
+ if (errorResponse.type === 'error') {
3648
+ // Check for specific error codes
3649
+ if (errorResponse.messageCode === 'INVALID_OTP') {
3650
+ // In the context of OAuth, this likely means expired code
3651
+ throw new OAuthError('expired_code', undefined, errorResponse.message);
3652
+ }
3653
+ throw new TokenErrorResponseError(errorResponse);
3654
+ }
3655
+ }
3656
+ catch (e) {
3657
+ if (e instanceof OAuthError || e instanceof TokenErrorResponseError) {
3658
+ throw e;
3659
+ }
3660
+ // Fall through to generic error
3661
+ }
3662
+ }
3663
+ const errorText = yield res.text().catch(() => res.statusText);
3664
+ throw new OAuthError('provider_error', undefined, `Token exchange failed: ${res.status} ${errorText}`);
3665
+ }
3666
+ const response = yield res.json();
3667
+ if (response.type === 'error') {
3668
+ throw new TokenErrorResponseError(response);
3669
+ }
3670
+ if (response.type !== 'tokens') {
3671
+ throw new OAuthError('provider_error', undefined, `Unexpected response type: ${response.type}`);
3672
+ }
3673
+ return response;
3674
+ }
3675
+ catch (error) {
3676
+ if (error instanceof OAuthError || error instanceof TokenErrorResponseError) {
3677
+ throw error;
3678
+ }
3679
+ if (error instanceof TypeError) {
3680
+ // Network error
3681
+ throw new OAuthError('network_error');
3682
+ }
3683
+ throw error;
3684
+ }
3685
+ });
3686
+ }
3687
+
3688
+ /** Default response when OAuth is disabled or unavailable */
3689
+ const OTP_ONLY_RESPONSE = {
3690
+ providers: [],
3691
+ otpEnabled: true,
3692
+ };
3693
+ /**
3694
+ * Fetches available authentication providers from the Dexie Cloud server.
3695
+ *
3696
+ * @param databaseUrl - The Dexie Cloud database URL
3697
+ * @param socialAuthEnabled - Whether social auth is enabled in client config (default: true)
3698
+ * @returns Promise resolving to AuthProvidersResponse
3699
+ *
3700
+ * Handles failures gracefully:
3701
+ * - 404 → Returns OTP-only (old server version)
3702
+ * - Network error → Returns OTP-only
3703
+ * - socialAuthEnabled: false → Returns OTP-only without fetching
3704
+ */
3705
+ function fetchAuthProviders(databaseUrl_1) {
3706
+ return __awaiter(this, arguments, void 0, function* (databaseUrl, socialAuthEnabled = true) {
3707
+ // If social auth is disabled, return OTP-only without fetching
3708
+ if (!socialAuthEnabled) {
3709
+ return OTP_ONLY_RESPONSE;
3710
+ }
3711
+ try {
3712
+ const res = yield fetch(`${databaseUrl}/auth-providers`, {
3713
+ method: 'GET',
3714
+ headers: { 'Accept': 'application/json' },
3715
+ mode: 'cors',
3716
+ });
3717
+ if (res.status === 404) {
3718
+ // Old server version without OAuth support
3719
+ console.debug('[dexie-cloud] Server does not support /auth-providers endpoint. Using OTP-only authentication.');
3720
+ return OTP_ONLY_RESPONSE;
3721
+ }
3722
+ if (!res.ok) {
3723
+ console.warn(`[dexie-cloud] Failed to fetch auth providers: ${res.status} ${res.statusText}`);
3724
+ return OTP_ONLY_RESPONSE;
3725
+ }
3726
+ return yield res.json();
3727
+ }
3728
+ catch (error) {
3729
+ // Network error or other failure - fall back to OTP
3730
+ console.debug('[dexie-cloud] Could not fetch auth providers, falling back to OTP:', error);
3731
+ return OTP_ONLY_RESPONSE;
3732
+ }
3733
+ });
3734
+ }
3735
+
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
+ /** Build the OAuth login URL */
3743
+ function buildOAuthLoginUrl(options, state) {
3744
+ const url = new URL(`${options.databaseUrl}/oauth/login/${options.provider}`);
3745
+ url.searchParams.set('state', state);
3746
+ // Set the redirect URI for postMessage or custom scheme
3747
+ const redirectUri = options.redirectUri ||
3748
+ (typeof window !== 'undefined' ? window.location.origin : '');
3749
+ if (redirectUri) {
3750
+ url.searchParams.set('redirect_uri', redirectUri);
3751
+ }
3752
+ return url.toString();
3753
+ }
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
+ /**
3766
+ * Initiates OAuth login flow using a popup window.
3767
+ *
3768
+ * Opens a popup to the OAuth provider, listens for postMessage with the result,
3769
+ * and returns the Dexie Cloud authorization code.
3770
+ *
3771
+ * @param options - OAuth login options
3772
+ * @returns Promise resolving to OAuthLoginResult
3773
+ * @throws OAuthError on failure
3774
+ */
3775
+ function oauthLogin(options) {
3776
+ return __awaiter(this, void 0, void 0, function* () {
3777
+ const { databaseUrl, provider, usePopup = true } = options;
3778
+ if (!usePopup) {
3779
+ // For redirect flows, we can't return a promise - the page will navigate away
3780
+ throw new Error('Non-popup OAuth flow requires handleOAuthCallback after redirect');
3781
+ }
3782
+ const state = generateState();
3783
+ const loginUrl = buildOAuthLoginUrl(options, state);
3784
+ // Calculate popup dimensions and position
3785
+ const width = 500;
3786
+ const height = 600;
3787
+ const { left, top } = getPopupPosition(width, height);
3788
+ // Open popup window
3789
+ const popup = window.open(loginUrl, 'dexie-cloud-oauth', `width=${width},height=${height},left=${left},top=${top},menubar=no,toolbar=no,location=yes,status=no`);
3790
+ if (!popup) {
3791
+ throw new OAuthError('popup_blocked', provider);
3792
+ }
3793
+ return new Promise((resolve, reject) => {
3794
+ let resolved = false;
3795
+ // Listen for postMessage from the popup
3796
+ const handleMessage = (event) => {
3797
+ // Validate origin - must be from the Dexie Cloud server
3798
+ const expectedOrigin = new URL(databaseUrl).origin;
3799
+ if (event.origin !== expectedOrigin) {
3800
+ return; // Ignore messages from other origins
3801
+ }
3802
+ // Check if this is our OAuth result message
3803
+ if (!isOAuthResultMessage(event.data)) {
3804
+ return;
3805
+ }
3806
+ const message = event.data;
3807
+ // Validate state to prevent CSRF
3808
+ if (message.state !== state) {
3809
+ console.warn('[dexie-cloud] OAuth state mismatch, ignoring message');
3810
+ return;
3811
+ }
3812
+ // Clean up
3813
+ cleanup();
3814
+ resolved = true;
3815
+ // Handle error from OAuth flow
3816
+ if (message.error) {
3817
+ const errorCode = mapOAuthError(message.error);
3818
+ reject(new OAuthError(errorCode, provider, message.error));
3819
+ return;
3820
+ }
3821
+ // Success - return the authorization code
3822
+ if (message.code) {
3823
+ resolve({
3824
+ code: message.code,
3825
+ provider: message.provider,
3826
+ state: message.state,
3827
+ });
3828
+ }
3829
+ else {
3830
+ reject(new OAuthError('provider_error', provider, 'No authorization code received'));
3831
+ }
3832
+ };
3833
+ // Check if popup was closed without completing
3834
+ const checkPopupClosed = setInterval(() => {
3835
+ if (popup.closed && !resolved) {
3836
+ cleanup();
3837
+ reject(new OAuthError('popup_closed', provider));
3838
+ }
3839
+ }, 500);
3840
+ // Cleanup function
3841
+ const cleanup = () => {
3842
+ window.removeEventListener('message', handleMessage);
3843
+ clearInterval(checkPopupClosed);
3844
+ try {
3845
+ if (!popup.closed) {
3846
+ popup.close();
3847
+ }
3848
+ }
3849
+ catch (_a) {
3850
+ // Ignore errors when closing popup
3851
+ }
3852
+ };
3853
+ // Start listening for messages
3854
+ window.addEventListener('message', handleMessage);
3855
+ });
3856
+ });
3857
+ }
3858
+ /** Map OAuth error strings to error codes */
3859
+ function mapOAuthError(error) {
3860
+ const lowerError = error.toLowerCase();
3861
+ if (lowerError.includes('access_denied') || lowerError.includes('access denied')) {
3862
+ return 'access_denied';
3863
+ }
3864
+ if (lowerError.includes('email') && lowerError.includes('verif')) {
3865
+ return 'email_not_verified';
3866
+ }
3867
+ if (lowerError.includes('expired')) {
3868
+ return 'expired_code';
3869
+ }
3870
+ if (lowerError.includes('state')) {
3871
+ return 'invalid_state';
3872
+ }
3873
+ return 'provider_error';
3874
+ }
3875
+
3545
3876
  function otpFetchTokenCallback(db) {
3546
3877
  const { userInteraction } = db.cloud;
3547
3878
  return function otpAuthenticate(_a) {
3548
3879
  return __awaiter(this, arguments, void 0, function* ({ public_key, hints }) {
3549
- var _b;
3880
+ var _b, _c;
3550
3881
  let tokenRequest;
3551
3882
  const url = (_b = db.cloud.options) === null || _b === void 0 ? void 0 : _b.databaseUrl;
3552
3883
  if (!url)
3553
3884
  throw new Error(`No database URL given.`);
3885
+ // Handle OAuth code exchange (from redirect/deep link flows)
3886
+ if ((hints === null || hints === void 0 ? void 0 : hints.oauthCode) && hints.provider) {
3887
+ return yield exchangeOAuthCode({
3888
+ databaseUrl: url,
3889
+ code: hints.oauthCode,
3890
+ publicKey: public_key,
3891
+ scopes: ['ACCESS_DB'],
3892
+ });
3893
+ }
3894
+ // Handle OAuth provider login (popup flow)
3895
+ if (hints === null || hints === void 0 ? void 0 : hints.provider) {
3896
+ return yield handleOAuthFlow(db, public_key, hints.provider);
3897
+ }
3554
3898
  if ((hints === null || hints === void 0 ? void 0 : hints.grant_type) === 'demo') {
3555
3899
  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));
3556
3900
  tokenRequest = {
@@ -3574,6 +3918,18 @@ function otpFetchTokenCallback(db) {
3574
3918
  };
3575
3919
  }
3576
3920
  else {
3921
+ // Check for available auth providers (OAuth + OTP)
3922
+ const socialAuthEnabled = ((_c = db.cloud.options) === null || _c === void 0 ? void 0 : _c.socialAuth) !== false;
3923
+ const authProviders = yield fetchAuthProviders(url, socialAuthEnabled);
3924
+ // If we have OAuth providers available, prompt for selection
3925
+ if (authProviders.providers.length > 0) {
3926
+ const selection = yield promptForProvider(userInteraction, authProviders.providers, authProviders.otpEnabled, 'Sign in');
3927
+ if (selection.type === 'provider') {
3928
+ // User selected an OAuth provider
3929
+ return yield handleOAuthFlow(db, public_key, selection.provider);
3930
+ }
3931
+ // User chose OTP - continue with email prompt below
3932
+ }
3577
3933
  const email = yield promptForEmail(userInteraction, 'Enter email address', hints === null || hints === void 0 ? void 0 : hints.email);
3578
3934
  if (/@demo.local$/.test(email)) {
3579
3935
  tokenRequest = {
@@ -3651,6 +4007,49 @@ function otpFetchTokenCallback(db) {
3651
4007
  });
3652
4008
  };
3653
4009
  }
4010
+ /**
4011
+ * Handles the OAuth popup flow and token exchange.
4012
+ */
4013
+ function handleOAuthFlow(db, publicKey, provider) {
4014
+ return __awaiter(this, void 0, void 0, function* () {
4015
+ var _a, _b, _c;
4016
+ const url = (_a = db.cloud.options) === null || _a === void 0 ? void 0 : _a.databaseUrl;
4017
+ if (!url)
4018
+ throw new Error(`No database URL given.`);
4019
+ const { userInteraction } = db.cloud;
4020
+ const usePopup = ((_b = db.cloud.options) === null || _b === void 0 ? void 0 : _b.oauthPopup) !== false;
4021
+ const redirectUri = ((_c = db.cloud.options) === null || _c === void 0 ? void 0 : _c.oauthRedirectUri) ||
4022
+ (typeof window !== 'undefined' ? window.location.origin : undefined);
4023
+ try {
4024
+ // Start OAuth popup flow
4025
+ const result = yield oauthLogin({
4026
+ databaseUrl: url,
4027
+ provider,
4028
+ redirectUri,
4029
+ usePopup,
4030
+ });
4031
+ // Exchange the auth code for tokens
4032
+ return yield exchangeOAuthCode({
4033
+ databaseUrl: url,
4034
+ code: result.code,
4035
+ publicKey,
4036
+ scopes: ['ACCESS_DB'],
4037
+ });
4038
+ }
4039
+ catch (error) {
4040
+ if (error instanceof OAuthError) {
4041
+ // Show user-friendly error message
4042
+ yield alertUser(userInteraction, 'Authentication Failed', {
4043
+ type: 'error',
4044
+ messageCode: 'GENERIC_ERROR',
4045
+ message: error.userMessage,
4046
+ messageParams: {},
4047
+ }).catch(() => { });
4048
+ }
4049
+ throw error;
4050
+ }
4051
+ });
4052
+ }
3654
4053
 
3655
4054
  /** A way to log to console in production without terser stripping out
3656
4055
  * it from the release bundle.
@@ -5388,6 +5787,99 @@ const Styles = {
5388
5787
  color: "#333",
5389
5788
  borderBottom: "1px solid #eee",
5390
5789
  paddingBottom: "10px"
5790
+ },
5791
+ // OAuth Provider Button Styles
5792
+ ProviderButton: {
5793
+ display: "flex",
5794
+ alignItems: "center",
5795
+ justifyContent: "center",
5796
+ width: "100%",
5797
+ padding: "12px 16px",
5798
+ marginBottom: "10px",
5799
+ border: "1px solid #d1d5db",
5800
+ borderRadius: "6px",
5801
+ backgroundColor: "#ffffff",
5802
+ cursor: "pointer",
5803
+ fontSize: "14px",
5804
+ fontWeight: "500",
5805
+ color: "#374151",
5806
+ transition: "all 0.2s ease",
5807
+ gap: "12px"
5808
+ },
5809
+ ProviderButtonIcon: {
5810
+ width: "20px",
5811
+ height: "20px",
5812
+ flexShrink: 0
5813
+ },
5814
+ ProviderButtonText: {
5815
+ flex: 1,
5816
+ textAlign: "left"
5817
+ },
5818
+ // Provider-specific colors
5819
+ ProviderGoogle: {
5820
+ backgroundColor: "#ffffff",
5821
+ border: "1px solid #dadce0",
5822
+ color: "#3c4043"
5823
+ },
5824
+ ProviderGitHub: {
5825
+ backgroundColor: "#24292e",
5826
+ border: "1px solid #24292e",
5827
+ color: "#ffffff"
5828
+ },
5829
+ ProviderMicrosoft: {
5830
+ backgroundColor: "#ffffff",
5831
+ border: "1px solid #8c8c8c",
5832
+ color: "#5e5e5e"
5833
+ },
5834
+ ProviderApple: {
5835
+ backgroundColor: "#000000",
5836
+ border: "1px solid #000000",
5837
+ color: "#ffffff"
5838
+ },
5839
+ ProviderCustom: {
5840
+ backgroundColor: "#4f46e5",
5841
+ border: "1px solid #4f46e5",
5842
+ color: "#ffffff"
5843
+ },
5844
+ // Divider styles
5845
+ Divider: {
5846
+ display: "flex",
5847
+ alignItems: "center",
5848
+ margin: "20px 0",
5849
+ color: "#6b7280",
5850
+ fontSize: "13px"
5851
+ },
5852
+ DividerLine: {
5853
+ flex: 1,
5854
+ height: "1px",
5855
+ backgroundColor: "#e5e7eb"
5856
+ },
5857
+ DividerText: {
5858
+ padding: "0 12px",
5859
+ color: "#9ca3af"
5860
+ },
5861
+ // OTP Button (Continue with email)
5862
+ OtpButton: {
5863
+ display: "flex",
5864
+ alignItems: "center",
5865
+ justifyContent: "center",
5866
+ width: "100%",
5867
+ padding: "12px 16px",
5868
+ border: "1px solid #d1d5db",
5869
+ borderRadius: "6px",
5870
+ backgroundColor: "#f9fafb",
5871
+ cursor: "pointer",
5872
+ fontSize: "14px",
5873
+ fontWeight: "500",
5874
+ color: "#374151",
5875
+ transition: "all 0.2s ease",
5876
+ gap: "12px"
5877
+ },
5878
+ // Cancel button for provider selection
5879
+ CancelButtonRow: {
5880
+ display: "flex",
5881
+ justifyContent: "center",
5882
+ marginTop: "16px"
5391
5883
  }
5392
5884
  };
5393
5885
 
@@ -5458,6 +5950,82 @@ function valueTransformer(type, value) {
5458
5950
  }
5459
5951
  }
5460
5952
 
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
+
5461
6029
  class LoginGui extends x {
5462
6030
  constructor(props) {
5463
6031
  super(props);
@@ -5476,7 +6044,11 @@ class LoginGui extends x {
5476
6044
  render(props, { userInteraction }) {
5477
6045
  if (!userInteraction)
5478
6046
  return null;
5479
- //if (props.db.cloud.userInteraction.observers.length > 1) return null; // Someone else subscribes.
6047
+ // Render appropriate dialog based on interaction type
6048
+ if (userInteraction.type === 'provider-selection') {
6049
+ return _$1(ProviderSelectionDialog, Object.assign({}, userInteraction));
6050
+ }
6051
+ // Default to LoginDialog for other interaction types
5480
6052
  return _$1(LoginDialog, Object.assign({}, userInteraction));
5481
6053
  }
5482
6054
  }
@@ -6075,7 +6647,7 @@ function dexieCloud(dexie) {
6075
6647
  const syncComplete = new Subject();
6076
6648
  dexie.cloud = {
6077
6649
  // @ts-ignore
6078
- version: "4.2.5",
6650
+ version: "4.3.0",
6079
6651
  options: Object.assign({}, DEFAULT_OPTIONS),
6080
6652
  schema: null,
6081
6653
  get currentUserId() {
@@ -6392,7 +6964,7 @@ function dexieCloud(dexie) {
6392
6964
  }
6393
6965
  }
6394
6966
  // @ts-ignore
6395
- dexieCloud.version = "4.2.5";
6967
+ dexieCloud.version = "4.3.0";
6396
6968
  Dexie.Cloud = dexieCloud;
6397
6969
 
6398
6970
  // In case the SW lives for a while, let it reuse already opened connections: