@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 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. Cancellation stops polling, not the
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.
@@ -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 the close capability for backend-polling cleanup. */
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.close();
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 { close };
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
- try {
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 { close: () => popup.close() };
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/connect",
3
- "version": "0.3.0",
3
+ "version": "0.3.1-canary.36927100590001",
4
4
  "description": "Framework-neutral OpenGeni connection setup and recovery controller.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -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
- return pollConnectAttempt(transport, attempt.workspaceId, attempt.id, {
41
- ...options,
42
- minimumRevision: attempt.revision,
43
- }).finally(() => {
44
- // Window cleanup must never replace the authoritative result or failure.
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.close();
54
+ if (popup.closed === true) observation.abort(closed);
47
55
  } catch {
48
- /* Host navigation may already have disposed it. */
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.close();
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 { close };
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 the close capability for backend-polling cleanup. */
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
- try {
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 { close: () => popup.close() };
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 { authorizeConnectAttempt, type ConnectNavigation } from "./authorization";
5
+ export {
6
+ authorizeConnectAttempt,
7
+ ConnectPopupClosedError,
8
+ type ConnectNavigation,
9
+ } from "./authorization";
6
10
  export {
7
11
  createBrowserConnectNavigation,
8
12
  reserveBrowserConnectNavigation,