radar-sdk-js 5.1.0-beta.1 → 5.1.1-beta.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.
package/README.md CHANGED
@@ -55,7 +55,7 @@ Radar.initialize({ publishableKey: 'prj_test_pk_...' });
55
55
  Add the following to your HTML:
56
56
 
57
57
  ```html
58
- <script src="https://js.radar.com/v5.1.0-beta.1/radar.min.js"></script>
58
+ <script src="https://js.radar.com/v5.1.1-beta.0/radar.min.js"></script>
59
59
 
60
60
  <script>
61
61
  Radar.initialize({ publishableKey: 'prj_test_pk_...' });
@@ -134,7 +134,7 @@ the core SDK first, then any plugins you need:
134
134
  <link href="https://js.radar.com/maps/v1.0.0/radar-maps.css" rel="stylesheet" />
135
135
  <link href="https://js.radar.com/autocomplete/v1.1.0/radar-autocomplete.css" rel="stylesheet" />
136
136
 
137
- <script src="https://js.radar.com/v5.1.0-beta.1/radar.min.js"></script>
137
+ <script src="https://js.radar.com/v5.1.1-beta.0/radar.min.js"></script>
138
138
  <script src="https://js.radar.com/maps/v1.0.0/radar-maps.min.js"></script>
139
139
  <script src="https://js.radar.com/autocomplete/v1.1.0/radar-autocomplete.min.js"></script>
140
140
  <script src="https://js.radar.com/fraud/v1.0.0/radar-fraud.min.js"></script>
@@ -151,7 +151,7 @@ by ID or element reference.
151
151
  <html>
152
152
  <head>
153
153
  <link href="https://js.radar.com/maps/v1.0.0/radar-maps.css" rel="stylesheet" />
154
- <script src="https://js.radar.com/v5.1.0-beta.1/radar.min.js"></script>
154
+ <script src="https://js.radar.com/v5.1.1-beta.0/radar.min.js"></script>
155
155
  <script src="https://js.radar.com/maps/v1.0.0/radar-maps.min.js"></script>
156
156
  </head>
157
157
 
@@ -178,7 +178,7 @@ by ID or element reference.
178
178
  <html>
179
179
  <head>
180
180
  <link href="https://js.radar.com/autocomplete/v1.1.0/radar-autocomplete.css" rel="stylesheet" />
181
- <script src="https://js.radar.com/v5.1.0-beta.1/radar.min.js"></script>
181
+ <script src="https://js.radar.com/v5.1.1-beta.0/radar.min.js"></script>
182
182
  <script src="https://js.radar.com/autocomplete/v1.1.0/radar-autocomplete.min.js"></script>
183
183
  </head>
184
184
 
@@ -211,7 +211,7 @@ are needed for geofencing.
211
211
  ```html
212
212
  <html>
213
213
  <head>
214
- <script src="https://js.radar.com/v5.1.0-beta.1/radar.min.js"></script>
214
+ <script src="https://js.radar.com/v5.1.1-beta.0/radar.min.js"></script>
215
215
  </head>
216
216
 
217
217
  <body>
package/dist/errors.d.ts CHANGED
@@ -1,6 +1,4 @@
1
- import type { DnsProbeResult } from './dns-over-https';
2
1
  import type { RadarResponse } from './http';
3
- export type { DnsProbeResult };
4
2
  /** base error class for all Radar SDK errors */
