@microsoft/fabric-embedded-host 1.19.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,63 @@
1
+ /**
2
+ * MessageProtocol — type definitions for the postMessage bridge
3
+ * between a hosted Rayfin SPA (inside an iframe) and the Fabric
4
+ * extension host (EmbeddedAppHost).
5
+ *
6
+ * All messages flowing through the bridge conform to the
7
+ * {@link MessageEnvelope} shape, which carries a `channel` for
8
+ * plugin routing, a monotonic `version`, a `kind` discriminator,
9
+ * and a `requestId` for response correlation.
10
+ */
11
+ /**
12
+ * Base envelope for every message exchanged over the postMessage bridge.
13
+ *
14
+ * The iframe sends requests and the host replies with responses that
15
+ * echo the same `channel`, `version`, and `requestId`.
16
+ */
17
+ export interface MessageEnvelope {
18
+ /** Plugin channel identifier (e.g. `"fabric-auth"`). */
19
+ channel: string;
20
+ /** Protocol version — currently `1`. */
21
+ version: number;
22
+ /** Discriminator for the message type (e.g. `"auth.requestHandoff"`, `"response"`). */
23
+ kind: string;
24
+ /** Unique identifier for request/response correlation. */
25
+ requestId: string;
26
+ }
27
+ /**
28
+ * Error detail included in an {@link ErrorResponse}.
29
+ */
30
+ export interface ResponseError {
31
+ /** Machine-readable error code (e.g. `"UNKNOWN_CHANNEL"`, `"PLUGIN_ERROR"`). */
32
+ code: string;
33
+ /** Human-readable error description. */
34
+ message: string;
35
+ }
36
+ /**
37
+ * Success response sent from the host back to the iframe.
38
+ */
39
+ export interface SuccessResponse<T = unknown> extends MessageEnvelope {
40
+ kind: 'response';
41
+ success: true;
42
+ /** Plugin-specific result payload. */
43
+ result: T;
44
+ }
45
+ /**
46
+ * Error response sent from the host back to the iframe.
47
+ */
48
+ export interface ErrorResponse extends MessageEnvelope {
49
+ kind: 'response';
50
+ success: false;
51
+ /** Structured error detail. */
52
+ error: ResponseError;
53
+ }
54
+ /**
55
+ * Union of all possible response shapes sent from the host.
56
+ */
57
+ export type BridgeResponse<T = unknown> = SuccessResponse<T> | ErrorResponse;
58
+ /**
59
+ * Type guard that validates whether a value conforms to the
60
+ * {@link MessageEnvelope} shape.
61
+ */
62
+ export declare function isMessageEnvelope(data: unknown): data is MessageEnvelope;
63
+ //# sourceMappingURL=MessageProtocol.d.ts.map
@@ -0,0 +1,25 @@
1
+ /**
2
+ * MessageProtocol — type definitions for the postMessage bridge
3
+ * between a hosted Rayfin SPA (inside an iframe) and the Fabric
4
+ * extension host (EmbeddedAppHost).
5
+ *
6
+ * All messages flowing through the bridge conform to the
7
+ * {@link MessageEnvelope} shape, which carries a `channel` for
8
+ * plugin routing, a monotonic `version`, a `kind` discriminator,
9
+ * and a `requestId` for response correlation.
10
+ */
11
+ // ── Envelope validation ──────────────────────────────────────
12
+ /**
13
+ * Type guard that validates whether a value conforms to the
14
+ * {@link MessageEnvelope} shape.
15
+ */
16
+ export function isMessageEnvelope(data) {
17
+ if (typeof data !== 'object' || data === null)
18
+ return false;
19
+ const obj = data;
20
+ return (typeof obj.channel === 'string' &&
21
+ typeof obj.version === 'number' &&
22
+ typeof obj.kind === 'string' &&
23
+ typeof obj.requestId === 'string');
24
+ }
25
+ //# sourceMappingURL=MessageProtocol.js.map
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Embedded-mode detection utilities.
3
+ *
4
+ * An app is considered "embedded" when it runs inside a Fabric extension
5
+ * iframe rather than as a standalone page. Detection uses a priority
6
+ * waterfall:
7
+ *
8
+ * 1. An explicit `fabricEmbedded` boolean option (highest priority).
9
+ * 2. A `?fabricEmbedded=true` query parameter in the current URL —
10
+ * set by the Fabric Portal when it loads the SPA in an iframe.
11
+ * 3. A `sessionStorage` flag persisted after step 2, so that
12
+ * client-side navigations that strip query params keep working.
13
+ */
14
+ /**
15
+ * Options accepted by {@link isEmbeddedMode}.
16
+ *
17
+ * Both the Rayfin auth SDK (`FabricAuthOptions`) and the Fabric SDK
18
+ * can satisfy this interface — only the `fabricEmbedded` flag matters.
19
+ */
20
+ export interface EmbeddedModeOptions {
21
+ /**
22
+ * When `true`, force embedded mode regardless of the URL or
23
+ * sessionStorage. When `undefined` or `false`, fall through to
24
+ * URL / sessionStorage detection.
25
+ */
26
+ fabricEmbedded?: boolean;
27
+ }
28
+ /**
29
+ * Detects whether the app is running in embedded mode.
30
+ *
31
+ * Priority:
32
+ * 1. `options.fabricEmbedded === true` — explicit opt-in.
33
+ * 2. `?fabricEmbedded=true` in `window.location.search` — set by
34
+ * the Fabric Portal; persisted to sessionStorage on first hit.
35
+ * 3. `sessionStorage` flag from a previous URL detection.
36
+ *
37
+ * @returns `true` when the app should use the embedded postMessage
38
+ * auth flow instead of the popup/redirect flow.
39
+ */
40
+ export declare function isEmbeddedMode(options: EmbeddedModeOptions): boolean;
41
+ /**
42
+ * Clears the persisted embedded-mode flag from sessionStorage.
43
+ *
44
+ * Useful in testing or when the app explicitly exits embedded mode.
45
+ */
46
+ export declare function clearEmbeddedMode(): void;
47
+ //# sourceMappingURL=embeddedMode.d.ts.map
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Embedded-mode detection utilities.
3
+ *
4
+ * An app is considered "embedded" when it runs inside a Fabric extension
5
+ * iframe rather than as a standalone page. Detection uses a priority
6
+ * waterfall:
7
+ *
8
+ * 1. An explicit `fabricEmbedded` boolean option (highest priority).
9
+ * 2. A `?fabricEmbedded=true` query parameter in the current URL —
10
+ * set by the Fabric Portal when it loads the SPA in an iframe.
11
+ * 3. A `sessionStorage` flag persisted after step 2, so that
12
+ * client-side navigations that strip query params keep working.
13
+ */
14
+ const EMBEDDED_QUERY_PARAM = 'fabricEmbedded';
15
+ const EMBEDDED_STORAGE_KEY = 'fabricEmbedded';
16
+ /**
17
+ * Detects whether the app is running in embedded mode.
18
+ *
19
+ * Priority:
20
+ * 1. `options.fabricEmbedded === true` — explicit opt-in.
21
+ * 2. `?fabricEmbedded=true` in `window.location.search` — set by
22
+ * the Fabric Portal; persisted to sessionStorage on first hit.
23
+ * 3. `sessionStorage` flag from a previous URL detection.
24
+ *
25
+ * @returns `true` when the app should use the embedded postMessage
26
+ * auth flow instead of the popup/redirect flow.
27
+ */
28
+ export function isEmbeddedMode(options) {
29
+ if (options.fabricEmbedded === true) {
30
+ return true;
31
+ }
32
+ if (new URLSearchParams(window.location.search).get(EMBEDDED_QUERY_PARAM) ===
33
+ 'true') {
34
+ try {
35
+ sessionStorage.setItem(EMBEDDED_STORAGE_KEY, 'true');
36
+ }
37
+ catch {
38
+ // sessionStorage may be unavailable in sandboxed iframes
39
+ }
40
+ return true;
41
+ }
42
+ try {
43
+ if (sessionStorage.getItem(EMBEDDED_STORAGE_KEY) === 'true') {
44
+ return true;
45
+ }
46
+ }
47
+ catch {
48
+ // sessionStorage unavailable
49
+ }
50
+ return false;
51
+ }
52
+ /**
53
+ * Clears the persisted embedded-mode flag from sessionStorage.
54
+ *
55
+ * Useful in testing or when the app explicitly exits embedded mode.
56
+ */
57
+ export function clearEmbeddedMode() {
58
+ try {
59
+ sessionStorage.removeItem(EMBEDDED_STORAGE_KEY);
60
+ }
61
+ catch {
62
+ // sessionStorage unavailable
63
+ }
64
+ }
65
+ //# sourceMappingURL=embeddedMode.js.map
@@ -0,0 +1,7 @@
1
+ export type { MessageEnvelope, ResponseError, SuccessResponse, ErrorResponse, BridgeResponse, } from './MessageProtocol';
2
+ export { isMessageEnvelope } from './MessageProtocol';
3
+ export type { EmbeddedModeOptions } from './embeddedMode';
4
+ export { isEmbeddedMode, clearEmbeddedMode } from './embeddedMode';
5
+ export type { BridgeRequestOptions } from './postMessageBridge';
6
+ export { BridgeError, sendBridgeRequest } from './postMessageBridge';
7
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { isMessageEnvelope } from './MessageProtocol';
2
+ export { isEmbeddedMode, clearEmbeddedMode } from './embeddedMode';
3
+ export { BridgeError, sendBridgeRequest } from './postMessageBridge';
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,51 @@
1
+ import { SdkError } from '@microsoft/rayfin-lib';
2
+ /**
3
+ * Options for {@link sendBridgeRequest}.
4
+ */
5
+ export interface BridgeRequestOptions<TPayload> {
6
+ /** Target window to post the message to (typically `window.parent`). */
7
+ target: Window;
8
+ /** Channel identifier routed by the host's plugin registry. */
9
+ channel: string;
10
+ /** Message kind discriminator (e.g. `"auth.requestHandoff"`). */
11
+ kind: string;
12
+ /** Arbitrary payload attached to the message envelope. */
13
+ payload: TPayload;
14
+ /**
15
+ * `targetOrigin` passed to `postMessage`.
16
+ *
17
+ * When set to a specific origin (e.g. `"https://app.fabric.microsoft.com"`),
18
+ * the browser restricts delivery to that origin **and** the response handler
19
+ * validates `event.origin` on incoming messages.
20
+ *
21
+ * Defaults to `"*"` — acceptable only when the payload contains no secrets
22
+ * and the iframe cannot determine `window.parent.origin` at runtime.
23
+ */
24
+ targetOrigin?: string;
25
+ /** Response timeout in milliseconds. Defaults to 30 000. */
26
+ timeoutMs?: number;
27
+ }
28
+ /**
29
+ * Error thrown when a bridge request fails or times out.
30
+ */
31
+ export declare class BridgeError extends SdkError {
32
+ name: string;
33
+ constructor(message: string, code?: string);
34
+ }
35
+ /**
36
+ * Send a request through the postMessage bridge and wait for a
37
+ * correlated response.
38
+ *
39
+ * The function:
40
+ * 1. Generates a unique `requestId` (UUID v4).
41
+ * 2. Posts a {@link MessageEnvelope}-shaped message to `target`.
42
+ * 3. Listens for a response whose `requestId` matches and
43
+ * `kind === "response"`.
44
+ * 4. Resolves with the `result` on success or rejects with a
45
+ * {@link BridgeError} on error/timeout.
46
+ *
47
+ * @typeParam TPayload Shape of the request payload.
48
+ * @typeParam TResult Shape of the successful response result.
49
+ */
50
+ export declare function sendBridgeRequest<TPayload, TResult>(options: BridgeRequestOptions<TPayload>): Promise<TResult>;
51
+ //# sourceMappingURL=postMessageBridge.d.ts.map
@@ -0,0 +1,95 @@
1
+ import { SdkError } from '@microsoft/rayfin-lib';
2
+ /**
3
+ * Default response timeout in milliseconds (30 seconds).
4
+ */
5
+ const DEFAULT_TIMEOUT_MS = 30_000;
6
+ /**
7
+ * Error thrown when a bridge request fails or times out.
8
+ */
9
+ export class BridgeError extends SdkError {
10
+ name = 'BridgeError';
11
+ constructor(message, code) {
12
+ super(message, code);
13
+ Object.setPrototypeOf(this, BridgeError.prototype);
14
+ }
15
+ }
16
+ /**
17
+ * Send a request through the postMessage bridge and wait for a
18
+ * correlated response.
19
+ *
20
+ * The function:
21
+ * 1. Generates a unique `requestId` (UUID v4).
22
+ * 2. Posts a {@link MessageEnvelope}-shaped message to `target`.
23
+ * 3. Listens for a response whose `requestId` matches and
24
+ * `kind === "response"`.
25
+ * 4. Resolves with the `result` on success or rejects with a
26
+ * {@link BridgeError} on error/timeout.
27
+ *
28
+ * @typeParam TPayload Shape of the request payload.
29
+ * @typeParam TResult Shape of the successful response result.
30
+ */
31
+ export function sendBridgeRequest(options) {
32
+ const { target, channel, kind, payload, targetOrigin = '*', timeoutMs = DEFAULT_TIMEOUT_MS, } = options;
33
+ if (!target || target === window) {
34
+ return Promise.reject(new BridgeError('No host window — embedded mode requires an iframe host.', 'NO_HOST_WINDOW'));
35
+ }
36
+ const requestId = crypto.randomUUID();
37
+ const request = {
38
+ channel,
39
+ version: 1,
40
+ kind,
41
+ requestId,
42
+ payload,
43
+ };
44
+ return new Promise((resolve, reject) => {
45
+ // eslint-disable-next-line prefer-const -- timeoutId must be declared before cleanup references it
46
+ let timeoutId;
47
+ function cleanup() {
48
+ window.removeEventListener('message', handleResponse);
49
+ clearTimeout(timeoutId);
50
+ }
51
+ function handleResponse(event) {
52
+ if (!event.data || typeof event.data !== 'object')
53
+ return;
54
+ if (event.data.requestId !== requestId)
55
+ return;
56
+ if (event.data.kind !== 'response')
57
+ return;
58
+ console.debug(`[PostMessageBridge] Response received from origin="${event.origin}", channel="${event.data.channel ?? '?'}"`);
59
+ // Validate that the response came from the window we sent the
60
+ // request to (window.parent). This prevents other iframes or
61
+ // windows from spoofing correlated responses.
62
+ if (event.source !== target) {
63
+ console.warn('[PostMessageBridge] Ignoring response: event.source does not match target window');
64
+ return;
65
+ }
66
+ // When a specific targetOrigin was provided, validate the response
67
+ // origin to ensure we only accept messages from the expected host.
68
+ if (targetOrigin !== '*' && event.origin !== targetOrigin) {
69
+ console.warn(`[PostMessageBridge] Ignoring response from origin="${event.origin}" (expected "${targetOrigin}")`);
70
+ return;
71
+ }
72
+ if (event.data.version !== undefined && event.data.version !== 1) {
73
+ console.warn(`[PostMessageBridge] Unexpected protocol version: ${event.data.version}`);
74
+ }
75
+ cleanup();
76
+ if (event.data.success === true) {
77
+ resolve(event.data.result);
78
+ }
79
+ else {
80
+ const error = event.data.error ?? {
81
+ code: 'UNKNOWN_ERROR',
82
+ message: 'Unknown error from host.',
83
+ };
84
+ reject(new BridgeError(error.message, error.code));
85
+ }
86
+ }
87
+ window.addEventListener('message', handleResponse);
88
+ timeoutId = setTimeout(() => {
89
+ cleanup();
90
+ reject(new BridgeError(`Bridge request timed out after ${timeoutMs}ms. The host did not respond.`, 'BRIDGE_TIMEOUT'));
91
+ }, timeoutMs);
92
+ target.postMessage(request, targetOrigin);
93
+ });
94
+ }
95
+ //# sourceMappingURL=postMessageBridge.js.map
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@microsoft/fabric-embedded-host",
3
+ "version": "1.19.0",
4
+ "description": "Shared postMessage bridge protocol and embedded mode detection for Rayfin SDKs hosted inside Fabric iframes",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist/**/*.js",
9
+ "dist/**/*.d.ts",
10
+ "!dist/**/__tests__/**",
11
+ "LICENSE"
12
+ ],
13
+ "type": "module",
14
+ "dependencies": {
15
+ "@microsoft/rayfin-lib": "1.21.0"
16
+ },
17
+ "devDependencies": {
18
+ "typescript": "^5.8.3",
19
+ "vitest": "^3.2.3",
20
+ "@vitest/coverage-v8": "~3.2.4",
21
+ "rimraf": "~6.0.1"
22
+ },
23
+ "publishConfig": {
24
+ "registry": "https://npm.pkg.github.com",
25
+ "access": "restricted"
26
+ },
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "https://github.com/microsoft/project-rayfin.git",
30
+ "directory": "packages/typescript-sdk/fabric-embedded-host"
31
+ },
32
+ "keywords": [],
33
+ "author": "",
34
+ "license": "MIT",
35
+ "scripts": {
36
+ "build": "tsc",
37
+ "build:watch": "tsc --watch",
38
+ "clean": "rimraf dist && rimraf .tsbuildinfo",
39
+ "test": "vitest run",
40
+ "test:watch": "vitest"
41
+ }
42
+ }