@open-webapp/drive-sync 0.5.4 → 0.5.5

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.
@@ -9,12 +9,19 @@ const TOKEN_REUSE_BUFFER_MS = 5 * 60 * 1000;
9
9
  * resolves the account email, and persists the durable Connection record.
10
10
  */
11
11
  export async function connect(opts) {
12
+ // On a re-auth the previous connection's email is the account the user is
13
+ // expected to consent as again; pass it as a hint so the popup_closed
14
+ // recovery probe (token.ts) can resolve a completed grant silently even
15
+ // when the browser holds several Google sessions. First-time connect has
16
+ // no prior email and simply passes undefined.
17
+ const existing = await getConn(opts.appId, opts.projectId);
12
18
  const token = await acquireToken({
13
19
  appId: opts.appId,
14
20
  projectId: opts.projectId,
15
21
  clientId: opts.clientId,
16
22
  scopes: opts.scopes,
17
23
  interactive: true,
24
+ hint: existing?.email,
18
25
  logger: opts.logger,
19
26
  });
20
27
  const email = await opts.fetchEmail(token.accessToken);
@@ -95,12 +102,16 @@ export async function getAccessToken(opts) {
95
102
  if (cached && cached.expiresAt > Date.now() + TOKEN_REUSE_BUFFER_MS) {
96
103
  return cached.accessToken;
97
104
  }
105
+ // Same rationale as connect(): hand the known account email to the
106
+ // popup_closed recovery probe so it can pick up a completed grant silently.
107
+ const existing = await getConn(opts.appId, opts.projectId);
98
108
  const token = await acquireToken({
99
109
  appId: opts.appId,
100
110
  projectId: opts.projectId,
101
111
  clientId: opts.clientId,
102
112
  scopes: opts.scopes,
103
113
  interactive: opts.interactive,
114
+ hint: existing?.email,
104
115
  logger: opts.logger,
105
116
  });
106
117
  return token.accessToken;
package/dist/token.js CHANGED
@@ -14,6 +14,17 @@ import { createBroadcast } from './broadcast.js';
14
14
  * long enough. `probeForCompletedGrant` below is what actually recovers those.
15
15
  */
16
16
  const POPUP_CLOSED_GRACE_MS = 2000;
17
+ /**
18
+ * The silent `prompt: 'none'` probe that recovers a completed sign-in GIS
19
+ * misreported as `popup_closed` is retried a few times: GIS's popup-closed
20
+ * poll can fire before the just-granted consent is durably registered at
21
+ * Google, so a single immediate probe races the grant into existence and
22
+ * loses. A few spaced retries let a real grant surface while still failing
23
+ * fast enough that a genuine cancellation is reported promptly.
24
+ */
25
+ const PROBE_ATTEMPTS = 3;
26
+ const PROBE_RETRY_DELAY_MS = 350;
27
+ const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
17
28
  /**
18
29
  * Persists a freshly-acquired GIS token response as a StoredToken, deriving
19
30
  * expiresAt from the response's own expires_in (never hardcoded) and
@@ -107,32 +118,46 @@ function isPopupClosedError(err) {
107
118
  return err instanceof NeedsReauthError && err.reason === 'popup_closed';
108
119
  }
109
120
  /**
110
- * Issues ONE `prompt: 'none'` request to find out whether the sign-in that
111
- * GIS reported as `popup_closed` actually completed. Resolves with the token
112
- * response when a live grant is found; otherwise rethrows `popupClosedError`
113
- * — the original interactive failure — so callers see the cancellation they
114
- * would have seen before, never a confusing silent-path error.
121
+ * Issues up to `PROBE_ATTEMPTS` silent `prompt: 'none'` requests to find out
122
+ * whether the sign-in that GIS reported as `popup_closed` actually completed.
123
+ * Resolves with the token response as soon as a live grant is found;
124
+ * otherwise rethrows `popupClosedError` — the original interactive failure —
125
+ * so callers see the cancellation they would have seen before, never a
126
+ * confusing silent-path error.
127
+ *
128
+ * `opts.hint` matters here: a bare `prompt: 'none'` request with no
129
+ * `login_hint` cannot be resolved by GIS when the browser holds more than one
130
+ * Google session, so the interactive callers pass the connection's known
131
+ * email through as the hint for this probe.
115
132
  */
116
133
  async function probeForCompletedGrant(initTokenClient, opts, popupClosedError) {
117
134
  opts.logger?.debug('drive-sync: popup_closed with no token; probing for a completed grant', {
118
135
  projectId: opts.projectId,
119
136
  });
120
- try {
121
- // No grace window: `prompt: 'none'` never opens a popup, so there is no
122
- // popup-closed poll to race and nothing to wait out on failure.
123
- const response = await requestGisToken(initTokenClient, opts, { prompt: 'none', hint: opts.hint }, 0);
124
- opts.logger?.debug('drive-sync: recovered a completed sign-in reported as popup_closed', {
125
- projectId: opts.projectId,
126
- });
127
- return response;
128
- }
129
- catch (probeError) {
130
- opts.logger?.debug('drive-sync: no live grant after popup_closed; treating as cancelled', {
131
- projectId: opts.projectId,
132
- probeError,
133
- });
134
- throw popupClosedError;
137
+ let lastProbeError;
138
+ for (let attempt = 1; attempt <= PROBE_ATTEMPTS; attempt += 1) {
139
+ try {
140
+ // No grace window: `prompt: 'none'` never opens a popup, so there is no
141
+ // popup-closed poll to race and nothing to wait out on failure.
142
+ const response = await requestGisToken(initTokenClient, opts, { prompt: 'none', hint: opts.hint }, 0);
143
+ opts.logger?.debug('drive-sync: recovered a completed sign-in reported as popup_closed', {
144
+ projectId: opts.projectId,
145
+ attempt,
146
+ });
147
+ return response;
148
+ }
149
+ catch (probeError) {
150
+ lastProbeError = probeError;
151
+ if (attempt < PROBE_ATTEMPTS) {
152
+ await delay(PROBE_RETRY_DELAY_MS);
153
+ }
154
+ }
135
155
  }
156
+ opts.logger?.debug('drive-sync: no live grant after popup_closed; treating as cancelled', {
157
+ projectId: opts.projectId,
158
+ probeError: lastProbeError,
159
+ });
160
+ throw popupClosedError;
136
161
  }
137
162
  /**
138
163
  * Wraps a single GIS token request in a promise.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-webapp/drive-sync",
3
- "version": "0.5.4",
3
+ "version": "0.5.5",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",