dexie-cloud-addon 4.2.5 → 4.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. package/TODO-SOCIALAUTH.md +274 -0
  2. package/dist/modern/DexieCloudAPI.d.ts +4 -0
  3. package/dist/modern/DexieCloudOptions.d.ts +17 -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 +63 -0
  7. package/dist/modern/authentication/interactWithUser.d.ts +22 -0
  8. package/dist/modern/authentication/oauthLogin.d.ts +38 -0
  9. package/dist/modern/default-ui/AuthProviderButton.d.ts +21 -0
  10. package/dist/modern/default-ui/LoginDialog.d.ts +30 -2
  11. package/dist/modern/default-ui/OptionButton.d.ts +21 -0
  12. package/dist/modern/default-ui/ProviderSelectionDialog.d.ts +7 -0
  13. package/dist/modern/default-ui/SelectDialog.d.ts +10 -0
  14. package/dist/modern/dexie-cloud-addon.js +686 -13
  15. package/dist/modern/dexie-cloud-addon.js.map +1 -1
  16. package/dist/modern/dexie-cloud-addon.min.js +1 -1
  17. package/dist/modern/dexie-cloud-addon.min.js.gz +0 -0
  18. package/dist/modern/dexie-cloud-addon.min.js.map +1 -1
  19. package/dist/modern/errors/OAuthError.d.ts +10 -0
  20. package/dist/modern/service-worker.js +686 -13
  21. package/dist/modern/service-worker.js.map +1 -1
  22. package/dist/modern/service-worker.min.js +1 -1
  23. package/dist/modern/service-worker.min.js.map +1 -1
  24. package/dist/modern/types/DXCUserInteraction.d.ts +32 -1
  25. package/dist/umd/DISABLE_SERVICEWORKER_STRATEGY.d.ts +1 -0
  26. package/dist/umd/DXCWebSocketStatus.d.ts +1 -0
  27. package/dist/umd/DexieCloudAPI.d.ts +75 -0
  28. package/dist/umd/DexieCloudOptions.d.ts +27 -0
  29. package/dist/umd/DexieCloudSyncOptions.d.ts +4 -0
  30. package/dist/umd/DexieCloudTable.d.ts +18 -0
  31. package/dist/umd/InvalidLicenseError.d.ts +5 -0
  32. package/dist/umd/Invite.d.ts +8 -0
  33. package/dist/umd/PermissionChecker.d.ts +15 -0
  34. package/dist/umd/TSON.d.ts +17 -0
  35. package/dist/umd/WSObservable.d.ts +72 -0
  36. package/dist/umd/associate.d.ts +1 -0
  37. package/dist/umd/authentication/AuthPersistedContext.d.ts +9 -0
  38. package/dist/umd/authentication/TokenErrorResponseError.d.ts +10 -0
  39. package/dist/umd/authentication/TokenExpiredError.d.ts +3 -0
  40. package/dist/umd/authentication/UNAUTHORIZED_USER.d.ts +2 -0
  41. package/dist/umd/authentication/authenticate.d.ts +13 -0
  42. package/dist/umd/authentication/currentUserObservable.d.ts +1 -0
  43. package/dist/umd/authentication/interactWithUser.d.ts +21 -0
  44. package/dist/umd/authentication/login.d.ts +3 -0
  45. package/dist/umd/authentication/logout.d.ts +5 -0
  46. package/dist/umd/authentication/otpFetchTokenCallback.d.ts +3 -0
  47. package/dist/umd/authentication/setCurrentUser.d.ts +14 -0
  48. package/dist/umd/authentication/waitUntil.d.ts +3 -0
  49. package/dist/umd/computeSyncState.d.ts +4 -0
  50. package/dist/umd/createSharedValueObservable.d.ts +3 -0
  51. package/dist/umd/currentUserEmitter.d.ts +3 -0
  52. package/dist/umd/db/DexieCloudDB.d.ts +61 -0
  53. package/dist/umd/db/entities/BaseRevisionMapEntry.d.ts +5 -0
  54. package/dist/umd/db/entities/EntityCommon.d.ts +5 -0
  55. package/dist/umd/db/entities/GuardedJob.d.ts +5 -0
  56. package/dist/umd/db/entities/Member.d.ts +19 -0
  57. package/dist/umd/db/entities/PersistedSyncState.d.ts +22 -0
  58. package/dist/umd/db/entities/Realm.d.ts +14 -0
  59. package/dist/umd/db/entities/Role.d.ts +11 -0
  60. package/dist/umd/db/entities/UserLogin.d.ts +23 -0
  61. package/dist/umd/default-ui/Dialog.d.ts +5 -0
  62. package/dist/umd/default-ui/LoginDialog.d.ts +3 -0
  63. package/dist/umd/default-ui/Styles.d.ts +3 -0
  64. package/dist/umd/default-ui/index.d.ts +24 -0
  65. package/dist/umd/define-ydoc-trigger.d.ts +3 -0
  66. package/dist/umd/dexie-cloud-addon.d.ts +3 -0
  67. package/dist/umd/dexie-cloud-addon.js +687 -14
  68. package/dist/umd/dexie-cloud-addon.js.gz +0 -0
  69. package/dist/umd/dexie-cloud-addon.js.map +1 -1
  70. package/dist/umd/dexie-cloud-addon.min.js +1 -1
  71. package/dist/umd/dexie-cloud-addon.min.js.gz +0 -0
  72. package/dist/umd/dexie-cloud-addon.min.js.map +1 -1
  73. package/dist/umd/dexie-cloud-client.d.ts +23 -0
  74. package/dist/umd/errors/HttpError.d.ts +5 -0
  75. package/dist/umd/extend-dexie-interface.d.ts +23 -0
  76. package/dist/umd/getGlobalRolesObservable.d.ts +5 -0
  77. package/dist/umd/getInternalAccessControlObservable.d.ts +12 -0
  78. package/dist/umd/getInvitesObservable.d.ts +23 -0
  79. package/dist/umd/getPermissionsLookupObservable.d.ts +16 -0
  80. package/dist/umd/getTiedRealmId.d.ts +2 -0
  81. package/dist/umd/helpers/BroadcastedAndLocalEvent.d.ts +8 -0
  82. package/dist/umd/helpers/CancelToken.d.ts +4 -0
  83. package/dist/umd/helpers/IS_SERVICE_WORKER.d.ts +1 -0
  84. package/dist/umd/helpers/SWBroadcastChannel.d.ts +12 -0
  85. package/dist/umd/helpers/allSettled.d.ts +1 -0
  86. package/dist/umd/helpers/bulkUpdate.d.ts +4 -0
  87. package/dist/umd/helpers/computeRealmSetHash.d.ts +2 -0
  88. package/dist/umd/helpers/date-constants.d.ts +5 -0
  89. package/dist/umd/helpers/flatten.d.ts +1 -0
  90. package/dist/umd/helpers/getMutationTable.d.ts +1 -0
  91. package/dist/umd/helpers/getSyncableTables.d.ts +4 -0
  92. package/dist/umd/helpers/getTableFromMutationTable.d.ts +1 -0
  93. package/dist/umd/helpers/makeArray.d.ts +1 -0
  94. package/dist/umd/helpers/randomString.d.ts +1 -0
  95. package/dist/umd/helpers/resolveText.d.ts +16 -0
  96. package/dist/umd/helpers/throwVersionIncrementNeeded.d.ts +1 -0
  97. package/dist/umd/helpers/visibilityState.d.ts +1 -0
  98. package/dist/umd/isEagerSyncDisabled.d.ts +2 -0
  99. package/dist/umd/isFirefox.d.ts +1 -0
  100. package/dist/umd/isSafari.d.ts +2 -0
  101. package/dist/umd/mapValueObservable.d.ts +5 -0
  102. package/dist/umd/mergePermissions.d.ts +2 -0
  103. package/dist/umd/middleware-helpers/guardedTable.d.ts +11 -0
  104. package/dist/umd/middleware-helpers/idGenerationHelpers.d.ts +18 -0
  105. package/dist/umd/middlewares/createIdGenerationMiddleware.d.ts +3 -0
  106. package/dist/umd/middlewares/createImplicitPropSetterMiddleware.d.ts +3 -0
  107. package/dist/umd/middlewares/createMutationTrackingMiddleware.d.ts +17 -0
  108. package/dist/umd/middlewares/outstandingTransaction.d.ts +4 -0
  109. package/dist/umd/overrideParseStoresSpec.d.ts +4 -0
  110. package/dist/umd/performInitialSync.d.ts +4 -0
  111. package/dist/umd/permissions.d.ts +9 -0
  112. package/dist/umd/prodLog.d.ts +9 -0
  113. package/dist/umd/service-worker.d.ts +1 -0
  114. package/dist/umd/service-worker.js +687 -14
  115. package/dist/umd/service-worker.js.map +1 -1
  116. package/dist/umd/service-worker.min.js +1 -1
  117. package/dist/umd/service-worker.min.js.map +1 -1
  118. package/dist/umd/sync/DEXIE_CLOUD_SYNCER_ID.d.ts +1 -0
  119. package/dist/umd/sync/LocalSyncWorker.d.ts +7 -0
  120. package/dist/umd/sync/SyncRequiredError.d.ts +3 -0
  121. package/dist/umd/sync/applyServerChanges.d.ts +3 -0
  122. package/dist/umd/sync/connectWebSocket.d.ts +2 -0
  123. package/dist/umd/sync/encodeIdsForServer.d.ts +4 -0
  124. package/dist/umd/sync/extractRealm.d.ts +2 -0
  125. package/dist/umd/sync/getLatestRevisionsPerTable.d.ts +6 -0
  126. package/dist/umd/sync/getTablesToSyncify.d.ts +3 -0
  127. package/dist/umd/sync/isOnline.d.ts +1 -0
  128. package/dist/umd/sync/isSyncNeeded.d.ts +2 -0
  129. package/dist/umd/sync/listClientChanges.d.ts +9 -0
  130. package/dist/umd/sync/listSyncifiedChanges.d.ts +5 -0
  131. package/dist/umd/sync/messageConsumerIsReady.d.ts +2 -0
  132. package/dist/umd/sync/messagesFromServerQueue.d.ts +8 -0
  133. package/dist/umd/sync/modifyLocalObjectsWithNewUserId.d.ts +4 -0
  134. package/dist/umd/sync/myId.d.ts +1 -0
  135. package/dist/umd/sync/numUnsyncedMutations.d.ts +2 -0
  136. package/dist/umd/sync/old_startSyncingClientChanges.d.ts +39 -0
  137. package/dist/umd/sync/performGuardedJob.d.ts +2 -0
  138. package/dist/umd/sync/ratelimit.d.ts +3 -0
  139. package/dist/umd/sync/registerSyncEvent.d.ts +3 -0
  140. package/dist/umd/sync/sync.d.ts +15 -0
  141. package/dist/umd/sync/syncIfPossible.d.ts +5 -0
  142. package/dist/umd/sync/syncWithServer.d.ts +6 -0
  143. package/dist/umd/sync/triggerSync.d.ts +2 -0
  144. package/dist/umd/sync/updateBaseRevs.d.ts +5 -0
  145. package/dist/umd/types/DXCAlert.d.ts +25 -0
  146. package/dist/umd/types/DXCInputField.d.ts +11 -0
  147. package/dist/umd/types/DXCUserInteraction.d.ts +93 -0
  148. package/dist/umd/types/NewIdOptions.d.ts +3 -0
  149. package/dist/umd/types/SWMessageEvent.d.ts +3 -0
  150. package/dist/umd/types/SWSyncEvent.d.ts +4 -0
  151. package/dist/umd/types/SyncState.d.ts +9 -0
  152. package/dist/umd/types/TXExpandos.d.ts +11 -0
  153. package/dist/umd/updateSchemaFromOptions.d.ts +3 -0
  154. package/dist/umd/userIsActive.d.ts +7 -0
  155. package/dist/umd/verifyConfig.d.ts +2 -0
  156. package/dist/umd/verifySchema.d.ts +2 -0
  157. package/dist/umd/yjs/YDexieCloudSyncState.d.ts +3 -0
  158. package/dist/umd/yjs/YTable.d.ts +3 -0
  159. package/dist/umd/yjs/applyYMessages.d.ts +9 -0
  160. package/dist/umd/yjs/awareness.d.ts +3 -0
  161. package/dist/umd/yjs/createYClientUpdateObservable.d.ts +4 -0
  162. package/dist/umd/yjs/createYHandler.d.ts +2 -0
  163. package/dist/umd/yjs/downloadYDocsFromServer.d.ts +3 -0
  164. package/dist/umd/yjs/getUpdatesTable.d.ts +3 -0
  165. package/dist/umd/yjs/listUpdatesSince.d.ts +3 -0
  166. package/dist/umd/yjs/listYClientMessagesAndStateVector.d.ts +26 -0
  167. package/dist/umd/yjs/reopenDocSignal.d.ts +10 -0
  168. package/dist/umd/yjs/updateYSyncStates.d.ts +6 -0
  169. package/oauth_flow.md +307 -0
  170. package/package.json +1 -1
