@open-webapp/drive-sync 0.5.0 → 0.5.2

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,14 @@ 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
+ * 300ms proved too tight in production: on a real network round-trip the
9
+ * success token can arrive well after GIS's popup-closed poll fires,
10
+ * causing genuinely successful sign-ins to be reported as NeedsReauthError.
11
+ */
12
+ const POPUP_CLOSED_GRACE_MS = 2000;
5
13
  /**
6
14
  * Persists a freshly-acquired GIS token response as a StoredToken, deriving
7
15
  * expiresAt from the response's own expires_in (never hardcoded) and
@@ -104,10 +112,14 @@ async function acquireTokenUncoalesced(opts) {
104
112
  // These resolve/reject are captured in THIS call's closure only, never
105
113
  // stored on a module-level variable, so a second concurrent call cannot
106
114
  // clobber the first caller's promise.
115
+ let settled = false;
107
116
  const client = initTokenClient({
108
117
  client_id: opts.clientId,
109
118
  scope: opts.scopes.join(' '),
110
119
  callback: (res) => {
120
+ if (settled)
121
+ return;
122
+ settled = true;
111
123
  if (res.error) {
112
124
  reject(new Error(`GIS token request failed: ${res.error}`));
113
125
  return;
@@ -119,11 +131,29 @@ async function acquireTokenUncoalesced(opts) {
119
131
  // the promise below would stay pending forever and every awaiting
120
132
  // Drive call would hang until the caller's own timeout (if any).
121
133
  error_callback: (err) => {
134
+ if (settled)
135
+ return;
136
+ if (err?.type === 'popup_closed') {
137
+ // GIS closes the popup itself at the end of a SUCCESSFUL flow too,
138
+ // and its popup-closed poll can win the race against delivery of
139
+ // the success token, firing this error_callback even though the
140
+ // token is already on its way via `callback`. Give `callback` a
141
+ // brief grace window to settle the promise first, so a completed
142
+ // OAuth flow doesn't get reported as a failed one.
143
+ setTimeout(() => {
144
+ if (settled)
145
+ return;
146
+ settled = true;
147
+ reject(new NeedsReauthError('Google sign-in popup was closed before completing', {
148
+ reason: 'popup_closed',
149
+ }));
150
+ }, POPUP_CLOSED_GRACE_MS);
151
+ return;
152
+ }
153
+ settled = true;
122
154
  reject(new NeedsReauthError(err?.type === 'popup_failed_to_open'
123
155
  ? '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' }));
156
+ : `Google sign-in failed: ${err?.type ?? 'unknown error'}`, { reason: err?.type ?? 'gis_error' }));
127
157
  },
128
158
  });
129
159
  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.2",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",