@microsoft/rayfin-auth-provider-fabric 1.20.0 → 1.22.0

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.
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Handoff result returned by the host extension's `AuthPlugin`.
3
+ */
4
+ export interface PostMessageHandoffResult {
5
+ /** Single-use handoff code to exchange for tokens. */
6
+ handoffCode: string;
7
+ /** Opaque nonce echoed back for correlation. */
8
+ state: string;
9
+ }
10
+ /**
11
+ * PostMessage transport for embedded (iframe) auth.
12
+ *
13
+ * Sends a `fabric-auth` / `auth.requestHandoff` message to the parent
14
+ * window and waits for a correlated response carrying the handoff code.
15
+ *
16
+ * Delegates the low-level postMessage send/receive/correlate to
17
+ * `sendBridgeRequest` from `@microsoft/fabric-embedded-host`.
18
+ *
19
+ * @param params.callbackUrl The callback origin where the SPA is hosted.
20
+ * @param params.codeChallenge PKCE S256 code challenge.
21
+ * @param params.codeChallengeMethod Always `"S256"`.
22
+ * @param params.state Opaque nonce for CSRF/correlation.
23
+ * @param params.timeoutMs Response timeout in milliseconds (default 30 000).
24
+ * @returns Promise resolving with the handoff code and state.
25
+ * @throws {AuthError} On timeout, error response, or missing parent.
26
+ */
27
+ export declare function requestHandoff(params: {
28
+ callbackUrl: string;
29
+ codeChallenge: string;
30
+ codeChallengeMethod: string;
31
+ state: string;
32
+ timeoutMs?: number;
33
+ }): Promise<PostMessageHandoffResult>;
34
+ //# sourceMappingURL=PostMessageAuthTransport.d.ts.map
@@ -0,0 +1,44 @@
1
+ import { sendBridgeRequest, BridgeError, } from '@microsoft/fabric-embedded-host';
2
+ import { AuthError } from '@microsoft/rayfin-lib';
3
+ /**
4
+ * PostMessage transport for embedded (iframe) auth.
5
+ *
6
+ * Sends a `fabric-auth` / `auth.requestHandoff` message to the parent
7
+ * window and waits for a correlated response carrying the handoff code.
8
+ *
9
+ * Delegates the low-level postMessage send/receive/correlate to
10
+ * `sendBridgeRequest` from `@microsoft/fabric-embedded-host`.
11
+ *
12
+ * @param params.callbackUrl The callback origin where the SPA is hosted.
13
+ * @param params.codeChallenge PKCE S256 code challenge.
14
+ * @param params.codeChallengeMethod Always `"S256"`.
15
+ * @param params.state Opaque nonce for CSRF/correlation.
16
+ * @param params.timeoutMs Response timeout in milliseconds (default 30 000).
17
+ * @returns Promise resolving with the handoff code and state.
18
+ * @throws {AuthError} On timeout, error response, or missing parent.
19
+ */
20
+ export function requestHandoff(params) {
21
+ const { callbackUrl, codeChallenge, codeChallengeMethod, state, timeoutMs } = params;
22
+ if (!window.parent || window.parent === window) {
23
+ return Promise.reject(new AuthError('No parent window — embedded auth requires an iframe host.', 'NO_PARENT_WINDOW'));
24
+ }
25
+ return sendBridgeRequest({
26
+ target: window.parent,
27
+ channel: 'fabric-auth',
28
+ kind: 'auth.requestHandoff',
29
+ payload: {
30
+ callbackUrl,
31
+ codeChallenge,
32
+ codeChallengeMethod,
33
+ state,
34
+ },
35
+ timeoutMs,
36
+ }).catch((err) => {
37
+ // Re-wrap BridgeError as AuthError for backward compatibility
38
+ if (err instanceof BridgeError) {
39
+ throw new AuthError(err.message, err.code);
40
+ }
41
+ throw err;
42
+ });
43
+ }
44
+ //# sourceMappingURL=PostMessageAuthTransport.js.map
@@ -0,0 +1,20 @@
1
+ import type { Auth } from '@microsoft/rayfin-auth';
2
+ import type { FabricAuthOptions } from './types';
3
+ /**
4
+ * Embedded-mode Fabric login — acquires a session via postMessage to the
5
+ * parent Fabric Extension Host without opening a popup.
6
+ *
7
+ * 1. Generates PKCE parameters in local variables (no `localStorage`).
8
+ * 2. Sends `auth.requestHandoff` to the parent via {@link requestHandoff}.
9
+ * 3. Receives the handoff code from the host's `AuthPlugin`.
10
+ * 4. Exchanges the handoff code for tokens via `exchangeVerificationCode`.
11
+ * 5. Creates a session via `createSessionFromTokenResponse`.
12
+ *
13
+ * The session is stored in the iframe's own `localStorage`.
14
+ *
15
+ * @param auth Auth instance for token exchange and session management.
16
+ * @param options Fabric auth options (`returnOrigin` required).
17
+ * @throws {AuthError} On missing options, transport failure, or token exchange error.
18
+ */
19
+ export declare function embeddedFabricLogin(auth: Auth, options: FabricAuthOptions): Promise<void>;
20
+ //# sourceMappingURL=embeddedFabricLogin.d.ts.map
@@ -0,0 +1,55 @@
1
+ import { generateCodeVerifier, generateCodeChallenge, generateState, } from '@microsoft/rayfin-auth';
2
+ import { AuthError } from '@microsoft/rayfin-lib';
3
+ import { requestHandoff } from './PostMessageAuthTransport';
4
+ /**
5
+ * Embedded-mode Fabric login — acquires a session via postMessage to the
6
+ * parent Fabric Extension Host without opening a popup.
7
+ *
8
+ * 1. Generates PKCE parameters in local variables (no `localStorage`).
9
+ * 2. Sends `auth.requestHandoff` to the parent via {@link requestHandoff}.
10
+ * 3. Receives the handoff code from the host's `AuthPlugin`.
11
+ * 4. Exchanges the handoff code for tokens via `exchangeVerificationCode`.
12
+ * 5. Creates a session via `createSessionFromTokenResponse`.
13
+ *
14
+ * The session is stored in the iframe's own `localStorage`.
15
+ *
16
+ * @param auth Auth instance for token exchange and session management.
17
+ * @param options Fabric auth options (`returnOrigin` required).
18
+ * @throws {AuthError} On missing options, transport failure, or token exchange error.
19
+ */
20
+ export async function embeddedFabricLogin(auth, options) {
21
+ if (!options.returnOrigin) {
22
+ throw new AuthError('returnOrigin is required for embedded Fabric authentication.', 'MISSING_RETURN_ORIGIN');
23
+ }
24
+ // PKCE in local variables only — not persisted to localStorage.
25
+ const codeVerifier = generateCodeVerifier();
26
+ const codeChallenge = await generateCodeChallenge(codeVerifier);
27
+ const state = generateState();
28
+ console.debug('[FabricAuth:embedded] Requesting handoff from host via postMessage');
29
+ // NOTE: We intentionally do NOT pass fabricPortalUrl as the targetOrigin.
30
+ // The SPA's window.parent is the Fabric *extension* iframe (AppViewMode),
31
+ // not the portal itself. The extension runs at a different origin than
32
+ // fabricPortalUrl, and the child iframe cannot read window.parent.origin.
33
+ // Security relies on UUID v4 requestId correlation + PKCE.
34
+ const result = await requestHandoff({
35
+ callbackUrl: options.returnOrigin,
36
+ codeChallenge,
37
+ codeChallengeMethod: 'S256',
38
+ state,
39
+ });
40
+ // Validate state to prevent replay attacks.
41
+ if (result.state !== state) {
42
+ throw new AuthError('State mismatch in embedded auth handoff response.', 'STATE_MISMATCH');
43
+ }
44
+ console.debug('[FabricAuth:embedded] Handoff received, exchanging code for tokens');
45
+ const redirectUri = options.returnOrigin;
46
+ const tokenResponse = await auth.getAuthApi().exchangeVerificationCode({
47
+ verificationCode: result.handoffCode,
48
+ codeVerifier,
49
+ codeType: 'fabric_handoff',
50
+ redirectUri,
51
+ });
52
+ auth.createSessionFromTokenResponse(tokenResponse);
53
+ console.debug('[FabricAuth:embedded] Session established');
54
+ }
55
+ //# sourceMappingURL=embeddedFabricLogin.js.map
@@ -3,20 +3,22 @@ import type { FabricAuthOptions } from './types';
3
3
  /**
4
4
  * Ensures the user is signed in via Fabric brokered authentication.
5
5
  *
6
- * Implements a 3-step waterfall — the first step that succeeds short-circuits the rest:
6
+ * Implements a multi-step waterfall — the first step that succeeds short-circuits the rest:
7
7
  *
8
8
  * 1. **Already authenticated** — if `auth.getSession().isAuthenticated` is true,
9
9
  * return the existing session immediately.
10
10
  * 2. **Refresh token** — if a refresh token is available, attempt `auth.refreshSession()`.
11
11
  * Return the refreshed session on success; continue on failure.
12
- * 3. **Open Fabric broker** — no existing auth path available. Open the Fabric Portal
12
+ * 3. **Embedded mode** — if running inside a Fabric iframe (`fabricEmbedded=true`),
13
+ * use `embeddedFabricLogin()` to acquire a session via `postMessage` handoff.
14
+ * 4. **Open Fabric broker** — no existing auth path available. Open the Fabric Portal
13
15
  * in a new tab via `initiateFabricLogin()` and wait for the Fabric extension to post
14
16
  * the handoff code via `postMessage`. The function exchanges the code internally
15
17
  * and creates the session. Once the promise resolves, return the new session.
16
18
  *
17
- * **Step 3 calls `window.open()`** — to avoid popup/tab blockers, call this function
19
+ * **Step 4 calls `window.open()`** — to avoid popup/tab blockers, call this function
18
20
  * from inside a synchronous user-gesture handler (e.g., a button click).
19
- * Steps 1–2 do not open windows and are safe to call on page load.
21
+ * Steps 1–3 do not open windows and are safe to call on page load.
20
22
  *
21
23
  * @param auth The Auth instance.
22
24
  * @param options Fabric authentication options (workspaceId, projectId, returnOrigin).
@@ -1,22 +1,26 @@
1
1
  import { AuthError } from '@microsoft/rayfin-lib';
2
+ import { embeddedFabricLogin } from './embeddedFabricLogin';
3
+ import { isEmbeddedMode, tryResumeSession } from './fabricAuthHelpers';
2
4
  import { initiateFabricLogin } from './initiateFabricLogin';
3
5
  /**
4
6
  * Ensures the user is signed in via Fabric brokered authentication.
5
7
  *
6
- * Implements a 3-step waterfall — the first step that succeeds short-circuits the rest:
8
+ * Implements a multi-step waterfall — the first step that succeeds short-circuits the rest:
7
9
  *
8
10
  * 1. **Already authenticated** — if `auth.getSession().isAuthenticated` is true,
9
11
  * return the existing session immediately.
10
12
  * 2. **Refresh token** — if a refresh token is available, attempt `auth.refreshSession()`.
11
13
  * Return the refreshed session on success; continue on failure.
12
- * 3. **Open Fabric broker** — no existing auth path available. Open the Fabric Portal
14
+ * 3. **Embedded mode** — if running inside a Fabric iframe (`fabricEmbedded=true`),
15
+ * use `embeddedFabricLogin()` to acquire a session via `postMessage` handoff.
16
+ * 4. **Open Fabric broker** — no existing auth path available. Open the Fabric Portal
13
17
  * in a new tab via `initiateFabricLogin()` and wait for the Fabric extension to post
14
18
  * the handoff code via `postMessage`. The function exchanges the code internally
15
19
  * and creates the session. Once the promise resolves, return the new session.
16
20
  *
17
- * **Step 3 calls `window.open()`** — to avoid popup/tab blockers, call this function
21
+ * **Step 4 calls `window.open()`** — to avoid popup/tab blockers, call this function
18
22
  * from inside a synchronous user-gesture handler (e.g., a button click).
19
- * Steps 1–2 do not open windows and are safe to call on page load.
23
+ * Steps 1–3 do not open windows and are safe to call on page load.
20
24
  *
21
25
  * @param auth The Auth instance.
22
26
  * @param options Fabric authentication options (workspaceId, projectId, returnOrigin).
@@ -24,42 +28,44 @@ import { initiateFabricLogin } from './initiateFabricLogin';
24
28
  * @throws {AuthError} If all steps fail or the broker tab is blocked.
25
29
  */
