dexie-cloud-addon 4.4.3 → 4.4.4

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.
@@ -1,7 +1,7 @@
1
1
  export type DXCAlert = DXCErrorAlert | DXCWarningAlert | DXCInfoAlert;
2
2
  export interface DXCErrorAlert {
3
3
  type: 'error';
4
- messageCode: 'INVALID_OTP' | 'INVALID_EMAIL' | 'LICENSE_LIMIT_REACHED' | 'GENERIC_ERROR';
4
+ messageCode: 'INVALID_OTP' | 'INVALID_EMAIL' | 'LICENSE_LIMIT_REACHED' | 'GENERIC_ERROR' | 'USER_NOT_REGISTERED' | 'USER_NOT_ACCEPTED' | 'NO_SEATS_AVAILABLE' | 'USER_DEACTIVATED' | 'WEBHOOK_ERROR';
5
5
  message: string;
6
6
  messageParams: {
7
7
  [paramName: string]: string;
@@ -8,7 +8,7 @@
8
8
  *
9
9
  * ==========================================================================
10
10
  *
11
- * Version 4.4.3, Sat Mar 21 2026
11
+ * Version 4.4.4, Wed Mar 25 2026
12
12
  *
13
13
  * https://dexie.org
14
14
  *
@@ -3158,9 +3158,10 @@
3158
3158
  cancelLabel: null,
3159
3159
  });
3160
3160
  }
