dexie-cloud-addon 4.4.2 → 4.4.4-alpha.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.
@@ -8,7 +8,7 @@
8
8
  *
9
9
  * ==========================================================================
10
10
  *
11
- * Version 4.4.2, Thu Mar 19 2026
11
+ * Version 4.4.4-alpha.0, Tue Mar 24 2026
12
12
  *
13
13
  * https://dexie.org
14
14
  *
@@ -3145,9 +3145,10 @@
3145
3145
  cancelLabel: null,
3146
3146
  });
3147
3147
  }
3148
- function promptForEmail(userInteraction, title, emailHint) {
3148
+ function promptForEmail(userInteraction, title, emailHint, initialAlert) {
3149
3149
  return __awaiter(this, void 0, void 0, function* () {
3150
3150
  let email = emailHint || '';
3151
+ let firstPrompt = true;
3151
3152
  // Regular expression for email validation
3152
3153
  // ^[\w-+.]+@([\w-]+\.)+[\w-]{2,10}(\sas\s[\w-+.]+@([\w-]+\.)+[\w-]{2,10})?$
3153
3154
  //
@@ -3170,19 +3171,21 @@
3170
3171
  // and GLOBAL_WRITE permissions on the database. The email will be checked on the server before
3171
3172
  // allowing it and giving out a token for email2, using the OTP sent to email1.
3172
3173
  while (!email || !/^[\w-+.]+@([\w-]+\.)+[\w-]{2,10}(\sas\s[\w-+.]+@([\w-]+\.)+[\w-]{2,10})?$/.test(email)) {
3174
+ const alerts = [];
3175
+ if (firstPrompt && initialAlert)
3176
+ alerts.push(initialAlert);
3177
+ if (email)
3178
+ alerts.push({
3179
+ type: 'error',
3180
+ messageCode: 'INVALID_EMAIL',
3181
+ message: 'Please enter a valid email address',
3182
+ messageParams: {},
3183
+ });
3184
+ firstPrompt = false;
3173
3185
  email = (yield interactWithUser(userInteraction, {
3174
3186
  type: 'email',
3175
3187
  title,
3176
- alerts: email
3177
- ? [
3178
- {
3179
- type: 'error',
3180
- messageCode: 'INVALID_EMAIL',
3181
- message: 'Please enter a valid email address',
3182
- messageParams: {},
3183
- },
3184
- ]
3185
- : [],
3188
+ alerts,
3186
3189
  fields: {
3187
3190
  email: {
3188
3191
  type: 'email',
@@ -3325,6 +3328,29 @@
3325
3328
  }
3326
3329
  }
3327
3330
 
3331
+ /** Thrown when the server rejects a user due to a policy rule.
3332
+ *
3333
+ * Unlike a generic 403, this error carries a machine-readable `code` so that
3334
+ * the addon can convert it into a DXCUserInteraction challenge rather than
3335
+ * simply throwing.
3336
+ */
3337
+ class PolicyRejectionError extends Error {
3338
+ constructor(body) {
3339
+ super(body.message);
3340
+ this.code = body.code;
3341
+ }
3342
+ get name() {
3343
+ return 'PolicyRejectionError';
3344
+ }
3345
+ }
3346
+ /** Returns true when a plain fetch Response contains a structured PolicyError body. */
3347
+ function isPolicyErrorBody(value) {
3348
+ return (typeof value === 'object' &&
3349
+ value !== null &&
3350
+ typeof value.code === 'string' &&
3351
+ typeof value.message === 'string');
3352
+ }
3353
+
3328
3354
  const SECONDS = 1000;
3329
3355
  const MINUTES = 60 * SECONDS;
3330
3356
 
@@ -3499,6 +3525,10 @@
3499
3525
  if (error instanceof OAuthRedirectError || (error === null || error === void 0 ? void 0 : error.name) === 'OAuthRedirectError') {
3500
3526
  throw error; // Re-throw without logging
3501
3527
  }
3528
+ // Policy rejections have already been shown to the user as a challenge
3529
+ if (error instanceof PolicyRejectionError || (error === null || error === void 0 ? void 0 : error.name) === 'PolicyRejectionError') {
3530
+ throw error;
3531
+ }
3502
3532
  if (error instanceof TokenErrorResponseError) {
3503
3533
  yield alertUser(userInteraction, error.title, {
3504
3534
  type: 'error',
@@ -13558,7 +13588,7 @@
13558
13588
  *
13559
13589
  * ==========================================================================
13560
13590
  *
13561
- * Version 4.4.0, Thu Mar 19 2026
13591
+ * Version 4.4.0, Tue Mar 24 2026
13562
13592
  *
13563
13593
  * https://dexie.org
13564
13594
  *
@@ -15645,13 +15675,8 @@
15645
15675
  */
15646
15676
  function exchangeOAuthCode(options) {
15647
15677
  return __awaiter(this, void 0, void 0, function* () {
15648
- const { databaseUrl, code, publicKey, scopes = ['ACCESS_DB'] } = options;
15649
- const tokenRequest = {
15650
- grant_type: 'authorization_code',
15651
- code,
15652
- public_key: publicKey,
15653
- scopes,
15654
- };
15678
+ const { databaseUrl, code, publicKey, scopes = ['ACCESS_DB'], intent } = options;
15679
+ const tokenRequest = Object.assign({ grant_type: 'authorization_code', code, public_key: publicKey, scopes }, (intent !== undefined ? { intent } : {}));
15655
15680
  try {
15656
15681
  const res = yield fetch(`${databaseUrl}/token`, {
15657
15682
  method: 'POST',
@@ -15662,6 +15687,20 @@
15662
15687
  if (!res.ok) {
15663
15688
  // Read body once as text to avoid stream consumption issues
15664
15689
  const bodyText = yield res.text().catch(() => res.statusText);
15690
+ // Check for structured policy rejection (403 with JSON body)
15691
+ if (res.status === 403) {
15692
+ try {
15693
+ const body = JSON.parse(bodyText);
15694
+ if (isPolicyErrorBody(body)) {
15695
+ throw new PolicyRejectionError(body);
15696
+ }
15697
+ }
15698
+ catch (e) {
15699
+ if (e instanceof PolicyRejectionError)
15700
+ throw e;
15701
+ // Fall through to generic error
15702
+ }
15703
+ }
15665
15704
  if (res.status === 400 || res.status === 401) {
15666
15705
  // Try to parse error response as JSON
15667
15706
  try {
@@ -15797,32 +15836,59 @@
15797
15836
 
15798
15837
  function otpFetchTokenCallback(db) {
15799
15838
  const { userInteraction } = db.cloud;
15800
- return function otpAuthenticate(_a) {
15801
- return __awaiter(this, arguments, void 0, function* ({ public_key, hints }) {
15839
+ /**
15840
+ * Core authentication function.
15841
+ *
15842
+ * @param public_key - RSA public key PEM for the session
15843
+ * @param hints - Optional login hints from the caller
15844
+ * @param policyAlert - When set, a previous attempt was rejected by a server
15845
+ * policy rule. The alert is injected into the first
15846
+ * interactive prompt so the user sees why they were
15847
+ * rejected without changing any other flow logic.
15848
+ */
15849
+ function otpAuthenticate(_a, policyAlert_1) {
15850
+ return __awaiter(this, arguments, void 0, function* ({ public_key, hints }, policyAlert) {
15802
15851
  var _b, _c;
15803
15852
  let tokenRequest;
15804
15853
  const url = (_b = db.cloud.options) === null || _b === void 0 ? void 0 : _b.databaseUrl;
15805
15854
  if (!url)
15806
15855
  throw new Error(`No database URL given.`);
15856
+ const intent = hints === null || hints === void 0 ? void 0 : hints.intent;
15857
+ // ── Non-interactive paths ──────────────────────────────────────────────
15858
+ // These paths POST directly without prompting the user. If a policyAlert
15859
+ // exists (from a previous rejected attempt), show it with a message-alert
15860
+ // before proceeding so the user understands what happened.
15807
15861
  // Handle OAuth code exchange (from redirect/deep link flows)
15808
15862
  if ((hints === null || hints === void 0 ? void 0 : hints.oauthCode) && hints.provider) {
15809
- return yield exchangeOAuthCode({
15810
- databaseUrl: url,
15811
- code: hints.oauthCode,
15812
- publicKey: public_key,
15813
- scopes: ['ACCESS_DB'],
15814
- });
15863
+ try {
15864
+ return yield exchangeOAuthCode({
15865
+ databaseUrl: url,
15866
+ code: hints.oauthCode,
15867
+ publicKey: public_key,
15868
+ scopes: ['ACCESS_DB'],
15869
+ intent,
15870
+ });
15871
+ }
15872
+ catch (err) {
15873
+ if (err instanceof PolicyRejectionError) {
15874
+ return yield otpAuthenticate({ public_key, hints: undefined }, toPolicyAlert(err));
15875
+ }
15876
+ throw err;
15877
+ }
15815
15878
  }
15816
- // Handle OAuth provider login via redirect
15879
+ // Handle OAuth provider login via redirect (programmatic, no interaction)
15817
15880
  if (hints === null || hints === void 0 ? void 0 : hints.provider) {
15881
+ if (policyAlert) {
15882
+ // A previous OAuth attempt was rejected. Fall through to the
15883
+ // interactive flow — policyAlert will be shown inside the prompt.
15884
+ return yield otpAuthenticate({ public_key, hints: undefined }, policyAlert);
15885
+ }
15818
15886
  let resolvedRedirectUri = undefined;
15819
15887
  if (hints.redirectPath) {
15820
- // If redirectPath is absolute, use as is. If relative, resolve against current location
15821
15888
  if (/^https?:\/\//i.test(hints.redirectPath)) {
15822
15889
  resolvedRedirectUri = hints.redirectPath;
15823
15890
  }
15824
15891
  else if (typeof window !== 'undefined' && window.location) {
15825
- // Use URL constructor to resolve relative path
15826
15892
  resolvedRedirectUri = new URL(hints.redirectPath, window.location.href).toString();
15827
15893
  }
15828
15894
  else if (typeof location !== 'undefined' && location.href) {
@@ -15830,23 +15896,27 @@
15830
15896
  }
15831
15897
  }
15832
15898
  initiateOAuthRedirect(db, hints.provider, resolvedRedirectUri);
15833
- // This function never returns - page navigates away
15834
15899
  throw new OAuthRedirectError(hints.provider);
15835
15900
  }
15901
+ // ── Interactive paths ──────────────────────────────────────────────────
15902
+ // policyAlert (if set) is injected into the first prompt so the user sees
15903
+ // it alongside the normal auth UI — no separate error screen needed.
15836
15904
  if ((hints === null || hints === void 0 ? void 0 : hints.grant_type) === 'demo') {
15837
- 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));
15905
+ 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), policyAlert);
15838
15906
  tokenRequest = {
15839
15907
  demo_user,
15840
15908
  grant_type: 'demo',
15841
15909
  scopes: ['ACCESS_DB'],
15842
- public_key
15910
+ public_key,
15843
15911
  };
15844
15912
  }
15845
15913
  else if ((hints === null || hints === void 0 ? void 0 : hints.otpId) && hints.otp) {
15846
- // User provided OTP ID and OTP code. This means that the OTP email
15847
- // has already gone out and the user may have clicked a magic link
15848
- // in the email with otp and otpId in query and the app has picked
15849
- // up those values and passed them to db.cloud.login().
15914
+ // Magic-link flow: OTP already supplied by the caller (e.g. from email).
15915
+ // No interaction show alert as a plain message if there is one.
15916
+ if (policyAlert) {
15917
+ yield alertUser(userInteraction, 'Access Denied', policyAlert);
15918
+ return yield otpAuthenticate({ public_key, hints: undefined }, policyAlert);
15919
+ }
15850
15920
  tokenRequest = {
15851
15921
  grant_type: 'otp',
15852
15922
  otp_id: hints.otpId,
@@ -15856,56 +15926,52 @@
15856
15926
  };
15857
15927
  }
15858
15928
  else if ((hints === null || hints === void 0 ? void 0 : hints.grant_type) === 'otp' || (hints === null || hints === void 0 ? void 0 : hints.email)) {
15859
- // User explicitly requested OTP flow - skip provider selection
15860
- const email = (hints === null || hints === void 0 ? void 0 : hints.email) || (yield promptForEmail(userInteraction, 'Enter email address'));
15929
+ // Caller explicitly requested OTP skip provider selection.
15930
+ const email = (hints === null || hints === void 0 ? void 0 : hints.email) ||
15931
+ (yield promptForEmail(userInteraction, 'Enter email address', undefined, policyAlert));
15861
15932
  if (/@demo.local$/.test(email)) {
15862
15933
  tokenRequest = {
15863
15934
  demo_user: email,
15864
15935
  grant_type: 'demo',
15865
15936
  scopes: ['ACCESS_DB'],
15866
- public_key
15937
+ public_key,
15867
15938
  };
15868
15939
  }
15869
15940
  else {
15870
- tokenRequest = {
15871
- email,
15872
- grant_type: 'otp',
15873
- scopes: ['ACCESS_DB'],
15874
- };
15941
+ tokenRequest = Object.assign({ email, grant_type: 'otp', scopes: ['ACCESS_DB'] }, (intent !== undefined ? { intent } : {}));
15875
15942
  }
15876
15943
  }
15877
15944
  else {
15878
- // Check for available auth providers (OAuth + OTP)
15945
+ // Default path: check for OAuth providers, then fall back to OTP.
15879
15946
  const socialAuthEnabled = ((_c = db.cloud.options) === null || _c === void 0 ? void 0 : _c.socialAuth) !== false;
15880
15947
  const authProviders = yield fetchAuthProviders(url, socialAuthEnabled);
15881
- // If we have OAuth providers available, prompt for selection
15882
15948
  if (authProviders.providers.length > 0) {
15883
- const selection = yield promptForProvider(userInteraction, authProviders.providers, authProviders.otpEnabled, 'Sign in');
15949
+ const providerAlerts = policyAlert ? [policyAlert] : [];
15950
+ const selection = yield promptForProvider(userInteraction, authProviders.providers, authProviders.otpEnabled, 'Sign in', providerAlerts);
15884
15951
  if (selection.type === 'provider') {
15885
- // User selected an OAuth provider - initiate redirect
15886
15952
  initiateOAuthRedirect(db, selection.provider);
15887
- // This function never returns - page navigates away
15888
15953
  throw new OAuthRedirectError(selection.provider);
15889
15954
  }
15890
- // User chose OTP - continue with email prompt below
15955
+ // User chose OTP fall through to email prompt (no policyAlert here;
15956
+ // it was already shown in the provider prompt above).
15891
15957
  }
15892
- const email = yield promptForEmail(userInteraction, 'Enter email address', hints === null || hints === void 0 ? void 0 : hints.email);
15958
+ const email = yield promptForEmail(userInteraction, 'Enter email address', hints === null || hints === void 0 ? void 0 : hints.email,
15959
+ // Show policyAlert in email prompt only if there were no providers
15960
+ // (otherwise it was already shown in the provider selection above).
15961
+ authProviders.providers.length === 0 ? policyAlert : undefined);
15893
15962
  if (/@demo.local$/.test(email)) {
15894
15963
  tokenRequest = {
15895
15964
  demo_user: email,
15896
15965
  grant_type: 'demo',
15897
15966
  scopes: ['ACCESS_DB'],
15898
- public_key
15967
+ public_key,
15899
15968
  };
15900
15969
  }
15901
15970
  else {
15902
- tokenRequest = {
15903
- email,
15904
- grant_type: 'otp',
15905
- scopes: ['ACCESS_DB'],
15906
- };
15971
+ tokenRequest = Object.assign({ email, grant_type: 'otp', scopes: ['ACCESS_DB'] }, (intent !== undefined ? { intent } : {}));
15907
15972
  }
15908
15973
  }
15974
+ // ── POST /token (step 1) ───────────────────────────────────────────────
15909
15975
  const res1 = yield fetch(`${url}/token`, {
15910
15976
  body: JSON.stringify(tokenRequest),
15911
15977
  method: 'post',
@@ -15913,19 +15979,22 @@
15913
15979
  mode: 'cors',
15914
15980
  });
15915
15981
  if (res1.status !== 200) {
15982
+ const alert = yield tryParsePolicyAlert(res1);
15983
+ if (alert) {
15984
+ // Policy rejection — restart the flow with the error injected.
15985
+ return yield otpAuthenticate({ public_key, hints: undefined }, alert);
15986
+ }
15916
15987
  const errMsg = yield res1.text();
15917
- yield alertUser(userInteraction, "Token request failed", {
15988
+ yield alertUser(userInteraction, 'Token request failed', {
15918
15989
  type: 'error',
15919
15990
  messageCode: 'GENERIC_ERROR',
15920
15991
  message: errMsg,
15921
- messageParams: {}
15992
+ messageParams: {},
15922
15993
  }).catch(() => { });
15923
15994
  throw new HttpError(res1, errMsg);
15924
15995
  }
15925
15996
  const response = yield res1.json();
15926
15997
  if (response.type === 'tokens' || response.type === 'error') {
15927
- // Demo user request can get a "tokens" response right away
15928
- // Error can also be returned right away.
15929
15998
  return response;
15930
15999
  }
15931
16000
  else if (tokenRequest.grant_type === 'otp' && 'email' in tokenRequest) {
@@ -15933,6 +16002,7 @@
15933
16002
  throw new Error(`Unexpected response from ${url}/token`);
15934
16003
  const otp = yield promptForOTP(userInteraction, tokenRequest.email);
15935
16004
  const tokenRequest2 = Object.assign(Object.assign({}, tokenRequest), { otp: otp || '', otp_id: response.otp_id, public_key });
16005
+ // ── POST /token (step 2: OTP verification) ─────────────────────────
15936
16006
  let res2 = yield fetch(`${url}/token`, {
15937
16007
  body: JSON.stringify(tokenRequest2),
15938
16008
  method: 'post',
@@ -15945,7 +16015,7 @@
15945
16015
  type: 'error',
15946
16016
  messageCode: 'INVALID_OTP',
15947
16017
  message: errorText,
15948
- messageParams: {}
16018
+ messageParams: {},
15949
16019
  });
15950
16020
  res2 = yield fetch(`${url}/token`, {
15951
16021
  body: JSON.stringify(tokenRequest2),
@@ -15955,6 +16025,10 @@
15955
16025
  });
15956
16026
  }
15957
16027
  if (res2.status !== 200) {
16028
+ const alert = yield tryParsePolicyAlert(res2);
16029
+ if (alert) {
16030
+ return yield otpAuthenticate({ public_key, hints: undefined }, alert);
16031
+ }
15958
16032
  const errMsg = yield res2.text();
15959
16033
  throw new HttpError(res2, errMsg);
15960
16034
  }
@@ -15965,14 +16039,11 @@
15965
16039
  throw new Error(`Unexpected response from ${url}/token`);
15966
16040
  }
15967
16041
  });
15968
- };
16042
+ }
16043
+ return ({ public_key, hints }) => otpAuthenticate({ public_key, hints });
15969
16044
  }
15970
16045
  /**
15971
16046
  * Initiates OAuth login via full page redirect.
15972
- *
15973
- * The page will navigate away to the OAuth provider. After authentication,
15974
- * the user is redirected back with a dxc-auth query parameter that is
15975
- * automatically detected by db.cloud.configure().
15976
16047
  */
15977
16048
  function initiateOAuthRedirect(db, provider, redirectUriOverride) {
15978
16049
  var _a, _b;
@@ -15982,17 +16053,44 @@
15982
16053
  const redirectUri = redirectUriOverride ||
15983
16054
  ((_b = db.cloud.options) === null || _b === void 0 ? void 0 : _b.oauthRedirectUri) ||
15984
16055
  (typeof location !== 'undefined' ? location.href : undefined);
15985
- // CodeRabbit suggested to fail fast here, but the only situation where
15986
- // redirectUri would be undefined is in non-browser environments, and
15987
- // in those environments OAuth redirect does not make sense anyway
15988
- // and will fail fast in startOAuthRedirect().
15989
- // Start OAuth redirect flow - page navigates away
15990
16056
  startOAuthRedirect({
15991
16057
  databaseUrl: url,
15992
16058
  provider,
15993
16059
  redirectUri,
15994
16060
  });
15995
16061
  }
16062
+ /**
16063
+ * Converts a PolicyRejectionError to a DXCAlert for injection into prompts.
16064
+ */
16065
+ function toPolicyAlert(err) {
16066
+ return {
16067
+ type: 'error',
16068
+ messageCode: err.code,
16069
+ message: err.message,
16070
+ messageParams: {},
16071
+ };
16072
+ }
16073
+ /**
16074
+ * Tries to parse a failed Response as a structured PolicyError body.
16075
+ * Returns a DXCAlert if it is one, otherwise returns null.
16076
+ * Safe to call: reads body via clone() so the original Response is untouched.
16077
+ */
16078
+ function tryParsePolicyAlert(res) {
16079
+ return __awaiter(this, void 0, void 0, function* () {
16080
+ if (res.status !== 403)
16081
+ return null;
16082
+ try {
16083
+ const body = yield res.clone().json();
16084
+ if (isPolicyErrorBody(body)) {
16085
+ return toPolicyAlert(new PolicyRejectionError(body));
16086
+ }
16087
+ }
16088
+ catch (_a) {
16089
+ // Not JSON
16090
+ }
16091
+ return null;
16092
+ });
16093
+ }
15996
16094
 
15997
16095
  /** A way to log to console in production without terser stripping out
15998
16096
  * it from the release bundle.
@@ -17735,7 +17833,7 @@
17735
17833
  const searchParams = new URLSearchParams();
17736
17834
  if (this.subscriber.closed)
17737
17835
  return;
17738
- searchParams.set('v', '2');
17836
+ searchParams.set('v', '3'); // v3 = supports BlobRef (blob offloading)
17739
17837
  if (this.rev)
17740
17838
  searchParams.set('rev', this.rev);
17741
17839
  if (this.yrev)
@@ -19332,7 +19430,7 @@
19332
19430
  const downloading$ = createDownloadingState();
19333
19431
  dexie.cloud = {
19334
19432
  // @ts-ignore
19335
- version: "4.4.2",
19433
+ version: "4.4.4-alpha.0",
19336
19434
  options: Object.assign({}, DEFAULT_OPTIONS),
19337
19435
  schema: null,
19338
19436
  get currentUserId() {
@@ -19759,7 +19857,7 @@
19759
19857
  }
19760
19858
  }
19761
19859
  // @ts-ignore
19762
- dexieCloud.version = "4.4.2";
19860
+ dexieCloud.version = "4.4.4-alpha.0";
19763
19861
  Dexie.Cloud = dexieCloud;
19764
19862
 
19765
19863
  // In case the SW lives for a while, let it reuse already opened connections: