@atlaskit/rovo-triggers 10.14.0 → 10.15.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/CHANGELOG.md +9 -0
- package/dist/cjs/common/utils/extension-context-bridge/ExtensionContextBridgeClient.js +130 -0
- package/dist/cjs/common/utils/extension-context-bridge/ExtensionContextBridgeHost.js +85 -0
- package/dist/cjs/common/utils/extension-context-bridge/constants.js +22 -0
- package/dist/cjs/common/utils/extension-context-bridge/policy.js +40 -0
- package/dist/cjs/common/utils/extension-context-bridge/registry.js +134 -0
- package/dist/cjs/common/utils/extension-context-bridge/transport.js +49 -0
- package/dist/es2019/common/utils/extension-context-bridge/ExtensionContextBridgeClient.js +128 -0
- package/dist/es2019/common/utils/extension-context-bridge/ExtensionContextBridgeHost.js +82 -0
- package/dist/es2019/common/utils/extension-context-bridge/constants.js +16 -0
- package/dist/es2019/common/utils/extension-context-bridge/policy.js +32 -0
- package/dist/es2019/common/utils/extension-context-bridge/registry.js +120 -0
- package/dist/es2019/common/utils/extension-context-bridge/transport.js +35 -0
- package/dist/esm/common/utils/extension-context-bridge/ExtensionContextBridgeClient.js +124 -0
- package/dist/esm/common/utils/extension-context-bridge/ExtensionContextBridgeHost.js +78 -0
- package/dist/esm/common/utils/extension-context-bridge/constants.js +16 -0
- package/dist/esm/common/utils/extension-context-bridge/policy.js +34 -0
- package/dist/esm/common/utils/extension-context-bridge/registry.js +127 -0
- package/dist/esm/common/utils/extension-context-bridge/transport.js +43 -0
- package/dist/types/common/utils/extension-context-bridge/ExtensionContextBridgeClient.d.ts +19 -0
- package/dist/types/common/utils/extension-context-bridge/ExtensionContextBridgeHost.d.ts +16 -0
- package/dist/types/common/utils/extension-context-bridge/constants.d.ts +13 -0
- package/dist/types/common/utils/extension-context-bridge/policy.d.ts +8 -0
- package/dist/types/common/utils/extension-context-bridge/registry.d.ts +6 -0
- package/dist/types/common/utils/extension-context-bridge/transport.d.ts +38 -0
- package/extension-context-bridge/client/package.json +10 -0
- package/extension-context-bridge/constants/package.json +10 -0
- package/extension-context-bridge/host/package.json +10 -0
- package/extension-context-bridge/transport/package.json +10 -0
- package/package.json +7 -1
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { fg } from '@atlaskit/platform-feature-flags/fg';
|
|
3
|
+
import { useSubscribeAll } from '../../../main';
|
|
4
|
+
import { BRIDGE_MESSAGE_MARKER, BRIDGE_SOURCE, BRIDGE_TO_EXTENSION } from './constants';
|
|
5
|
+
import { getInboundDecision } from './policy';
|
|
6
|
+
import { serializePayload } from './registry';
|
|
7
|
+
import { createWindowTransport } from './transport';
|
|
8
|
+
/**
|
|
9
|
+
* Stamps the page id onto `editor-context-payload` when the product left it unpopulated, since the
|
|
10
|
+
* extension chat cannot derive its own `contentId` from `window.location` (that's the side panel, not
|
|
11
|
+
* the page). `getContentId` is supplied by the consumer rather than read here, to avoid this package
|
|
12
|
+
* taking a dependency on `conversation-assistant-store` (a cyclic-dependency risk); the consumer
|
|
13
|
+
* already depends on that store and can resolve the id from it directly.
|
|
14
|
+
*/
|
|
15
|
+
const stampProductContentId = (serialized, targetContentId) => {
|
|
16
|
+
if (serialized.type !== 'editor-context-payload') {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const data = serialized.data;
|
|
20
|
+
if (!data || data.contentMauiId) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (targetContentId) {
|
|
24
|
+
data.contentMauiId = targetContentId;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Product-window half of the bridge. Observes the `ai-mate` bus and mirrors allowlisted context out
|
|
29
|
+
* to the extension via same-window `postMessage` (see `./policy` for the allow/deny rules).
|
|
30
|
+
*
|
|
31
|
+
* The reverse direction is handled by the sibling `RovoPostMessagePubsubListener` in
|
|
32
|
+
* `ChatOpenerSubscriber`, not here. Headless; mounts at product root via `ChatOpenerSubscriber`.
|
|
33
|
+
*/
|
|
34
|
+
export const ExtensionContextBridgeHost = ({
|
|
35
|
+
transport,
|
|
36
|
+
getContentId
|
|
37
|
+
}) => {
|
|
38
|
+
// Self-gated (not taken from the consumer) so this cannot mount by mistake wherever
|
|
39
|
+
// `ChatOpenerSubscriber` renders. Matches `BRIDGE_FEATURE_GATE` in `./constants`.
|
|
40
|
+
const active = fg('rovo-ext_context_bridge');
|
|
41
|
+
const activeTransport = useMemo(() => {
|
|
42
|
+
if (!active) {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
if (transport) {
|
|
46
|
+
return transport;
|
|
47
|
+
}
|
|
48
|
+
if (typeof window === 'undefined') {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
return createWindowTransport({
|
|
52
|
+
targetWindow: window
|
|
53
|
+
});
|
|
54
|
+
}, [active, transport]);
|
|
55
|
+
useSubscribeAll(payload => {
|
|
56
|
+
if (!activeTransport) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
// Loop guard.
|
|
60
|
+
if (payload.interactionSource === BRIDGE_SOURCE) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (getInboundDecision(payload.type) !== 'allow') {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const serialized = serializePayload(payload);
|
|
67
|
+
if (!serialized) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
stampProductContentId(serialized, getContentId === null || getContentId === void 0 ? void 0 : getContentId());
|
|
71
|
+
try {
|
|
72
|
+
activeTransport.send({
|
|
73
|
+
[BRIDGE_MESSAGE_MARKER]: true,
|
|
74
|
+
direction: BRIDGE_TO_EXTENSION,
|
|
75
|
+
payload: serialized
|
|
76
|
+
});
|
|
77
|
+
} catch {
|
|
78
|
+
// Never let bridging break the host page.
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
return null;
|
|
82
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feature gate for the extension context bridge; both Host and Client self-gate on it.
|
|
3
|
+
* `fg()` requires a static literal, so call sites inline this value rather than importing it;
|
|
4
|
+
* tests resolve the gate through this constant so the two cannot drift apart.
|
|
5
|
+
*/
|
|
6
|
+
export const BRIDGE_FEATURE_GATE = 'rovo-ext_context_bridge';
|
|
7
|
+
|
|
8
|
+
/** Marker on every bridge `postMessage`, so listeners can ignore unrelated messages. */
|
|
9
|
+
export const BRIDGE_MESSAGE_MARKER = '__rovoExtContextBridge';
|
|
10
|
+
|
|
11
|
+
/** Read context flows product -> extension; actions flow extension -> product. */
|
|
12
|
+
export const BRIDGE_TO_EXTENSION = 'to-extension';
|
|
13
|
+
export const BRIDGE_TO_PRODUCT = 'to-product';
|
|
14
|
+
|
|
15
|
+
/** Loop-guard sentinel stamped on re-published payloads so they never ping-pong. */
|
|
16
|
+
export const BRIDGE_SOURCE = 'rovo-extension-bridge';
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bridge relay policy, asymmetric by direction:
|
|
3
|
+
* - Product → extension (context): relayed iff allowlisted and not denylisted. The denylist is a
|
|
4
|
+
* backstop for control-plane/auth events that must never cross.
|
|
5
|
+
* - Extension → product (actions): allowlist only, so the chat's internal state never drives the
|
|
6
|
+
* product.
|
|
7
|
+
*
|
|
8
|
+
* Independent of both lists: the loop guard (`BRIDGE_SOURCE`) and auto-drop of non-serialisable
|
|
9
|
+
* payloads.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** Inbound allowlist — payload types the bridge supports. Widen only as a feature needs it. */
|
|
13
|
+
const INBOUND_ALLOWLIST = new Set(['editor-context-payload', 'set-message-context']);
|
|
14
|
+
|
|
15
|
+
/** Inbound denylist — control-plane/auth events that must never reach the extension chat. */
|
|
16
|
+
const INBOUND_DENYLIST = new Set(['message-send', 'chat-new', 'chat-open', 'chat-close', 'open-smart-creation-modal', 'open-browse-agent-modal', 'open-browse-agent-sidebar', 'open-chat-debug-modal', 'open-chat-feedback-modal', 'forge-auth-success', 'forge-auth-failure', 'chat-smartlink-3p-post-auth-launch']);
|
|
17
|
+
|
|
18
|
+
/** Outbound allowlist — chat-originated events allowed to cross back to the product. */
|
|
19
|
+
const OUTBOUND_ACTIONS = new Set(['editor-suggestion']);
|
|
20
|
+
|
|
21
|
+
/** Why a product-bus payload was or was not relayed. */
|
|
22
|
+
|
|
23
|
+
/** Classify a product-bus payload. Relay iff `allow`. */
|
|
24
|
+
export const getInboundDecision = type => {
|
|
25
|
+
if (INBOUND_DENYLIST.has(type)) {
|
|
26
|
+
return 'denylisted';
|
|
27
|
+
}
|
|
28
|
+
return INBOUND_ALLOWLIST.has(type) ? 'allow' : 'not-allowlisted';
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** True if a chat-bus payload of this type may be relayed back to the product. */
|
|
32
|
+
export const shouldRelayToProduct = type => OUTBOUND_ACTIONS.has(type);
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { BRIDGE_SOURCE } from './constants';
|
|
2
|
+
const base = payload => ({
|
|
3
|
+
type: payload.type,
|
|
4
|
+
source: payload.source,
|
|
5
|
+
product: payload.product,
|
|
6
|
+
openChat: payload.openChat
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Boundary cast: the `Payload` union cannot be expressed structurally at this seam.
|
|
11
|
+
* Always stamps the loop-guard sentinel so the mirroring side skips it.
|
|
12
|
+
*/
|
|
13
|
+
const toPayload = value => (
|
|
14
|
+
// eslint-disable-next-line @atlaskit/platform/no-double-type-assertions -- serialization boundary reconstruction
|
|
15
|
+
{
|
|
16
|
+
...value,
|
|
17
|
+
interactionSource: BRIDGE_SOURCE
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Per-type overrides for payloads carrying non-cloneable data (functions). Anything not listed
|
|
22
|
+
* uses the generic JSON (de)serializer.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/** Sentinel used to detect merge-style `setContext` implementations. */
|
|
26
|
+
const MERGE_PROBE_KEY = '__rovoExtBridgeMergeProbe__';
|
|
27
|
+
const SHIMS = {
|
|
28
|
+
/**
|
|
29
|
+
* `set-message-context` carries a `setContext` function, which cannot be structured-cloned.
|
|
30
|
+
* Resolves REPLACE-style publishers (return a fixed value) to their value and rebuilds a
|
|
31
|
+
* trivial `setContext` on the far side; merge-style publishers are dropped (detected via a
|
|
32
|
+
* probe object, since a throw-based check would silently ship a truncated context).
|
|
33
|
+
*/
|
|
34
|
+
'set-message-context': {
|
|
35
|
+
serialize: payload => {
|
|
36
|
+
const data = payload.data;
|
|
37
|
+
if (!data || typeof data.setContext !== 'function') {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
let value;
|
|
41
|
+
try {
|
|
42
|
+
value = data.setContext({
|
|
43
|
+
[MERGE_PROBE_KEY]: true
|
|
44
|
+
});
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
// Sentinel survived: merge-style, cannot be resolved to a standalone value.
|
|
49
|
+
if (typeof value === 'object' && value !== null && MERGE_PROBE_KEY in value) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
JSON.stringify(value);
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
...base(payload),
|
|
59
|
+
data: {
|
|
60
|
+
contextKey: data.contextKey,
|
|
61
|
+
value
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
},
|
|
65
|
+
deserialize: serialized => {
|
|
66
|
+
var _serialized$source;
|
|
67
|
+
const data = serialized.data;
|
|
68
|
+
if (!data) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
const rebuilt = {
|
|
72
|
+
contextKey: data.contextKey,
|
|
73
|
+
setContext: () => data.value
|
|
74
|
+
};
|
|
75
|
+
return toPayload({
|
|
76
|
+
type: 'set-message-context',
|
|
77
|
+
source: (_serialized$source = serialized.source) !== null && _serialized$source !== void 0 ? _serialized$source : BRIDGE_SOURCE,
|
|
78
|
+
product: serialized.product,
|
|
79
|
+
openChat: serialized.openChat,
|
|
80
|
+
data: rebuilt
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
const getShim = type => SHIMS[type];
|
|
86
|
+
|
|
87
|
+
/** Serialise a payload for the wire, using a shim if one exists, else a generic JSON round-trip. */
|
|
88
|
+
export const serializePayload = payload => {
|
|
89
|
+
const shim = getShim(payload.type);
|
|
90
|
+
if (shim) {
|
|
91
|
+
return shim.serialize(payload);
|
|
92
|
+
}
|
|
93
|
+
const rawData = payload.data;
|
|
94
|
+
let data;
|
|
95
|
+
try {
|
|
96
|
+
data = rawData === undefined ? undefined : JSON.parse(JSON.stringify(rawData));
|
|
97
|
+
} catch {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
...base(payload),
|
|
102
|
+
data
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/** Reconstruct a payload from the wire, stamping the loop-guard sentinel. */
|
|
107
|
+
export const deserializePayload = serialized => {
|
|
108
|
+
var _serialized$source2;
|
|
109
|
+
const shim = getShim(serialized.type);
|
|
110
|
+
if (shim) {
|
|
111
|
+
return shim.deserialize(serialized);
|
|
112
|
+
}
|
|
113
|
+
return toPayload({
|
|
114
|
+
type: serialized.type,
|
|
115
|
+
source: (_serialized$source2 = serialized.source) !== null && _serialized$source2 !== void 0 ? _serialized$source2 : BRIDGE_SOURCE,
|
|
116
|
+
product: serialized.product,
|
|
117
|
+
openChat: serialized.openChat,
|
|
118
|
+
data: serialized.data
|
|
119
|
+
});
|
|
120
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { bind } from 'bind-event-listener';
|
|
2
|
+
import { BRIDGE_MESSAGE_MARKER } from './constants';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A `rovo-triggers` payload reduced to a structured-clone-safe shape. Functions (e.g.
|
|
6
|
+
* `set-message-context`'s `setContext`) are resolved to values by `./registry` first.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Transport abstraction so Host/Client work over any carrier: same-window `postMessage` on the
|
|
11
|
+
* product side, iframe/side-panel `postMessage` in the chat iframe. Carries `BridgeMessage` only;
|
|
12
|
+
* `rovo-content-bridge-api` commands travel separately, relayed verbatim by the extension.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export const isBridgeMessage = value => typeof value === 'object' && value !== null && value[BRIDGE_MESSAGE_MARKER] === true && typeof value.direction === 'string' && typeof value.payload === 'object' && value.payload !== null;
|
|
16
|
+
|
|
17
|
+
/** `postMessage`-based transport. Callers override `targetWindow`/`self` for the iframe side. */
|
|
18
|
+
export const createWindowTransport = ({
|
|
19
|
+
targetWindow,
|
|
20
|
+
self: selfWindow = window,
|
|
21
|
+
targetOrigin = selfWindow.location.origin
|
|
22
|
+
}) => ({
|
|
23
|
+
send: message => {
|
|
24
|
+
targetWindow.postMessage(message, targetOrigin);
|
|
25
|
+
},
|
|
26
|
+
// `bind` returns its own unbind function, which is exactly the `subscribe` contract.
|
|
27
|
+
subscribe: handler => bind(selfWindow, {
|
|
28
|
+
type: 'message',
|
|
29
|
+
listener: event => {
|
|
30
|
+
if (isBridgeMessage(event.data)) {
|
|
31
|
+
handler(event.data);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
});
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import _defineProperty from "@babel/runtime/helpers/defineProperty";
|
|
2
|
+
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
|
|
3
|
+
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
|
|
4
|
+
import { useEffect, useMemo, useRef } from 'react';
|
|
5
|
+
import { fg } from '@atlaskit/platform-feature-flags/fg';
|
|
6
|
+
import { usePublish, useSubscribeAll } from '../../../main';
|
|
7
|
+
import { BRIDGE_MESSAGE_MARKER, BRIDGE_SOURCE, BRIDGE_TO_EXTENSION, BRIDGE_TO_PRODUCT } from './constants';
|
|
8
|
+
import { shouldRelayToProduct } from './policy';
|
|
9
|
+
import { deserializePayload, serializePayload } from './registry';
|
|
10
|
+
import { createWindowTransport } from './transport';
|
|
11
|
+
|
|
12
|
+
/** Selection shape preserved across editor focus loss. */
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Whether focus currently sits inside the chat. Confluence clears `selection` on
|
|
16
|
+
* `editor-context-payload` both when the user clicks into chat and when they genuinely deselect;
|
|
17
|
+
* this distinguishes the two so the former can preserve the selection and the latter can drop it.
|
|
18
|
+
*/
|
|
19
|
+
var isChatFocused = function isChatFocused() {
|
|
20
|
+
// eslint-disable-next-line @atlaskit/platform/no-direct-document-usage -- reading focus of the chat iframe's own document is the point
|
|
21
|
+
return typeof document !== 'undefined' && document.hasFocus();
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Chat-iframe half of the bridge.
|
|
26
|
+
* - Inbound: deserializes context from the extension transport and re-publishes it onto the
|
|
27
|
+
* iframe's own `ai-mate` bus for the existing `ChatContextSubscriber` to consume unchanged.
|
|
28
|
+
* - Outbound: forwards allowlisted action payloads (e.g. `editor-suggestion`) back to the product,
|
|
29
|
+
* via the extension's content script.
|
|
30
|
+
*
|
|
31
|
+
* Content writes (`rovo-content-bridge-api` commands) are not handled here — that API already
|
|
32
|
+
* crosses this boundary on its own, so the extension only relays envelopes for it.
|
|
33
|
+
*
|
|
34
|
+
* Headless.
|
|
35
|
+
*/
|
|
36
|
+
export var ExtensionContextBridgeClient = function ExtensionContextBridgeClient(_ref) {
|
|
37
|
+
var transport = _ref.transport;
|
|
38
|
+
// Self-gated. Matches `BRIDGE_FEATURE_GATE` in `./constants`.
|
|
39
|
+
var active = fg('rovo-ext_context_bridge');
|
|
40
|
+
var publish = usePublish('ai-mate');
|
|
41
|
+
var lastKnownSelectionRef = useRef(null);
|
|
42
|
+
var activeTransport = useMemo(function () {
|
|
43
|
+
if (!active) {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
if (transport) {
|
|
47
|
+
return transport;
|
|
48
|
+
}
|
|
49
|
+
if (typeof window === 'undefined') {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
// `targetOrigin: '*'`: the receiving window (the side panel) is a different origin, so the
|
|
53
|
+
// default (`selfWindow.location.origin`) would not match.
|
|
54
|
+
return createWindowTransport({
|
|
55
|
+
targetWindow: window.parent,
|
|
56
|
+
self: window,
|
|
57
|
+
targetOrigin: '*'
|
|
58
|
+
});
|
|
59
|
+
}, [active, transport]);
|
|
60
|
+
|
|
61
|
+
// Inbound (READ): product context -> iframe bus.
|
|
62
|
+
useEffect(function () {
|
|
63
|
+
if (!activeTransport) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
return activeTransport.subscribe(function (message) {
|
|
67
|
+
if (message.direction !== BRIDGE_TO_EXTENSION) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
var payloadToPublish = deserializePayload(message.payload);
|
|
71
|
+
if (!payloadToPublish) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
var editorData = message.payload.data;
|
|
75
|
+
if (message.payload.type === 'editor-context-payload') {
|
|
76
|
+
var _editorData$selection;
|
|
77
|
+
if (!(editorData !== null && editorData !== void 0 && editorData.document) || editorData !== null && editorData !== void 0 && editorData.isViewMode) {
|
|
78
|
+
// Editor closed or switched to view mode.
|
|
79
|
+
lastKnownSelectionRef.current = null;
|
|
80
|
+
} else if ((_editorData$selection = editorData.selection) !== null && _editorData$selection !== void 0 && _editorData$selection.content) {
|
|
81
|
+
// New selection.
|
|
82
|
+
lastKnownSelectionRef.current = editorData.selection;
|
|
83
|
+
} else if (lastKnownSelectionRef.current && isChatFocused()) {
|
|
84
|
+
// Focus moved into the chat; re-inject the selection so Insert/Replace still resolves.
|
|
85
|
+
payloadToPublish = deserializePayload(_objectSpread(_objectSpread({}, message.payload), {}, {
|
|
86
|
+
data: _objectSpread(_objectSpread({}, editorData), {}, {
|
|
87
|
+
selection: lastKnownSelectionRef.current
|
|
88
|
+
})
|
|
89
|
+
}));
|
|
90
|
+
} else {
|
|
91
|
+
// Real deselection; drop it so context falls back to the page.
|
|
92
|
+
lastKnownSelectionRef.current = null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (payloadToPublish) {
|
|
96
|
+
publish(payloadToPublish);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
}, [activeTransport, publish]);
|
|
100
|
+
|
|
101
|
+
// Outbound (WRITE, payloads): allowlisted chat actions -> product.
|
|
102
|
+
useSubscribeAll(function (payload) {
|
|
103
|
+
if (!activeTransport) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
// Loop guard.
|
|
107
|
+
if (payload.interactionSource === BRIDGE_SOURCE) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (!shouldRelayToProduct(payload.type)) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
var serialized = serializePayload(payload);
|
|
114
|
+
if (!serialized) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
activeTransport.send(_defineProperty(_defineProperty(_defineProperty({}, BRIDGE_MESSAGE_MARKER, true), "direction", BRIDGE_TO_PRODUCT), "payload", serialized));
|
|
119
|
+
} catch (_unused) {
|
|
120
|
+
// Never let bridging break the chat iframe.
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
return null;
|
|
124
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import _defineProperty from "@babel/runtime/helpers/defineProperty";
|
|
2
|
+
import { useMemo } from 'react';
|
|
3
|
+
import { fg } from '@atlaskit/platform-feature-flags/fg';
|
|
4
|
+
import { useSubscribeAll } from '../../../main';
|
|
5
|
+
import { BRIDGE_MESSAGE_MARKER, BRIDGE_SOURCE, BRIDGE_TO_EXTENSION } from './constants';
|
|
6
|
+
import { getInboundDecision } from './policy';
|
|
7
|
+
import { serializePayload } from './registry';
|
|
8
|
+
import { createWindowTransport } from './transport';
|
|
9
|
+
/**
|
|
10
|
+
* Stamps the page id onto `editor-context-payload` when the product left it unpopulated, since the
|
|
11
|
+
* extension chat cannot derive its own `contentId` from `window.location` (that's the side panel, not
|
|
12
|
+
* the page). `getContentId` is supplied by the consumer rather than read here, to avoid this package
|
|
13
|
+
* taking a dependency on `conversation-assistant-store` (a cyclic-dependency risk); the consumer
|
|
14
|
+
* already depends on that store and can resolve the id from it directly.
|
|
15
|
+
*/
|
|
16
|
+
var stampProductContentId = function stampProductContentId(serialized, targetContentId) {
|
|
17
|
+
if (serialized.type !== 'editor-context-payload') {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
var data = serialized.data;
|
|
21
|
+
if (!data || data.contentMauiId) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (targetContentId) {
|
|
25
|
+
data.contentMauiId = targetContentId;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Product-window half of the bridge. Observes the `ai-mate` bus and mirrors allowlisted context out
|
|
30
|
+
* to the extension via same-window `postMessage` (see `./policy` for the allow/deny rules).
|
|
31
|
+
*
|
|
32
|
+
* The reverse direction is handled by the sibling `RovoPostMessagePubsubListener` in
|
|
33
|
+
* `ChatOpenerSubscriber`, not here. Headless; mounts at product root via `ChatOpenerSubscriber`.
|
|
34
|
+
*/
|
|
35
|
+
export var ExtensionContextBridgeHost = function ExtensionContextBridgeHost(_ref) {
|
|
36
|
+
var transport = _ref.transport,
|
|
37
|
+
getContentId = _ref.getContentId;
|
|
38
|
+
// Self-gated (not taken from the consumer) so this cannot mount by mistake wherever
|
|
39
|
+
// `ChatOpenerSubscriber` renders. Matches `BRIDGE_FEATURE_GATE` in `./constants`.
|
|
40
|
+
var active = fg('rovo-ext_context_bridge');
|
|
41
|
+
var activeTransport = useMemo(function () {
|
|
42
|
+
if (!active) {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
if (transport) {
|
|
46
|
+
return transport;
|
|
47
|
+
}
|
|
48
|
+
if (typeof window === 'undefined') {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
return createWindowTransport({
|
|
52
|
+
targetWindow: window
|
|
53
|
+
});
|
|
54
|
+
}, [active, transport]);
|
|
55
|
+
useSubscribeAll(function (payload) {
|
|
56
|
+
if (!activeTransport) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
// Loop guard.
|
|
60
|
+
if (payload.interactionSource === BRIDGE_SOURCE) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (getInboundDecision(payload.type) !== 'allow') {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
var serialized = serializePayload(payload);
|
|
67
|
+
if (!serialized) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
stampProductContentId(serialized, getContentId === null || getContentId === void 0 ? void 0 : getContentId());
|
|
71
|
+
try {
|
|
72
|
+
activeTransport.send(_defineProperty(_defineProperty(_defineProperty({}, BRIDGE_MESSAGE_MARKER, true), "direction", BRIDGE_TO_EXTENSION), "payload", serialized));
|
|
73
|
+
} catch (_unused) {
|
|
74
|
+
// Never let bridging break the host page.
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
return null;
|
|
78
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feature gate for the extension context bridge; both Host and Client self-gate on it.
|
|
3
|
+
* `fg()` requires a static literal, so call sites inline this value rather than importing it;
|
|
4
|
+
* tests resolve the gate through this constant so the two cannot drift apart.
|
|
5
|
+
*/
|
|
6
|
+
export var BRIDGE_FEATURE_GATE = 'rovo-ext_context_bridge';
|
|
7
|
+
|
|
8
|
+
/** Marker on every bridge `postMessage`, so listeners can ignore unrelated messages. */
|
|
9
|
+
export var BRIDGE_MESSAGE_MARKER = '__rovoExtContextBridge';
|
|
10
|
+
|
|
11
|
+
/** Read context flows product -> extension; actions flow extension -> product. */
|
|
12
|
+
export var BRIDGE_TO_EXTENSION = 'to-extension';
|
|
13
|
+
export var BRIDGE_TO_PRODUCT = 'to-product';
|
|
14
|
+
|
|
15
|
+
/** Loop-guard sentinel stamped on re-published payloads so they never ping-pong. */
|
|
16
|
+
export var BRIDGE_SOURCE = 'rovo-extension-bridge';
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bridge relay policy, asymmetric by direction:
|
|
3
|
+
* - Product → extension (context): relayed iff allowlisted and not denylisted. The denylist is a
|
|
4
|
+
* backstop for control-plane/auth events that must never cross.
|
|
5
|
+
* - Extension → product (actions): allowlist only, so the chat's internal state never drives the
|
|
6
|
+
* product.
|
|
7
|
+
*
|
|
8
|
+
* Independent of both lists: the loop guard (`BRIDGE_SOURCE`) and auto-drop of non-serialisable
|
|
9
|
+
* payloads.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** Inbound allowlist — payload types the bridge supports. Widen only as a feature needs it. */
|
|
13
|
+
var INBOUND_ALLOWLIST = new Set(['editor-context-payload', 'set-message-context']);
|
|
14
|
+
|
|
15
|
+
/** Inbound denylist — control-plane/auth events that must never reach the extension chat. */
|
|
16
|
+
var INBOUND_DENYLIST = new Set(['message-send', 'chat-new', 'chat-open', 'chat-close', 'open-smart-creation-modal', 'open-browse-agent-modal', 'open-browse-agent-sidebar', 'open-chat-debug-modal', 'open-chat-feedback-modal', 'forge-auth-success', 'forge-auth-failure', 'chat-smartlink-3p-post-auth-launch']);
|
|
17
|
+
|
|
18
|
+
/** Outbound allowlist — chat-originated events allowed to cross back to the product. */
|
|
19
|
+
var OUTBOUND_ACTIONS = new Set(['editor-suggestion']);
|
|
20
|
+
|
|
21
|
+
/** Why a product-bus payload was or was not relayed. */
|
|
22
|
+
|
|
23
|
+
/** Classify a product-bus payload. Relay iff `allow`. */
|
|
24
|
+
export var getInboundDecision = function getInboundDecision(type) {
|
|
25
|
+
if (INBOUND_DENYLIST.has(type)) {
|
|
26
|
+
return 'denylisted';
|
|
27
|
+
}
|
|
28
|
+
return INBOUND_ALLOWLIST.has(type) ? 'allow' : 'not-allowlisted';
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** True if a chat-bus payload of this type may be relayed back to the product. */
|
|
32
|
+
export var shouldRelayToProduct = function shouldRelayToProduct(type) {
|
|
33
|
+
return OUTBOUND_ACTIONS.has(type);
|
|
34
|
+
};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import _typeof from "@babel/runtime/helpers/typeof";
|
|
2
|
+
import _defineProperty from "@babel/runtime/helpers/defineProperty";
|
|
3
|
+
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
|
|
4
|
+
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
|
|
5
|
+
import { BRIDGE_SOURCE } from './constants';
|
|
6
|
+
var base = function base(payload) {
|
|
7
|
+
return {
|
|
8
|
+
type: payload.type,
|
|
9
|
+
source: payload.source,
|
|
10
|
+
product: payload.product,
|
|
11
|
+
openChat: payload.openChat
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Boundary cast: the `Payload` union cannot be expressed structurally at this seam.
|
|
17
|
+
* Always stamps the loop-guard sentinel so the mirroring side skips it.
|
|
18
|
+
*/
|
|
19
|
+
var toPayload = function toPayload(value) {
|
|
20
|
+
return (// eslint-disable-next-line @atlaskit/platform/no-double-type-assertions -- serialization boundary reconstruction
|
|
21
|
+
_objectSpread(_objectSpread({}, value), {}, {
|
|
22
|
+
interactionSource: BRIDGE_SOURCE
|
|
23
|
+
})
|
|
24
|
+
);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Per-type overrides for payloads carrying non-cloneable data (functions). Anything not listed
|
|
29
|
+
* uses the generic JSON (de)serializer.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/** Sentinel used to detect merge-style `setContext` implementations. */
|
|
33
|
+
var MERGE_PROBE_KEY = '__rovoExtBridgeMergeProbe__';
|
|
34
|
+
var SHIMS = {
|
|
35
|
+
/**
|
|
36
|
+
* `set-message-context` carries a `setContext` function, which cannot be structured-cloned.
|
|
37
|
+
* Resolves REPLACE-style publishers (return a fixed value) to their value and rebuilds a
|
|
38
|
+
* trivial `setContext` on the far side; merge-style publishers are dropped (detected via a
|
|
39
|
+
* probe object, since a throw-based check would silently ship a truncated context).
|
|
40
|
+
*/
|
|
41
|
+
'set-message-context': {
|
|
42
|
+
serialize: function serialize(payload) {
|
|
43
|
+
var data = payload.data;
|
|
44
|
+
if (!data || typeof data.setContext !== 'function') {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
var value;
|
|
48
|
+
try {
|
|
49
|
+
value = data.setContext(_defineProperty({}, MERGE_PROBE_KEY, true));
|
|
50
|
+
} catch (_unused) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
// Sentinel survived: merge-style, cannot be resolved to a standalone value.
|
|
54
|
+
if (_typeof(value) === 'object' && value !== null && MERGE_PROBE_KEY in value) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
JSON.stringify(value);
|
|
59
|
+
} catch (_unused2) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
return _objectSpread(_objectSpread({}, base(payload)), {}, {
|
|
63
|
+
data: {
|
|
64
|
+
contextKey: data.contextKey,
|
|
65
|
+
value: value
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
},
|
|
69
|
+
deserialize: function deserialize(serialized) {
|
|
70
|
+
var _serialized$source;
|
|
71
|
+
var data = serialized.data;
|
|
72
|
+
if (!data) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
var rebuilt = {
|
|
76
|
+
contextKey: data.contextKey,
|
|
77
|
+
setContext: function setContext() {
|
|
78
|
+
return data.value;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
return toPayload({
|
|
82
|
+
type: 'set-message-context',
|
|
83
|
+
source: (_serialized$source = serialized.source) !== null && _serialized$source !== void 0 ? _serialized$source : BRIDGE_SOURCE,
|
|
84
|
+
product: serialized.product,
|
|
85
|
+
openChat: serialized.openChat,
|
|
86
|
+
data: rebuilt
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
var getShim = function getShim(type) {
|
|
92
|
+
return SHIMS[type];
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/** Serialise a payload for the wire, using a shim if one exists, else a generic JSON round-trip. */
|
|
96
|
+
export var serializePayload = function serializePayload(payload) {
|
|
97
|
+
var shim = getShim(payload.type);
|
|
98
|
+
if (shim) {
|
|
99
|
+
return shim.serialize(payload);
|
|
100
|
+
}
|
|
101
|
+
var rawData = payload.data;
|
|
102
|
+
var data;
|
|
103
|
+
try {
|
|
104
|
+
data = rawData === undefined ? undefined : JSON.parse(JSON.stringify(rawData));
|
|
105
|
+
} catch (_unused3) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
return _objectSpread(_objectSpread({}, base(payload)), {}, {
|
|
109
|
+
data: data
|
|
110
|
+
});
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/** Reconstruct a payload from the wire, stamping the loop-guard sentinel. */
|
|
114
|
+
export var deserializePayload = function deserializePayload(serialized) {
|
|
115
|
+
var _serialized$source2;
|
|
116
|
+
var shim = getShim(serialized.type);
|
|
117
|
+
if (shim) {
|
|
118
|
+
return shim.deserialize(serialized);
|
|
119
|
+
}
|
|
120
|
+
return toPayload({
|
|
121
|
+
type: serialized.type,
|
|
122
|
+
source: (_serialized$source2 = serialized.source) !== null && _serialized$source2 !== void 0 ? _serialized$source2 : BRIDGE_SOURCE,
|
|
123
|
+
product: serialized.product,
|
|
124
|
+
openChat: serialized.openChat,
|
|
125
|
+
data: serialized.data
|
|
126
|
+
});
|
|
127
|
+
};
|