@jbrowse/plugin-authentication 4.3.0 → 5.0.0-beta.1

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 (50) hide show
  1. package/esm/DropboxOAuthModel/DropboxIcon.d.ts +1 -1
  2. package/esm/DropboxOAuthModel/configSchema.d.ts +110 -82
  3. package/esm/DropboxOAuthModel/configSchema.js +32 -2
  4. package/esm/DropboxOAuthModel/index.js +2 -2
  5. package/esm/DropboxOAuthModel/model.d.ts +235 -201
  6. package/esm/DropboxOAuthModel/model.js +46 -21
  7. package/esm/DropboxOAuthModel/util.d.ts +1 -1
  8. package/esm/DropboxOAuthModel/util.js +9 -12
  9. package/esm/ExternalTokenModel/ExternalTokenEntryForm.d.ts +1 -1
  10. package/esm/ExternalTokenModel/ExternalTokenEntryForm.js +11 -15
  11. package/esm/ExternalTokenModel/configSchema.d.ts +43 -24
  12. package/esm/ExternalTokenModel/configSchema.js +22 -1
  13. package/esm/ExternalTokenModel/index.js +2 -2
  14. package/esm/ExternalTokenModel/model.d.ts +47 -71
  15. package/esm/ExternalTokenModel/model.js +11 -54
  16. package/esm/GoogleDriveOAuthModel/GoogleDriveFilehandle.d.ts +2 -9
  17. package/esm/GoogleDriveOAuthModel/GoogleDriveFilehandle.js +27 -6
  18. package/esm/GoogleDriveOAuthModel/GoogleDriveIcon.d.ts +1 -1
  19. package/esm/GoogleDriveOAuthModel/configSchema.d.ts +109 -82
  20. package/esm/GoogleDriveOAuthModel/configSchema.js +32 -3
  21. package/esm/GoogleDriveOAuthModel/index.js +2 -2
  22. package/esm/GoogleDriveOAuthModel/model.d.ts +230 -202
  23. package/esm/GoogleDriveOAuthModel/model.js +45 -22
  24. package/esm/GoogleDriveOAuthModel/util.d.ts +1 -1
  25. package/esm/GoogleDriveOAuthModel/util.js +2 -11
  26. package/esm/HTTPBasicModel/HTTPBasicLoginForm.d.ts +1 -1
  27. package/esm/HTTPBasicModel/HTTPBasicLoginForm.js +17 -22
  28. package/esm/HTTPBasicModel/configSchema.d.ts +51 -28
  29. package/esm/HTTPBasicModel/configSchema.js +26 -1
  30. package/esm/HTTPBasicModel/index.js +2 -2
  31. package/esm/HTTPBasicModel/model.d.ts +55 -76
  32. package/esm/HTTPBasicModel/model.js +21 -51
  33. package/esm/OAuthModel/OAuthLoginPrompt.d.ts +4 -0
  34. package/esm/OAuthModel/OAuthLoginPrompt.js +12 -0
  35. package/esm/OAuthModel/configSchema.d.ts +79 -52
  36. package/esm/OAuthModel/configSchema.js +30 -1
  37. package/esm/OAuthModel/index.js +2 -2
  38. package/esm/OAuthModel/model.d.ts +218 -114
  39. package/esm/OAuthModel/model.js +361 -217
  40. package/esm/OAuthModel/util.d.ts +46 -7
  41. package/esm/OAuthModel/util.js +159 -16
  42. package/esm/index.d.ts +295 -981
  43. package/esm/index.js +29 -45
  44. package/esm/lazyLoginForms.d.ts +18 -0
  45. package/esm/lazyLoginForms.js +25 -0
  46. package/esm/tokenEntryModelFactory.d.ts +125 -0
  47. package/esm/tokenEntryModelFactory.js +85 -0
  48. package/esm/util.d.ts +10 -0
  49. package/esm/util.js +24 -2
  50. package/package.json +10 -13
@@ -1,280 +1,424 @@
1
- import { ConfigurationReference, getConf } from '@jbrowse/core/configuration';
1
+ import { ConfigurationReference, readConfObject, } from '@jbrowse/core/configuration';
2
2
  import { InternetAccount } from '@jbrowse/core/pluggableElementTypes/models';
