@open-webapp/drive-sync 0.5.0 → 0.5.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.
@@ -56,6 +56,12 @@ export interface GisFake {
56
56
  * for a blocked or dismissed popup, which never reaches `callback`.
57
57
  */
58
58
  queuePopupError(type: string): void;
59
+ /**
60
+ * Queue a `popup_closed` error_callback that is followed, after `delayMs`,
61
+ * by a successful token `callback` — reproducing the real GIS race where
62
+ * the popup-closed poll fires before the success message is delivered.
63
+ */
64
+ queuePopupClosedRace(response: GisTokenResponse, delayMs: number): void;
59
65
  /** Stub `window.google.accounts.oauth2.initTokenClient` with this fake. */
60
66
  install(): void;
61
67
  /** Remove the stub installed by `install()`, restoring prior state. */
@@ -16,6 +16,7 @@
16
16
  export function createGisFake() {
17
17
  const responseQueue = [];
18
18
  const popupErrorQueue = [];
19
+ const popupClosedRaceQueue = [];
19
20
  const calls = [];
20
21
  let previousGoogle;
21
22
  let hadGoogle = false;
@@ -33,6 +34,18 @@ export function createGisFake() {
33
34
  const hint = overrideConfig?.hint ?? config.hint;
34
35
  const scope = overrideConfig?.scope ?? config.scope ?? '';
35
36
  calls.push({ prompt, hint, scope });
37
+ const popupClosedRace = popupClosedRaceQueue.shift();
38
+ if (popupClosedRace) {
39
+ const errorCallback = config.error_callback;
40
+ const callback = config.callback;
41
+ queueMicrotask(() => {
42
+ errorCallback?.({ type: 'popup_closed' });
43
+ });
44
+ setTimeout(() => {
45
+ callback?.(popupClosedRace.response);
46
+ }, popupClosedRace.delayMs);
47
+ return;
48
+ }
36
49
  const popupError = popupErrorQueue.shift();
37
50
  if (popupError) {
38
51
  const errorCallback = config.error_callback;
@@ -66,6 +79,9 @@ export function createGisFake() {
66
79
  queuePopupError(type) {
67
80
  popupErrorQueue.push(type);
68
81
  },
82
+ queuePopupClosedRace(response, delayMs) {
83
+ popupClosedRaceQueue.push({ response, delayMs });
84
+ },
69
85
  install() {
70
86
  const w = globalThis;
71
87
  hadGoogle = Object.prototype.hasOwnProperty.call(w, 'google');
@@ -93,6 +109,7 @@ export function createGisFake() {
93
109
  reset() {
94
110
  responseQueue.length = 0;
95
111
  popupErrorQueue.length = 0;
112
+ popupClosedRaceQueue.length = 0;
96
113
  calls.length = 0;
97
114
  },
98
115
  };
package/dist/token.js CHANGED
@@ -2,6 +2,11 @@ import { setToken, getToken } from './storage.js';
2
2
  import { waitForGoogleIdentityServices } from './gis.js';
3
3
  import { NeedsReauthError } from './errors.js';
4
4
  import { createBroadcast } from './broadcast.js';
5
+ /**
6
+ * How long to wait, after GIS reports `popup_closed`, for the success
7
+ * `callback` to still win the race before treating it as a real failure.
8
+ */
9
+ const POPUP_CLOSED_GRACE_MS = 300;
5
10
  /**
6
11
  * Persists a freshly-acquired GIS token response as a StoredToken, deriving
7
12
  * expiresAt from the response's own expires_in (never hardcoded) and
@@ -104,10 +109,14 @@ async function acquireTokenUncoalesced(opts) {
104
109
  // These resolve/reject are captured in THIS call's closure only, never
105
110
  // stored on a module-level variable, so a second concurrent call cannot
106
111
  // clobber the first caller's promise.
112
+ let settled = false;
107
113
  const client = initTokenClient({
108
114
  client_id: opts.clientId,
109
115
  scope: opts.scopes.join(' '),
110
116
  callback: (res) => {
117
+ if (settled)
118
+ return;
119
+ settled = true;
111
120
  if (res.error) {
112
121
  reject(new Error(`GIS token request failed: ${res.error}`));
113
122
  return;
@@ -119,11 +128,29 @@ async function acquireTokenUncoalesced(opts) {
119
128
  // the promise below would stay pending forever and every awaiting
120
129
  // Drive call would hang until the caller's own timeout (if any).
121
130
  error_callback: (err) => {
131
+ if (settled)
132
+ return;
133
+ if (err?.type === 'popup_closed') {
134
+ // GIS closes the popup itself at the end of a SUCCESSFUL flow too,
135
+ // and its popup-closed poll can win the race against delivery of
136
+ // the success token, firing this error_callback even though the
137
+ // token is already on its way via `callback`. Give `callback` a
138
+ // brief grace window to settle the promise first, so a completed
139
+ // OAuth flow doesn't get reported as a failed one.
140
+ setTimeout(() => {
141
+ if (settled)
142
+ return;
143
+ settled = true;
144
+ reject(new NeedsReauthError('Google sign-in popup was closed before completing', {
145
+ reason: 'popup_closed',
146
+ }));
147
+ }, POPUP_CLOSED_GRACE_MS);
148
+ return;
149
+ }
150
+ settled = true;
122
151
  reject(new NeedsReauthError(err?.type === 'popup_failed_to_open'
123
152
  ? 'Google sign-in popup was blocked by the browser'
124
- : err?.type === 'popup_closed'
125
- ? 'Google sign-in popup was closed before completing'
126
- : `Google sign-in failed: ${err?.type ?? 'unknown error'}`, { reason: err?.type ?? 'gis_error' }));
153
+ : `Google sign-in failed: ${err?.type ?? 'unknown error'}`, { reason: err?.type ?? 'gis_error' }));
127
154
  },
128
155
  });
129
156
  opts.logger?.debug('drive-sync: requesting access token', {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-webapp/drive-sync",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",