@opengeni/connect 0.3.0 → 0.3.1-canary.36927100590001
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/README.md +4 -2
- package/dist/authorization.d.ts +4 -0
- package/dist/browser-navigation.d.ts +3 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +64 -13
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/authorization.ts +55 -9
- package/src/browser-navigation.ts +30 -16
- package/src/index.ts +5 -1
package/README.md
CHANGED
|
@@ -12,7 +12,9 @@ completion is still obtained from the backend, never from that handle.
|
|
|
12
12
|
injected `ConnectNavigation` adapter and either `popup` or `redirect` mode.
|
|
13
13
|
Call popup mode directly from a user gesture: opening is synchronous, a blocked
|
|
14
14
|
popup throws explicitly, and only backend polling determines the result. The
|
|
15
|
-
popup is closed when polling settles
|
|
15
|
+
popup is closed when polling settles if the browser still permits it. The human
|
|
16
|
+
closes provider windows that remain on a foreign origin or have become detached.
|
|
17
|
+
Cancellation stops polling, not the
|
|
16
18
|
durable server attempt; cancel that explicitly through the controller if desired.
|
|
17
19
|
|
|
18
20
|
For redirect mode, retain the opaque attempt ID in host-owned state before
|
|
@@ -109,4 +111,4 @@ remain mandatory; browser-disabled options are not an access-control boundary.
|
|
|
109
111
|
the resulting action or terminal state to subscribers (also exposed by
|
|
110
112
|
`useConnect`). It rejects revisions older than the selected snapshot, never
|
|
111
113
|
retries mutations, and stops when the controller is disposed or another attempt
|
|
112
|
-
is selected. A timeout stops observation, not the durable server operation.
|
|
114
|
+
is selected. A timeout stops observation, not the durable server operation.
|
package/dist/authorization.d.ts
CHANGED
|
@@ -3,9 +3,13 @@ import type { ConnectAttempt, ConnectTransport } from "./types.js";
|
|
|
3
3
|
export type ConnectNavigation = {
|
|
4
4
|
openPopup(url: string): {
|
|
5
5
|
close(): void;
|
|
6
|
+
readonly closed?: boolean | undefined;
|
|
6
7
|
} | null;
|
|
7
8
|
redirect(url: string): void;
|
|
8
9
|
};
|
|
10
|
+
export declare class ConnectPopupClosedError extends Error {
|
|
11
|
+
constructor();
|
|
12
|
+
}
|
|
9
13
|
/** Invoke directly from a user gesture for popup mode. Persist the opaque
|
|
10
14
|
* attempt ID in host-owned state before redirect mode, then recover/poll on
|
|
11
15
|
* return. Neither URL parameters nor popup messages prove completion. */
|
|
@@ -3,6 +3,7 @@ import type { ConnectNavigation } from "./authorization.js";
|
|
|
3
3
|
export type ConnectBrowserWindow = {
|
|
4
4
|
open(url: string, target: string, features: string): {
|
|
5
5
|
opener: unknown;
|
|
6
|
+
readonly closed?: boolean;
|
|
6
7
|
location: {
|
|
7
8
|
replace(url: string): void;
|
|
8
9
|
};
|
|
@@ -21,5 +22,6 @@ export declare function reserveBrowserConnectNavigation(browser: ConnectBrowserW
|
|
|
21
22
|
/** Pass window from the host's browser entry point. Opens a fresh blank window
|
|
22
23
|
* synchronously and severs its opener BEFORE any provider content can load.
|
|
23
24
|
* Never uses a reusable named target or relies on provider window messages.
|
|
24
|
-
* The caller retains only
|
|
25
|
+
* The caller retains only best-effort close capability for polling cleanup;
|
|
26
|
+
* the human closes provider windows that remain on a foreign origin. */
|
|
25
27
|
export declare function createBrowserConnectNavigation(browser: ConnectBrowserWindow): ConnectNavigation;
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export * from "./types.js";
|
|
|
2
2
|
export { pollDeviceAuthorization } from "./device.js";
|
|
3
3
|
export { findConnectRecoveryAccount } from "./recovery.js";
|
|
4
4
|
export { pollConnectAttempt } from "./poll.js";
|
|
5
|
-
export { authorizeConnectAttempt, type ConnectNavigation } from "./authorization.js";
|
|
5
|
+
export { authorizeConnectAttempt, ConnectPopupClosedError, type ConnectNavigation, } from "./authorization.js";
|
|
6
6
|
export { createBrowserConnectNavigation, reserveBrowserConnectNavigation, type ConnectBrowserWindow, } from "./browser-navigation.js";
|
|
7
7
|
import type { ConnectAdvance, ConnectAttempt, ConnectOwnership, ConnectTransport } from "./types.js";
|
|
8
8
|
export type ConnectSnapshot = {
|
package/dist/index.js
CHANGED
|
@@ -141,6 +141,12 @@ function abortable(pending, signal) {
|
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
// src/authorization.ts
|
|
144
|
+
var ConnectPopupClosedError = class extends Error {
|
|
145
|
+
constructor() {
|
|
146
|
+
super("Sign-in window closed. You can try connecting again.");
|
|
147
|
+
this.name = "ConnectPopupClosedError";
|
|
148
|
+
}
|
|
149
|
+
};
|
|
144
150
|
function authorizeConnectAttempt(transport, attempt, navigation, options) {
|
|
145
151
|
options.signal?.throwIfAborted();
|
|
146
152
|
if (attempt.nextAction.type !== "authorize") {
|
|
@@ -158,10 +164,44 @@ function authorizeConnectAttempt(transport, attempt, navigation, options) {
|
|
|
158
164
|
}
|
|
159
165
|
const popup = navigation.openPopup(attempt.nextAction.url);
|
|
160
166
|
if (!popup) throw new Error("Connect popup was blocked; retry with redirect mode");
|
|
167
|
+
const observation = new AbortController();
|
|
168
|
+
const closed = new ConnectPopupClosedError();
|
|
169
|
+
const abort = () => observation.abort(options.signal?.reason);
|
|
170
|
+
options.signal?.addEventListener("abort", abort, { once: true });
|
|
171
|
+
if (options.signal?.aborted) abort();
|
|
172
|
+
const watch = setInterval(() => {
|
|
173
|
+
try {
|
|
174
|
+
if (popup.closed === true) observation.abort(closed);
|
|
175
|
+
} catch {
|
|
176
|
+
}
|
|
177
|
+
}, 250);
|
|
161
178
|
return pollConnectAttempt(transport, attempt.workspaceId, attempt.id, {
|
|
162
179
|
...options,
|
|
180
|
+
signal: observation.signal,
|
|
163
181
|
minimumRevision: attempt.revision
|
|
182
|
+
}).catch(async (failure) => {
|
|
183
|
+
if (failure !== closed) throw failure;
|
|
184
|
+
const recovery = new AbortController();
|
|
185
|
+
const abortRecovery = () => recovery.abort(options.signal?.reason);
|
|
186
|
+
options.signal?.addEventListener("abort", abortRecovery, { once: true });
|
|
187
|
+
if (options.signal?.aborted) abortRecovery();
|
|
188
|
+
const deadline = setTimeout(() => recovery.abort(closed), 1500);
|
|
189
|
+
try {
|
|
190
|
+
return await pollConnectAttempt(transport, attempt.workspaceId, attempt.id, {
|
|
191
|
+
signal: recovery.signal,
|
|
192
|
+
minimumRevision: attempt.revision,
|
|
193
|
+
timeoutMs: 2e3
|
|
194
|
+
});
|
|
195
|
+
} catch (recoveryFailure) {
|
|
196
|
+
if (recoveryFailure === closed) throw closed;
|
|
197
|
+
throw recoveryFailure;
|
|
198
|
+
} finally {
|
|
199
|
+
clearTimeout(deadline);
|
|
200
|
+
options.signal?.removeEventListener("abort", abortRecovery);
|
|
201
|
+
}
|
|
164
202
|
}).finally(() => {
|
|
203
|
+
clearInterval(watch);
|
|
204
|
+
options.signal?.removeEventListener("abort", abort);
|
|
165
205
|
try {
|
|
166
206
|
popup.close();
|
|
167
207
|
} catch {
|
|
@@ -175,6 +215,14 @@ function validateDestination(url) {
|
|
|
175
215
|
if (parsed.protocol !== "https:" || parsed.username || parsed.password)
|
|
176
216
|
throw new Error("Connect authorization requires an HTTPS destination without credentials");
|
|
177
217
|
}
|
|
218
|
+
function closeIsolatedPopup(popup) {
|
|
219
|
+
try {
|
|
220
|
+
if (popup.closed) return;
|
|
221
|
+
Reflect.get(popup.location, "href");
|
|
222
|
+
popup.close();
|
|
223
|
+
} catch {
|
|
224
|
+
}
|
|
225
|
+
}
|
|
178
226
|
function reserveBrowserConnectNavigation(browser) {
|
|
179
227
|
const popup = browser.open("about:blank", "_blank", "popup,width=520,height=720");
|
|
180
228
|
if (!popup) throw new Error("Allow popups to connect your account, then try again.");
|
|
@@ -182,22 +230,22 @@ function reserveBrowserConnectNavigation(browser) {
|
|
|
182
230
|
popup.opener = null;
|
|
183
231
|
if (popup.opener !== null) throw new Error("Connect popup isolation failed");
|
|
184
232
|
} catch {
|
|
185
|
-
popup
|
|
233
|
+
closeIsolatedPopup(popup);
|
|
186
234
|
throw new Error("Connect popup could not open safely. Please try again.");
|
|
187
235
|
}
|
|
188
|
-
const close = () =>
|
|
189
|
-
try {
|
|
190
|
-
popup.close();
|
|
191
|
-
} catch {
|
|
192
|
-
}
|
|
193
|
-
};
|
|
236
|
+
const close = () => closeIsolatedPopup(popup);
|
|
194
237
|
return {
|
|
195
238
|
close,
|
|
196
239
|
navigation: {
|
|
197
240
|
openPopup(url) {
|
|
198
241
|
validateDestination(url);
|
|
199
242
|
popup.location.replace(url);
|
|
200
|
-
return {
|
|
243
|
+
return {
|
|
244
|
+
close,
|
|
245
|
+
get closed() {
|
|
246
|
+
return popup.closed;
|
|
247
|
+
}
|
|
248
|
+
};
|
|
201
249
|
},
|
|
202
250
|
redirect(url) {
|
|
203
251
|
validateDestination(url);
|
|
@@ -218,13 +266,15 @@ function createBrowserConnectNavigation(browser) {
|
|
|
218
266
|
if (popup.opener !== null) throw new Error("Connect popup isolation failed");
|
|
219
267
|
popup.location.replace(url);
|
|
220
268
|
} catch {
|
|
221
|
-
|
|
222
|
-
popup.close();
|
|
223
|
-
} catch {
|
|
224
|
-
}
|
|
269
|
+
closeIsolatedPopup(popup);
|
|
225
270
|
throw new Error("Connect popup could not navigate safely; retry with redirect mode");
|
|
226
271
|
}
|
|
227
|
-
return {
|
|
272
|
+
return {
|
|
273
|
+
close: () => closeIsolatedPopup(popup),
|
|
274
|
+
get closed() {
|
|
275
|
+
return popup.closed;
|
|
276
|
+
}
|
|
277
|
+
};
|
|
228
278
|
},
|
|
229
279
|
redirect(url) {
|
|
230
280
|
validateDestination(url);
|
|
@@ -393,6 +443,7 @@ function freezeTree(value) {
|
|
|
393
443
|
}
|
|
394
444
|
export {
|
|
395
445
|
ConnectController,
|
|
446
|
+
ConnectPopupClosedError,
|
|
396
447
|
authorizeConnectAttempt,
|
|
397
448
|
createBrowserConnectNavigation,
|
|
398
449
|
findConnectRecoveryAccount,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/device.ts","../src/recovery.ts","../src/poll.ts","../src/authorization.ts","../src/browser-navigation.ts","../src/index.ts"],"sourcesContent":["/** Shared device-code behavior for model-account domains. These accounts keep\n * their existing APIs and ownership semantics; they are not generic credentials.\n * Keep opaque provider state on the host backend when reload recovery is needed. */\nexport async function pollDeviceAuthorization<\n T extends { status: string; intervalSeconds?: number },\n>(options: {\n poll: () => Promise<T>;\n expired: T;\n initialIntervalSeconds: number;\n expiresAtMs: number;\n signal: AbortSignal;\n retryable?: (error: unknown) => boolean;\n now?: () => number;\n wait?: (delayMs: number, signal: AbortSignal) => Promise<boolean>;\n maxRetryDelaySeconds?: number;\n}): Promise<T | null> {\n const now = options.now ?? Date.now;\n if (\n !Number.isFinite(options.expiresAtMs) ||\n !Number.isFinite(options.initialIntervalSeconds) ||\n options.initialIntervalSeconds <= 0 ||\n (options.maxRetryDelaySeconds !== undefined &&\n (!Number.isFinite(options.maxRetryDelaySeconds) || options.maxRetryDelaySeconds <= 0))\n )\n throw new Error(\"Device authorization requires a finite expiry and polling interval\");\n const wait = options.wait ?? waitForDeviceDelay;\n const initial = Math.max(1, options.initialIntervalSeconds);\n const maximum = Math.max(initial, options.maxRetryDelaySeconds ?? 30);\n let delay = initial;\n while (!options.signal.aborted) {\n const remaining = options.expiresAtMs - now();\n if (remaining <= 0) return options.expired;\n if (!(await wait(Math.min(delay * 1000, remaining), options.signal)) || options.signal.aborted)\n return null;\n if (now() >= options.expiresAtMs) return options.expired;\n let result: T;\n try {\n const observed = await observeDevicePoll(\n options.poll,\n options.expiresAtMs - now(),\n options.signal,\n );\n if (observed.kind === \"aborted\") return null;\n if (observed.kind === \"expired\") return options.expired;\n result = observed.result;\n } catch (error) {\n if (!options.retryable?.(error)) throw error;\n delay = Math.min(maximum, Math.max(initial, delay * 2));\n continue;\n }\n if (options.signal.aborted) return null;\n if (result.status !== \"pending\" && result.status !== \"slow_down\") return result;\n if (\n result.intervalSeconds !== undefined &&\n (!Number.isFinite(result.intervalSeconds) || result.intervalSeconds <= 0)\n )\n throw new Error(\"Provider returned an invalid device polling interval\");\n delay = Math.max(\n 1,\n result.intervalSeconds ?? (result.status === \"slow_down\" ? delay + 5 : delay),\n );\n }\n return null;\n}\n\n/** A transport may ignore cancellation. Bound observation without retrying an\n * in-flight request or treating the loss of observation as provider success. */\nasync function observeDevicePoll<T>(\n poll: () => Promise<T>,\n remainingMs: number,\n signal: AbortSignal,\n): Promise<{ kind: \"result\"; result: T } | { kind: \"aborted\" } | { kind: \"expired\" }> {\n if (signal.aborted) return { kind: \"aborted\" };\n if (remainingMs <= 0) return { kind: \"expired\" };\n let timer: ReturnType<typeof setTimeout> | undefined;\n let abort: (() => void) | undefined;\n try {\n const stopped = new Promise<{ kind: \"aborted\" } | { kind: \"expired\" }>((resolve) => {\n abort = () => resolve({ kind: \"aborted\" });\n signal.addEventListener(\"abort\", abort, { once: true });\n timer = setTimeout(() => resolve({ kind: \"expired\" }), remainingMs);\n });\n return await Promise.race([\n Promise.resolve()\n .then(poll)\n .then((result) => ({ kind: \"result\" as const, result })),\n stopped,\n ]);\n } finally {\n if (timer) clearTimeout(timer);\n if (abort) signal.removeEventListener(\"abort\", abort);\n }\n}\n\nasync function waitForDeviceDelay(delayMs: number, signal: AbortSignal): Promise<boolean> {\n if (signal.aborted) return false;\n return new Promise((resolve) => {\n const abort = () => {\n clearTimeout(timer);\n resolve(false);\n };\n const timer = setTimeout(() => {\n signal.removeEventListener(\"abort\", abort);\n resolve(true);\n }, delayMs);\n signal.addEventListener(\"abort\", abort, { once: true });\n if (signal.aborted) abort();\n });\n}\n","import type { ConnectAccount } from \"./types\";\n\n/** A provider/domain match is not account identity. Never silently substitute\n * another account when the original connection has disappeared. */\nexport function findConnectRecoveryAccount(\n accounts: readonly ConnectAccount[],\n connectionId: string | null | undefined,\n): ConnectAccount | null {\n if (!connectionId) return null;\n const matches = accounts.filter(\n (account) => account.id === connectionId || account.id === `social:${connectionId}`,\n );\n if (matches.length > 1)\n throw new Error(\"Connection recovery is ambiguous; choose the exact account.\");\n return matches[0] ?? null;\n}\n","import type { ConnectAttempt, ConnectTransport } from \"./types\";\n\n/** Read-only continuation. The backend remains authoritative for expiry and\n * completion; redirects and popup messages are never completion evidence. */\nexport async function pollConnectAttempt(\n transport: Pick<ConnectTransport, \"get\">,\n workspaceId: string,\n attemptId: string,\n options: { signal?: AbortSignal; timeoutMs?: number; minimumRevision?: number } = {},\n): Promise<ConnectAttempt> {\n const timeoutMs = options.timeoutMs ?? 120_000;\n if (\n !workspaceId ||\n !attemptId ||\n !Number.isFinite(timeoutMs) ||\n timeoutMs < 1 ||\n timeoutMs > 600_000 ||\n !Number.isSafeInteger(options.minimumRevision ?? 1) ||\n (options.minimumRevision ?? 1) < 1\n ) {\n throw new Error(\"Connect polling requires a scope and a bounded timeout\");\n }\n const abort = new AbortController();\n const cancel = () => abort.abort(options.signal?.reason);\n options.signal?.addEventListener(\"abort\", cancel, { once: true });\n if (options.signal?.aborted) cancel();\n const timer = setTimeout(() => abort.abort(new Error(\"Connect polling timed out\")), timeoutMs);\n let revision = options.minimumRevision ?? 1;\n try {\n while (true) {\n abort.signal.throwIfAborted();\n const result = await abortable(\n transport.get(workspaceId, attemptId, { signal: abort.signal }),\n abort.signal,\n );\n if (\n result.workspaceId !== workspaceId ||\n result.id !== attemptId ||\n !Number.isSafeInteger(result.revision) ||\n result.revision < Math.max(1, revision)\n ) {\n throw new Error(\"Connect polling response scope or revision mismatch\");\n }\n revision = result.revision;\n if (\n [\"complete\", \"cancelled\", \"expired\", \"failed\", \"uncertain\"].includes(result.state) ||\n ![\"authorize\", \"wait\"].includes(result.nextAction.type)\n )\n return result;\n const requested = result.nextAction.type === \"wait\" ? result.nextAction.pollAfterMs : 1000;\n const delay = Number.isFinite(requested) ? Math.min(60_000, Math.max(250, requested)) : 1000;\n await new Promise<void>((resolve, reject) => {\n const stop = () => {\n clearTimeout(wait);\n reject(abort.signal.reason);\n };\n const wait = setTimeout(() => {\n abort.signal.removeEventListener(\"abort\", stop);\n resolve();\n }, delay);\n abort.signal.addEventListener(\"abort\", stop, { once: true });\n if (abort.signal.aborted) stop();\n });\n }\n } finally {\n clearTimeout(timer);\n options.signal?.removeEventListener(\"abort\", cancel);\n }\n}\n\nfunction abortable<T>(pending: Promise<T>, signal: AbortSignal): Promise<T> {\n return new Promise((resolve, reject) => {\n const stop = () => reject(signal.reason);\n signal.addEventListener(\"abort\", stop, { once: true });\n if (signal.aborted) stop();\n pending.then(resolve, reject).finally(() => signal.removeEventListener(\"abort\", stop));\n });\n}\n","import { pollConnectAttempt } from \"./poll\";\nimport type { ConnectAttempt, ConnectTransport } from \"./types\";\n\n/** Inject navigation so hosts own routing and this package needs no DOM globals. */\nexport type ConnectNavigation = {\n openPopup(url: string): { close(): void } | null;\n redirect(url: string): void;\n};\n\n/** Invoke directly from a user gesture for popup mode. Persist the opaque\n * attempt ID in host-owned state before redirect mode, then recover/poll on\n * return. Neither URL parameters nor popup messages prove completion. */\nexport function authorizeConnectAttempt(\n transport: Pick<ConnectTransport, \"get\">,\n attempt: ConnectAttempt,\n navigation: ConnectNavigation,\n options: { mode: \"popup\" | \"redirect\"; signal?: AbortSignal; timeoutMs?: number },\n): Promise<ConnectAttempt | null> {\n options.signal?.throwIfAborted();\n if (attempt.nextAction.type !== \"authorize\") {\n throw new Error(\"Connect attempt does not require authorization\");\n }\n const destination = new URL(attempt.nextAction.url);\n if (destination.protocol !== \"https:\" || destination.username || destination.password) {\n throw new Error(\"Connect authorization requires an HTTPS destination without credentials\");\n }\n if (\n !attempt.id ||\n !attempt.workspaceId ||\n !Number.isSafeInteger(attempt.revision) ||\n attempt.revision < 1\n )\n throw new Error(\"Connect authorization requires a scope\");\n if (options.mode === \"redirect\") {\n navigation.redirect(attempt.nextAction.url);\n return Promise.resolve(null);\n }\n const popup = navigation.openPopup(attempt.nextAction.url);\n if (!popup) throw new Error(\"Connect popup was blocked; retry with redirect mode\");\n return pollConnectAttempt(transport, attempt.workspaceId, attempt.id, {\n ...options,\n minimumRevision: attempt.revision,\n }).finally(() => {\n // Window cleanup must never replace the authoritative result or failure.\n try {\n popup.close();\n } catch {\n /* Host navigation may already have disposed it. */\n }\n });\n}\n","import type { ConnectNavigation } from \"./authorization\";\n\n/** Structural browser surface so importing Connect never reads DOM globals. */\nexport type ConnectBrowserWindow = {\n open(\n url: string,\n target: string,\n features: string,\n ): {\n opener: unknown;\n location: { replace(url: string): void };\n close(): void;\n } | null;\n location: { assign(url: string): void };\n};\n\nfunction validateDestination(url: string): void {\n const parsed = new URL(url);\n if (parsed.protocol !== \"https:\" || parsed.username || parsed.password)\n throw new Error(\"Connect authorization requires an HTTPS destination without credentials\");\n}\n\n/** Reserve the isolated window during the human's click, before asynchronous\n * discovery. The caller must close it if setup fails before navigation. */\nexport function reserveBrowserConnectNavigation(browser: ConnectBrowserWindow): {\n navigation: ConnectNavigation;\n close(): void;\n} {\n const popup = browser.open(\"about:blank\", \"_blank\", \"popup,width=520,height=720\");\n if (!popup) throw new Error(\"Allow popups to connect your account, then try again.\");\n try {\n popup.opener = null;\n if (popup.opener !== null) throw new Error(\"Connect popup isolation failed\");\n } catch {\n popup.close();\n throw new Error(\"Connect popup could not open safely. Please try again.\");\n }\n const close = () => {\n try {\n popup.close();\n } catch {\n /* Already closed. */\n }\n };\n return {\n close,\n navigation: {\n openPopup(url) {\n validateDestination(url);\n popup.location.replace(url);\n return { close };\n },\n redirect(url) {\n validateDestination(url);\n close();\n browser.location.assign(url);\n },\n },\n };\n}\n\n/** Pass window from the host's browser entry point. Opens a fresh blank window\n * synchronously and severs its opener BEFORE any provider content can load.\n * Never uses a reusable named target or relies on provider window messages.\n * The caller retains only the close capability for backend-polling cleanup. */\nexport function createBrowserConnectNavigation(browser: ConnectBrowserWindow): ConnectNavigation {\n return {\n openPopup(url) {\n validateDestination(url);\n const popup = browser.open(\"about:blank\", \"_blank\", \"popup,width=520,height=720\");\n if (!popup) return null;\n try {\n popup.opener = null;\n if (popup.opener !== null) throw new Error(\"Connect popup isolation failed\");\n popup.location.replace(url);\n } catch {\n try {\n popup.close();\n } catch {\n /* Preserve the isolated navigation failure. */\n }\n throw new Error(\"Connect popup could not navigate safely; retry with redirect mode\");\n }\n return { close: () => popup.close() };\n },\n redirect(url) {\n validateDestination(url);\n browser.location.assign(url);\n },\n };\n}\n","export * from \"./types\";\nexport { pollDeviceAuthorization } from \"./device\";\nexport { findConnectRecoveryAccount } from \"./recovery\";\nexport { pollConnectAttempt } from \"./poll\";\nexport { authorizeConnectAttempt, type ConnectNavigation } from \"./authorization\";\nexport {\n createBrowserConnectNavigation,\n reserveBrowserConnectNavigation,\n type ConnectBrowserWindow,\n} from \"./browser-navigation\";\nimport type { ConnectAdvance, ConnectAttempt, ConnectOwnership, ConnectTransport } from \"./types\";\nimport { pollConnectAttempt } from \"./poll\";\n\nexport type ConnectSnapshot = {\n attempt: ConnectAttempt | null;\n busy: boolean;\n error: Error | null;\n};\n\n/** A transport-injected, framework-neutral view of one durable setup attempt.\n * OAuth redirects are hints to navigate; completion is read from the backend.\n * Secret form values are never stored in a snapshot or browser persistence. */\nexport class ConnectController {\n private snapshot: ConnectSnapshot = Object.freeze({ attempt: null, busy: false, error: null });\n private readonly listeners = new Set<() => void>();\n private generation = 0;\n private request: AbortController | null = null;\n private disposed = false;\n\n constructor(\n readonly transport: ConnectTransport,\n readonly workspaceId: string,\n ) {\n if (!workspaceId) throw new Error(\"Connect requires an explicit workspace\");\n }\n\n getSnapshot = (): ConnectSnapshot => this.snapshot;\n subscribe = (listener: () => void): (() => void) => {\n this.assertActive();\n this.listeners.add(listener);\n return () => {\n this.listeners.delete(listener);\n };\n };\n\n begin(input: {\n providerId: string;\n ownership: ConnectOwnership;\n returnUrl: string;\n idempotencyKey: string;\n reconnectAccountId?: string;\n installationTarget?: import(\"./types\").ConnectInstallationTarget;\n }): Promise<ConnectAttempt> {\n // Validation must not serialize or decorate the host's exact return string.\n const url = new URL(input.returnUrl);\n if (![\"https:\", \"http:\"].includes(url.protocol) || url.username || url.password) {\n throw new Error(\"Connect return URL must be an HTTP(S) destination without credentials\");\n }\n return this.run((signal) => this.transport.begin(this.workspaceId, input, { signal }), true);\n }\n\n recover(attemptId: string): Promise<ConnectAttempt> {\n if (!attemptId) throw new Error(\"Connect recovery requires an attempt ID\");\n return this.run(\n (signal) => this.transport.get(this.workspaceId, attemptId, { signal }),\n true,\n attemptId,\n );\n }\n\n refresh(): Promise<ConnectAttempt> {\n const attempt = this.requireAttempt();\n if (this.snapshot.busy) throw new Error(\"A Connect operation is already in progress\");\n return this.run(\n (signal) => this.transport.get(this.workspaceId, attempt.id, { signal }),\n false,\n attempt.id,\n );\n }\n\n advance(action: ConnectAdvance, idempotencyKey: string): Promise<ConnectAttempt> {\n const attempt = this.requireAttempt();\n if (this.snapshot.busy) throw new Error(\"A Connect operation is already in progress\");\n return this.run(\n (signal) =>\n this.transport.advance(\n this.workspaceId,\n attempt.id,\n {\n expectedRevision: attempt.revision,\n idempotencyKey,\n action,\n },\n { signal },\n ),\n false,\n attempt.id,\n );\n }\n\n /** Observe backend progress without replaying a setup mutation. Disposal or\n * selecting another attempt aborts the read loop and fences its late result. */\n waitForAction(options: { timeoutMs?: number } = {}): Promise<ConnectAttempt> {\n const attempt = this.requireAttempt();\n if (this.snapshot.busy) throw new Error(\"A Connect operation is already in progress\");\n return this.run(\n (signal) =>\n pollConnectAttempt(this.transport, this.workspaceId, attempt.id, {\n ...options,\n signal,\n minimumRevision: attempt.revision,\n }),\n false,\n attempt.id,\n );\n }\n\n cancel(idempotencyKey: string): Promise<ConnectAttempt> {\n const attempt = this.requireAttempt();\n if (this.snapshot.busy) throw new Error(\"A Connect operation is already in progress\");\n return this.run(\n (signal) =>\n this.transport.cancel(\n this.workspaceId,\n attempt.id,\n {\n expectedRevision: attempt.revision,\n idempotencyKey,\n },\n { signal },\n ),\n false,\n attempt.id,\n );\n }\n\n dispose(): void {\n if (this.disposed) return;\n this.disposed = true;\n this.generation++;\n this.request?.abort();\n this.request = null;\n this.listeners.clear();\n }\n\n private assertActive(): void {\n if (this.disposed) throw new Error(\"Connect controller is disposed\");\n }\n private requireAttempt(): ConnectAttempt {\n this.assertActive();\n if (!this.snapshot.attempt) throw new Error(\"No Connect attempt is selected\");\n return this.snapshot.attempt;\n }\n private publish(snapshot: ConnectSnapshot): void {\n this.snapshot = Object.freeze(snapshot);\n for (const listener of this.listeners) listener();\n }\n private async run(\n operation: (signal: AbortSignal) => Promise<ConnectAttempt>,\n replace: boolean,\n expectedId?: string,\n ): Promise<ConnectAttempt> {\n this.assertActive();\n const generation = ++this.generation;\n this.request?.abort();\n const request = new AbortController();\n this.request = request;\n this.publish({ attempt: replace ? null : this.snapshot.attempt, busy: true, error: null });\n try {\n const result = await operation(request.signal);\n if (generation !== this.generation || this.disposed)\n throw new Error(\"Connect operation was superseded\");\n if (result.workspaceId !== this.workspaceId || (expectedId && result.id !== expectedId)) {\n throw new Error(\"Connect response scope mismatch\");\n }\n if (\n !Number.isSafeInteger(result.revision) ||\n result.revision < 1 ||\n (this.snapshot.attempt?.id === result.id &&\n result.revision < this.snapshot.attempt.revision)\n ) {\n throw new Error(\"Connect response revision is stale\");\n }\n const attempt = freezeTree(structuredClone(result));\n this.publish({ attempt, busy: false, error: null });\n return structuredClone(attempt);\n } catch (cause) {\n const error = cause instanceof Error ? cause : new Error(\"Connect operation failed\");\n if (generation === this.generation && !this.disposed) {\n // Transport errors can retain request bodies, headers, nested causes or\n // echoed credentials. Keep only a fixed, credential-free UI error in\n // the long-lived snapshot; direct callers still receive the rejection.\n this.publish({\n attempt: this.snapshot.attempt,\n busy: false,\n error: Object.freeze(\n new Error(\"Connect operation failed; refresh its status before retrying\"),\n ),\n });\n }\n throw error;\n } finally {\n if (generation === this.generation) this.request = null;\n }\n }\n}\n\nfunction freezeTree<T>(value: T): T {\n if (value !== null && typeof value === \"object\") {\n for (const child of Object.values(value)) freezeTree(child);\n Object.freeze(value);\n }\n return value;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/device.ts","../src/recovery.ts","../src/poll.ts","../src/authorization.ts","../src/browser-navigation.ts","../src/index.ts"],"sourcesContent":["/** Shared device-code behavior for model-account domains. These accounts keep\n * their existing APIs and ownership semantics; they are not generic credentials.\n * Keep opaque provider state on the host backend when reload recovery is needed. */\nexport async function pollDeviceAuthorization<\n T extends { status: string; intervalSeconds?: number },\n>(options: {\n poll: () => Promise<T>;\n expired: T;\n initialIntervalSeconds: number;\n expiresAtMs: number;\n signal: AbortSignal;\n retryable?: (error: unknown) => boolean;\n now?: () => number;\n wait?: (delayMs: number, signal: AbortSignal) => Promise<boolean>;\n maxRetryDelaySeconds?: number;\n}): Promise<T | null> {\n const now = options.now ?? Date.now;\n if (\n !Number.isFinite(options.expiresAtMs) ||\n !Number.isFinite(options.initialIntervalSeconds) ||\n options.initialIntervalSeconds <= 0 ||\n (options.maxRetryDelaySeconds !== undefined &&\n (!Number.isFinite(options.maxRetryDelaySeconds) || options.maxRetryDelaySeconds <= 0))\n )\n throw new Error(\"Device authorization requires a finite expiry and polling interval\");\n const wait = options.wait ?? waitForDeviceDelay;\n const initial = Math.max(1, options.initialIntervalSeconds);\n const maximum = Math.max(initial, options.maxRetryDelaySeconds ?? 30);\n let delay = initial;\n while (!options.signal.aborted) {\n const remaining = options.expiresAtMs - now();\n if (remaining <= 0) return options.expired;\n if (!(await wait(Math.min(delay * 1000, remaining), options.signal)) || options.signal.aborted)\n return null;\n if (now() >= options.expiresAtMs) return options.expired;\n let result: T;\n try {\n const observed = await observeDevicePoll(\n options.poll,\n options.expiresAtMs - now(),\n options.signal,\n );\n if (observed.kind === \"aborted\") return null;\n if (observed.kind === \"expired\") return options.expired;\n result = observed.result;\n } catch (error) {\n if (!options.retryable?.(error)) throw error;\n delay = Math.min(maximum, Math.max(initial, delay * 2));\n continue;\n }\n if (options.signal.aborted) return null;\n if (result.status !== \"pending\" && result.status !== \"slow_down\") return result;\n if (\n result.intervalSeconds !== undefined &&\n (!Number.isFinite(result.intervalSeconds) || result.intervalSeconds <= 0)\n )\n throw new Error(\"Provider returned an invalid device polling interval\");\n delay = Math.max(\n 1,\n result.intervalSeconds ?? (result.status === \"slow_down\" ? delay + 5 : delay),\n );\n }\n return null;\n}\n\n/** A transport may ignore cancellation. Bound observation without retrying an\n * in-flight request or treating the loss of observation as provider success. */\nasync function observeDevicePoll<T>(\n poll: () => Promise<T>,\n remainingMs: number,\n signal: AbortSignal,\n): Promise<{ kind: \"result\"; result: T } | { kind: \"aborted\" } | { kind: \"expired\" }> {\n if (signal.aborted) return { kind: \"aborted\" };\n if (remainingMs <= 0) return { kind: \"expired\" };\n let timer: ReturnType<typeof setTimeout> | undefined;\n let abort: (() => void) | undefined;\n try {\n const stopped = new Promise<{ kind: \"aborted\" } | { kind: \"expired\" }>((resolve) => {\n abort = () => resolve({ kind: \"aborted\" });\n signal.addEventListener(\"abort\", abort, { once: true });\n timer = setTimeout(() => resolve({ kind: \"expired\" }), remainingMs);\n });\n return await Promise.race([\n Promise.resolve()\n .then(poll)\n .then((result) => ({ kind: \"result\" as const, result })),\n stopped,\n ]);\n } finally {\n if (timer) clearTimeout(timer);\n if (abort) signal.removeEventListener(\"abort\", abort);\n }\n}\n\nasync function waitForDeviceDelay(delayMs: number, signal: AbortSignal): Promise<boolean> {\n if (signal.aborted) return false;\n return new Promise((resolve) => {\n const abort = () => {\n clearTimeout(timer);\n resolve(false);\n };\n const timer = setTimeout(() => {\n signal.removeEventListener(\"abort\", abort);\n resolve(true);\n }, delayMs);\n signal.addEventListener(\"abort\", abort, { once: true });\n if (signal.aborted) abort();\n });\n}\n","import type { ConnectAccount } from \"./types\";\n\n/** A provider/domain match is not account identity. Never silently substitute\n * another account when the original connection has disappeared. */\nexport function findConnectRecoveryAccount(\n accounts: readonly ConnectAccount[],\n connectionId: string | null | undefined,\n): ConnectAccount | null {\n if (!connectionId) return null;\n const matches = accounts.filter(\n (account) => account.id === connectionId || account.id === `social:${connectionId}`,\n );\n if (matches.length > 1)\n throw new Error(\"Connection recovery is ambiguous; choose the exact account.\");\n return matches[0] ?? null;\n}\n","import type { ConnectAttempt, ConnectTransport } from \"./types\";\n\n/** Read-only continuation. The backend remains authoritative for expiry and\n * completion; redirects and popup messages are never completion evidence. */\nexport async function pollConnectAttempt(\n transport: Pick<ConnectTransport, \"get\">,\n workspaceId: string,\n attemptId: string,\n options: { signal?: AbortSignal; timeoutMs?: number; minimumRevision?: number } = {},\n): Promise<ConnectAttempt> {\n const timeoutMs = options.timeoutMs ?? 120_000;\n if (\n !workspaceId ||\n !attemptId ||\n !Number.isFinite(timeoutMs) ||\n timeoutMs < 1 ||\n timeoutMs > 600_000 ||\n !Number.isSafeInteger(options.minimumRevision ?? 1) ||\n (options.minimumRevision ?? 1) < 1\n ) {\n throw new Error(\"Connect polling requires a scope and a bounded timeout\");\n }\n const abort = new AbortController();\n const cancel = () => abort.abort(options.signal?.reason);\n options.signal?.addEventListener(\"abort\", cancel, { once: true });\n if (options.signal?.aborted) cancel();\n const timer = setTimeout(() => abort.abort(new Error(\"Connect polling timed out\")), timeoutMs);\n let revision = options.minimumRevision ?? 1;\n try {\n while (true) {\n abort.signal.throwIfAborted();\n const result = await abortable(\n transport.get(workspaceId, attemptId, { signal: abort.signal }),\n abort.signal,\n );\n if (\n result.workspaceId !== workspaceId ||\n result.id !== attemptId ||\n !Number.isSafeInteger(result.revision) ||\n result.revision < Math.max(1, revision)\n ) {\n throw new Error(\"Connect polling response scope or revision mismatch\");\n }\n revision = result.revision;\n if (\n [\"complete\", \"cancelled\", \"expired\", \"failed\", \"uncertain\"].includes(result.state) ||\n ![\"authorize\", \"wait\"].includes(result.nextAction.type)\n )\n return result;\n const requested = result.nextAction.type === \"wait\" ? result.nextAction.pollAfterMs : 1000;\n const delay = Number.isFinite(requested) ? Math.min(60_000, Math.max(250, requested)) : 1000;\n await new Promise<void>((resolve, reject) => {\n const stop = () => {\n clearTimeout(wait);\n reject(abort.signal.reason);\n };\n const wait = setTimeout(() => {\n abort.signal.removeEventListener(\"abort\", stop);\n resolve();\n }, delay);\n abort.signal.addEventListener(\"abort\", stop, { once: true });\n if (abort.signal.aborted) stop();\n });\n }\n } finally {\n clearTimeout(timer);\n options.signal?.removeEventListener(\"abort\", cancel);\n }\n}\n\nfunction abortable<T>(pending: Promise<T>, signal: AbortSignal): Promise<T> {\n return new Promise((resolve, reject) => {\n const stop = () => reject(signal.reason);\n signal.addEventListener(\"abort\", stop, { once: true });\n if (signal.aborted) stop();\n pending.then(resolve, reject).finally(() => signal.removeEventListener(\"abort\", stop));\n });\n}\n","import { pollConnectAttempt } from \"./poll\";\nimport type { ConnectAttempt, ConnectTransport } from \"./types\";\n\n/** Inject navigation so hosts own routing and this package needs no DOM globals. */\nexport type ConnectNavigation = {\n openPopup(url: string): { close(): void; readonly closed?: boolean | undefined } | null;\n redirect(url: string): void;\n};\n\nexport class ConnectPopupClosedError extends Error {\n constructor() {\n super(\"Sign-in window closed. You can try connecting again.\");\n this.name = \"ConnectPopupClosedError\";\n }\n}\n\n/** Invoke directly from a user gesture for popup mode. Persist the opaque\n * attempt ID in host-owned state before redirect mode, then recover/poll on\n * return. Neither URL parameters nor popup messages prove completion. */\nexport function authorizeConnectAttempt(\n transport: Pick<ConnectTransport, \"get\">,\n attempt: ConnectAttempt,\n navigation: ConnectNavigation,\n options: { mode: \"popup\" | \"redirect\"; signal?: AbortSignal; timeoutMs?: number },\n): Promise<ConnectAttempt | null> {\n options.signal?.throwIfAborted();\n if (attempt.nextAction.type !== \"authorize\") {\n throw new Error(\"Connect attempt does not require authorization\");\n }\n const destination = new URL(attempt.nextAction.url);\n if (destination.protocol !== \"https:\" || destination.username || destination.password) {\n throw new Error(\"Connect authorization requires an HTTPS destination without credentials\");\n }\n if (\n !attempt.id ||\n !attempt.workspaceId ||\n !Number.isSafeInteger(attempt.revision) ||\n attempt.revision < 1\n )\n throw new Error(\"Connect authorization requires a scope\");\n if (options.mode === \"redirect\") {\n navigation.redirect(attempt.nextAction.url);\n return Promise.resolve(null);\n }\n const popup = navigation.openPopup(attempt.nextAction.url);\n if (!popup) throw new Error(\"Connect popup was blocked; retry with redirect mode\");\n const observation = new AbortController();\n const closed = new ConnectPopupClosedError();\n const abort = () => observation.abort(options.signal?.reason);\n options.signal?.addEventListener(\"abort\", abort, { once: true });\n if (options.signal?.aborted) abort();\n const watch = setInterval(() => {\n try {\n if (popup.closed === true) observation.abort(closed);\n } catch {\n // Some hosts cannot observe their popup after cross-origin navigation.\n }\n }, 250);\n return pollConnectAttempt(transport, attempt.workspaceId, attempt.id, {\n ...options,\n signal: observation.signal,\n minimumRevision: attempt.revision,\n })\n .catch(async (failure: unknown) => {\n if (failure !== closed) throw failure;\n // The callback may have committed just as the provider window closed.\n // Recheck the backend briefly; the window itself is never success evidence.\n const recovery = new AbortController();\n const abortRecovery = () => recovery.abort(options.signal?.reason);\n options.signal?.addEventListener(\"abort\", abortRecovery, { once: true });\n if (options.signal?.aborted) abortRecovery();\n const deadline = setTimeout(() => recovery.abort(closed), 1_500);\n try {\n return await pollConnectAttempt(transport, attempt.workspaceId, attempt.id, {\n signal: recovery.signal,\n minimumRevision: attempt.revision,\n timeoutMs: 2_000,\n });\n } catch (recoveryFailure) {\n if (recoveryFailure === closed) throw closed;\n throw recoveryFailure;\n } finally {\n clearTimeout(deadline);\n options.signal?.removeEventListener(\"abort\", abortRecovery);\n }\n })\n .finally(() => {\n clearInterval(watch);\n options.signal?.removeEventListener(\"abort\", abort);\n // Window cleanup must never replace the authoritative result or failure.\n try {\n popup.close();\n } catch {\n /* Host navigation may already have disposed it. */\n }\n });\n}\n","import type { ConnectNavigation } from \"./authorization\";\n\n/** Structural browser surface so importing Connect never reads DOM globals. */\nexport type ConnectBrowserWindow = {\n open(\n url: string,\n target: string,\n features: string,\n ): {\n opener: unknown;\n readonly closed?: boolean;\n location: { replace(url: string): void };\n close(): void;\n } | null;\n location: { assign(url: string): void };\n};\n\nfunction validateDestination(url: string): void {\n const parsed = new URL(url);\n if (parsed.protocol !== \"https:\" || parsed.username || parsed.password)\n throw new Error(\"Connect authorization requires an HTTPS destination without credentials\");\n}\n\nfunction closeIsolatedPopup(popup: NonNullable<ReturnType<ConnectBrowserWindow[\"open\"]>>): void {\n try {\n if (popup.closed) return;\n // After opener isolation, browsers cannot close a foreign-origin window.\n // Check access first to avoid a futile close and its browser warning.\n Reflect.get(popup.location, \"href\");\n popup.close();\n } catch {\n /* The human closes foreign-origin or detached provider windows. */\n }\n}\n\n/** Reserve the isolated window during the human's click, before asynchronous\n * discovery. The caller must close it if setup fails before navigation. */\nexport function reserveBrowserConnectNavigation(browser: ConnectBrowserWindow): {\n navigation: ConnectNavigation;\n close(): void;\n} {\n const popup = browser.open(\"about:blank\", \"_blank\", \"popup,width=520,height=720\");\n if (!popup) throw new Error(\"Allow popups to connect your account, then try again.\");\n try {\n popup.opener = null;\n if (popup.opener !== null) throw new Error(\"Connect popup isolation failed\");\n } catch {\n closeIsolatedPopup(popup);\n throw new Error(\"Connect popup could not open safely. Please try again.\");\n }\n const close = () => closeIsolatedPopup(popup);\n return {\n close,\n navigation: {\n openPopup(url) {\n validateDestination(url);\n popup.location.replace(url);\n return {\n close,\n get closed() {\n return popup.closed;\n },\n };\n },\n redirect(url) {\n validateDestination(url);\n close();\n browser.location.assign(url);\n },\n },\n };\n}\n\n/** Pass window from the host's browser entry point. Opens a fresh blank window\n * synchronously and severs its opener BEFORE any provider content can load.\n * Never uses a reusable named target or relies on provider window messages.\n * The caller retains only best-effort close capability for polling cleanup;\n * the human closes provider windows that remain on a foreign origin. */\nexport function createBrowserConnectNavigation(browser: ConnectBrowserWindow): ConnectNavigation {\n return {\n openPopup(url) {\n validateDestination(url);\n const popup = browser.open(\"about:blank\", \"_blank\", \"popup,width=520,height=720\");\n if (!popup) return null;\n try {\n popup.opener = null;\n if (popup.opener !== null) throw new Error(\"Connect popup isolation failed\");\n popup.location.replace(url);\n } catch {\n closeIsolatedPopup(popup);\n throw new Error(\"Connect popup could not navigate safely; retry with redirect mode\");\n }\n return {\n close: () => closeIsolatedPopup(popup),\n get closed() {\n return popup.closed;\n },\n };\n },\n redirect(url) {\n validateDestination(url);\n browser.location.assign(url);\n },\n };\n}\n","export * from \"./types\";\nexport { pollDeviceAuthorization } from \"./device\";\nexport { findConnectRecoveryAccount } from \"./recovery\";\nexport { pollConnectAttempt } from \"./poll\";\nexport {\n authorizeConnectAttempt,\n ConnectPopupClosedError,\n type ConnectNavigation,\n} from \"./authorization\";\nexport {\n createBrowserConnectNavigation,\n reserveBrowserConnectNavigation,\n type ConnectBrowserWindow,\n} from \"./browser-navigation\";\nimport type { ConnectAdvance, ConnectAttempt, ConnectOwnership, ConnectTransport } from \"./types\";\nimport { pollConnectAttempt } from \"./poll\";\n\nexport type ConnectSnapshot = {\n attempt: ConnectAttempt | null;\n busy: boolean;\n error: Error | null;\n};\n\n/** A transport-injected, framework-neutral view of one durable setup attempt.\n * OAuth redirects are hints to navigate; completion is read from the backend.\n * Secret form values are never stored in a snapshot or browser persistence. */\nexport class ConnectController {\n private snapshot: ConnectSnapshot = Object.freeze({ attempt: null, busy: false, error: null });\n private readonly listeners = new Set<() => void>();\n private generation = 0;\n private request: AbortController | null = null;\n private disposed = false;\n\n constructor(\n readonly transport: ConnectTransport,\n readonly workspaceId: string,\n ) {\n if (!workspaceId) throw new Error(\"Connect requires an explicit workspace\");\n }\n\n getSnapshot = (): ConnectSnapshot => this.snapshot;\n subscribe = (listener: () => void): (() => void) => {\n this.assertActive();\n this.listeners.add(listener);\n return () => {\n this.listeners.delete(listener);\n };\n };\n\n begin(input: {\n providerId: string;\n ownership: ConnectOwnership;\n returnUrl: string;\n idempotencyKey: string;\n reconnectAccountId?: string;\n installationTarget?: import(\"./types\").ConnectInstallationTarget;\n }): Promise<ConnectAttempt> {\n // Validation must not serialize or decorate the host's exact return string.\n const url = new URL(input.returnUrl);\n if (![\"https:\", \"http:\"].includes(url.protocol) || url.username || url.password) {\n throw new Error(\"Connect return URL must be an HTTP(S) destination without credentials\");\n }\n return this.run((signal) => this.transport.begin(this.workspaceId, input, { signal }), true);\n }\n\n recover(attemptId: string): Promise<ConnectAttempt> {\n if (!attemptId) throw new Error(\"Connect recovery requires an attempt ID\");\n return this.run(\n (signal) => this.transport.get(this.workspaceId, attemptId, { signal }),\n true,\n attemptId,\n );\n }\n\n refresh(): Promise<ConnectAttempt> {\n const attempt = this.requireAttempt();\n if (this.snapshot.busy) throw new Error(\"A Connect operation is already in progress\");\n return this.run(\n (signal) => this.transport.get(this.workspaceId, attempt.id, { signal }),\n false,\n attempt.id,\n );\n }\n\n advance(action: ConnectAdvance, idempotencyKey: string): Promise<ConnectAttempt> {\n const attempt = this.requireAttempt();\n if (this.snapshot.busy) throw new Error(\"A Connect operation is already in progress\");\n return this.run(\n (signal) =>\n this.transport.advance(\n this.workspaceId,\n attempt.id,\n {\n expectedRevision: attempt.revision,\n idempotencyKey,\n action,\n },\n { signal },\n ),\n false,\n attempt.id,\n );\n }\n\n /** Observe backend progress without replaying a setup mutation. Disposal or\n * selecting another attempt aborts the read loop and fences its late result. */\n waitForAction(options: { timeoutMs?: number } = {}): Promise<ConnectAttempt> {\n const attempt = this.requireAttempt();\n if (this.snapshot.busy) throw new Error(\"A Connect operation is already in progress\");\n return this.run(\n (signal) =>\n pollConnectAttempt(this.transport, this.workspaceId, attempt.id, {\n ...options,\n signal,\n minimumRevision: attempt.revision,\n }),\n false,\n attempt.id,\n );\n }\n\n cancel(idempotencyKey: string): Promise<ConnectAttempt> {\n const attempt = this.requireAttempt();\n if (this.snapshot.busy) throw new Error(\"A Connect operation is already in progress\");\n return this.run(\n (signal) =>\n this.transport.cancel(\n this.workspaceId,\n attempt.id,\n {\n expectedRevision: attempt.revision,\n idempotencyKey,\n },\n { signal },\n ),\n false,\n attempt.id,\n );\n }\n\n dispose(): void {\n if (this.disposed) return;\n this.disposed = true;\n this.generation++;\n this.request?.abort();\n this.request = null;\n this.listeners.clear();\n }\n\n private assertActive(): void {\n if (this.disposed) throw new Error(\"Connect controller is disposed\");\n }\n private requireAttempt(): ConnectAttempt {\n this.assertActive();\n if (!this.snapshot.attempt) throw new Error(\"No Connect attempt is selected\");\n return this.snapshot.attempt;\n }\n private publish(snapshot: ConnectSnapshot): void {\n this.snapshot = Object.freeze(snapshot);\n for (const listener of this.listeners) listener();\n }\n private async run(\n operation: (signal: AbortSignal) => Promise<ConnectAttempt>,\n replace: boolean,\n expectedId?: string,\n ): Promise<ConnectAttempt> {\n this.assertActive();\n const generation = ++this.generation;\n this.request?.abort();\n const request = new AbortController();\n this.request = request;\n this.publish({ attempt: replace ? null : this.snapshot.attempt, busy: true, error: null });\n try {\n const result = await operation(request.signal);\n if (generation !== this.generation || this.disposed)\n throw new Error(\"Connect operation was superseded\");\n if (result.workspaceId !== this.workspaceId || (expectedId && result.id !== expectedId)) {\n throw new Error(\"Connect response scope mismatch\");\n }\n if (\n !Number.isSafeInteger(result.revision) ||\n result.revision < 1 ||\n (this.snapshot.attempt?.id === result.id &&\n result.revision < this.snapshot.attempt.revision)\n ) {\n throw new Error(\"Connect response revision is stale\");\n }\n const attempt = freezeTree(structuredClone(result));\n this.publish({ attempt, busy: false, error: null });\n return structuredClone(attempt);\n } catch (cause) {\n const error = cause instanceof Error ? cause : new Error(\"Connect operation failed\");\n if (generation === this.generation && !this.disposed) {\n // Transport errors can retain request bodies, headers, nested causes or\n // echoed credentials. Keep only a fixed, credential-free UI error in\n // the long-lived snapshot; direct callers still receive the rejection.\n this.publish({\n attempt: this.snapshot.attempt,\n busy: false,\n error: Object.freeze(\n new Error(\"Connect operation failed; refresh its status before retrying\"),\n ),\n });\n }\n throw error;\n } finally {\n if (generation === this.generation) this.request = null;\n }\n }\n}\n\nfunction freezeTree<T>(value: T): T {\n if (value !== null && typeof value === \"object\") {\n for (const child of Object.values(value)) freezeTree(child);\n Object.freeze(value);\n }\n return value;\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
package/src/authorization.ts
CHANGED
|
@@ -3,10 +3,17 @@ import type { ConnectAttempt, ConnectTransport } from "./types";
|
|
|
3
3
|
|
|
4
4
|
/** Inject navigation so hosts own routing and this package needs no DOM globals. */
|
|
5
5
|
export type ConnectNavigation = {
|
|
6
|
-
openPopup(url: string): { close(): void } | null;
|
|
6
|
+
openPopup(url: string): { close(): void; readonly closed?: boolean | undefined } | null;
|
|
7
7
|
redirect(url: string): void;
|
|
8
8
|
};
|
|
9
9
|
|
|
10
|
+
export class ConnectPopupClosedError extends Error {
|
|
11
|
+
constructor() {
|
|
12
|
+
super("Sign-in window closed. You can try connecting again.");
|
|
13
|
+
this.name = "ConnectPopupClosedError";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
10
17
|
/** Invoke directly from a user gesture for popup mode. Persist the opaque
|
|
11
18
|
* attempt ID in host-owned state before redirect mode, then recover/poll on
|
|
12
19
|
* return. Neither URL parameters nor popup messages prove completion. */
|
|
@@ -37,15 +44,54 @@ export function authorizeConnectAttempt(
|
|
|
37
44
|
}
|
|
38
45
|
const popup = navigation.openPopup(attempt.nextAction.url);
|
|
39
46
|
if (!popup) throw new Error("Connect popup was blocked; retry with redirect mode");
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
47
|
+
const observation = new AbortController();
|
|
48
|
+
const closed = new ConnectPopupClosedError();
|
|
49
|
+
const abort = () => observation.abort(options.signal?.reason);
|
|
50
|
+
options.signal?.addEventListener("abort", abort, { once: true });
|
|
51
|
+
if (options.signal?.aborted) abort();
|
|
52
|
+
const watch = setInterval(() => {
|
|
45
53
|
try {
|
|
46
|
-
popup.
|
|
54
|
+
if (popup.closed === true) observation.abort(closed);
|
|
47
55
|
} catch {
|
|
48
|
-
|
|
56
|
+
// Some hosts cannot observe their popup after cross-origin navigation.
|
|
49
57
|
}
|
|
50
|
-
});
|
|
58
|
+
}, 250);
|
|
59
|
+
return pollConnectAttempt(transport, attempt.workspaceId, attempt.id, {
|
|
60
|
+
...options,
|
|
61
|
+
signal: observation.signal,
|
|
62
|
+
minimumRevision: attempt.revision,
|
|
63
|
+
})
|
|
64
|
+
.catch(async (failure: unknown) => {
|
|
65
|
+
if (failure !== closed) throw failure;
|
|
66
|
+
// The callback may have committed just as the provider window closed.
|
|
67
|
+
// Recheck the backend briefly; the window itself is never success evidence.
|
|
68
|
+
const recovery = new AbortController();
|
|
69
|
+
const abortRecovery = () => recovery.abort(options.signal?.reason);
|
|
70
|
+
options.signal?.addEventListener("abort", abortRecovery, { once: true });
|
|
71
|
+
if (options.signal?.aborted) abortRecovery();
|
|
72
|
+
const deadline = setTimeout(() => recovery.abort(closed), 1_500);
|
|
73
|
+
try {
|
|
74
|
+
return await pollConnectAttempt(transport, attempt.workspaceId, attempt.id, {
|
|
75
|
+
signal: recovery.signal,
|
|
76
|
+
minimumRevision: attempt.revision,
|
|
77
|
+
timeoutMs: 2_000,
|
|
78
|
+
});
|
|
79
|
+
} catch (recoveryFailure) {
|
|
80
|
+
if (recoveryFailure === closed) throw closed;
|
|
81
|
+
throw recoveryFailure;
|
|
82
|
+
} finally {
|
|
83
|
+
clearTimeout(deadline);
|
|
84
|
+
options.signal?.removeEventListener("abort", abortRecovery);
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
.finally(() => {
|
|
88
|
+
clearInterval(watch);
|
|
89
|
+
options.signal?.removeEventListener("abort", abort);
|
|
90
|
+
// Window cleanup must never replace the authoritative result or failure.
|
|
91
|
+
try {
|
|
92
|
+
popup.close();
|
|
93
|
+
} catch {
|
|
94
|
+
/* Host navigation may already have disposed it. */
|
|
95
|
+
}
|
|
96
|
+
});
|
|
51
97
|
}
|
|
@@ -8,6 +8,7 @@ export type ConnectBrowserWindow = {
|
|
|
8
8
|
features: string,
|
|
9
9
|
): {
|
|
10
10
|
opener: unknown;
|
|
11
|
+
readonly closed?: boolean;
|
|
11
12
|
location: { replace(url: string): void };
|
|
12
13
|
close(): void;
|
|
13
14
|
} | null;
|
|
@@ -20,6 +21,18 @@ function validateDestination(url: string): void {
|
|
|
20
21
|
throw new Error("Connect authorization requires an HTTPS destination without credentials");
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
function closeIsolatedPopup(popup: NonNullable<ReturnType<ConnectBrowserWindow["open"]>>): void {
|
|
25
|
+
try {
|
|
26
|
+
if (popup.closed) return;
|
|
27
|
+
// After opener isolation, browsers cannot close a foreign-origin window.
|
|
28
|
+
// Check access first to avoid a futile close and its browser warning.
|
|
29
|
+
Reflect.get(popup.location, "href");
|
|
30
|
+
popup.close();
|
|
31
|
+
} catch {
|
|
32
|
+
/* The human closes foreign-origin or detached provider windows. */
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
23
36
|
/** Reserve the isolated window during the human's click, before asynchronous
|
|
24
37
|
* discovery. The caller must close it if setup fails before navigation. */
|
|
25
38
|
export function reserveBrowserConnectNavigation(browser: ConnectBrowserWindow): {
|
|
@@ -32,23 +45,22 @@ export function reserveBrowserConnectNavigation(browser: ConnectBrowserWindow):
|
|
|
32
45
|
popup.opener = null;
|
|
33
46
|
if (popup.opener !== null) throw new Error("Connect popup isolation failed");
|
|
34
47
|
} catch {
|
|
35
|
-
popup
|
|
48
|
+
closeIsolatedPopup(popup);
|
|
36
49
|
throw new Error("Connect popup could not open safely. Please try again.");
|
|
37
50
|
}
|
|
38
|
-
const close = () =>
|
|
39
|
-
try {
|
|
40
|
-
popup.close();
|
|
41
|
-
} catch {
|
|
42
|
-
/* Already closed. */
|
|
43
|
-
}
|
|
44
|
-
};
|
|
51
|
+
const close = () => closeIsolatedPopup(popup);
|
|
45
52
|
return {
|
|
46
53
|
close,
|
|
47
54
|
navigation: {
|
|
48
55
|
openPopup(url) {
|
|
49
56
|
validateDestination(url);
|
|
50
57
|
popup.location.replace(url);
|
|
51
|
-
return {
|
|
58
|
+
return {
|
|
59
|
+
close,
|
|
60
|
+
get closed() {
|
|
61
|
+
return popup.closed;
|
|
62
|
+
},
|
|
63
|
+
};
|
|
52
64
|
},
|
|
53
65
|
redirect(url) {
|
|
54
66
|
validateDestination(url);
|
|
@@ -62,7 +74,8 @@ export function reserveBrowserConnectNavigation(browser: ConnectBrowserWindow):
|
|
|
62
74
|
/** Pass window from the host's browser entry point. Opens a fresh blank window
|
|
63
75
|
* synchronously and severs its opener BEFORE any provider content can load.
|
|
64
76
|
* Never uses a reusable named target or relies on provider window messages.
|
|
65
|
-
* The caller retains only
|
|
77
|
+
* The caller retains only best-effort close capability for polling cleanup;
|
|
78
|
+
* the human closes provider windows that remain on a foreign origin. */
|
|
66
79
|
export function createBrowserConnectNavigation(browser: ConnectBrowserWindow): ConnectNavigation {
|
|
67
80
|
return {
|
|
68
81
|
openPopup(url) {
|
|
@@ -74,14 +87,15 @@ export function createBrowserConnectNavigation(browser: ConnectBrowserWindow): C
|
|
|
74
87
|
if (popup.opener !== null) throw new Error("Connect popup isolation failed");
|
|
75
88
|
popup.location.replace(url);
|
|
76
89
|
} catch {
|
|
77
|
-
|
|
78
|
-
popup.close();
|
|
79
|
-
} catch {
|
|
80
|
-
/* Preserve the isolated navigation failure. */
|
|
81
|
-
}
|
|
90
|
+
closeIsolatedPopup(popup);
|
|
82
91
|
throw new Error("Connect popup could not navigate safely; retry with redirect mode");
|
|
83
92
|
}
|
|
84
|
-
return {
|
|
93
|
+
return {
|
|
94
|
+
close: () => closeIsolatedPopup(popup),
|
|
95
|
+
get closed() {
|
|
96
|
+
return popup.closed;
|
|
97
|
+
},
|
|
98
|
+
};
|
|
85
99
|
},
|
|
86
100
|
redirect(url) {
|
|
87
101
|
validateDestination(url);
|
package/src/index.ts
CHANGED
|
@@ -2,7 +2,11 @@ export * from "./types";
|
|
|
2
2
|
export { pollDeviceAuthorization } from "./device";
|
|
3
3
|
export { findConnectRecoveryAccount } from "./recovery";
|
|
4
4
|
export { pollConnectAttempt } from "./poll";
|
|
5
|
-
export {
|
|
5
|
+
export {
|
|
6
|
+
authorizeConnectAttempt,
|
|
7
|
+
ConnectPopupClosedError,
|
|
8
|
+
type ConnectNavigation,
|
|
9
|
+
} from "./authorization";
|
|
6
10
|
export {
|
|
7
11
|
createBrowserConnectNavigation,
|
|
8
12
|
reserveBrowserConnectNavigation,
|