26
30
  export async function ensureSignedInWithFabric(auth, options) {
27
- // Step 1: Already authenticated
28
- console.debug('[FabricAuth] Step 1: checking for existing session');
29
- const currentSession = auth.getSession();
30
- if (currentSession.isAuthenticated) {
31
- console.debug('[FabricAuth] Step 1: existing session found, skipping remaining steps');
32
- return currentSession;
31
+ // Steps 1-2: existing session or refresh
32
+ const resumed = await tryResumeSession(auth);
33
+ if (resumed) {
34
+ return resumed;
33
35
  }
34
- // Step 2: Try refresh token
35
- console.debug('[FabricAuth] Step 2: checking for refresh token');
36
- if (auth.hasRefreshToken()) {
36
+ // Step 3: Embedded mode — postMessage auth via parent iframe host
37
+ if (isEmbeddedMode(options)) {
38
+ console.debug('[FabricAuth] Embedded mode detected, using postMessage auth');
37
39
  try {
38
- await auth.refreshSession();
39
- const refreshedSession = auth.getSession();
40
- if (refreshedSession.isAuthenticated) {
41
- console.debug('[FabricAuth] Step 2: session refreshed successfully');
42
- return refreshedSession;
40
+ await embeddedFabricLogin(auth, options);
41
+ const embeddedSession = auth.getSession();
42
+ if (embeddedSession.isAuthenticated) {
43
+ console.debug('[FabricAuth] Embedded auth succeeded');
44
+ return embeddedSession;
43
45
  }
46
+ throw new AuthError('Fabric embedded authentication completed but no session was established.', 'SESSION_NOT_ESTABLISHED');
44
47
  }
45
- catch {
46
- console.warn('[FabricAuth] Step 2: session refresh failed, continuing to step 3');
48
+ catch (err) {
49
+ // If there's no parent window the embedded flag is stale (e.g.
50
+ // leftover sessionStorage or URL param without an actual iframe).
51
+ // Fall through to the popup path instead of failing outright.
52
+ if (err instanceof AuthError && err.code === 'NO_PARENT_WINDOW') {
53
+ console.debug('[FabricAuth] No parent window — falling back to popup auth');
54
+ }
55
+ else {
56
+ throw err;
57
+ }
47
58
  }
48
59
  }
49
- else {
50
- console.debug('[FabricAuth] Step 2: no refresh token available, skipping to step 3');
51
- }
52
- // Step 3: Open Fabric broker in a new tab
60
+ // Step 4: Open Fabric broker in a new tab (popup flow)
53
61
  // WARNING: This calls window.open() — must be in a user-gesture context
54
- console.debug('[FabricAuth] Step 3: initiating Fabric broker login');
62
+ console.debug('[FabricAuth] Initiating Fabric broker login');
55
63
  await initiateFabricLogin(auth, options);
56
- // Session was created inside initiateFabricLogin via createSessionFromTokenResponse
57
64
  const newSession = auth.getSession();
58
65
  if (newSession.isAuthenticated) {
59
- console.debug('[FabricAuth] Step 3: Fabric broker login succeeded, session established');
66
+ console.debug('[FabricAuth] Fabric broker login succeeded');
60
67
  return newSession;
61
68
  }
62
- console.warn('[FabricAuth] Step 3: Fabric broker login completed but no session was established');
63
69
  throw new AuthError('Fabric authentication completed but no session was established.', 'SESSION_NOT_ESTABLISHED');
64
70
  }
65
71
  //# sourceMappingURL=ensureSignedInWithFabric.js.map
@@ -0,0 +1,25 @@
1
+ import type { Auth, OpaqueSession } from '@microsoft/rayfin-auth';
2
+ import type { FabricAuthOptions } from './types';
3
+ /**
4
+ * Detects embedded mode from the `fabricEmbedded` option, the
5
+ * `?fabricEmbedded=true` URL query parameter, or a previously stored
6
+ * `sessionStorage` flag.
7
+ *
8
+ * Delegates to `@microsoft/fabric-embedded-host` for the actual
9
+ * detection logic. `FabricAuthOptions` satisfies the shared
10
+ * `EmbeddedModeOptions` interface.
11
+ */
12
+ export declare function isEmbeddedMode(options: FabricAuthOptions): boolean;
13
+ /**
14
+ * Attempts to resume an existing session without any user interaction.
15
+ *
16
+ * 1. Returns the current session if already authenticated.
17
+ * 2. If a refresh token is available, tries `auth.refreshSession()` and
18
+ * returns the refreshed session on success.
19
+ * 3. Returns `null` when neither path produces an authenticated session.
20
+ *
21
+ * This helper never opens windows, popups, or postMessage channels —
22
+ * it is always safe to call on page load.
23
+ */
24
+ export declare function tryResumeSession(auth: Auth): Promise<OpaqueSession | null>;
25
+ //# sourceMappingURL=fabricAuthHelpers.d.ts.map
@@ -0,0 +1,48 @@
1
+ import { isEmbeddedMode as sharedIsEmbeddedMode } from '@microsoft/fabric-embedded-host';
2
+ /**
3
+ * Detects embedded mode from the `fabricEmbedded` option, the
4
+ * `?fabricEmbedded=true` URL query parameter, or a previously stored
5
+ * `sessionStorage` flag.
6
+ *
7
+ * Delegates to `@microsoft/fabric-embedded-host` for the actual
8
+ * detection logic. `FabricAuthOptions` satisfies the shared
9
+ * `EmbeddedModeOptions` interface.
10
+ */
11
+ export function isEmbeddedMode(options) {
12
+ return sharedIsEmbeddedMode(options);
13
+ }
14
+ /**
15
+ * Attempts to resume an existing session without any user interaction.
16
+ *
17
+ * 1. Returns the current session if already authenticated.
18
+ * 2. If a refresh token is available, tries `auth.refreshSession()` and
19
+ * returns the refreshed session on success.
20
+ * 3. Returns `null` when neither path produces an authenticated session.
21
+ *
22
+ * This helper never opens windows, popups, or postMessage channels —
23
+ * it is always safe to call on page load.
24
+ */
25
+ export async function tryResumeSession(auth) {
26
+ // Step 1: Already authenticated
27
+ const currentSession = auth.getSession();
28
+ if (currentSession.isAuthenticated) {
29
+ console.debug('[FabricAuth] Existing session found');
30
+ return currentSession;
31
+ }
32
+ // Step 2: Try refresh token
33
+ if (auth.hasRefreshToken()) {
34
+ try {
35
+ await auth.refreshSession();
36
+ const refreshedSession = auth.getSession();
37
+ if (refreshedSession.isAuthenticated) {
38
+ console.debug('[FabricAuth] Session refreshed successfully');
39
+ return refreshedSession;
40
+ }
41
+ }
42
+ catch {
43
+ console.warn('[FabricAuth] Session refresh failed, continuing');
44
+ }
45
+ }
46
+ return null;
47
+ }
48
+ //# sourceMappingURL=fabricAuthHelpers.js.map
package/dist/index.d.ts CHANGED
@@ -1,5 +1,9 @@
1
1
  export type { FabricAuthOptions } from './types';
2
2
  export { bridgeFabricCallback } from './bridgeFabricCallback';
3
+ export { embeddedFabricLogin } from './embeddedFabricLogin';
3
4
  export { ensureSignedInWithFabric } from './ensureSignedInWithFabric';
5
+ export { initEmbeddedAuth } from './initEmbeddedAuth';
4
6
  export { initiateFabricLogin } from './initiateFabricLogin';
7
+ export { requestHandoff } from './PostMessageAuthTransport';
8
+ export type { PostMessageHandoffResult } from './PostMessageAuthTransport';
5
9
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -1,5 +1,8 @@
1
1
  // Functions
2
2
  export { bridgeFabricCallback } from './bridgeFabricCallback';
3
+ export { embeddedFabricLogin } from './embeddedFabricLogin';
3
4
  export { ensureSignedInWithFabric } from './ensureSignedInWithFabric';
5
+ export { initEmbeddedAuth } from './initEmbeddedAuth';
4
6
  export { initiateFabricLogin } from './initiateFabricLogin';
7
+ export { requestHandoff } from './PostMessageAuthTransport';
5
8
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,29 @@
1
+ import type { Auth, OpaqueSession } from '@microsoft/rayfin-auth';
2
+ import type { FabricAuthOptions } from './types';
3
+ /**
4
+ * Initializes embedded Fabric authentication if running inside an iframe
5
+ * with `?fabricEmbedded=true`.
6
+ *
7
+ * Call this once at app startup (e.g., in a `useEffect` or initialization
8
+ * routine). It is safe to call on page load — it never opens a popup or
9
+ * new tab.
10
+ *
11
+ * **Behaviour:**
12
+ * - If `fabricEmbedded=true` is **not** in the URL and `options.fabricEmbedded`
13
+ * is not `true`, returns `null` immediately (no-op).
14
+ * - If embedded mode is detected, runs a 3-step waterfall:
15
+ * 1. Return existing session if already authenticated.
16
+ * 2. Attempt refresh via refresh token.
17
+ * 3. Request a handoff code from the parent host via `postMessage`
18
+ * and exchange it for a session (PKCE, no popup).
19
+ *
20
+ * Apps that also support the popup flow should continue to call
21
+ * {@link ensureSignedInWithFabric} from a user-gesture handler for the
22
+ * non-embedded case.
23
+ *
24
+ * @param auth The Auth instance.
25
+ * @param options Fabric authentication options.
26
+ * @returns The authenticated session, or `null` if not in embedded mode.
27
+ */
28
+ export declare function initEmbeddedAuth(auth: Auth, options: FabricAuthOptions): Promise<OpaqueSession | null>;
29
+ //# sourceMappingURL=initEmbeddedAuth.d.ts.map
@@ -0,0 +1,54 @@
1
+ import { embeddedFabricLogin } from './embeddedFabricLogin';
2
+ import { isEmbeddedMode, tryResumeSession } from './fabricAuthHelpers';
3
+ /**
4
+ * Initializes embedded Fabric authentication if running inside an iframe
5
+ * with `?fabricEmbedded=true`.
6
+ *
7
+ * Call this once at app startup (e.g., in a `useEffect` or initialization
8
+ * routine). It is safe to call on page load — it never opens a popup or
9
+ * new tab.
10
+ *
11
+ * **Behaviour:**
12
+ * - If `fabricEmbedded=true` is **not** in the URL and `options.fabricEmbedded`
13
+ * is not `true`, returns `null` immediately (no-op).
14
+ * - If embedded mode is detected, runs a 3-step waterfall:
15
+ * 1. Return existing session if already authenticated.
16
+ * 2. Attempt refresh via refresh token.
17
+ * 3. Request a handoff code from the parent host via `postMessage`
18
+ * and exchange it for a session (PKCE, no popup).
19
+ *
20
+ * Apps that also support the popup flow should continue to call
21
+ * {@link ensureSignedInWithFabric} from a user-gesture handler for the
22
+ * non-embedded case.
23
+ *
24
+ * @param auth The Auth instance.
25
+ * @param options Fabric authentication options.
26
+ * @returns The authenticated session, or `null` if not in embedded mode.
27
+ */
28
+ export async function initEmbeddedAuth(auth, options) {
29
+ if (!isEmbeddedMode(options)) {
30
+ return null;
31
+ }
32
+ // Steps 1-2: existing session or refresh
33
+ const resumed = await tryResumeSession(auth);
34
+ if (resumed) {
35
+ return resumed;
36
+ }
37
+ // Step 3: postMessage handoff (no popup, no window.open)
38
+ console.debug('[FabricAuth:initEmbedded] Starting postMessage handoff flow');
39
+ try {
40
+ await embeddedFabricLogin(auth, options);
41
+ }
42
+ catch (err) {
43
+ console.warn('[FabricAuth:initEmbedded] Embedded login failed', err);
44
+ return null;
45
+ }
46
+ const newSession = auth.getSession();
47
+ if (newSession.isAuthenticated) {
48
+ console.debug('[FabricAuth:initEmbedded] Session established');
49
+ return newSession;
50
+ }
51
+ console.warn('[FabricAuth:initEmbedded] Handoff completed but no session was established');
52
+ return null;
53
+ }
54
+ //# sourceMappingURL=initEmbeddedAuth.js.map
@@ -41,8 +41,11 @@ export async function initiateFabricLogin(auth, options) {
41
41
  let channel = null;
42
42
  // 5-minute timeout — matches the backend's handoff code TTL.
43
43
  const TIMEOUT_MS = 5 * 60 * 1000;
44
+ /** Whether the brokeredAuth.ready signal has been handled. */
45
+ let readyHandled = false;
44
46
  function cleanup() {
45
47
  window.removeEventListener('message', handleMessage);
48
+ window.removeEventListener('message', handleReady);
46
49
  channel?.close();
47
50
  channel = null;
48
51
  clearTimeout(timeoutId);
@@ -54,7 +57,7 @@ export async function initiateFabricLogin(auth, options) {
54
57
  function handleMessage(event) {
55
58
  // Origin check is advisory — state + PKCE provide the real security.
56
59
  if (event.origin !== expectedBrokerOrigin) {
57
- console.warn(`[FabricAuth] postMessage origin mismatch (expected="${expectedBrokerOrigin}", actual="${event.origin}") — continuing (secured by state + PKCE)`);
60
+ console.info(`[FabricAuth] postMessage from origin="${event.origin}" (expected portal="${expectedBrokerOrigin}") — continuing (secured by state + PKCE)`);
58
61
  }
59
62
  if (!event.data || typeof event.data !== 'object') {
60
63
  console.debug('[FabricAuth] postMessage ignored: non-object payload');
@@ -101,6 +104,53 @@ export async function initiateFabricLogin(auth, options) {
101
104
  cleanup();
102
105
  reject(new AuthError('Fabric authentication timed out after 5 minutes. Please try again.', 'FABRIC_AUTH_TIMEOUT'));
103
106
  }, TIMEOUT_MS);
107
+ /**
108
+ * Handles `brokeredAuth.ready` from the broker extension.
109
+ *
110
+ * When the broker extension sends a ready signal the SDK responds with a
111
+ * `brokeredAuth.challenge` containing the PKCE parameters. Only the
112
+ * first ready signal is handled — subsequent signals are ignored.
113
+ *
114
+ * The challenge is sent to `event.source` (the window that sent the ready
115
+ * signal) rather than `fabricWindow`. In Fabric the extension runs in a
116
+ * cross-origin iframe inside the popup — `postMessage` sent to the
117
+ * popup's top-level window does not propagate to child iframes.
118
+ */
119
+ function handleReady(event) {
120
+ if (readyHandled)
121
+ return;
122
+ if (!event.data || typeof event.data !== 'object')
123
+ return;
124
+ if (event.data.type !== 'brokeredAuth.ready')
125
+ return;
126
+ // Origin check is advisory — the extension iframe runs at a different
127
+ // origin than the Fabric portal. PKCE + state provide the real security.
128
+ if (event.origin !== expectedBrokerOrigin) {
129
+ console.info(`[FabricAuth] brokeredAuth.ready from origin="${event.origin}" (expected portal="${expectedBrokerOrigin}") — continuing (secured by state + PKCE)`);
130
+ }
131
+ readyHandled = true;
132
+ window.removeEventListener('message', handleReady);
133
+ const challengePayload = {
134
+ type: 'brokeredAuth.challenge',
135
+ returnOrigin: options.returnOrigin,
136
+ codeChallenge,
137
+ codeChallengeMethod: 'S256',
138
+ state,
139
+ };
140
+ // Reply to the window that sent the ready signal (the extension iframe).
141
+ const source = event.source;
142
+ if (source) {
143
+ source.postMessage(challengePayload, event.origin || '*');
144
+ console.debug(`[FabricAuth] Sent brokeredAuth.challenge to event.source (origin=${event.origin || '*'})`);
145
+ }
146
+ else if (fabricWindow && !fabricWindow.closed) {
147
+ // Fallback: if event.source is unavailable, try the popup directly.
148
+ fabricWindow.postMessage(challengePayload, expectedBrokerOrigin);
149
+ console.debug('[FabricAuth] Sent brokeredAuth.challenge to fabricWindow (event.source unavailable)');
150
+ }
151
+ }
152
+ // Listen for brokeredAuth.ready from the popup (postMessage PKCE).
153
+ window.addEventListener('message', handleReady);
104
154
  // Listen for handoff via postMessage and BroadcastChannel (legacy bridge).
105
155
  console.debug('[FabricAuth] Registering postMessage listener for broker handoff');
106
156
  window.addEventListener('message', handleMessage);
package/dist/types.d.ts CHANGED
@@ -34,5 +34,13 @@ export interface FabricAuthOptions {
34
34
  * @deprecated Backward-compat only — will be removed once postMessage rollout is complete.
35
35
  */
36
36
  callbackUrl?: string;
37
+ /**
38
+ * When `true`, use embedded (iframe) authentication via postMessage instead
39
+ * of the popup flow. The SDK also auto-detects embedded mode when
40
+ * `?fabricEmbedded=true` is present in `window.location.search`.
41
+ *
42
+ * Set explicitly to override URL-based detection.
43
+ */
44
+ fabricEmbedded?: boolean;
37
45
  }
38
46
  //# sourceMappingURL=types.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@microsoft/rayfin-auth-provider-fabric",
3
- "version": "1.20.0",
3
+ "version": "1.22.0",
4
4
  "description": "Fabric brokered authentication provider for Rayfin SDK",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -12,8 +12,9 @@
12
12
  ],
13
13
  "type": "module",
14
14
  "dependencies": {
15
- "@microsoft/rayfin-lib": "1.20.0",
16
- "@microsoft/rayfin-auth": "1.20.0"
15
+ "@microsoft/rayfin-auth": "1.22.0",
16
+ "@microsoft/fabric-embedded-host": "1.22.0",
17
+ "@microsoft/rayfin-lib": "1.22.0"
17
18
  },
18
19
  "devDependencies": {
19
20
  "typescript": "^5.8.3",