5
3
  export declare abstract class RadarError extends Error {
6
4
  /** legacy status code string (e.g. `'ERROR_PUBLISHABLE_KEY'`) */
@@ -83,59 +81,11 @@ export declare class RadarServerError extends RadarError {
83
81
  readonly response?: RadarResponse;
84
82
  constructor(response?: RadarResponse);
85
83
  }
86
- /**
87
- * Diagnostics captured when fetch() rejects before any HTTP response is received.
88
- * Safe to stringify for logs — does not include request headers or bodies.
89
- */
90
- export interface RadarNetworkFailureDetails {
91
- readonly phase: 'fetch';
92
- readonly method: string;
93
- readonly url: string;
94
- readonly pathname: string;
95
- readonly search: string;
96
- /** hostname parsed from the request URL */
97
- readonly apiHostname?: string;
98
- /** document origin when in a browser */
99
- readonly pageOrigin?: string;
100
- /** document pathname when in a browser */
101
- readonly pagePath?: string;
102
- /** `navigator.userAgent` when available */
103
- readonly userAgent?: string;
104
- /** true when API hostname differs from the document hostname */
105
- readonly crossSiteApiCall?: boolean;
106
- /**
107
- * Wall-clock ms from immediately before `fetch()` until it rejected (rounded integer).
108
- * Does not include JSON parse time when a response was received.
109
- */
110
- readonly durationMs: number;
111
- /** whether `navigator.onLine` was false at failure time */
112
- readonly online: boolean;
113
- /** true when failure was triggered by AbortController / user abort */
114
- readonly aborted: boolean;
115
- readonly errorName: string;
116
- readonly errorMessage: string;
117
- /** present when the environment threw an Error with a stack */
118
- readonly errorStack?: string;
119
- readonly connectionEffectiveType?: string;
120
- readonly connectionDownlink?: number;
121
- readonly connectionRtt?: number;
122
- readonly connectionSaveData?: boolean;
123
- readonly requestId?: string;
124
- }
125
84
  /** thrown when a request times out or the network is unavailable */
126
85
  export declare class RadarNetworkError extends RadarError {
127
86
  readonly name = "RadarNetworkError";
128
87
  readonly status = "ERROR_NETWORK";
129
- /** set when fetch() fails in the SDK HTTP client (omit for API `ERROR_NETWORK` responses) */
130
- readonly details?: RadarNetworkFailureDetails;
131
- /** original rejection from fetch; often a `TypeError` or `DOMException` */
132
- readonly fetchError?: unknown;
133
- /**
134
- * Resolves when the optional Cloudflare DNS-over-HTTPS (A record) probe finishes.
135
- * Only set for non-aborted fetch failures where a probe was scheduled (`null` if skipped in tests, no hostname, or a probe already ran for this host).
136
- */
137
- readonly dnsProbe?: Promise<DnsProbeResult>;
138
- constructor(message?: string, details?: RadarNetworkFailureDetails, fetchError?: unknown, dnsProbe?: Promise<DnsProbeResult> | null);
88
+ constructor();
139
89
  }
140
90
  /** thrown for unexpected/unclassified errors */
141
91
  export declare class RadarUnknownError extends RadarError {
package/dist/index.d.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  export { default, default as Radar } from './api';
2
2
  export * as errors from './errors';
3
3
  export type * from './types';
4
- export type { DnsProbeResult } from './errors';
5
4
  export type { RadarPlugin, RadarPluginContext } from './plugin';
package/dist/logger.d.ts CHANGED
@@ -6,7 +6,7 @@ declare class Logger {
6
6
  static info(message: string): void;
7
7
  /** log a warning-level message */
8
8
  static warn(message: string): void;
9
- /** log an error-level message; optional second argument for structured diagnostics (printed as a separate console.error arg) */
10
- static error(message: string, data?: unknown): void;
9
+ /** log an error-level message */
10
+ static error(message: string): void;
11
11
  }
12
12
  export default Logger;
package/dist/radar.js CHANGED
@@ -1,4 +1,4 @@
1
- var SDK_VERSION = '5.1.0-beta.1';
1
+ var SDK_VERSION = '5.1.1-beta.0';
2
2
 
3
3
  /** global SDK configuration singleton */
4
4
  class Config {
@@ -57,167 +57,6 @@ Config.defaultOptions = {
57
57
  debug: false,
58
58
  };
59
59
 
60
- const LOG_LEVELS = {
61
- none: 0,
62
- error: 1,
63
- warn: 2,
64
- info: 3,
65
- debug: 4,
66
- };
67
- // get the numeric level for logLevel option
68
- const getLevel = () => {
69
- // disable logging in tests
70
- if (typeof window !== 'undefined' && window.RADAR_TEST_ENV) {
71
- return LOG_LEVELS.none;
72
- }
73
- const { logLevel } = Config.get();
74
- return logLevel ? LOG_LEVELS[logLevel] : LOG_LEVELS.error;
75
- };
76
- /** leveled console logger controlled by SDK config */
77
- class Logger {
78
- /** log a debug-level message (only when debug mode is enabled) */
79
- static debug(message, options) {
80
- if (getLevel() === LOG_LEVELS.debug) {
81
- console.log(`Radar SDK (debug): ${message.trim()}`, options);
82
- }
83
- }
84
- /** log an info-level message */
85
- static info(message) {
86
- if (getLevel() >= LOG_LEVELS.info) {
87
- console.log(`Radar SDK: ${message.trim()}`);
88
- }
89
- }
90
- /** log a warning-level message */
91
- static warn(message) {
92
- if (getLevel() >= LOG_LEVELS.warn) {
93
- console.warn(`Radar SDK: ${message.trim()}`);
94
- }
95
- }
96
- /** log an error-level message; optional second argument for structured diagnostics (printed as a separate console.error arg) */
97
- static error(message, data) {
98
- if (getLevel() >= LOG_LEVELS.error) {
99
- const trimmed = message.trim();
100
- if (data !== undefined) {
101
- console.error(`Radar SDK: ${trimmed}`, data);
102
- }
103
- else {
104
- console.error(`Radar SDK: ${trimmed}`);
105
- }
106
- }
107
- }
108
- }
109
-
110
- const CLOUDFLARE_DOH_JSON = 'https://cloudflare-dns.com/dns-query';
111
- /** hostname -> already ran a probe this session */
112
- const dnsProbeScheduledForHostname = new Set();
113
- function getDnsProbeAbortSignal() {
114
- if (typeof AbortSignal !== 'undefined' && typeof AbortSignal.timeout === 'function') {
115
- return AbortSignal.timeout(8000);
116
- }
117
- return undefined;
118
- }
119
- async function dnsLookupAJson(hostname) {
120
- const query = new URL(CLOUDFLARE_DOH_JSON);
121
- query.searchParams.set('name', hostname);
122
- query.searchParams.set('type', 'A');
123
- const res = await fetch(query.toString(), {
124
- headers: { Accept: 'application/dns-json' },
125
- signal: getDnsProbeAbortSignal(),
126
- });
127
- if (!res.ok) {
128
- return { kind: 'http_error', httpStatus: res.status };
129
- }
130
- return { kind: 'ok', data: (await res.json()) };
131
- }
132
- async function runDnsOverHttpsProbe(hostname, correlation) {
133
- try {
134
- const outcome = await dnsLookupAJson(hostname);
135
- if (outcome.kind === 'http_error') {
136
- const result = {
137
- status: 'http_error',
138
- hostname,
139
- resolver: 'cloudflare-dns.com',
140
- httpStatus: outcome.httpStatus,
141
- };
142
- Logger.error(`DNS-over-HTTPS HTTP error (${hostname})`, {
143
- resolver: 'cloudflare-dns.com',
144
- httpStatus: outcome.httpStatus,
145
- correlation,
146
- });
147
- return result;
148
- }
149
- const data = outcome.data;
150
- const ips = data.Status === 0 && Array.isArray(data.Answer) ? data.Answer.map((a) => a.data).filter(Boolean) : [];
151
- if (ips.length > 0) {
152
- const result = {
153
- status: 'success',
154
- hostname,
155
- resolver: 'cloudflare-dns.com',
156
- dnsStatus: data.Status,
157
- ipv4Answers: ips,
158
- };
159
- Logger.error(`DNS-over-HTTPS (${hostname})`, {
160
- resolver: 'cloudflare-dns.com',
161
- dnsStatus: data.Status,
162
- ipv4Answers: ips,
163
- correlation,
164
- });
165
- return result;
166
- }
167
- const note = data.Status !== 0
168
- ? `Resolver returned DNS status ${String(data.Status)} (non-zero)`
169
- : 'No IPv4 Answer records returned';
170
- const result = {
171
- status: 'no_answers',
172
- hostname,
173
- resolver: 'cloudflare-dns.com',
174
- dnsStatus: data.Status,
175
- answerCount: Array.isArray(data.Answer) ? data.Answer.length : 0,
176
- note,
177
- };
178
- Logger.error(`DNS-over-HTTPS (${hostname})`, {
179
- resolver: 'cloudflare-dns.com',
180
- dnsStatus: data.Status,
181
- answerCount: result.answerCount,
182
- correlation,
183
- note,
184
- });
185
- return result;
186
- }
187
- catch (err) {
188
- const message = err instanceof Error ? err.message : String(err);
189
- const result = {
190
- status: 'fetch_error',
191
- hostname,
192
- resolver: 'cloudflare-dns.com',
193
- errorMessage: message,
194
- };
195
- Logger.error(`DNS-over-HTTPS probe failed (${hostname})`, {
196
- resolver: 'cloudflare-dns.com',
197
- errorMessage: message,
198
- correlation,
199
- });
200
- return result;
201
- }
202
- }
203
- /**
204
- * Runs at most one DoH probe per hostname per page load. Returns a promise you can await
205
- * from `RadarNetworkError.dnsProbe` — does not block the throwing error path.
206
- */
207
- function scheduleDnsOverHttpsProbe(hostname, correlation) {
208
- if (!hostname || hostname.length === 0) {
209
- return null;
210
- }
211
- if (typeof window !== 'undefined' && window.RADAR_TEST_ENV === true) {
212
- return null;
213
- }
214
- if (dnsProbeScheduledForHostname.has(hostname)) {
215
- return null;
216
- }
217
- dnsProbeScheduledForHostname.add(hostname);
218
- return runDnsOverHttpsProbe(hostname, correlation);
219
- }
220
-
221
60
  /** base error class for all Radar SDK errors */
222
61
  class RadarError extends Error {
223
62
  constructor(message) {
@@ -320,13 +159,10 @@ class RadarServerError extends RadarError {
320
159
  }
321
160
  /** thrown when a request times out or the network is unavailable */
322
161
  class RadarNetworkError extends RadarError {
323
- constructor(message, details, fetchError, dnsProbe) {
324
- super(message ?? 'Request timed out.');
162
+ constructor() {
163
+ super('Request timed out.');
325
164
  this.name = 'RadarNetworkError';
326
165
  this.status = 'ERROR_NETWORK';
327
- this.details = details;
328
- this.fetchError = fetchError;
329
- this.dnsProbe = dnsProbe ?? undefined;
330
166
  }
331
167
  }
332
168
  /** thrown for unexpected/unclassified errors */
@@ -356,6 +192,50 @@ var errors = /*#__PURE__*/Object.freeze({
356
192
  RadarUnknownError: RadarUnknownError
357
193
  });
358
194
 
195
+ const LOG_LEVELS = {
196
+ none: 0,
197
+ error: 1,
198
+ warn: 2,
199
+ info: 3,
200
+ debug: 4,
201
+ };
202
+ // get the numeric level for logLevel option
203
+ const getLevel = () => {
204
+ // disable logging in tests
205
+ if (typeof window !== 'undefined' && window.RADAR_TEST_ENV) {
206
+ return LOG_LEVELS.none;
207
+ }
208
+ const { logLevel } = Config.get();
209
+ return logLevel ? LOG_LEVELS[logLevel] : LOG_LEVELS.error;
210
+ };
211
+ /** leveled console logger controlled by SDK config */
212
+ class Logger {
213
+ /** log a debug-level message (only when debug mode is enabled) */
214
+ static debug(message, options) {
215
+ if (getLevel() === LOG_LEVELS.debug) {
216
+ console.log(`Radar SDK (debug): ${message.trim()}`, options);
217
+ }
218
+ }
219
+ /** log an info-level message */
220
+ static info(message) {
221
+ if (getLevel() >= LOG_LEVELS.info) {
222
+ console.log(`Radar SDK: ${message.trim()}`);
223
+ }
224
+ }
225
+ /** log a warning-level message */
226
+ static warn(message) {
227
+ if (getLevel() >= LOG_LEVELS.warn) {
228
+ console.warn(`Radar SDK: ${message.trim()}`);
229
+ }
230
+ }
231
+ /** log an error-level message */
232
+ static error(message) {
233
+ if (getLevel() >= LOG_LEVELS.error) {
234
+ console.error(`Radar SDK: ${message.trim()}`);
235
+ }
236
+ }
237
+ }
238
+
359
239
  /** typed localStorage wrapper with `radar-*` namespaced keys */
360
240
  class Storage {
361
241
  /** localStorage key for user ID */
@@ -588,108 +468,6 @@ class Navigator {
588
468
  }
589
469
 
590
470
  const inFlightRequests = new Map();
591
- /** gather optional Network Information API snapshot (best-effort) */
592
- function getConnectionSnapshot(nav) {
593
- if (!nav) {
594
- return {};
595
- }
596
- const c = nav.connection;
597
- if (!c) {
598
- return {};
599
- }
600
- return {
601
- ...(c.effectiveType !== undefined ? { connectionEffectiveType: String(c.effectiveType) } : {}),
602
- ...(typeof c.downlink === 'number' ? { connectionDownlink: c.downlink } : {}),
603
- ...(typeof c.rtt === 'number' ? { connectionRtt: c.rtt } : {}),
604
- ...(typeof c.saveData === 'boolean' ? { connectionSaveData: c.saveData } : {}),
605
- };
606
- }
607
- function isAbortError(err, signal) {
608
- if (signal.aborted) {
609
- return true;
610
- }
611
- if (err instanceof DOMException && err.name === 'AbortError') {
612
- return true;
613
- }
614
- return err instanceof Error && err.name === 'AbortError';
615
- }
616
- function parseThrowable(err) {
617
- if (err instanceof Error) {
618
- return { name: err.name, message: err.message, stack: err.stack };
619
- }
620
- if (typeof err === 'string') {
621
- return { name: 'string', message: err };
622
- }
623
- return { name: 'non_error', message: String(err) };
624
- }
625
- /** monotonic-ish timestamp in ms (`performance.now` when available; else `Date.now`) */
626
- function getHighResTime() {
627
- return typeof performance !== 'undefined' && typeof performance.now === 'function' ? performance.now() : Date.now();
628
- }
629
- function buildFetchFailureDetails(method, url, requestId, err, signal,
630
- /** elapsed ms from immediately before `fetch()` until rejection */
631
- durationMs) {
632
- let pathname = '';
633
- let search = '';
634
- let apiHostname;
635
- try {
636
- const parsed = new URL(url);
637
- pathname = parsed.pathname;
638
- search = parsed.search;
639
- apiHostname = parsed.hostname;
640
- }
641
- catch {
642
- pathname = url;
643
- }
644
- const { name, message, stack } = parseThrowable(err);
645
- const aborted = isAbortError(err, signal);
646
- const nav = typeof navigator !== 'undefined' ? navigator : undefined;
647
- let pageOrigin;
648
- let pagePath;
649
- let userAgent;
650
- let crossSiteApiCall;
651
- if (typeof window !== 'undefined' && window.location && nav) {
652
- pageOrigin = window.location.origin;
653
- pagePath = window.location.pathname === '' ? '/' : window.location.pathname;
654
- userAgent = nav.userAgent;
655
- if (apiHostname !== undefined && window.location.hostname !== '') {
656
- crossSiteApiCall = window.location.hostname !== apiHostname;
657
- }
658
- }
659
- const online = Boolean(nav && nav.onLine);
660
- return {
661
- phase: 'fetch',
662
- method,
663
- url,
664
- pathname,
665
- search,
666
- durationMs: Math.max(0, Math.round(durationMs)),
667
- ...(apiHostname !== undefined ? { apiHostname } : {}),
668
- ...(pageOrigin !== undefined ? { pageOrigin } : {}),
669
- ...(pagePath !== undefined ? { pagePath } : {}),
670
- ...(userAgent !== undefined ? { userAgent } : {}),
671
- ...(crossSiteApiCall !== undefined ? { crossSiteApiCall } : {}),
672
- online,
673
- aborted,
674
- errorName: name,
675
- errorMessage: message,
676
- ...(stack ? { errorStack: stack } : {}),
677
- ...getConnectionSnapshot(nav),
678
- ...(requestId ? { requestId } : {}),
679
- };
680
- }
681
- function fetchFailureUserMessage(details) {
682
- if (details.aborted) {
683
- return 'Request aborted.';
684
- }
685
- if (details.errorMessage) {
686
- return details.errorMessage;
687
- }
688
- if (!details.online) {
689
- return 'Network unavailable (browser offline).';
690
- }
691
- return 'Network request failed.';
692
- }
693
471
  /** fetch-based HTTP client for Radar API requests */
694
472
  class Http {
695
473
  /**
@@ -714,7 +492,10 @@ class Http {
714
492
  let body;
715
493
  if (method === 'GET') {
716
494
  const params = new URLSearchParams(Object.entries(filtered).map(([k, v]) => [k, String(v)]));
717
- const qs = params.toString();
495
+ // URLSearchParams serializes spaces as '+' (application/x-www-form-urlencoded)
496
+ // a query string is part of a URI, so encode per RFC 3986 (%20)
497
+ // the only difference is really how spaces are handled between the two protocols
498
+ const qs = params.toString().replace(/\+/g, '%20');
718
499
  if (qs) {
719
500
  url = `${url}?${qs}`;
720
501
  }
@@ -736,7 +517,6 @@ class Http {
736
517
  ...headers,
737
518
  };
738
519
  let response;
739
- const fetchStartedAt = getHighResTime();
740
520
  try {
741
521
  response = await fetch(url, {
742
522
  method,
@@ -745,35 +525,19 @@ class Http {
745
525
  signal: abortController.signal,
746
526
  });
747
527
  }
748
- catch (fetchErr) {
749
- let dnsProbePromise = null;
750
- const durationMs = getHighResTime() - fetchStartedAt;
528
+ catch {
751
529
  // Delete abort controller instance for this request ID if it hasn't yet been replaced with a different one
752
530
  if (requestId && inFlightRequests.get(requestId) === abortController) {
753
531
  inFlightRequests.delete(requestId);
754
532
  }
755
- const fetchFailureDetails = buildFetchFailureDetails(method, url, requestId, fetchErr, abortController.signal, durationMs);
756
- Logger.error(`Fetch failed (${fetchFailureDetails.aborted ? 'aborted' : fetchFailureDetails.errorName})`, fetchFailureDetails);
757
- if (!fetchFailureDetails.aborted) {
758
- dnsProbePromise = scheduleDnsOverHttpsProbe(fetchFailureDetails.apiHostname, {
759
- requestFailure: {
760
- method: fetchFailureDetails.method,
761
- url: fetchFailureDetails.url,
762
- durationMs: fetchFailureDetails.durationMs,
763
- phase: fetchFailureDetails.phase,
764
- errorName: fetchFailureDetails.errorName,
765
- errorMessage: fetchFailureDetails.errorMessage,
766
- crossSiteApiCall: fetchFailureDetails.crossSiteApiCall,
767
- },
768
- });
769
- }
770
- for (const [pattern, interceptorHandler] of Http.errorInterceptors) {
771
- if ((urlHost ?? '').includes(pattern)) {
772
- throw interceptorHandler(!!Navigator.online());
533
+ if (host) {
534
+ for (const [pattern, handler] of Http.errorInterceptors) {
535
+ if (host.includes(pattern)) {
536
+ throw handler(!!Navigator.online());
537
+ }
773
538
  }
774
539
  }
775
- const userMsg = fetchFailureUserMessage(fetchFailureDetails);
776
- throw new RadarNetworkError(userMsg, fetchFailureDetails, fetchErr, dnsProbePromise);
540
+ throw new RadarNetworkError();
777
541
  }
778
542
  if (requestId && inFlightRequests.get(requestId) === abortController) {
779
543
  inFlightRequests.delete(requestId);
@@ -808,12 +572,6 @@ class Http {
808
572
  throw new RadarLocationError('Could not determine location.');
809
573
  }
810
574
  else if (error === 'ERROR_NETWORK') {
811
- Logger.error('Radar API returned ERROR_NETWORK in response meta', {
812
- httpStatus: response.status,
813
- method,
814
- url,
815
- meta: parsed.meta ?? null,
816
- });
817
575
  throw new RadarNetworkError();
818
576
  }
819
577
  }
@@ -1664,7 +1422,7 @@ class Radar {
1664
1422
  static initialize(publishableKeyOrOptions, extraOptions = {}) {
1665
1423
  // NOTE(jasonl): shim the string signature into the options object to handle both cases
1666
1424
  const options = typeof publishableKeyOrOptions === 'string'
1667
- ? { ...extraOptions, publishableKey: publishableKeyOrOptions }
1425
+ ? { publishableKey: publishableKeyOrOptions, ...extraOptions }
1668
1426
  : { ...publishableKeyOrOptions };
1669
1427
  if (options.publishableKey && options.authToken) {
1670
1428
  throw new RadarPublishableKeyError('Token and publishableKey are mutually exclusive.');