3
- import { isElectron } from '@jbrowse/core/util';
4
- import { types } from '@jbrowse/mobx-state-tree';
5
- import { generateChallenge, processError, processTokenResponse, toBase64Url, } from "./util.js";
6
- import { getResponseError } from "../util.js";
3
+ import { isElectron, isSessionModel, localStorageGetItem, localStorageRemoveItem, localStorageSetItem, sha256Base64Url, toBase64Url, } from '@jbrowse/core/util';
4
+ import { getRoot, types } from '@jbrowse/mobx-state-tree';
5
+ import { OAuthLoginPrompt } from '../lazyLoginForms.js';
6
+ import { getResponseError } from '../util.js';
7
+ import { finishOAuthRedirect, finishOAuthWindow, getOAuthRedirectUri, parseOAuthError, waitForOAuthMessage, } from './util.js';
8
+ function randomBytes(length) {
9
+ return globalThis.crypto.getRandomValues(new Uint8Array(length));
10
+ }
11
+ // Whether a popup opened right now would be one the browser lets through.
12
+ // `in`, because the property is typed as always present but Firefox has no
13
+ // implementation — and a browser that cannot answer gets the attempt.
14
+ const hasTransientActivation = () => 'userActivation' in navigator ? navigator.userActivation.isActive : true;
15
+ /**
16
+ * Puts the login window behind a click, for the flow that has no user
17
+ * activation to open one with — a session loading a track of an authenticated
18
+ * resource, which is every share link to one. Rejects if the user dismisses
19
+ * the prompt, and `getToken` drops the cached promise on a rejection so a
20
+ * later read asks again.
21
+ */
22
+ function promptForLoginWindow({ internetAccountId, session, openLoginWindow, waitForToken, }) {
23
+ const blocked = new Error(`Could not open the ${internetAccountId} login window. Allow popups for this site and try again.`);
24
+ // an embedded root need not have a session to queue the prompt on, and a
25
+ // popup that never opens used to leave this promise pending forever — which
26
+ // `getToken` caches, so the account stayed wedged for the rest of the
27
+ // session with nothing shown to the user
28
+ if (!isSessionModel(session)) {
29
+ throw blocked;
30
+ }
31
+ return new Promise((resolve, reject) => {
32
+ session.queueDialog(doneCallback => [
33
+ OAuthLoginPrompt,
34
+ {
35
+ internetAccountId,
36
+ handleClose: (proceed) => {
37
+ // opened before doneCallback, so the open still runs inside the
38
+ // click whose activation it is spending
39
+ const popup = proceed ? openLoginWindow() : null;
40
+ doneCallback();
41
+ if (popup) {
42
+ resolve(waitForToken(popup));
43
+ }
44
+ else {
45
+ reject(proceed ? blocked : new Error('OAuth login prompt was dismissed'));
46
+ }
47
+ },
48
+ },
49
+ ]);
50
+ });
51
+ }
52
+ /**
53
+ * #stateModel OAuthInternetAccount
54
+ */
7
55
  const stateModelFactory = (configSchema) => {
8
56
  return InternetAccount.named('OAuthInternetAccount')
9
57
  .props({
58
+ /**
59
+ * #property
60
+ */
10
61
  type: types.literal('OAuthInternetAccount'),
62
+ /**
63
+ * #property
64
+ */
11
65
  configuration: ConfigurationReference(configSchema),
12
66
  })
13
- .views(() => {
14
- let codeVerifier = undefined;
15
- return {
16
- get codeVerifierPKCE() {
17
- if (!codeVerifier) {
18
- const array = new Uint8Array(32);
19
- globalThis.crypto.getRandomValues(array);
20
- codeVerifier = toBase64Url(array);
21
- }
22
- return codeVerifier;
23
- },
24
- };
25
- })
26
67
  .views(self => ({
68
+ /**
69
+ * #getter
70
+ * The config typed off the concrete schema. `ConfigurationReference`
71
+ * erases `self.configuration` to `any` (the reference's MST instance brand
72
+ * doesn't carry the schema's slot definitions), so reads go through this
73
+ * getter to recover per-slot types and slot-name validation.
74
+ */
75
+ get conf() {
76
+ return self.configuration;
77
+ },
78
+ }))
79
+ .views(self => ({
80
+ /**
81
+ * #getter
82
+ */
27
83
  get authEndpoint() {
28
- return getConf(self, 'authEndpoint');
84
+ return readConfObject(self.conf, 'authEndpoint');
29
85
  },
86
+ /**
87
+ * #getter
88
+ */
30
89
  get tokenEndpoint() {
31
- return getConf(self, 'tokenEndpoint');
90
+ return readConfObject(self.conf, 'tokenEndpoint');
32
91
  },
92
+ /**
93
+ * #getter
94
+ */
33
95
  get needsPKCE() {
34
- return getConf(self, 'needsPKCE');
96
+ return readConfObject(self.conf, 'needsPKCE');
35
97
  },
98
+ /**
99
+ * #getter
100
+ */
36
101
  get clientId() {
37
- return getConf(self, 'clientId');
102
+ return readConfObject(self.conf, 'clientId');
38
103
  },
104
+ /**
105
+ * #getter
106
+ */
39
107
  get scopes() {
40
- return getConf(self, 'scopes');
108
+ return readConfObject(self.conf, 'scopes');
41
109
  },
42
- state() {
43
- return getConf(self, 'state');
110
+ /**
111
+ * #getter
112
+ * OAuth state parameter:
113
+ * https://www.rfc-editor.org/rfc/rfc6749#section-4.1.1
114
+ *
115
+ * Can override or extend if dynamic state is needed.
116
+ */
117
+ get state() {
118
+ return readConfObject(self.conf, 'state');
44
119
  },
120
+ /**
121
+ * #getter
122
+ */
45
123
  get responseType() {
46
- return getConf(self, 'responseType');
124
+ return readConfObject(self.conf, 'responseType');
47
125
  },
126
+ /**
127
+ * #getter
128
+ */
48
129
  get refreshTokenKey() {
49
130
  return `${self.internetAccountId}-refreshToken`;
50
131
  },
132
+ /**
133
+ * #getter
134
+ * Extra parameters to add to the authorization request. Empty here;
135
+ * a provider that needs one of its own overrides this.
136
+ */
137
+ get authFlowParams() {
138
+ return {};
139
+ },
51
140
  }))
52
141
  .actions(self => ({
142
+ /**
143
+ * #action
144
+ */
145
+ // through the guarded helpers rather than `localStorage` directly: this
146
+ // runs on someone else's page in the embedded products, where reading the
147
+ // global at all throws if third-party storage is blocked. A refresh token
148
+ // that cannot be cached only costs an extra auth prompt; a throw here
149
+ // takes down the whole auth flow.
53
150
  storeRefreshToken(refreshToken) {
54
- localStorage.setItem(self.refreshTokenKey, refreshToken);
151
+ localStorageSetItem(self.refreshTokenKey, refreshToken);
55
152
  },
153
+ /**
154
+ * #action
155
+ */
56
156
  removeRefreshToken() {
57
- localStorage.removeItem(self.refreshTokenKey);
157
+ localStorageRemoveItem(self.refreshTokenKey);
58
158
  },
159
+ /**
160
+ * #method
161
+ */
59
162
  retrieveRefreshToken() {
60
- return localStorage.getItem(self.refreshTokenKey);
61
- },
62
- async exchangeAuthorizationForAccessToken(token, redirectUri) {
63
- const params = new URLSearchParams(Object.entries({
64
- code: token,
65
- grant_type: 'authorization_code',
66
- client_id: self.clientId,
67
- redirect_uri: redirectUri,
68
- ...(self.needsPKCE ? { code_verifier: self.codeVerifierPKCE } : {}),
69
- }));
70
- const response = await fetch(self.tokenEndpoint, {
71
- method: 'POST',
72
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
73
- body: params.toString(),
74
- });
75
- if (!response.ok) {
76
- throw new Error(await getResponseError({
77
- response,
78
- reason: 'Failed to obtain token',
79
- }));
80
- }
81
- const data = await response.json();
82
- return processTokenResponse(data, token => {
83
- this.storeRefreshToken(token);
84
- });
163
+ return localStorageGetItem(self.refreshTokenKey);
85
164
  },
86
- async exchangeRefreshForAccessToken(refreshToken) {
165
+ }))
166
+ .actions(self => ({
167
+ /**
168
+ * #action
169
+ * POST a grant to the token endpoint and read the access token out of the
170
+ * answer. Both grants this account makes — trading the authorization code
171
+ * on the way in, trading the refresh token when the access token expires
172
+ * — are the same form-encoded request to the same endpoint answered by
173
+ * the same body, and OAuth 2 says so (RFC 6749 §4.1.3, §6). They differ
174
+ * in the grant's own parameters and in what a failure means, which is
175
+ * what stays at the call sites.
176
+ *
177
+ * @param grant - the grant-specific parameters; `client_id` is added
178
+ * @param describeError - turns a failed exchange into a message, and is
179
+ * where a grant does whatever invalidating its failure implies
180
+ */
181
+ async postTokenGrant(grant, describeError) {
87
182
  const response = await fetch(self.tokenEndpoint, {
88
183
  method: 'POST',
89
184
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
90
- body: new URLSearchParams(Object.entries({
91
- grant_type: 'refresh_token',
92
- refresh_token: refreshToken,
185
+ body: new URLSearchParams({
93
186
  client_id: self.clientId,
94
- })).toString(),
187
+ ...grant,
188
+ }).toString(),
95
189
  });
96
190
  if (!response.ok) {
97
- self.removeToken();
98
- const text = await response.text();
99
- throw new Error(await getResponseError({
100
- response,
101
- statusText: processError(text, () => {
102
- this.removeRefreshToken();
103
- }),
104
- }));
191
+ throw new Error(await describeError(response));
105
192
  }
106
- const data = await response.json();
107
- return processTokenResponse(data, token => {
108
- this.storeRefreshToken(token);
109
- });
193
+ const data = (await response.json());
194
+ // Either grant may rotate the refresh token, and a provider that does
195
+ // so invalidates the old one — so this is not specific to the code
196
+ // exchange even though that is where one first arrives.
197
+ if (data.refresh_token) {
198
+ self.storeRefreshToken(data.refresh_token);
199
+ }
200
+ return data.access_token;
110
201
  },
111
202
  }))
112
203
  .actions(self => {
113
- let listener;
114
- let exchangedTokenPromise = undefined;
204
+ // Shared across concurrent exchangeRefreshForAccessToken calls so parallel
205
+ // 401s all wait on the same refresh request rather than each triggering a
206
+ // separate one.
207
+ let exchangePromise;
115
208
  return {
116
- addMessageChannel(resolve, reject) {
117
- listener = event => {
118
- this.finishOAuthWindow(event, resolve, reject);
119
- };
120
- window.addEventListener('message', listener);
121
- },
122
- deleteMessageChannel() {
123
- window.removeEventListener('message', listener);
209
+ /**
210
+ * #action
211
+ */
212
+ async exchangeAuthorizationForAccessToken(code, redirectUri,
213
+ // required, not optional: an omitted verifier is a silently
214
+ // PKCE-less exchange, and the caller running the flow is the only
215
+ // thing that knows whether this one used PKCE
216
+ codeVerifier) {
217
+ return self.postTokenGrant({
218
+ grant_type: 'authorization_code',
219
+ code,
220
+ redirect_uri: redirectUri,
221
+ ...(codeVerifier ? { code_verifier: codeVerifier } : {}),
222
+ }, response => getResponseError({ response, reason: 'Failed to obtain token' }));
124
223
  },
125
- async finishOAuthWindow(event, resolve, reject) {
126
- if (event.data.name !== `JBrowseAuthWindow-${self.internetAccountId}`) {
127
- this.deleteMessageChannel();
128
- return;
129
- }
130
- const redirectUriWithInfo = event.data.redirectUri;
131
- const fixedQueryString = redirectUriWithInfo.replace('#', '?');
132
- const redirectUrl = new URL(fixedQueryString);
133
- const queryStringSearch = redirectUrl.search;
134
- const urlParams = new URLSearchParams(queryStringSearch);
135
- if (urlParams.has('access_token')) {
136
- const token = urlParams.get('access_token');
137
- if (!token) {
138
- reject(new Error('Error with token endpoint'));
139
- return;
224
+ /**
225
+ * #action
226
+ */
227
+ async exchangeRefreshForAccessToken(refreshToken) {
228
+ const inFlight = (exchangePromise ??= self.postTokenGrant({ grant_type: 'refresh_token', refresh_token: refreshToken }, async (response) => {
229
+ // the access token this was going to replace is gone either way,
230
+ // and an invalid_grant means the refresh token itself is spent
231
+ self.removeToken();
232
+ const { isInvalidGrant, statusText } = parseOAuthError(await response.text());
233
+ if (isInvalidGrant) {
234
+ self.removeRefreshToken();
140
235
  }
141
- self.storeToken(token);
142
- resolve(token);
143
- return;
236
+ return getResponseError({ response, statusText });
237
+ }));
238
+ try {
239
+ return await inFlight;
144
240
  }
145
- if (urlParams.has('code')) {
146
- const code = urlParams.get('code');
147
- if (!code) {
148
- reject(new Error('Error with authorization endpoint'));
149
- return;
241
+ finally {
242
+ // only if it is still ours: every awaiter of a shared exchange runs
243
+ // this, and a later caller can have started its own between two of
244
+ // them — clearing unconditionally then dropped that one from the
245
+ // slot and let the next 401 open a second refresh in parallel
246
+ if (exchangePromise === inFlight) {
247
+ exchangePromise = undefined;
150
248
  }
151
- try {
152
- const token = await self.exchangeAuthorizationForAccessToken(code, redirectUrl.origin + redirectUrl.pathname);
153
- self.storeToken(token);
154
- resolve(token);
155
- return;
156
- }
157
- catch (e) {
158
- if (e instanceof Error) {
159
- reject(e);
160
- }
161
- else {
162
- reject(new Error(String(e)));
163
- }
164
- return;
165
- }
166
- }
167
- if (redirectUriWithInfo.includes('access_denied')) {
168
- reject(new Error('OAuth flow was cancelled'));
169
- return;
170
- }
171
- if (redirectUriWithInfo.includes('error')) {
172
- reject(new Error(`OAuth flow error: ${queryStringSearch}`));
173
- return;
174
249
  }
175
- this.deleteMessageChannel();
176
250
  },
177
- async useEndpointForAuthorization(resolve, reject) {
178
- const redirectUri = isElectron
179
- ? 'http://localhost/auth'
180
- : window.location.origin + window.location.pathname;
181
- const data = {
182
- client_id: self.clientId,
183
- redirect_uri: redirectUri,
184
- response_type: self.responseType,
185
- token_access_type: 'offline',
186
- };
187
- if (self.state()) {
188
- data.state = self.state();
189
- }
190
- if (self.scopes) {
191
- data.scope = self.scopes;
251
+ };
252
+ })
253
+ .actions(self => ({
254
+ /**
255
+ * #action
256
+ * Prove a token against the resource and, if that fails, refresh it once
257
+ * and prove the new one. Returns whichever token worked; throws if
258
+ * neither does. Every OAuth account validates this way and they differ
259
+ * only in what a probe is — a HEAD of the resource here, a metadata call
260
+ * for Dropbox and Google Drive.
261
+ *
262
+ * @param token - the token to prove
263
+ * @param probe - issues a request that succeeds iff the token is good
264
+ * @param describeError - turns a failed probe into a message
265
+ */
266
+ async validateTokenWithProbe(token, probe, describeError) {
267
+ const response = await probe(token);
268
+ if (response.ok) {
269
+ return token;
270
+ }
271
+ // Every path out of here leaves this token unusable, so drop it before
272
+ // taking any of them — that is what lets the next getToken re-prompt.
273
+ // Without it an account with no refresh token to fall back on (Google
274
+ // Drive's implicit flow issues none, and its access tokens last an
275
+ // hour) kept handing the expired token back out of getToken's cached
276
+ // promise: every later request threw this same error, and nothing
277
+ // short of a page reload ever asked the user to log in again.
278
+ self.removeToken();
279
+ const refreshToken = self.retrieveRefreshToken();
280
+ if (!refreshToken) {
281
+ throw new Error(await describeError(response, 'Error validating token'));
282
+ }
283
+ // Exactly once. Recursing here instead looped forever on a resource
284
+ // that fails for a reason no new token fixes — a file the account
285
+ // cannot see — refreshing against the token endpoint on every pass.
286
+ const newToken = await self.exchangeRefreshForAccessToken(refreshToken);
287
+ const retry = await probe(newToken);
288
+ if (!retry.ok) {
289
+ throw new Error(await describeError(retry, 'Error validating token'));
290
+ }
291
+ // removeToken above dropped the cached promise, so storing is enough
292
+ self.storeToken(newToken);
293
+ return newToken;
294
+ },
295
+ /**
296
+ * #action
297
+ * Opens the provider's auth page and returns a promise for the resulting
298
+ * token. For Electron, drives the flow directly via IPC; for web, opens a
299
+ * popup and waits for the redirect message.
300
+ */
301
+ async getTokenViaAuthFlow() {
302
+ // the slot defaults to '', and `new URL('')` reports only
303
+ // "TypeError: Invalid URL:" — say which account is unconfigured
304
+ if (!self.authEndpoint) {
305
+ throw new Error(`Internet account ${self.internetAccountId} has no authEndpoint configured`);
306
+ }
307
+ const redirectUri = isElectron
308
+ ? 'http://localhost/auth'
309
+ : window.location.origin + window.location.pathname;
310
+ // RFC 6749 §10.12: without a state there is nothing tying the redirect
311
+ // back to the request this window started, so a page that can post to
312
+ // us can hand us its own authorization. A configured state is a
313
+ // constant and so guessable — generate a per-flow nonce unless one was
314
+ // set explicitly. Providers are required to echo it back.
315
+ const state = self.state || toBase64Url(randomBytes(16));
316
+ // RFC 7636 §4.1 wants one verifier per authorization request, and this
317
+ // is the request — so it lives here rather than on the model, where a
318
+ // single lazily-created one was shared by every flow the account ever
319
+ // ran. `state` immediately above was already rotating for the same
320
+ // reason.
321
+ const codeVerifier = self.needsPKCE
322
+ ? toBase64Url(randomBytes(32))
323
+ : undefined;
324
+ const codeChallenge = codeVerifier
325
+ ? await sha256Base64Url(codeVerifier)
326
+ : undefined;
327
+ const data = {
328
+ client_id: self.clientId,
329
+ redirect_uri: redirectUri,
330
+ response_type: self.responseType,
331
+ state,
332
+ ...self.authFlowParams,
333
+ ...(self.scopes ? { scope: self.scopes } : {}),
334
+ ...(codeChallenge
335
+ ? { code_challenge: codeChallenge, code_challenge_method: 'S256' }
336
+ : {}),
337
+ };
338
+ const url = new URL(self.authEndpoint);
339
+ url.search = new URLSearchParams(data).toString();
340
+ const oauthParams = {
341
+ internetAccountId: self.internetAccountId,
342
+ expectedState: state,
343
+ exchangeAuthorizationCode: async (code, uri) => self.exchangeAuthorizationForAccessToken(code, uri, codeVerifier),
344
+ storeToken: token => {
345
+ self.storeToken(token);
346
+ },
347
+ };
348
+ if (isElectron) {
349
+ // @ts-ignore - electron injects require onto window
350
+ const { ipcRenderer } = window.require('electron');
351
+ const electronRedirectUri = await ipcRenderer.invoke('openAuthWindow', {
352
+ internetAccountId: self.internetAccountId,
353
+ data,
354
+ url: url.toString(),
355
+ });
356
+ // the main process resolves undefined when the user closes the auth
357
+ // window without completing the flow
358
+ if (electronRedirectUri === undefined) {
359
+ throw new Error('OAuth flow was cancelled');
192
360
  }
193
- if (self.needsPKCE) {
194
- data.code_challenge = await generateChallenge(self.codeVerifierPKCE);
195
- data.code_challenge_method = 'S256';
361
+ const token = await finishOAuthRedirect(electronRedirectUri, oauthParams);
362
+ if (token === undefined) {
363
+ throw new Error('Electron OAuth flow returned no token');
196
364
  }
197
- const params = new URLSearchParams(Object.entries(data));
198
- const url = new URL(self.authEndpoint);
199
- url.search = params.toString();
200
- const eventName = `JBrowseAuthWindow-${self.internetAccountId}`;
201
- if (isElectron) {
202
- const { ipcRenderer } = window.require('electron');
203
- const redirectUri = await ipcRenderer.invoke('openAuthWindow', {
365
+ return token;
366
+ }
367
+ else {
368
+ const openLoginWindow = () => window.open(url, `JBrowseAuthWindow-${self.internetAccountId}`, 'width=500,height=600,left=0,top=0');
369
+ // no await between the open and this listener, so the redirect
370
+ // message cannot be delivered before we are listening
371
+ const waitForToken = (popup) => waitForOAuthMessage(event => finishOAuthWindow(event, oauthParams), {
372
+ popup,
373
+ isOwnMessage: event => getOAuthRedirectUri(event, self.internetAccountId) !==
374
+ undefined,
375
+ });
376
+ // A popup only opens while the page holds transient activation, and
377
+ // a session that loads an authenticated track has none — so the open
378
+ // is what trips the pop-up blocker, on a share link the user did
379
+ // nothing else to. Ask for the click that carries the activation
380
+ // instead, and don't spend the blocked attempt first where the
381
+ // browser will tell us there is no activation to spend.
382
+ const popup = hasTransientActivation() ? openLoginWindow() : null;
383
+ return popup
384
+ ? waitForToken(popup)
385
+ : promptForLoginWindow({
204
386
  internetAccountId: self.internetAccountId,
205
- data,
206
- url: url.toString(),
207
- });
208
- const eventFromDesktop = new MessageEvent('message', {
209
- data: { name: eventName, redirectUri: redirectUri },
387
+ session: getRoot(self).session,
388
+ openLoginWindow,
389
+ waitForToken,
210
390
  });
211
- this.finishOAuthWindow(eventFromDesktop, resolve, reject);
212
- }
213
- else {
214
- window.open(url, eventName, 'width=500,height=600,left=0,top=0');
215
- }
216
- },
217
- async getTokenFromUser(resolve, reject) {
391
+ }
392
+ },
393
+ }))
394
+ .actions(self => ({
395
+ /**
396
+ * #action
397
+ */
398
+ async getTokenFromUser(resolve, reject) {
399
+ try {
218
400
  const refreshToken = self.retrieveRefreshToken();
219
- let doUserFlow = true;
401
+ let token;
220
402
  if (refreshToken) {
221
403
  try {
222
- const token = await self.exchangeRefreshForAccessToken(refreshToken);
223
- resolve(token);
224
- doUserFlow = false;
404
+ token = await self.exchangeRefreshForAccessToken(refreshToken);
225
405
  }
226
406
  catch (e) {
227
407
  console.error(e);
228
- self.removeRefreshToken();
229
408
  }
230
409
  }
231
- if (doUserFlow) {
232
- this.addMessageChannel(resolve, reject);
233
- this.useEndpointForAuthorization(resolve, reject);
234
- }
235
- },
236
- async validateToken(token, location) {
237
- const newInit = self.addAuthHeaderToInit({ method: 'HEAD' }, token);
238
- const response = await fetch(location.uri, newInit);
239
- if (!response.ok) {
240
- self.removeToken();
241
- const refreshToken = self.retrieveRefreshToken();
242
- if (refreshToken) {
243
- try {
244
- if (!exchangedTokenPromise) {
245
- exchangedTokenPromise =
246
- self.exchangeRefreshForAccessToken(refreshToken);
247
- }
248
- const newToken = await exchangedTokenPromise;
249
- exchangedTokenPromise = undefined;
250
- return newToken;
251
- }
252
- catch (err) {
253
- console.error('Token could not be refreshed', err);
254
- }
255
- }
256
- throw new Error(await getResponseError({
257
- response,
258
- reason: 'Error validating token',
259
- }));
260
- }
261
- return token;
262
- },
263
- };
264
- })
265
- .actions(self => {
266
- const superGetFetcher = self.getFetcher;
267
- return {
268
- getFetcher(loc) {
269
- const fetcher = superGetFetcher(loc);
270
- return async (input, init) => {
271
- if (loc) {
272
- await self.validateToken(await self.getToken(loc), loc);
273
- }
274
- return fetcher(input, init);
275
- };
276
- },
277
- };
278
- });
410
+ resolve(token !== undefined ? token : await self.getTokenViaAuthFlow());
411
+ }
412
+ catch (e) {
413
+ reject(e instanceof Error ? e : new Error(String(e)));
414
+ }
415
+ },
416
+ /**
417
+ * #action
418
+ */
419
+ async validateToken(token, location) {
420
+ return self.validateTokenWithProbe(token, probeToken => fetch(location.uri, self.addAuthHeaderToInit({ method: 'HEAD' }, probeToken)), (response, reason) => getResponseError({ response, reason }));
421
+ },
422
+ }));
279
423
  };
280
424
  export default stateModelFactory;