3161
- function promptForEmail(userInteraction, title, emailHint) {
3161
+ function promptForEmail(userInteraction, title, emailHint, initialAlert) {
3162
3162
  return __awaiter(this, void 0, void 0, function* () {
3163
3163
  let email = emailHint || '';
3164
+ let firstPrompt = true;
3164
3165
  // Regular expression for email validation
3165
3166
  // ^[\w-+.]+@([\w-]+\.)+[\w-]{2,10}(\sas\s[\w-+.]+@([\w-]+\.)+[\w-]{2,10})?$
3166
3167
  //
@@ -3183,19 +3184,21 @@
3183
3184
  // and GLOBAL_WRITE permissions on the database. The email will be checked on the server before
3184
3185
  // allowing it and giving out a token for email2, using the OTP sent to email1.
3185
3186
  while (!email || !/^[\w-+.]+@([\w-]+\.)+[\w-]{2,10}(\sas\s[\w-+.]+@([\w-]+\.)+[\w-]{2,10})?$/.test(email)) {
3187
+ const alerts = [];
3188
+ if (firstPrompt && initialAlert)
3189
+ alerts.push(initialAlert);
3190
+ if (email)
3191
+ alerts.push({
3192
+ type: 'error',
3193
+ messageCode: 'INVALID_EMAIL',
3194
+ message: 'Please enter a valid email address',
3195
+ messageParams: {},
3196
+ });
3197
+ firstPrompt = false;
3186
3198
  email = (yield interactWithUser(userInteraction, {
3187
3199
  type: 'email',
3188
3200
  title,
3189
- alerts: email
3190
- ? [
3191
- {
3192
- type: 'error',
3193
- messageCode: 'INVALID_EMAIL',
3194
- message: 'Please enter a valid email address',
3195
- messageParams: {},
3196
- },
3197
- ]
3198
- : [],
3201
+ alerts,
3199
3202
  fields: {
3200
3203
  email: {
3201
3204
  type: 'email',
@@ -3338,6 +3341,29 @@
3338
3341
  }
3339
3342
  }
3340
3343
 
3344
+ /** Thrown when the server rejects a user due to a policy rule.
3345
+ *
3346
+ * Unlike a generic 403, this error carries a machine-readable `code` so that
3347
+ * the addon can convert it into a DXCUserInteraction challenge rather than
3348
+ * simply throwing.
3349
+ */
3350
+ class PolicyRejectionError extends Error {
3351
+ constructor(body) {
3352
+ super(body.message);
3353
+ this.code = body.code;
3354
+ }
3355
+ get name() {
3356
+ return 'PolicyRejectionError';
3357
+ }
3358
+ }
3359
+ /** Returns true when a plain fetch Response contains a structured PolicyError body. */
3360
+ function isPolicyErrorBody(value) {
3361
+ return (typeof value === 'object' &&
3362
+ value !== null &&
3363
+ typeof value.code === 'string' &&
3364
+ typeof value.message === 'string');
3365
+ }
3366
+
3341
3367
  const SECONDS = 1000;
3342
3368
  const MINUTES = 60 * SECONDS;
3343
3369
 
@@ -3512,6 +3538,10 @@
3512
3538
  if (error instanceof OAuthRedirectError || (error === null || error === void 0 ? void 0 : error.name) === 'OAuthRedirectError') {
3513
3539
  throw error; // Re-throw without logging
3514
3540
  }
3541
+ // Policy rejections have already been shown to the user as a challenge
3542
+ if (error instanceof PolicyRejectionError || (error === null || error === void 0 ? void 0 : error.name) === 'PolicyRejectionError') {
3543
+ throw error;
3544
+ }
3515
3545
  if (error instanceof TokenErrorResponseError) {
3516
3546
  yield alertUser(userInteraction, error.title, {
3517
3547
  type: 'error',
@@ -3710,13 +3740,8 @@
3710
3740
  */
3711
3741
  function exchangeOAuthCode(options) {
3712
3742
  return __awaiter(this, void 0, void 0, function* () {
3713
- const { databaseUrl, code, publicKey, scopes = ['ACCESS_DB'] } = options;
3714
- const tokenRequest = {
3715
- grant_type: 'authorization_code',
3716
- code,
3717
- public_key: publicKey,
3718
- scopes,
3719
- };
3743
+ const { databaseUrl, code, publicKey, scopes = ['ACCESS_DB'], intent } = options;
3744
+ const tokenRequest = Object.assign({ grant_type: 'authorization_code', code, public_key: publicKey, scopes }, (intent !== undefined ? { intent } : {}));
3720
3745
  try {
3721
3746
  const res = yield fetch(`${databaseUrl}/token`, {
3722
3747
  method: 'POST',
@@ -3727,6 +3752,20 @@
3727
3752
  if (!res.ok) {
3728
3753
  // Read body once as text to avoid stream consumption issues
3729
3754
  const bodyText = yield res.text().catch(() => res.statusText);
3755
+ // Check for structured policy rejection (403 with JSON body)
3756
+ if (res.status === 403) {
3757
+ try {
3758
+ const body = JSON.parse(bodyText);
3759
+ if (isPolicyErrorBody(body)) {
3760
+ throw new PolicyRejectionError(body);
3761
+ }
3762
+ }
3763
+ catch (e) {
3764
+ if (e instanceof PolicyRejectionError)
3765
+ throw e;
3766
+ // Fall through to generic error
3767
+ }
3768
+ }
3730
3769
  if (res.status === 400 || res.status === 401) {
3731
3770
  // Try to parse error response as JSON
3732
3771
  try {
@@ -3862,32 +3901,59 @@
3862
3901
 
3863
3902
  function otpFetchTokenCallback(db) {
3864
3903
  const { userInteraction } = db.cloud;
3865
- return function otpAuthenticate(_a) {
3866
- return __awaiter(this, arguments, void 0, function* ({ public_key, hints }) {
3904
+ /**
3905
+ * Core authentication function.
3906
+ *
3907
+ * @param public_key - RSA public key PEM for the session
3908
+ * @param hints - Optional login hints from the caller
3909
+ * @param policyAlert - When set, a previous attempt was rejected by a server
3910
+ * policy rule. The alert is injected into the first
3911
+ * interactive prompt so the user sees why they were
3912
+ * rejected without changing any other flow logic.
3913
+ */
3914
+ function otpAuthenticate(_a, policyAlert_1) {
3915
+ return __awaiter(this, arguments, void 0, function* ({ public_key, hints }, policyAlert) {
3867
3916
  var _b, _c;
3868
3917
  let tokenRequest;
3869
3918
  const url = (_b = db.cloud.options) === null || _b === void 0 ? void 0 : _b.databaseUrl;
3870
3919
  if (!url)
3871
3920
  throw new Error(`No database URL given.`);
3921
+ const intent = hints === null || hints === void 0 ? void 0 : hints.intent;
3922
+ // ── Non-interactive paths ──────────────────────────────────────────────
3923
+ // These paths POST directly without prompting the user. If a policyAlert
3924
+ // exists (from a previous rejected attempt), show it with a message-alert
3925
+ // before proceeding so the user understands what happened.
3872
3926
  // Handle OAuth code exchange (from redirect/deep link flows)
3873
3927
  if ((hints === null || hints === void 0 ? void 0 : hints.oauthCode) && hints.provider) {
3874
- return yield exchangeOAuthCode({
3875
- databaseUrl: url,
3876
- code: hints.oauthCode,
3877
- publicKey: public_key,
3878
- scopes: ['ACCESS_DB'],
3879
- });
3928
+ try {
3929
+ return yield exchangeOAuthCode({
3930
+ databaseUrl: url,
3931
+ code: hints.oauthCode,
3932
+ publicKey: public_key,
3933
+ scopes: ['ACCESS_DB'],
3934
+ intent,
3935
+ });
3936
+ }
3937
+ catch (err) {
3938
+ if (err instanceof PolicyRejectionError) {
3939
+ return yield otpAuthenticate({ public_key, hints: undefined }, toPolicyAlert(err));
3940
+ }
3941
+ throw err;
3942
+ }
3880
3943
  }
3881
- // Handle OAuth provider login via redirect
3944
+ // Handle OAuth provider login via redirect (programmatic, no interaction)
3882
3945
  if (hints === null || hints === void 0 ? void 0 : hints.provider) {
3946
+ if (policyAlert) {
3947
+ // A previous OAuth attempt was rejected. Fall through to the
3948
+ // interactive flow — policyAlert will be shown inside the prompt.
3949
+ return yield otpAuthenticate({ public_key, hints: undefined }, policyAlert);
3950
+ }
3883
3951
  let resolvedRedirectUri = undefined;
3884
3952
  if (hints.redirectPath) {
3885
- // If redirectPath is absolute, use as is. If relative, resolve against current location
3886
3953
  if (/^https?:\/\//i.test(hints.redirectPath)) {
3887
3954
  resolvedRedirectUri = hints.redirectPath;
3888
3955
  }
3889
3956
  else if (typeof window !== 'undefined' && window.location) {
3890
- // Use URL constructor to resolve relative path
3891
3957
  resolvedRedirectUri = new URL(hints.redirectPath, window.location.href).toString();
3892
3958
  }
3893
3959
  else if (typeof location !== 'undefined' && location.href) {
@@ -3895,23 +3961,27 @@
3895
3961
  }
3896
3962
  }
3897
3963
  initiateOAuthRedirect(db, hints.provider, resolvedRedirectUri);
3898
- // This function never returns - page navigates away
3899
3964
  throw new OAuthRedirectError(hints.provider);
3900
3965
  }
3966
+ // ── Interactive paths ──────────────────────────────────────────────────
3967
+ // policyAlert (if set) is injected into the first prompt so the user sees
3968
+ // it alongside the normal auth UI — no separate error screen needed.
3901
3969
  if ((hints === null || hints === void 0 ? void 0 : hints.grant_type) === 'demo') {
3902
- 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));
3970
+ 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);
3903
3971
  tokenRequest = {
3904
3972
  demo_user,
3905
3973
  grant_type: 'demo',
3906
3974
  scopes: ['ACCESS_DB'],
3907
- public_key
3975
+ public_key,
3908
3976
  };
3909
3977
  }
3910
3978
  else if ((hints === null || hints === void 0 ? void 0 : hints.otpId) && hints.otp) {
3911
- // User provided OTP ID and OTP code. This means that the OTP email
3912
- // has already gone out and the user may have clicked a magic link
3913
- // in the email with otp and otpId in query and the app has picked
3914
- // up those values and passed them to db.cloud.login().
3979
+ // Magic-link flow: OTP already supplied by the caller (e.g. from email).
3980
+ // No interaction show alert as a plain message if there is one.
3981
+ if (policyAlert) {
3982
+ yield alertUser(userInteraction, 'Access Denied', policyAlert);
3983
+ return yield otpAuthenticate({ public_key, hints: undefined }, policyAlert);
3984
+ }
3915
3985
  tokenRequest = {
3916
3986
  grant_type: 'otp',
3917
3987
  otp_id: hints.otpId,
@@ -3921,56 +3991,52 @@
3921
3991
  };
3922
3992
  }
3923
3993
  else if ((hints === null || hints === void 0 ? void 0 : hints.grant_type) === 'otp' || (hints === null || hints === void 0 ? void 0 : hints.email)) {
3924
- // User explicitly requested OTP flow - skip provider selection
3925
- const email = (hints === null || hints === void 0 ? void 0 : hints.email) || (yield promptForEmail(userInteraction, 'Enter email address'));
3994
+ // Caller explicitly requested OTP skip provider selection.
3995
+ const email = (hints === null || hints === void 0 ? void 0 : hints.email) ||
3996
+ (yield promptForEmail(userInteraction, 'Enter email address', undefined, policyAlert));
3926
3997
  if (/@demo.local$/.test(email)) {
3927
3998
  tokenRequest = {
3928
3999
  demo_user: email,
3929
4000
  grant_type: 'demo',
3930
4001
  scopes: ['ACCESS_DB'],
3931
- public_key
4002
+ public_key,
3932
4003
  };
3933
4004
  }
3934
4005
  else {
3935
- tokenRequest = {
3936
- email,
3937
- grant_type: 'otp',
3938
- scopes: ['ACCESS_DB'],
3939
- };
4006
+ tokenRequest = Object.assign({ email, grant_type: 'otp', scopes: ['ACCESS_DB'] }, (intent !== undefined ? { intent } : {}));
3940
4007
  }
3941
4008
  }
3942
4009
  else {
3943
- // Check for available auth providers (OAuth + OTP)
4010
+ // Default path: check for OAuth providers, then fall back to OTP.
3944
4011
  const socialAuthEnabled = ((_c = db.cloud.options) === null || _c === void 0 ? void 0 : _c.socialAuth) !== false;
3945
4012
  const authProviders = yield fetchAuthProviders(url, socialAuthEnabled);
3946
- // If we have OAuth providers available, prompt for selection
3947
4013
  if (authProviders.providers.length > 0) {
3948
- const selection = yield promptForProvider(userInteraction, authProviders.providers, authProviders.otpEnabled, 'Sign in');
4014
+ const providerAlerts = policyAlert ? [policyAlert] : [];
4015
+ const selection = yield promptForProvider(userInteraction, authProviders.providers, authProviders.otpEnabled, 'Sign in', providerAlerts);
3949
4016
  if (selection.type === 'provider') {
3950
- // User selected an OAuth provider - initiate redirect
3951
4017
  initiateOAuthRedirect(db, selection.provider);
3952
- // This function never returns - page navigates away
3953
4018
  throw new OAuthRedirectError(selection.provider);
3954
4019
  }
3955
- // User chose OTP - continue with email prompt below
4020
+ // User chose OTP fall through to email prompt (no policyAlert here;
4021
+ // it was already shown in the provider prompt above).
3956
4022
  }
3957
- const email = yield promptForEmail(userInteraction, 'Enter email address', hints === null || hints === void 0 ? void 0 : hints.email);
4023
+ const email = yield promptForEmail(userInteraction, 'Enter email address', hints === null || hints === void 0 ? void 0 : hints.email,
4024
+ // Show policyAlert in email prompt only if there were no providers
4025
+ // (otherwise it was already shown in the provider selection above).
4026
+ authProviders.providers.length === 0 ? policyAlert : undefined);
3958
4027
  if (/@demo.local$/.test(email)) {
3959
4028
  tokenRequest = {
3960
4029
  demo_user: email,
3961
4030
  grant_type: 'demo',
3962
4031
  scopes: ['ACCESS_DB'],
3963
- public_key
4032
+ public_key,
3964
4033
  };
3965
4034
  }
3966
4035
  else {
3967
- tokenRequest = {
3968
- email,
3969
- grant_type: 'otp',
3970
- scopes: ['ACCESS_DB'],
3971
- };
4036
+ tokenRequest = Object.assign({ email, grant_type: 'otp', scopes: ['ACCESS_DB'] }, (intent !== undefined ? { intent } : {}));
3972
4037
  }
3973
4038
  }
4039
+ // ── POST /token (step 1) ───────────────────────────────────────────────
3974
4040
  const res1 = yield fetch(`${url}/token`, {
3975
4041
  body: JSON.stringify(tokenRequest),
3976
4042
  method: 'post',
@@ -3978,19 +4044,22 @@
3978
4044
  mode: 'cors',
3979
4045
  });
3980
4046
  if (res1.status !== 200) {
4047
+ const alert = yield tryParsePolicyAlert(res1);
4048
+ if (alert) {
4049
+ // Policy rejection — restart the flow with the error injected.
4050
+ return yield otpAuthenticate({ public_key, hints: undefined }, alert);
4051
+ }
3981
4052
  const errMsg = yield res1.text();
3982
- yield alertUser(userInteraction, "Token request failed", {
4053
+ yield alertUser(userInteraction, 'Token request failed', {
3983
4054
  type: 'error',
3984
4055
  messageCode: 'GENERIC_ERROR',
3985
4056
  message: errMsg,
3986
- messageParams: {}
4057
+ messageParams: {},
3987
4058
  }).catch(() => { });
3988
4059
  throw new HttpError(res1, errMsg);
3989
4060
  }
3990
4061
  const response = yield res1.json();
3991
4062
  if (response.type === 'tokens' || response.type === 'error') {
3992
- // Demo user request can get a "tokens" response right away
3993
- // Error can also be returned right away.
3994
4063
  return response;
3995
4064
  }
3996
4065
  else if (tokenRequest.grant_type === 'otp' && 'email' in tokenRequest) {
@@ -3998,6 +4067,7 @@
3998
4067
  throw new Error(`Unexpected response from ${url}/token`);
3999
4068
  const otp = yield promptForOTP(userInteraction, tokenRequest.email);
4000
4069
  const tokenRequest2 = Object.assign(Object.assign({}, tokenRequest), { otp: otp || '', otp_id: response.otp_id, public_key });
4070
+ // ── POST /token (step 2: OTP verification) ─────────────────────────
4001
4071
  let res2 = yield fetch(`${url}/token`, {
4002
4072
  body: JSON.stringify(tokenRequest2),
4003
4073
  method: 'post',
@@ -4010,7 +4080,7 @@
4010
4080
  type: 'error',
4011
4081
  messageCode: 'INVALID_OTP',
4012
4082
  message: errorText,
4013
- messageParams: {}
4083
+ messageParams: {},
4014
4084
  });
4015
4085
  res2 = yield fetch(`${url}/token`, {
4016
4086
  body: JSON.stringify(tokenRequest2),
@@ -4020,6 +4090,10 @@
4020
4090
  });
4021
4091
  }
4022
4092
  if (res2.status !== 200) {
4093
+ const alert = yield tryParsePolicyAlert(res2);
4094
+ if (alert) {
4095
+ return yield otpAuthenticate({ public_key, hints: undefined }, alert);
4096
+ }
4023
4097
  const errMsg = yield res2.text();
4024
4098
  throw new HttpError(res2, errMsg);
4025
4099
  }
@@ -4030,14 +4104,11 @@
4030
4104
  throw new Error(`Unexpected response from ${url}/token`);
4031
4105
  }
4032
4106
  });
4033
- };
4107
+ }
4108
+ return ({ public_key, hints }) => otpAuthenticate({ public_key, hints });
4034
4109
  }
4035
4110
  /**
4036
4111
  * Initiates OAuth login via full page redirect.
4037
- *
4038
- * The page will navigate away to the OAuth provider. After authentication,
4039
- * the user is redirected back with a dxc-auth query parameter that is
4040
- * automatically detected by db.cloud.configure().
4041
4112
  */
4042
4113
  function initiateOAuthRedirect(db, provider, redirectUriOverride) {
4043
4114
  var _a, _b;
@@ -4047,17 +4118,44 @@
4047
4118
  const redirectUri = redirectUriOverride ||
4048
4119
  ((_b = db.cloud.options) === null || _b === void 0 ? void 0 : _b.oauthRedirectUri) ||
4049
4120
  (typeof location !== 'undefined' ? location.href : undefined);
4050
- // CodeRabbit suggested to fail fast here, but the only situation where
4051
- // redirectUri would be undefined is in non-browser environments, and
4052
- // in those environments OAuth redirect does not make sense anyway
4053
- // and will fail fast in startOAuthRedirect().
4054
- // Start OAuth redirect flow - page navigates away
4055
4121
  startOAuthRedirect({
4056
4122
  databaseUrl: url,
4057
4123
  provider,
4058
4124
  redirectUri,
4059
4125
  });
4060
4126
  }
4127
+ /**
4128
+ * Converts a PolicyRejectionError to a DXCAlert for injection into prompts.
4129
+ */
4130
+ function toPolicyAlert(err) {
4131
+ return {
4132
+ type: 'error',
4133
+ messageCode: err.code,
4134
+ message: err.message,
4135
+ messageParams: {},
4136
+ };
4137
+ }
4138
+ /**
4139
+ * Tries to parse a failed Response as a structured PolicyError body.
4140
+ * Returns a DXCAlert if it is one, otherwise returns null.
4141
+ * Safe to call: reads body via clone() so the original Response is untouched.
4142
+ */
4143
+ function tryParsePolicyAlert(res) {
4144
+ return __awaiter(this, void 0, void 0, function* () {
4145
+ if (res.status !== 403)
4146
+ return null;
4147
+ try {
4148
+ const body = yield res.clone().json();
4149
+ if (isPolicyErrorBody(body)) {
4150
+ return toPolicyAlert(new PolicyRejectionError(body));
4151
+ }
4152
+ }
4153
+ catch (_a) {
4154
+ // Not JSON
4155
+ }
4156
+ return null;
4157
+ });
4158
+ }
4061
4159
 
4062
4160
  /** A way to log to console in production without terser stripping out
4063
4161
  * it from the release bundle.
@@ -14359,7 +14457,7 @@
14359
14457
  *
14360
14458
  * ==========================================================================
14361
14459
  *
14362
- * Version 4.4.0, Sat Mar 21 2026
14460
+ * Version 4.4.0, Wed Mar 25 2026
14363
14461
  *
14364
14462
  * https://dexie.org
14365
14463
  *
@@ -17006,6 +17104,7 @@
17006
17104
  return cursor.start(() => {
17007
17105
  const rawValue = cursor.value;
17008
17106
  if (!rawValue || !hasUnresolvedBlobRefs(rawValue)) {
17107
+ wrappedCursor.value = rawValue;
17009
17108
  onNext();
17010
17109
  return;
17011
17110
  }
@@ -17014,6 +17113,7 @@
17014
17113
  onNext();
17015
17114
  }, err => {
17016
17115
  console.error('Failed to resolve BlobRefs for cursor value:', err);
17116
+ wrappedCursor.value = rawValue;
17017
17117
  onNext();
17018
17118
  });
17019
17119
  });
@@ -19503,7 +19603,7 @@
19503
19603
  const downloading$ = createDownloadingState();
19504
19604
  dexie.cloud = {
19505
19605
  // @ts-ignore
19506
- version: "4.4.3",
19606
+ version: "4.4.4",
19507
19607
  options: Object.assign({}, DEFAULT_OPTIONS),
19508
19608
  schema: null,
19509
19609
  get currentUserId() {
@@ -19930,7 +20030,7 @@
19930
20030
  }
19931
20031
  }
19932
20032
  // @ts-ignore
19933
- dexieCloud.version = "4.4.3";
20033
+ dexieCloud.version = "4.4.4";
19934
20034
  Dexie.Cloud = dexieCloud;
19935
20035
 
19936
20036
  exports.default = dexieCloud;