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