@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.
- package/dist/MessageProtocol.d.ts +63 -0
- package/dist/MessageProtocol.js +25 -0
- package/dist/embeddedMode.d.ts +47 -0
- package/dist/embeddedMode.js +65 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +4 -0
- package/dist/postMessageBridge.d.ts +51 -0
- package/dist/postMessageBridge.js +95 -0
- package/package.json +42 -0
|
@@ -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
|
package/dist/index.d.ts
ADDED
|
@@ -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,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
|
+
}
|