@open-webapp/drive-sync 0.5.4 → 0.5.6
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/dist/connection.d.ts +3 -2
- package/dist/connection.js +14 -2
- package/dist/token.d.ts +4 -3
- package/dist/token.js +62 -25
- package/package.json +1 -1
package/dist/connection.d.ts
CHANGED
|
@@ -15,8 +15,9 @@ export interface ConnectOptions {
|
|
|
15
15
|
fetchEmail: (accessToken: string) => Promise<string>;
|
|
16
16
|
}
|
|
17
17
|
/**
|
|
18
|
-
* Interactive connection flow: acquires a token
|
|
19
|
-
* resolves the account email, and persists the durable Connection
|
|
18
|
+
* Interactive connection flow: acquires a token without forcing a consent
|
|
19
|
+
* screen, resolves the account email, and persists the durable Connection
|
|
20
|
+
* record.
|
|
20
21
|
*/
|
|
21
22
|
export declare function connect(opts: ConnectOptions): Promise<Connection>;
|
|
22
23
|
export interface RefreshSilentlyOptions {
|
package/dist/connection.js
CHANGED
|
@@ -5,16 +5,24 @@ import { WrongAccountError } from './errors.js';
|
|
|
5
5
|
/** Mirrors refresh.ts's own buffer: a cached token this close to expiry is treated as unusable. */
|
|
6
6
|
const TOKEN_REUSE_BUFFER_MS = 5 * 60 * 1000;
|
|
7
7
|
/**
|
|
8
|
-
* Interactive connection flow: acquires a token
|
|
9
|
-
* resolves the account email, and persists the durable Connection
|
|
8
|
+
* Interactive connection flow: acquires a token without forcing a consent
|
|
9
|
+
* screen, resolves the account email, and persists the durable Connection
|
|
10
|
+
* record.
|
|
10
11
|
*/
|
|
11
12
|
export async function connect(opts) {
|
|
13
|
+
// On a re-auth the previous connection's email is the account the user is
|
|
14
|
+
// expected to consent as again; pass it as a hint so the popup_closed
|
|
15
|
+
// recovery probe (token.ts) can resolve a completed grant silently even
|
|
16
|
+
// when the browser holds several Google sessions. First-time connect has
|
|
17
|
+
// no prior email and simply passes undefined.
|
|
18
|
+
const existing = await getConn(opts.appId, opts.projectId);
|
|
12
19
|
const token = await acquireToken({
|
|
13
20
|
appId: opts.appId,
|
|
14
21
|
projectId: opts.projectId,
|
|
15
22
|
clientId: opts.clientId,
|
|
16
23
|
scopes: opts.scopes,
|
|
17
24
|
interactive: true,
|
|
25
|
+
hint: existing?.email,
|
|
18
26
|
logger: opts.logger,
|
|
19
27
|
});
|
|
20
28
|
const email = await opts.fetchEmail(token.accessToken);
|
|
@@ -95,12 +103,16 @@ export async function getAccessToken(opts) {
|
|
|
95
103
|
if (cached && cached.expiresAt > Date.now() + TOKEN_REUSE_BUFFER_MS) {
|
|
96
104
|
return cached.accessToken;
|
|
97
105
|
}
|
|
106
|
+
// Same rationale as connect(): hand the known account email to the
|
|
107
|
+
// popup_closed recovery probe so it can pick up a completed grant silently.
|
|
108
|
+
const existing = await getConn(opts.appId, opts.projectId);
|
|
98
109
|
const token = await acquireToken({
|
|
99
110
|
appId: opts.appId,
|
|
100
111
|
projectId: opts.projectId,
|
|
101
112
|
clientId: opts.clientId,
|
|
102
113
|
scopes: opts.scopes,
|
|
103
114
|
interactive: opts.interactive,
|
|
115
|
+
hint: existing?.email,
|
|
104
116
|
logger: opts.logger,
|
|
105
117
|
});
|
|
106
118
|
return token.accessToken;
|
package/dist/token.d.ts
CHANGED
|
@@ -33,9 +33,10 @@ export interface AcquireTokenOptions {
|
|
|
33
33
|
export declare function notifyExternalTokenRefresh(projectId: string): void;
|
|
34
34
|
/**
|
|
35
35
|
* Single entry point for acquiring a Drive access token, used by BOTH the
|
|
36
|
-
* interactive "connect" path (interactive: true -> prompt: '
|
|
37
|
-
*
|
|
38
|
-
*
|
|
36
|
+
* interactive "connect" path (interactive: true -> prompt: '', i.e. no
|
|
37
|
+
* forced consent screen) and the silent "refresh" path (interactive: false
|
|
38
|
+
* -> prompt: 'none'). Both pass `hint`: the connection's known email, when
|
|
39
|
+
* there is one.
|
|
39
40
|
*
|
|
40
41
|
* Design contract (see report): if `interactive` is false and GIS reports an
|
|
41
42
|
* error on the silent attempt, this function throws a NeedsReauthError
|
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
|
|
@@ -60,9 +71,10 @@ export function notifyExternalTokenRefresh(projectId) {
|
|
|
60
71
|
}
|
|
61
72
|
/**
|
|
62
73
|
* Single entry point for acquiring a Drive access token, used by BOTH the
|
|
63
|
-
* interactive "connect" path (interactive: true -> prompt: '
|
|
64
|
-
*
|
|
65
|
-
*
|
|
74
|
+
* interactive "connect" path (interactive: true -> prompt: '', i.e. no
|
|
75
|
+
* forced consent screen) and the silent "refresh" path (interactive: false
|
|
76
|
+
* -> prompt: 'none'). Both pass `hint`: the connection's known email, when
|
|
77
|
+
* there is one.
|
|
66
78
|
*
|
|
67
79
|
* Design contract (see report): if `interactive` is false and GIS reports an
|
|
68
80
|
* error on the silent attempt, this function throws a NeedsReauthError
|
|
@@ -107,32 +119,46 @@ function isPopupClosedError(err) {
|
|
|
107
119
|
return err instanceof NeedsReauthError && err.reason === 'popup_closed';
|
|
108
120
|
}
|
|
109
121
|
/**
|
|
110
|
-
* Issues
|
|
111
|
-
* GIS reported as `popup_closed` actually completed.
|
|
112
|
-
* response
|
|
113
|
-
* — the original interactive failure —
|
|
114
|
-
* would have seen before, never a
|
|
122
|
+
* Issues up to `PROBE_ATTEMPTS` silent `prompt: 'none'` requests to find out
|
|
123
|
+
* whether the sign-in that GIS reported as `popup_closed` actually completed.
|
|
124
|
+
* Resolves with the token response as soon as a live grant is found;
|
|
125
|
+
* otherwise rethrows `popupClosedError` — the original interactive failure —
|
|
126
|
+
* so callers see the cancellation they would have seen before, never a
|
|
127
|
+
* confusing silent-path error.
|
|
128
|
+
*
|
|
129
|
+
* `opts.hint` matters here: a bare `prompt: 'none'` request with no
|
|
130
|
+
* `login_hint` cannot be resolved by GIS when the browser holds more than one
|
|
131
|
+
* Google session, so the interactive callers pass the connection's known
|
|
132
|
+
* email through as the hint for this probe.
|
|
115
133
|
*/
|
|
116
134
|
async function probeForCompletedGrant(initTokenClient, opts, popupClosedError) {
|
|
117
135
|
opts.logger?.debug('drive-sync: popup_closed with no token; probing for a completed grant', {
|
|
118
136
|
projectId: opts.projectId,
|
|
119
137
|
});
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
138
|
+
let lastProbeError;
|
|
139
|
+
for (let attempt = 1; attempt <= PROBE_ATTEMPTS; attempt += 1) {
|
|
140
|
+
try {
|
|
141
|
+
// No grace window: `prompt: 'none'` never opens a popup, so there is no
|
|
142
|
+
// popup-closed poll to race and nothing to wait out on failure.
|
|
143
|
+
const response = await requestGisToken(initTokenClient, opts, { prompt: 'none', hint: opts.hint }, 0);
|
|
144
|
+
opts.logger?.debug('drive-sync: recovered a completed sign-in reported as popup_closed', {
|
|
145
|
+
projectId: opts.projectId,
|
|
146
|
+
attempt,
|
|
147
|
+
});
|
|
148
|
+
return response;
|
|
149
|
+
}
|
|
150
|
+
catch (probeError) {
|
|
151
|
+
lastProbeError = probeError;
|
|
152
|
+
if (attempt < PROBE_ATTEMPTS) {
|
|
153
|
+
await delay(PROBE_RETRY_DELAY_MS);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
135
156
|
}
|
|
157
|
+
opts.logger?.debug('drive-sync: no live grant after popup_closed; treating as cancelled', {
|
|
158
|
+
projectId: opts.projectId,
|
|
159
|
+
probeError: lastProbeError,
|
|
160
|
+
});
|
|
161
|
+
throw popupClosedError;
|
|
136
162
|
}
|
|
137
163
|
/**
|
|
138
164
|
* Wraps a single GIS token request in a promise.
|
|
@@ -223,8 +249,19 @@ async function acquireTokenUncoalesced(opts) {
|
|
|
223
249
|
let response;
|
|
224
250
|
try {
|
|
225
251
|
response = await requestGisToken(initTokenClient, opts, {
|
|
226
|
-
|
|
227
|
-
|
|
252
|
+
// The interactive path deliberately does NOT force `prompt: 'consent'`.
|
|
253
|
+
// Forcing the full consent screen on every connect buys nothing in the
|
|
254
|
+
// implicit (token) flow — there is no refresh token to obtain — while
|
|
255
|
+
// holding a popup open for seconds. That popup lifetime IS the window
|
|
256
|
+
// in which GIS's popup-closed poll beats delivery of the token, so
|
|
257
|
+
// forcing consent manufactures the very race the probe below recovers
|
|
258
|
+
// from. `prompt: ''` lets Google skip straight through when the grant
|
|
259
|
+
// already exists (and still shows consent on the first grant, or when
|
|
260
|
+
// new scopes are requested), which closes the race instead of racing
|
|
261
|
+
// it. The hint goes on both paths so an already-known account can skip
|
|
262
|
+
// the chooser too.
|
|
263
|
+
prompt: opts.interactive ? '' : 'none',
|
|
264
|
+
hint: opts.hint,
|
|
228
265
|
});
|
|
229
266
|
}
|
|
230
267
|
catch (err) {
|