@@ -8,7 +8,7 @@
8
8
  *
9
9
  * ==========================================================================
10
10
  *
11
- * Version 4.2.5, Sat Dec 20 2025
11
+ * Version 4.3.2, Thu Jan 22 2026
12
12
  *
13
13
  * https://dexie.org
14
14
  *
@@ -1145,6 +1145,74 @@ class TokenErrorResponseError extends Error {
1145
1145
  }
1146
1146
  }
1147
1147
 
1148
+ /** Cache for fetched SVG content to avoid re-fetching */
1149
+ const svgCache = {};
1150
+ /** Default SVG icons for built-in OAuth providers */
1151
+ const ProviderIcons = {
1152
+ google: `<svg viewBox="0 0 24 24" width="20" height="20"><path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/><path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/><path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/><path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/></svg>`,
1153
+ github: `<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z"/></svg>`,
1154
+ microsoft: `<svg viewBox="0 0 24 24" width="20" height="20"><rect fill="#F25022" x="1" y="1" width="10" height="10"/><rect fill="#00A4EF" x="1" y="13" width="10" height="10"/><rect fill="#7FBA00" x="13" y="1" width="10" height="10"/><rect fill="#FFB900" x="13" y="13" width="10" height="10"/></svg>`,
1155
+ apple: `<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.81-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z"/></svg>`,
1156
+ };
1157
+ /** Email/envelope icon for OTP option */
1158
+ const EmailIcon = `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="M22 6L12 13 2 6"/></svg>`;
1159
+ /**
1160
+ * Fetches SVG content from a URL and caches it.
1161
+ * Returns the SVG string or null if fetch fails.
1162
+ */
1163
+ function fetchSvgIcon(url) {
1164
+ return __awaiter(this, void 0, void 0, function* () {
1165
+ if (svgCache[url]) {
1166
+ return svgCache[url];
1167
+ }
1168
+ try {
1169
+ const res = yield fetch(url);
1170
+ if (res.ok) {
1171
+ const svg = yield res.text();
1172
+ // Validate it looks like SVG
1173
+ if (svg.includes('<svg')) {
1174
+ svgCache[url] = svg;
1175
+ return svg;
1176
+ }
1177
+ }
1178
+ }
1179
+ catch (_a) {
1180
+ // Silently fail - will show no icon
1181
+ }
1182
+ return null;
1183
+ });
1184
+ }
1185
+ /**
1186
+ * Converts an OAuthProviderInfo to a generic DXCOption.
1187
+ * Fetches SVG icons from URLs if needed.
1188
+ */
1189
+ function providerToOption(provider) {
1190
+ return __awaiter(this, void 0, void 0, function* () {
1191
+ var _a;
1192
+ let iconSvg;
1193
+ // First check for built-in icons
1194
+ if (ProviderIcons[provider.type]) {
1195
+ iconSvg = ProviderIcons[provider.type];
1196
+ }
1197
+ // If provider has iconUrl pointing to SVG, fetch and inline it
1198
+ else if ((_a = provider.iconUrl) === null || _a === void 0 ? void 0 : _a.toLowerCase().endsWith('.svg')) {
1199
+ const fetched = yield fetchSvgIcon(provider.iconUrl);
1200
+ if (fetched) {
1201
+ iconSvg = fetched;
1202
+ }
1203
+ }
1204
+ return {
1205
+ name: 'provider',
1206
+ value: provider.name,
1207
+ displayName: `Continue with ${provider.displayName}`,
1208
+ iconSvg,
1209
+ // If iconUrl is not SVG, pass it through for img tag rendering
1210
+ iconUrl: (!iconSvg && provider.iconUrl) ? provider.iconUrl : undefined,
1211
+ // Use provider type as style hint for branding
1212
+ styleHint: provider.type,
1213
+ };
1214
+ });
1215
+ }
1148
1216
  function interactWithUser(userInteraction, req) {
1149
1217
  return new Promise((resolve, reject) => {
1150
1218
  const interactionProps = Object.assign(Object.assign({ submitLabel: 'Submit', cancelLabel: 'Cancel' }, req), { onSubmit: (res) => {
@@ -1279,6 +1347,67 @@ function confirmLogout(userInteraction, currentUserId, numUnsyncedChanges) {
1279
1347
  .catch(() => false);
1280
1348
  });
1281
1349
  }
1350
+ /**
1351
+ * Prompts the user to select an authentication method (OAuth provider or OTP).
1352
+ *
1353
+ * This function converts OAuth providers and OTP option into generic DXCOption[]
1354
+ * for the DXCSelect interaction, handling icon fetching and style hints.
1355
+ *
1356
+ * @param userInteraction - The user interaction BehaviorSubject
1357
+ * @param providers - Available OAuth providers
1358
+ * @param otpEnabled - Whether OTP is available
1359
+ * @param title - Dialog title
1360
+ * @param alerts - Optional alerts to display
1361
+ * @returns Promise resolving to the user's selection
1362
+ */
1363
+ function promptForProvider(userInteraction_1, providers_1, otpEnabled_1) {
1364
+ return __awaiter(this, arguments, void 0, function* (userInteraction, providers, otpEnabled, title = 'Choose login method', alerts = []) {
1365
+ // Convert providers to generic options (with icon fetching)
1366
+ const providerOptions = yield Promise.all(providers.map(providerToOption));
1367
+ // Build the options array
1368
+ const options = [...providerOptions];
1369
+ // Add OTP option if enabled
1370
+ if (otpEnabled) {
1371
+ options.push({
1372
+ name: 'otp',
1373
+ value: 'email',
1374
+ displayName: 'Continue with email',
1375
+ iconSvg: EmailIcon,
1376
+ styleHint: 'otp',
1377
+ });
1378
+ }
1379
+ return new Promise((resolve, reject) => {
1380
+ const interactionProps = {
1381
+ type: 'generic',
1382
+ title,
1383
+ alerts,
1384
+ options,
1385
+ fields: {},
1386
+ submitLabel: '', // No submit button - just options
1387
+ cancelLabel: 'Cancel',
1388
+ onSubmit: (params) => {
1389
+ userInteraction.next(undefined);
1390
+ // Check which option was selected
1391
+ if ('otp' in params) {
1392
+ resolve({ type: 'otp' });
1393
+ }
1394
+ else if ('provider' in params) {
1395
+ resolve({ type: 'provider', provider: params.provider });
1396
+ }
1397
+ else {
1398
+ // Unknown - default to OTP
1399
+ resolve({ type: 'otp' });
1400
+ }
1401
+ },
1402
+ onCancel: () => {
1403
+ userInteraction.next(undefined);
1404
+ reject(new Dexie.AbortError('User cancelled'));
1405
+ },
1406
+ };
1407
+ userInteraction.next(interactionProps);
1408
+ });
1409
+ });
1410
+ }
1282
1411
 
1283
1412
  function loadAccessToken(db) {
1284
1413
  return __awaiter(this, void 0, void 0, function* () {
@@ -3542,15 +3671,215 @@ function _logout(db_1) {
3542
3671
  });
3543
3672
  }
3544
3673
 
3674
+ /** User-friendly messages for OAuth error codes */
3675
+ const ERROR_MESSAGES = {
3676
+ access_denied: 'Access was denied by the authentication provider.',
3677
+ invalid_state: 'The authentication response could not be verified. Please try again.',
3678
+ email_not_verified: 'Your email address must be verified before you can log in.',
3679
+ expired_code: 'The authentication code has expired. Please try again.',
3680
+ provider_error: 'An error occurred with the authentication provider.',
3681
+ network_error: 'A network error occurred during authentication. Please check your connection and try again.',
3682
+ };
3683
+ /** Error class for OAuth-specific errors */
3684
+ class OAuthError extends Error {
3685
+ constructor(code, provider, customMessage) {
3686
+ super(customMessage || ERROR_MESSAGES[code]);
3687
+ this.name = 'OAuthError';
3688
+ this.code = code;
3689
+ this.provider = provider;
3690
+ }
3691
+ /** Get user-friendly message for this error */
3692
+ get userMessage() {
3693
+ return ERROR_MESSAGES[this.code] || this.message;
3694
+ }
3695
+ }
3696
+
3697
+ /**
3698
+ * Exchanges a Dexie Cloud authorization code for access and refresh tokens.
3699
+ *
3700
+ * This is called after the OAuth callback delivers the authorization code
3701
+ * via postMessage (popup flow) or redirect.
3702
+ *
3703
+ * @param options - Exchange options
3704
+ * @returns Promise resolving to TokenFinalResponse
3705
+ * @throws OAuthError or TokenErrorResponseError on failure
3706
+ */
3707
+ function exchangeOAuthCode(options) {
3708
+ return __awaiter(this, void 0, void 0, function* () {
3709
+ const { databaseUrl, code, publicKey, scopes = ['ACCESS_DB'] } = options;
3710
+ const tokenRequest = {
3711
+ grant_type: 'authorization_code',
3712
+ code,
3713
+ public_key: publicKey,
3714
+ scopes,
3715
+ };
3716
+ try {
3717
+ const res = yield fetch(`${databaseUrl}/token`, {
3718
+ method: 'POST',
3719
+ headers: { 'Content-Type': 'application/json' },
3720
+ body: JSON.stringify(tokenRequest),
3721
+ mode: 'cors',
3722
+ });
3723
+ if (!res.ok) {
3724
+ if (res.status === 400 || res.status === 401) {
3725
+ // Try to parse error response
3726
+ try {
3727
+ const errorResponse = yield res.json();
3728
+ if (errorResponse.type === 'error') {
3729
+ // Check for specific error codes
3730
+ if (errorResponse.messageCode === 'INVALID_OTP') {
3731
+ // In the context of OAuth, this likely means expired code
3732
+ throw new OAuthError('expired_code', undefined, errorResponse.message);
3733
+ }
3734
+ throw new TokenErrorResponseError(errorResponse);
3735
+ }
3736
+ }
3737
+ catch (e) {
3738
+ if (e instanceof OAuthError || e instanceof TokenErrorResponseError) {
3739
+ throw e;
3740
+ }
3741
+ // Fall through to generic error
3742
+ }
3743
+ }
3744
+ const errorText = yield res.text().catch(() => res.statusText);
3745
+ throw new OAuthError('provider_error', undefined, `Token exchange failed: ${res.status} ${errorText}`);
3746
+ }
3747
+ const response = yield res.json();
3748
+ if (response.type === 'error') {
3749
+ throw new TokenErrorResponseError(response);
3750
+ }
3751
+ if (response.type !== 'tokens') {
3752
+ throw new OAuthError('provider_error', undefined, `Unexpected response type: ${response.type}`);
3753
+ }
3754
+ return response;
3755
+ }
3756
+ catch (error) {
3757
+ if (error instanceof OAuthError || error instanceof TokenErrorResponseError) {
3758
+ throw error;
3759
+ }
3760
+ if (error instanceof TypeError) {
3761
+ // Network error
3762
+ throw new OAuthError('network_error');
3763
+ }
3764
+ throw error;
3765
+ }
3766
+ });
3767
+ }
3768
+
3769
+ /** Default response when OAuth is disabled or unavailable */
3770
+ const OTP_ONLY_RESPONSE = {
3771
+ providers: [],
3772
+ otpEnabled: true,
3773
+ };
3774
+ /**
3775
+ * Fetches available authentication providers from the Dexie Cloud server.
3776
+ *
3777
+ * @param databaseUrl - The Dexie Cloud database URL
3778
+ * @param socialAuthEnabled - Whether social auth is enabled in client config (default: true)
3779
+ * @returns Promise resolving to AuthProvidersResponse
3780
+ *
3781
+ * Handles failures gracefully:
3782
+ * - 404 → Returns OTP-only (old server version)
3783
+ * - Network error → Returns OTP-only
3784
+ * - socialAuthEnabled: false → Returns OTP-only without fetching
3785
+ */
3786
+ function fetchAuthProviders(databaseUrl_1) {
3787
+ return __awaiter(this, arguments, void 0, function* (databaseUrl, socialAuthEnabled = true) {
3788
+ // If social auth is disabled, return OTP-only without fetching
3789
+ if (!socialAuthEnabled) {
3790
+ return OTP_ONLY_RESPONSE;
3791
+ }
3792
+ try {
3793
+ const res = yield fetch(`${databaseUrl}/auth-providers`, {
3794
+ method: 'GET',
3795
+ headers: { 'Accept': 'application/json' },
3796
+ mode: 'cors',
3797
+ });
3798
+ if (res.status === 404) {
3799
+ // Old server version without OAuth support
3800
+ console.debug('[dexie-cloud] Server does not support /auth-providers endpoint. Using OTP-only authentication.');
3801
+ return OTP_ONLY_RESPONSE;
3802
+ }
3803
+ if (!res.ok) {
3804
+ console.warn(`[dexie-cloud] Failed to fetch auth providers: ${res.status} ${res.statusText}`);
3805
+ return OTP_ONLY_RESPONSE;
3806
+ }
3807
+ return yield res.json();
3808
+ }
3809
+ catch (error) {
3810
+ // Network error or other failure - fall back to OTP
3811
+ console.debug('[dexie-cloud] Could not fetch auth providers, falling back to OTP:', error);
3812
+ return OTP_ONLY_RESPONSE;
3813
+ }
3814
+ });
3815
+ }
3816
+
3817
+ /** Build the OAuth login URL */
3818
+ function buildOAuthLoginUrl(options) {
3819
+ const url = new URL(`${options.databaseUrl}/oauth/login/${options.provider}`);
3820
+ // Set the redirect URI - defaults to current page URL for web SPAs
3821
+ const redirectUri = options.redirectUri ||
3822
+ (typeof window !== 'undefined' ? window.location.href : '');
3823
+ if (redirectUri) {
3824
+ url.searchParams.set('redirect_uri', redirectUri);
3825
+ }
3826
+ return url.toString();
3827
+ }
3828
+ /**
3829
+ * Initiates OAuth login via full page redirect.
3830
+ *
3831
+ * The page will navigate to the OAuth provider. After authentication,
3832
+ * the user is redirected back to the app with a `dxc-auth` query parameter
3833
+ * containing base64url-encoded JSON with the authorization code.
3834
+ *
3835
+ * The dexie-cloud-addon automatically detects and processes this parameter
3836
+ * when db.cloud.configure() is called on page load.
3837
+ *
3838
+ * @param options - OAuth redirect options
3839
+ *
3840
+ * @example
3841
+ * ```typescript
3842
+ * // Initiate OAuth login
3843
+ * startOAuthRedirect({
3844
+ * databaseUrl: 'https://mydb.dexie.cloud',
3845
+ * provider: 'google'
3846
+ * });
3847
+ * // Page navigates away, user authenticates, then returns with auth code
3848
+ * ```
3849
+ */
3850
+ function startOAuthRedirect(options) {
3851
+ // Store provider in sessionStorage for reference on callback
3852
+ if (typeof sessionStorage !== 'undefined') {
3853
+ sessionStorage.setItem('dexie-cloud-oauth-provider', options.provider);
3854
+ }
3855
+ const loginUrl = buildOAuthLoginUrl(options);
3856
+ window.location.href = loginUrl;
3857
+ }
3858
+
3545
3859
  function otpFetchTokenCallback(db) {
3546
3860
  const { userInteraction } = db.cloud;
3547
3861
  return function otpAuthenticate(_a) {
3548
3862
  return __awaiter(this, arguments, void 0, function* ({ public_key, hints }) {
3549
- var _b;
3863
+ var _b, _c;
3550
3864
  let tokenRequest;
3551
3865
  const url = (_b = db.cloud.options) === null || _b === void 0 ? void 0 : _b.databaseUrl;
3552
3866
  if (!url)
3553
3867
  throw new Error(`No database URL given.`);
3868
+ // Handle OAuth code exchange (from redirect/deep link flows)
3869
+ if ((hints === null || hints === void 0 ? void 0 : hints.oauthCode) && hints.provider) {
3870
+ return yield exchangeOAuthCode({
3871
+ databaseUrl: url,
3872
+ code: hints.oauthCode,
3873
+ publicKey: public_key,
3874
+ scopes: ['ACCESS_DB'],
3875
+ });
3876
+ }
3877
+ // Handle OAuth provider login via redirect
3878
+ if (hints === null || hints === void 0 ? void 0 : hints.provider) {
3879
+ initiateOAuthRedirect(db, hints.provider);
3880
+ // This function never returns - page navigates away
3881
+ throw new Error('OAuth redirect initiated');
3882
+ }
3554
3883
  if ((hints === null || hints === void 0 ? void 0 : hints.grant_type) === 'demo') {
3555
3884
  const demo_user = yield promptForEmail(userInteraction, 'Enter a demo user email', (hints === null || hints === void 0 ? void 0 : hints.email) || (hints === null || hints === void 0 ? void 0 : hints.userId));
3556
3885
  tokenRequest = {
@@ -3574,6 +3903,20 @@ function otpFetchTokenCallback(db) {
3574
3903
  };
3575
3904
  }
3576
3905
  else {
3906
+ // Check for available auth providers (OAuth + OTP)
3907
+ const socialAuthEnabled = ((_c = db.cloud.options) === null || _c === void 0 ? void 0 : _c.socialAuth) !== false;
3908
+ const authProviders = yield fetchAuthProviders(url, socialAuthEnabled);
3909
+ // If we have OAuth providers available, prompt for selection
3910
+ if (authProviders.providers.length > 0) {
3911
+ const selection = yield promptForProvider(userInteraction, authProviders.providers, authProviders.otpEnabled, 'Sign in');
3912
+ if (selection.type === 'provider') {
3913
+ // User selected an OAuth provider - initiate redirect
3914
+ initiateOAuthRedirect(db, selection.provider);
3915
+ // This function never returns - page navigates away
3916
+ throw new Error('OAuth redirect initiated');
3917
+ }
3918
+ // User chose OTP - continue with email prompt below
3919
+ }
3577
3920
  const email = yield promptForEmail(userInteraction, 'Enter email address', hints === null || hints === void 0 ? void 0 : hints.email);
3578
3921
  if (/@demo.local$/.test(email)) {
3579
3922
  tokenRequest = {
@@ -3651,6 +3994,27 @@ function otpFetchTokenCallback(db) {
3651
3994
  });
3652
3995
  };
3653
3996
  }
3997
+ /**
3998
+ * Initiates OAuth login via full page redirect.
3999
+ *
4000
+ * The page will navigate away to the OAuth provider. After authentication,
4001
+ * the user is redirected back with a dxc-auth query parameter that is
4002
+ * automatically detected by db.cloud.configure().
4003
+ */
4004
+ function initiateOAuthRedirect(db, provider) {
4005
+ var _a, _b;
4006
+ const url = (_a = db.cloud.options) === null || _a === void 0 ? void 0 : _a.databaseUrl;
4007
+ if (!url)
4008
+ throw new Error(`No database URL given.`);
4009
+ const redirectUri = ((_b = db.cloud.options) === null || _b === void 0 ? void 0 : _b.oauthRedirectUri) ||
4010
+ (typeof window !== 'undefined' ? window.location.href : undefined);
4011
+ // Start OAuth redirect flow - page navigates away
4012
+ startOAuthRedirect({
4013
+ databaseUrl: url,
4014
+ provider,
4015
+ redirectUri,
4016
+ });
4017
+ }
3654
4018
 
3655
4019
  /** A way to log to console in production without terser stripping out
3656
4020
  * it from the release bundle.
@@ -5388,8 +5752,97 @@ const Styles = {
5388
5752
  color: "#333",
5389
5753
  borderBottom: "1px solid #eee",
5390
5754
  paddingBottom: "10px"
5391
- }
5392
- };
5755
+ },
5756
+ // OAuth Provider Button Styles
5757
+ ProviderButton: {
5758
+ display: "flex",
5759
+ alignItems: "center",
5760
+ justifyContent: "center",
5761
+ width: "100%",
5762
+ padding: "12px 16px",
5763
+ marginBottom: "10px",
5764
+ border: "1px solid #d1d5db",
5765
+ borderRadius: "6px",
5766
+ backgroundColor: "#ffffff",
5767
+ cursor: "pointer",
5768
+ fontSize: "14px",
5769
+ fontWeight: "500",
5770
+ color: "#374151",
5771
+ transition: "all 0.2s ease",
5772
+ gap: "12px"
5773
+ },
5774
+ ProviderButtonIcon: {
5775
+ width: "20px",
5776
+ height: "20px",
5777
+ flexShrink: 0,
5778
+ display: "flex",
5779
+ alignItems: "center",
5780
+ justifyContent: "center"
5781
+ },
5782
+ ProviderButtonText: {
5783
+ flex: 1,
5784
+ textAlign: "left"
5785
+ },
5786
+ // Provider-specific colors
5787
+ ProviderGoogle: {
5788
+ backgroundColor: "#ffffff",
5789
+ border: "1px solid #dadce0",
5790
+ color: "#3c4043"
5791
+ },
5792
+ ProviderGitHub: {
5793
+ backgroundColor: "#24292e",
5794
+ border: "1px solid #24292e",
5795
+ color: "#ffffff"
5796
+ },
5797
+ ProviderMicrosoft: {
5798
+ backgroundColor: "#ffffff",
5799
+ border: "1px solid #8c8c8c",
5800
+ color: "#5e5e5e"
5801
+ },
5802
+ ProviderApple: {
5803
+ backgroundColor: "#000000",
5804
+ border: "1px solid #000000",
5805
+ color: "#ffffff"
5806
+ },
5807
+ ProviderCustom: {
5808
+ backgroundColor: "#4f46e5",
5809
+ border: "1px solid #4f46e5",
5810
+ color: "#ffffff"
5811
+ },
5812
+ // Divider styles
5813
+ Divider: {
5814
+ display: "flex",
5815
+ alignItems: "center",
5816
+ margin: "20px 0",
5817
+ color: "#6b7280",
5818
+ fontSize: "13px"
5819
+ },
5820
+ DividerLine: {
5821
+ flex: 1,
5822
+ height: "1px",
5823
+ backgroundColor: "#e5e7eb"
5824
+ },
5825
+ DividerText: {
5826
+ padding: "0 12px",
5827
+ color: "#9ca3af"
5828
+ },
5829
+ // OTP Button (Continue with email)
5830
+ OtpButton: {
5831
+ display: "flex",
5832
+ alignItems: "center",
5833
+ justifyContent: "center",
5834
+ width: "100%",
5835
+ padding: "12px 16px",
5836
+ border: "1px solid #d1d5db",
5837
+ borderRadius: "6px",
5838
+ backgroundColor: "#f9fafb",
5839
+ cursor: "pointer",
5840
+ fontSize: "14px",
5841
+ fontWeight: "500",
5842
+ color: "#374151",
5843
+ transition: "all 0.2s ease",
5844
+ gap: "12px"
5845
+ }};
5393
5846
 
5394
5847
  function Dialog({ children, className }) {
5395
5848
  return (_$1("div", { className: `dexie-dialog ${className || ''}` },
@@ -5418,19 +5871,126 @@ function resolveText({ message, messageCode, messageParams }) {
5418
5871
  return message.replace(/\{\w+\}/ig, n => messageParams[n.substring(1, n.length - 1)]);
5419
5872
  }
5420
5873
 
5874
+ /** Get style based on styleHint (for provider branding, etc.) */
5875
+ function getOptionStyle(styleHint) {
5876
+ const baseStyle = Object.assign({}, Styles.ProviderButton);
5877
+ if (!styleHint) {
5878
+ return baseStyle;
5879
+ }
5880
+ switch (styleHint) {
5881
+ case 'google':
5882
+ return Object.assign(Object.assign({}, baseStyle), Styles.ProviderGoogle);
5883
+ case 'github':
5884
+ return Object.assign(Object.assign({}, baseStyle), Styles.ProviderGitHub);
5885
+ case 'microsoft':
5886
+ return Object.assign(Object.assign({}, baseStyle), Styles.ProviderMicrosoft);
5887
+ case 'apple':
5888
+ return Object.assign(Object.assign({}, baseStyle), Styles.ProviderApple);
5889
+ case 'otp':
5890
+ return Object.assign({}, Styles.OtpButton);
5891
+ case 'custom-oauth2':
5892
+ return Object.assign(Object.assign({}, baseStyle), Styles.ProviderCustom);
5893
+ default:
5894
+ return baseStyle;
5895
+ }
5896
+ }
5897
+ /**
5898
+ * Generic button component for selectable options.
5899
+ * Displays the option's icon and display name.
5900
+ *
5901
+ * The icon can be:
5902
+ * - Inline SVG (iconSvg) - rendered directly with dangerouslySetInnerHTML
5903
+ * - Image URL (iconUrl) - rendered as an img tag
5904
+ *
5905
+ * Style is determined by the styleHint property for branding purposes.
5906
+ */
5907
+ function OptionButton({ option, onClick }) {
5908
+ const { displayName, iconUrl, iconSvg, styleHint, value } = option;
5909
+ const style = getOptionStyle(styleHint);
5910
+ // Get the text color from the button style for SVG fill processing
5911
+ const textColor = style.color || '#000000';
5912
+ // Process SVG to replace currentColor with actual text color
5913
+ const processedSvg = iconSvg
5914
+ ? iconSvg
5915
+ .replace(/fill="currentColor"/gi, `fill="${textColor}"`)
5916
+ .replace(/fill='currentColor'/gi, `fill='${textColor}'`)
5917
+ .replace(/stroke="currentColor"/gi, `stroke="${textColor}"`)
5918
+ .replace(/stroke='currentColor'/gi, `stroke='${textColor}'`)
5919
+ : null;
5920
+ // Render the appropriate icon
5921
+ const renderIcon = () => {
5922
+ // Inline SVG
5923
+ if (processedSvg) {
5924
+ return (_$1("span", { style: Styles.ProviderButtonIcon, "aria-hidden": "true", dangerouslySetInnerHTML: { __html: processedSvg } }));
5925
+ }
5926
+ // Image URL
5927
+ if (iconUrl) {
5928
+ return (_$1("img", { src: iconUrl, alt: "", style: Styles.ProviderButtonIcon, "aria-hidden": "true" }));
5929
+ }
5930
+ return null;
5931
+ };
5932
+ return (_$1("button", { type: "button", style: style, onClick: onClick, class: `dxc-option-btn${styleHint ? ` dxc-option-${styleHint}` : ''}`, "aria-label": displayName },
5933
+ renderIcon(),
5934
+ _$1("span", { style: Styles.ProviderButtonText }, displayName)));
5935
+ }
5936
+ /**
5937
+ * Visual divider with "or" text.
5938
+ */
5939
+ function Divider() {
5940
+ return (_$1("div", { style: Styles.Divider },
5941
+ _$1("div", { style: Styles.DividerLine }),
5942
+ _$1("span", { style: Styles.DividerText }, "or"),
5943
+ _$1("div", { style: Styles.DividerLine })));
5944
+ }
5945
+
5421
5946
  const OTP_LENGTH = 8;
5422
- function LoginDialog({ title, type, alerts, fields, submitLabel, cancelLabel, onCancel, onSubmit, }) {
5947
+ /**
5948
+ * Generic dialog that can render:
5949
+ * - Form fields (text inputs)
5950
+ * - Selectable options (buttons)
5951
+ * - Or both together
5952
+ *
5953
+ * When an option is clicked, calls onSubmit({ [option.name]: option.value }).
5954
+ * This unified approach means the same callback handles both form submission
5955
+ * and option selection.
5956
+ */
5957
+ function LoginDialog({ title, alerts, fields, options, submitLabel, cancelLabel, onCancel, onSubmit, }) {
5423
5958
  const [params, setParams] = d({});
5424
5959
  const firstFieldRef = A(null);
5425
5960
  _(() => { var _a; return (_a = firstFieldRef.current) === null || _a === void 0 ? void 0 : _a.focus(); }, []);
5961
+ const fieldEntries = Object.entries(fields || {});
5962
+ const hasFields = fieldEntries.length > 0;
5963
+ const hasOptions = options && options.length > 0;
5964
+ // Group options by name to detect if we have multiple groups
5965
+ const optionGroups = new Map();
5966
+ if (options) {
5967
+ for (const option of options) {
5968
+ const group = optionGroups.get(option.name) || [];
5969
+ group.push(option);
5970
+ optionGroups.set(option.name, group);
5971
+ }
5972
+ }
5973
+ const hasMultipleGroups = optionGroups.size > 1;
5974
+ // Handler for option clicks - calls onSubmit with { [option.name]: option.value }
5975
+ const handleOptionClick = (option) => {
5976
+ onSubmit({ [option.name]: option.value });
5977
+ };
5426
5978
  return (_$1(Dialog, { className: "dxc-login-dlg" },
5427
5979
  _$1(k$1, null,
5428
5980
  _$1("h3", { style: Styles.WindowHeader }, title),
5429
- alerts.map((alert) => (_$1("p", { style: Styles.Alert[alert.type] }, resolveText(alert)))),
5430
- _$1("form", { onSubmit: (ev) => {
5981
+ alerts.map((alert, idx) => (_$1("p", { key: idx, style: Styles.Alert[alert.type] }, resolveText(alert)))),
5982
+ hasOptions && (_$1("div", { class: "dxc-options" }, hasMultipleGroups ? (
5983
+ // Render with dividers between groups
5984
+ Array.from(optionGroups.entries()).map(([groupName, groupOptions], groupIdx) => (_$1(k$1, { key: groupName },
5985
+ groupIdx > 0 && _$1(Divider, null),
5986
+ groupOptions.map((option) => (_$1(OptionButton, { key: `${option.name}-${option.value}`, option: option, onClick: () => handleOptionClick(option) }))))))) : (
5987
+ // Simple case: all options in one group
5988
+ options.map((option) => (_$1(OptionButton, { key: `${option.name}-${option.value}`, option: option, onClick: () => handleOptionClick(option) })))))),
5989
+ hasOptions && hasFields && _$1(Divider, null),
5990
+ hasFields && (_$1("form", { onSubmit: (ev) => {
5431
5991
  ev.preventDefault();
5432
5992
  onSubmit(params);
5433
- } }, Object.entries(fields).map(([fieldName, { type, label, placeholder }], idx) => (_$1("label", { style: Styles.Label, key: idx },
5993
+ } }, fieldEntries.map(([fieldName, { type, label, placeholder }], idx) => (_$1("label", { style: Styles.Label, key: idx },
5434
5994
  label ? `${label}: ` : '',
5435
5995
  _$1("input", { ref: idx === 0 ? firstFieldRef : undefined, type: type, name: fieldName, autoComplete: "on", style: Styles.Input, autoFocus: true, placeholder: placeholder, value: params[fieldName] || '', onInput: (ev) => {
5436
5996
  var _a;
@@ -5441,10 +6001,10 @@ function LoginDialog({ title, type, alerts, fields, submitLabel, cancelLabel, on
5441
6001
  // Auto-submit when OTP is filled in.
5442
6002
  onSubmit(updatedParams);
5443
6003
  }
5444
- } })))))),
6004
+ } }))))))),
5445
6005
  _$1("div", { style: Styles.ButtonsDiv },
5446
6006
  _$1(k$1, null,
5447
- _$1("button", { type: "submit", style: Styles.PrimaryButton, onClick: () => onSubmit(params) }, submitLabel),
6007
+ hasFields && submitLabel && (_$1("button", { type: "submit", style: Styles.PrimaryButton, onClick: () => onSubmit(params) }, submitLabel)),
5448
6008
  cancelLabel && (_$1("button", { style: Styles.Button, onClick: onCancel }, cancelLabel))))));
5449
6009
  }
5450
6010
  function valueTransformer(type, value) {
@@ -5476,7 +6036,8 @@ class LoginGui extends x {
5476
6036
  render(props, { userInteraction }) {
5477
6037
  if (!userInteraction)
5478
6038
  return null;
5479
- //if (props.db.cloud.userInteraction.observers.length > 1) return null; // Someone else subscribes.
6039
+ // LoginDialog handles all interaction types uniformly
6040
+ // (forms with fields, options, or both)
5480
6041
  return _$1(LoginDialog, Object.assign({}, userInteraction));
5481
6042
  }
5482
6043
  }
@@ -6035,6 +6596,83 @@ function createAwareness(db, doc, provider) {
6035
6596
  return awareness;
6036
6597
  }
6037
6598
 
6599
+ /**
6600
+ * Decodes a base64url-encoded string to a regular string.
6601
+ * Base64url uses - instead of + and _ instead of /, and may omit padding.
6602
+ */
6603
+ function decodeBase64Url(encoded) {
6604
+ // Add padding if needed
6605
+ const padded = encoded + '='.repeat((4 - (encoded.length % 4)) % 4);
6606
+ // Convert base64url to base64
6607
+ const base64 = padded.replace(/-/g, '+').replace(/_/g, '/');
6608
+ return atob(base64);
6609
+ }
6610
+ /**
6611
+ * Parses OAuth callback parameters from the dxc-auth query parameter.
6612
+ *
6613
+ * The dxc-auth parameter contains base64url-encoded JSON with the following structure:
6614
+ * - On success: { "code": "...", "provider": "...", "state": "..." }
6615
+ * - On error: { "error": "...", "provider": "...", "state": "..." }
6616
+ *
6617
+ * @param url - The URL to parse (defaults to window.location.href)
6618
+ * @returns OAuthCallbackParams if valid callback, null otherwise
6619
+ * @throws OAuthError if there's an error in the callback
6620
+ */
6621
+ function parseOAuthCallback(url) {
6622
+ const targetUrl = (typeof window !== 'undefined' ? window.location.href : '');
6623
+ if (!targetUrl) {
6624
+ return null;
6625
+ }
6626
+ const parsed = new URL(targetUrl);
6627
+ const encoded = parsed.searchParams.get('dxc-auth');
6628
+ if (!encoded) {
6629
+ return null; // Not an OAuth callback URL
6630
+ }
6631
+ let payload;
6632
+ try {
6633
+ const json = decodeBase64Url(encoded);
6634
+ payload = JSON.parse(json);
6635
+ }
6636
+ catch (e) {
6637
+ console.warn('[dexie-cloud] Failed to parse dxc-auth parameter:', e);
6638
+ return null;
6639
+ }
6640
+ const { code, provider, state, error } = payload;
6641
+ // Check for error first
6642
+ if (error) {
6643
+ if (error.toLowerCase().includes('access_denied') || error.toLowerCase().includes('access denied')) {
6644
+ throw new OAuthError('access_denied', provider, error);
6645
+ }
6646
+ if (error.toLowerCase().includes('email') && error.toLowerCase().includes('verif')) {
6647
+ throw new OAuthError('email_not_verified', provider, error);
6648
+ }
6649
+ throw new OAuthError('provider_error', provider, error);
6650
+ }
6651
+ // Validate required fields for success case
6652
+ if (!code || !provider || !state) {
6653
+ console.warn('[dexie-cloud] Invalid dxc-auth payload: missing required fields');
6654
+ return null;
6655
+ }
6656
+ return { code, provider, state };
6657
+ }
6658
+ /**
6659
+ * Cleans up the dxc-auth query parameter from the URL.
6660
+ * Call this after successfully handling the callback to clean up the browser URL.
6661
+ */
6662
+ function cleanupOAuthUrl() {
6663
+ var _a;
6664
+ if (typeof window === 'undefined' || !((_a = window.history) === null || _a === void 0 ? void 0 : _a.replaceState)) {
6665
+ return;
6666
+ }
6667
+ const url = new URL(window.location.href);
6668
+ if (!url.searchParams.has('dxc-auth')) {
6669
+ return;
6670
+ }
6671
+ url.searchParams.delete('dxc-auth');
6672
+ const cleanUrl = url.pathname + (url.searchParams.toString() ? `?${url.searchParams.toString()}` : '') + url.hash;
6673
+ window.history.replaceState(null, '', cleanUrl);
6674
+ }
6675
+
6038
6676
  const DEFAULT_OPTIONS = {
6039
6677
  nameSuffix: true,
6040
6678
  };
@@ -6046,6 +6684,8 @@ function dexieCloud(dexie) {
6046
6684
  const currentUserEmitter = getCurrentUserEmitter(dexie);
6047
6685
  const subscriptions = [];
6048
6686
  let configuredProgramatically = false;
6687
+ // Pending OAuth auth code from dxc-auth redirect (detected in configure())
6688
+ let pendingOAuthCode = null;
6049
6689
  // local sync worker - used when there's no service worker.
6050
6690
  let localSyncWorker = null;
6051
6691
  dexie.on('ready', (dexie) => __awaiter(this, void 0, void 0, function* () {
@@ -6075,7 +6715,7 @@ function dexieCloud(dexie) {
6075
6715
  const syncComplete = new Subject();
6076
6716
  dexie.cloud = {
6077
6717
  // @ts-ignore
6078
- version: "4.2.5",
6718
+ version: "4.3.2",
6079
6719
  options: Object.assign({}, DEFAULT_OPTIONS),
6080
6720
  schema: null,
6081
6721
  get currentUserId() {
@@ -6110,6 +6750,26 @@ function dexieCloud(dexie) {
6110
6750
  DexieCloudDB(dexie).reconfigure(); // Update observable from new dexie.name
6111
6751
  }
6112
6752
  updateSchemaFromOptions(dexie.cloud.schema, dexie.cloud.options);
6753
+ // Check for OAuth callback (dxc-auth query parameter)
6754
+ // Only check in DOM environment, not workers
6755
+ if (typeof window !== 'undefined' && window.location) {
6756
+ try {
6757
+ const callback = parseOAuthCallback();
6758
+ if (callback) {
6759
+ // Clean up URL immediately (remove dxc-auth param)
6760
+ cleanupOAuthUrl();
6761
+ // Store the pending auth code for processing when db is ready
6762
+ pendingOAuthCode = { code: callback.code, provider: callback.provider };
6763
+ console.debug('[dexie-cloud] OAuth callback detected, auth code stored for processing');
6764
+ }
6765
+ }
6766
+ catch (error) {
6767
+ // parseOAuthCallback throws OAuthError on error callbacks
6768
+ // Store null for code but log the error
6769
+ console.warn('[dexie-cloud] OAuth callback error:', error);
6770
+ cleanupOAuthUrl();
6771
+ }
6772
+ }
6113
6773
  },
6114
6774
  logout() {
6115
6775
  return __awaiter(this, arguments, void 0, function* ({ force } = {}) {
@@ -6304,6 +6964,19 @@ function dexieCloud(dexie) {
6304
6964
  // HERE: If requireAuth, do athentication now.
6305
6965
  let changedUser = false;
6306
6966
  const user = yield db.getCurrentUser();
6967
+ // Process pending OAuth callback if present (from dxc-auth redirect)
6968
+ if (pendingOAuthCode && !db.cloud.isServiceWorkerDB) {
6969
+ const { code, provider } = pendingOAuthCode;
6970
+ pendingOAuthCode = null; // Clear pending code
6971
+ console.debug('[dexie-cloud] Processing OAuth callback, provider:', provider);
6972
+ try {
6973
+ changedUser = yield login(db, { oauthCode: code, provider });
6974
+ }
6975
+ catch (error) {
6976
+ console.error('[dexie-cloud] OAuth login failed:', error);
6977
+ // Continue with normal flow - user can try again
6978
+ }
6979
+ }
6307
6980
  const requireAuth = (_b = db.cloud.options) === null || _b === void 0 ? void 0 : _b.requireAuth;
6308
6981
  if (requireAuth) {
6309
6982
  if (db.cloud.isServiceWorkerDB) {
@@ -6392,7 +7065,7 @@ function dexieCloud(dexie) {
6392
7065
  }
6393
7066
  }
6394
7067
  // @ts-ignore
6395
- dexieCloud.version = "4.2.5";
7068
+ dexieCloud.version = "4.3.2";
6396
7069
  Dexie.Cloud = dexieCloud;
6397
7070
 
6398
7071
  // In case the SW lives for a while, let it reuse already opened connections: