@atlaskit/rovo-triggers 10.37.0 → 11.0.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 +53 -0
- package/dist/cjs/common/utils/extension-context-bridge/ExtensionContextBridgeClient.js +7 -6
- package/dist/cjs/common/utils/extension-context-bridge/ExtensionContextBridgeHost.js +34 -17
- package/dist/cjs/common/utils/extension-context-bridge/analytics.js +87 -0
- package/dist/cjs/common/utils/extension-context-bridge/constants.js +11 -5
- package/dist/cjs/common/utils/extension-context-bridge/registry.js +33 -15
- package/dist/es2019/common/utils/extension-context-bridge/ExtensionContextBridgeClient.js +7 -6
- package/dist/es2019/common/utils/extension-context-bridge/ExtensionContextBridgeHost.js +34 -16
- package/dist/es2019/common/utils/extension-context-bridge/analytics.js +76 -0
- package/dist/es2019/common/utils/extension-context-bridge/constants.js +10 -4
- package/dist/es2019/common/utils/extension-context-bridge/registry.js +29 -12
- package/dist/esm/common/utils/extension-context-bridge/ExtensionContextBridgeClient.js +7 -6
- package/dist/esm/common/utils/extension-context-bridge/ExtensionContextBridgeHost.js +34 -17
- package/dist/esm/common/utils/extension-context-bridge/analytics.js +80 -0
- package/dist/esm/common/utils/extension-context-bridge/constants.js +10 -4
- package/dist/esm/common/utils/extension-context-bridge/registry.js +34 -15
- package/dist/types/common/utils/extension-context-bridge/ExtensionContextBridgeHost.d.ts +8 -1
- package/dist/types/common/utils/extension-context-bridge/analytics.d.ts +55 -0
- package/dist/types/common/utils/extension-context-bridge/constants.d.ts +10 -4
- package/dist/types/common/utils/extension-context-bridge/policy.d.ts +1 -2
- package/dist/types/common/utils/extension-context-bridge/registry.d.ts +14 -1
- package/dist/types/types.d.ts +1 -1
- package/extension-context-bridge/analytics/package.json +10 -0
- package/package.json +4 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,58 @@
|
|
|
1
1
|
# @atlaskit/rovo-triggers
|
|
2
2
|
|
|
3
|
+
## 11.0.0
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- [`de8897276a43e`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/de8897276a43e) -
|
|
8
|
+
Replace the `rovo-ext_context_bridge` feature gate with the `rovo-ext_context_bridge_exp`
|
|
9
|
+
experiment, read through its boolean `isEnabled` parameter.
|
|
10
|
+
|
|
11
|
+
**Breaking:** `@atlaskit/rovo-triggers/extension-context-bridge/constants` no longer exports
|
|
12
|
+
`BRIDGE_FEATURE_GATE`. Use `EXT_CONTEXT_BRIDGE_EXPERIMENT` instead, which holds the experiment key
|
|
13
|
+
rather than the gate key:
|
|
14
|
+
|
|
15
|
+
```diff
|
|
16
|
+
- import { BRIDGE_FEATURE_GATE } from '@atlaskit/rovo-triggers/extension-context-bridge/constants';
|
|
17
|
+
+ import { EXT_CONTEXT_BRIDGE_EXPERIMENT } from '@atlaskit/rovo-triggers/extension-context-bridge/constants';
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`ExtensionContextBridgeHost`, `ExtensionContextBridgeClient` and `ChatOpenerSubscriber` read the
|
|
21
|
+
experiment through `UNSAFE_expValNoExposure`, so none of them log exposure: the Host mounts at the
|
|
22
|
+
root of every page of an opted-in product, and exposure there would count product page loads
|
|
23
|
+
rather than extension users. The Rovo browser extension owns the single exposure call.
|
|
24
|
+
|
|
25
|
+
No behaviour change for consumers beyond the assignment source. The
|
|
26
|
+
`extensionContextBridgeEnabled` prop on `ChatOpenerSubscriber` is unchanged and still required to
|
|
27
|
+
opt in.
|
|
28
|
+
|
|
29
|
+
### Minor Changes
|
|
30
|
+
|
|
31
|
+
- [`487372cfb38b7`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/487372cfb38b7) -
|
|
32
|
+
Send the `remix_custom_edit` experience, source page ID, and required editor context for editor
|
|
33
|
+
Custom Remix requests behind the `aifc_remix_custom_page_edit` gate.
|
|
34
|
+
|
|
35
|
+
## 10.38.0
|
|
36
|
+
|
|
37
|
+
### Minor Changes
|
|
38
|
+
|
|
39
|
+
- [`2124250692aba`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/2124250692aba) -
|
|
40
|
+
ASIMO-5048: report what the extension context bridge's inbound relay policy decided for each
|
|
41
|
+
payload a product publishes, so a refusal is observable instead of silent.
|
|
42
|
+
|
|
43
|
+
`ExtensionContextBridgeHost` takes a new optional `onRelayDecision` callback, with its contract on
|
|
44
|
+
a new `./extension-context-bridge/analytics` entrypoint, and the serializer now returns a
|
|
45
|
+
discriminated result so a refusal names its reason. `ChatOpenerSubscriber` wires the callback to
|
|
46
|
+
the analytics client it already holds, emitting a `rovoExtensionBridgePayload evaluated`
|
|
47
|
+
operational event into the host product's stream.
|
|
48
|
+
|
|
49
|
+
Product to extension direction only. Deduplicated to one report per distinct outcome per mount,
|
|
50
|
+
and only active while `rovo-ext_context_bridge` is on.
|
|
51
|
+
|
|
52
|
+
### Patch Changes
|
|
53
|
+
|
|
54
|
+
- Updated dependencies
|
|
55
|
+
|
|
3
56
|
## 10.37.0
|
|
4
57
|
|
|
5
58
|
### Minor Changes
|
|
@@ -7,7 +7,7 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
7
7
|
exports.ExtensionContextBridgeClient = void 0;
|
|
8
8
|
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
|
|
9
9
|
var _react = require("react");
|
|
10
|
-
var
|
|
10
|
+
var _unsafeExpValNoExposure = require("@atlaskit/platform-feature-experiments/unsafe-exp-val-no-exposure");
|
|
11
11
|
var _main = require("../../../main");
|
|
12
12
|
var _constants = require("./constants");
|
|
13
13
|
var _policy = require("./policy");
|
|
@@ -60,8 +60,9 @@ var isChatFocused = function isChatFocused() {
|
|
|
60
60
|
var ExtensionContextBridgeClient = exports.ExtensionContextBridgeClient = function ExtensionContextBridgeClient(_ref) {
|
|
61
61
|
var transport = _ref.transport,
|
|
62
62
|
allowedOrigins = _ref.allowedOrigins;
|
|
63
|
-
// Self-gated. Matches `
|
|
64
|
-
|
|
63
|
+
// Self-gated. Matches `EXT_CONTEXT_BRIDGE_EXPERIMENT` in `./constants`. No-exposure read: the
|
|
64
|
+
// extension decides whether to mount this and owns the exposure call.
|
|
65
|
+
var active = (0, _unsafeExpValNoExposure.UNSAFE_expValNoExposure)('rovo-ext_context_bridge_exp', 'isEnabled', false) === true;
|
|
65
66
|
var publish = (0, _main.usePublish)('ai-mate');
|
|
66
67
|
var lastKnownSelectionRef = (0, _react.useRef)(null);
|
|
67
68
|
var activeTransport = (0, _react.useMemo)(function () {
|
|
@@ -161,12 +162,12 @@ var ExtensionContextBridgeClient = exports.ExtensionContextBridgeClient = functi
|
|
|
161
162
|
if (!(0, _policy.shouldRelayToProduct)(payload.type)) {
|
|
162
163
|
return;
|
|
163
164
|
}
|
|
164
|
-
var
|
|
165
|
-
if (!
|
|
165
|
+
var result = (0, _registry.serializePayload)(payload);
|
|
166
|
+
if (!result.ok) {
|
|
166
167
|
return;
|
|
167
168
|
}
|
|
168
169
|
try {
|
|
169
|
-
activeTransport.send((0, _defineProperty2.default)((0, _defineProperty2.default)((0, _defineProperty2.default)({}, _constants.BRIDGE_MESSAGE_MARKER, true), "direction", _constants.BRIDGE_TO_PRODUCT), "payload",
|
|
170
|
+
activeTransport.send((0, _defineProperty2.default)((0, _defineProperty2.default)((0, _defineProperty2.default)({}, _constants.BRIDGE_MESSAGE_MARKER, true), "direction", _constants.BRIDGE_TO_PRODUCT), "payload", result.payload));
|
|
170
171
|
} catch (_unused3) {
|
|
171
172
|
// Never let bridging break the chat iframe.
|
|
172
173
|
}
|
|
@@ -6,10 +6,10 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
6
6
|
});
|
|
7
7
|
exports.ExtensionContextBridgeHost = void 0;
|
|
8
8
|
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
|
|
9
|
-
var _typeof2 = _interopRequireDefault(require("@babel/runtime/helpers/typeof"));
|
|
10
9
|
var _react = require("react");
|
|
11
|
-
var
|
|
10
|
+
var _unsafeExpValNoExposure = require("@atlaskit/platform-feature-experiments/unsafe-exp-val-no-exposure");
|
|
12
11
|
var _main = require("../../../main");
|
|
12
|
+
var _analytics = require("./analytics");
|
|
13
13
|
var _constants = require("./constants");
|
|
14
14
|
var _policy = require("./policy");
|
|
15
15
|
var _registry = require("./registry");
|
|
@@ -40,14 +40,8 @@ var stampProductContentId = function stampProductContentId(serialized, targetCon
|
|
|
40
40
|
* else is keyed by payload type, of which there is one live instance at a time.
|
|
41
41
|
*/
|
|
42
42
|
var getContextSlotKey = function getContextSlotKey(payload) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
var contextKey = (0, _typeof2.default)(data) === 'object' && data !== null && 'contextKey' in data ? data.contextKey : undefined;
|
|
46
|
-
if (typeof contextKey === 'string') {
|
|
47
|
-
return "".concat(payload.type, ":").concat(contextKey);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
return payload.type;
|
|
43
|
+
var contextKey = (0, _analytics.getContextKey)(payload);
|
|
44
|
+
return contextKey === undefined ? payload.type : "".concat(payload.type, ":").concat(contextKey);
|
|
51
45
|
};
|
|
52
46
|
/**
|
|
53
47
|
* Product-window half of the bridge. Observes the `ai-mate` bus and mirrors allowlisted context out
|
|
@@ -59,10 +53,15 @@ var getContextSlotKey = function getContextSlotKey(payload) {
|
|
|
59
53
|
var ExtensionContextBridgeHost = exports.ExtensionContextBridgeHost = function ExtensionContextBridgeHost(_ref) {
|
|
60
54
|
var transport = _ref.transport,
|
|
61
55
|
getContentId = _ref.getContentId,
|
|
62
|
-
isAiEnabled = _ref.isAiEnabled
|
|
56
|
+
isAiEnabled = _ref.isAiEnabled,
|
|
57
|
+
onRelayDecision = _ref.onRelayDecision;
|
|
63
58
|
// Self-gated (not taken from the consumer) so this cannot mount by mistake wherever
|
|
64
|
-
// `ChatOpenerSubscriber` renders. Matches `
|
|
65
|
-
|
|
59
|
+
// `ChatOpenerSubscriber` renders. Matches `EXT_CONTEXT_BRIDGE_EXPERIMENT` in `./constants`.
|
|
60
|
+
//
|
|
61
|
+
// No-exposure read on purpose: this component mounts on every page of an opted-in product, so
|
|
62
|
+
// firing exposure here would measure product page loads instead of extension users. The
|
|
63
|
+
// extension logs the one exposure. See `./constants`.
|
|
64
|
+
var active = (0, _unsafeExpValNoExposure.UNSAFE_expValNoExposure)('rovo-ext_context_bridge_exp', 'isEnabled', false) === true;
|
|
66
65
|
var activeTransport = (0, _react.useMemo)(function () {
|
|
67
66
|
if (!active) {
|
|
68
67
|
return undefined;
|
|
@@ -87,6 +86,9 @@ var ExtensionContextBridgeHost = exports.ExtensionContextBridgeHost = function E
|
|
|
87
86
|
*/
|
|
88
87
|
var lastRelayedRef = (0, _react.useRef)(new Map());
|
|
89
88
|
|
|
89
|
+
/** Reported decisions, so a per-keystroke payload yields one event rather than thousands. */
|
|
90
|
+
var reportedDecisionsRef = (0, _react.useRef)(new Set());
|
|
91
|
+
|
|
90
92
|
// Answers `request-context` from the chat, which cannot observe context published before it
|
|
91
93
|
// mounted. Products such as Jira publish once per navigation, so without this a chat opened after
|
|
92
94
|
// the page settled would describe the wrong view — or none at all.
|
|
@@ -124,17 +126,32 @@ var ExtensionContextBridgeHost = exports.ExtensionContextBridgeHost = function E
|
|
|
124
126
|
if (isAiEnabled === false) {
|
|
125
127
|
return;
|
|
126
128
|
}
|
|
127
|
-
// Loop guard.
|
|
129
|
+
// Loop guard. Not reported: our own echo, not a decision about the product.
|
|
128
130
|
if (payload.interactionSource === _constants.BRIDGE_SOURCE) {
|
|
129
131
|
return;
|
|
130
132
|
}
|
|
131
|
-
|
|
133
|
+
var report = function report(decision) {
|
|
134
|
+
(0, _analytics.reportRelayDecision)(reportedDecisionsRef.current, onRelayDecision, {
|
|
135
|
+
payloadType: payload.type,
|
|
136
|
+
contextKey: (0, _analytics.getContextKey)(payload),
|
|
137
|
+
relayed: decision === 'allow',
|
|
138
|
+
decision: decision,
|
|
139
|
+
publisherSource: payload.source,
|
|
140
|
+
payloadProduct: payload.product
|
|
141
|
+
});
|
|
142
|
+
};
|
|
143
|
+
var inboundDecision = (0, _policy.getInboundDecision)(payload.type);
|
|
144
|
+
if (inboundDecision !== 'allow') {
|
|
145
|
+
report(inboundDecision);
|
|
132
146
|
return;
|
|
133
147
|
}
|
|
134
|
-
var
|
|
135
|
-
if (!
|
|
148
|
+
var result = (0, _registry.serializePayload)(payload);
|
|
149
|
+
if (!result.ok) {
|
|
150
|
+
report(result.reason);
|
|
136
151
|
return;
|
|
137
152
|
}
|
|
153
|
+
report('allow');
|
|
154
|
+
var serialized = result.payload;
|
|
138
155
|
stampProductContentId(serialized, getContentId === null || getContentId === void 0 ? void 0 : getContentId());
|
|
139
156
|
var message = (0, _defineProperty2.default)((0, _defineProperty2.default)((0, _defineProperty2.default)({}, _constants.BRIDGE_MESSAGE_MARKER, true), "direction", _constants.BRIDGE_TO_EXTENSION), "payload", serialized);
|
|
140
157
|
// Keyed by context slot, not by message, so a re-publish replaces rather than accumulates.
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
|
|
4
|
+
Object.defineProperty(exports, "__esModule", {
|
|
5
|
+
value: true
|
|
6
|
+
});
|
|
7
|
+
exports.reportRelayDecision = exports.getContextKey = void 0;
|
|
8
|
+
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
|
|
9
|
+
var _typeof2 = _interopRequireDefault(require("@babel/runtime/helpers/typeof"));
|
|
10
|
+
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; }
|
|
11
|
+
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; }
|
|
12
|
+
/**
|
|
13
|
+
* Inbound relay decision reporting: what the product asked the bridge to relay, and what the policy
|
|
14
|
+
* in `./policy` / `./registry` decided. Every refusal is otherwise silent, so the allowlist can only
|
|
15
|
+
* be widened from data with this.
|
|
16
|
+
*
|
|
17
|
+
* Product → extension only, hence no direction field. The outbound allowlist has one entry and is
|
|
18
|
+
* owned by the chat, so products cannot drift into it.
|
|
19
|
+
*
|
|
20
|
+
* Consumer-supplied callback rather than an event fired here, for the same reason as the Host's
|
|
21
|
+
* `getContentId`: this package has no analytics dependency.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Why `./registry` refused to serialise a payload. Kept distinct because the fixes differ:
|
|
26
|
+
* `context-key-not-allowlisted` is a list we can widen, the rest are publisher-side shape problems.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/** Every outcome the bridge can reach for a payload it was asked to relay. */
|
|
30
|
+
|
|
31
|
+
/** A slot identifier by contract, capped in case a publisher ever interpolates an id into one. */
|
|
32
|
+
var MAX_CONTEXT_KEY_LENGTH = 80;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* (payload types × decisions) is finite and far below this; the cap is for `contextKey`, which is
|
|
36
|
+
* not. A publisher generating keys would otherwise grow the dedupe set for the life of the page.
|
|
37
|
+
*/
|
|
38
|
+
var MAX_REPORTED_DECISIONS = 50;
|
|
39
|
+
/**
|
|
40
|
+
* The context slot a `set-message-context` payload addresses. Shape-checked rather than cast: runs
|
|
41
|
+
* against the raw bus payload, the serialized form, and types carrying neither. The Host also keys
|
|
42
|
+
* its replay map on this.
|
|
43
|
+
*/
|
|
44
|
+
var getContextKey = exports.getContextKey = function getContextKey(payload) {
|
|
45
|
+
if (payload.type !== 'set-message-context') {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
var data = payload.data;
|
|
49
|
+
var contextKey = (0, _typeof2.default)(data) === 'object' && data !== null && 'contextKey' in data ? data.contextKey : undefined;
|
|
50
|
+
return typeof contextKey === 'string' ? contextKey : undefined;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/** Identity of a decision for deduplication purposes. */
|
|
54
|
+
var getDecisionKey = function getDecisionKey(_ref) {
|
|
55
|
+
var payloadType = _ref.payloadType,
|
|
56
|
+
contextKey = _ref.contextKey,
|
|
57
|
+
decision = _ref.decision;
|
|
58
|
+
return "".concat(payloadType, "|").concat(contextKey !== null && contextKey !== void 0 ? contextKey : '', "|").concat(decision);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Report a relay decision at most once per distinct outcome. Deduplicated because the bridge sees
|
|
63
|
+
* the whole product bus and `editor-context-payload` recurs on roughly every keystroke; which keys
|
|
64
|
+
* exist and how they were decided is the question, not how often each recurred.
|
|
65
|
+
*
|
|
66
|
+
* `seen` is caller-owned (a ref in the Host), so the window is one mount. Never throws.
|
|
67
|
+
*/
|
|
68
|
+
var reportRelayDecision = exports.reportRelayDecision = function reportRelayDecision(seen, onRelayDecision, decision) {
|
|
69
|
+
var _decision$contextKey;
|
|
70
|
+
if (!onRelayDecision) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
var truncatedContextKey = (_decision$contextKey = decision.contextKey) === null || _decision$contextKey === void 0 ? void 0 : _decision$contextKey.slice(0, MAX_CONTEXT_KEY_LENGTH);
|
|
74
|
+
var reportable = _objectSpread(_objectSpread({}, decision), {}, {
|
|
75
|
+
contextKey: truncatedContextKey
|
|
76
|
+
});
|
|
77
|
+
var key = getDecisionKey(reportable);
|
|
78
|
+
if (seen.has(key) || seen.size >= MAX_REPORTED_DECISIONS) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
seen.add(key);
|
|
82
|
+
try {
|
|
83
|
+
onRelayDecision(reportable);
|
|
84
|
+
} catch (_unused) {
|
|
85
|
+
// Reporting is observability, never a reason for the relay to fail.
|
|
86
|
+
}
|
|
87
|
+
};
|
|
@@ -3,13 +3,19 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
-
exports.
|
|
6
|
+
exports.EXT_CONTEXT_BRIDGE_EXPERIMENT = exports.BRIDGE_TO_PRODUCT = exports.BRIDGE_TO_EXTENSION = exports.BRIDGE_SOURCE = exports.BRIDGE_MESSAGE_MARKER = exports.BRIDGE_CONTROL_REQUEST_CONTEXT = void 0;
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
9
|
-
* `
|
|
10
|
-
* tests resolve the
|
|
8
|
+
* Statsig experiment for the extension context bridge; both Host and Client self-gate on its
|
|
9
|
+
* boolean `isEnabled` parameter. Call sites inline this value rather than importing it, since the
|
|
10
|
+
* experiment runtime takes a static literal; tests resolve the experiment through this constant so
|
|
11
|
+
* the two cannot drift apart.
|
|
12
|
+
*
|
|
13
|
+
* Read here **without** logging exposure. Host mounts at the root of every page of an opted-in
|
|
14
|
+
* product, so an exposure-firing read would count product page loads rather than extension users.
|
|
15
|
+
* The extension owns the single exposure call, at the point the user is on a page the bridge can
|
|
16
|
+
* act on.
|
|
11
17
|
*/
|
|
12
|
-
var
|
|
18
|
+
var EXT_CONTEXT_BRIDGE_EXPERIMENT = exports.EXT_CONTEXT_BRIDGE_EXPERIMENT = 'rovo-ext_context_bridge_exp';
|
|
13
19
|
|
|
14
20
|
/** Marker on every bridge `postMessage`, so listeners can ignore unrelated messages. */
|
|
15
21
|
var BRIDGE_MESSAGE_MARKER = exports.BRIDGE_MESSAGE_MARKER = '__rovoExtContextBridge';
|
|
@@ -11,6 +11,18 @@ var _constants = require("./constants");
|
|
|
11
11
|
var _policy = require("./policy");
|
|
12
12
|
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; }
|
|
13
13
|
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; }
|
|
14
|
+
/**
|
|
15
|
+
* Outcome of serialising a payload for the wire. A bare `| null` would collapse five distinct
|
|
16
|
+
* refusals into one, notably a non-allowlisted context key versus a merge-style publisher, which
|
|
17
|
+
* need different fixes. `reason` is reported as analytics (see `./analytics`).
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
var dropped = function dropped(reason) {
|
|
21
|
+
return {
|
|
22
|
+
ok: false,
|
|
23
|
+
reason: reason
|
|
24
|
+
};
|
|
25
|
+
};
|
|
14
26
|
var base = function base(payload) {
|
|
15
27
|
return {
|
|
16
28
|
type: payload.type,
|
|
@@ -50,33 +62,36 @@ var SHIMS = {
|
|
|
50
62
|
serialize: function serialize(payload) {
|
|
51
63
|
var data = payload.data;
|
|
52
64
|
if (!data || typeof data.setContext !== 'function') {
|
|
53
|
-
return
|
|
65
|
+
return dropped('missing-set-context');
|
|
54
66
|
}
|
|
55
67
|
// Checked before `setContext` runs, so a non-allowlisted key never invokes product code.
|
|
56
68
|
if (!(0, _policy.isRelayableContextKey)(data.contextKey)) {
|
|
57
|
-
return
|
|
69
|
+
return dropped('context-key-not-allowlisted');
|
|
58
70
|
}
|
|
59
71
|
var value;
|
|
60
72
|
try {
|
|
61
73
|
value = data.setContext((0, _defineProperty2.default)({}, MERGE_PROBE_KEY, true));
|
|
62
74
|
} catch (_unused) {
|
|
63
|
-
return
|
|
75
|
+
return dropped('set-context-threw');
|
|
64
76
|
}
|
|
65
77
|
// Sentinel survived: merge-style, cannot be resolved to a standalone value.
|
|
66
78
|
if ((0, _typeof2.default)(value) === 'object' && value !== null && MERGE_PROBE_KEY in value) {
|
|
67
|
-
return
|
|
79
|
+
return dropped('merge-style-publisher');
|
|
68
80
|
}
|
|
69
81
|
try {
|
|
70
82
|
JSON.stringify(value);
|
|
71
83
|
} catch (_unused2) {
|
|
72
|
-
return
|
|
84
|
+
return dropped('not-serializable');
|
|
73
85
|
}
|
|
74
|
-
return
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
86
|
+
return {
|
|
87
|
+
ok: true,
|
|
88
|
+
payload: _objectSpread(_objectSpread({}, base(payload)), {}, {
|
|
89
|
+
data: {
|
|
90
|
+
contextKey: data.contextKey,
|
|
91
|
+
value: value
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
};
|
|
80
95
|
},
|
|
81
96
|
deserialize: function deserialize(serialized) {
|
|
82
97
|
var _serialized$source;
|
|
@@ -115,11 +130,14 @@ var serializePayload = exports.serializePayload = function serializePayload(payl
|
|
|
115
130
|
try {
|
|
116
131
|
data = rawData === undefined ? undefined : JSON.parse(JSON.stringify(rawData));
|
|
117
132
|
} catch (_unused3) {
|
|
118
|
-
return
|
|
133
|
+
return dropped('not-serializable');
|
|
119
134
|
}
|
|
120
|
-
return
|
|
121
|
-
|
|
122
|
-
|
|
135
|
+
return {
|
|
136
|
+
ok: true,
|
|
137
|
+
payload: _objectSpread(_objectSpread({}, base(payload)), {}, {
|
|
138
|
+
data: data
|
|
139
|
+
})
|
|
140
|
+
};
|
|
123
141
|
};
|
|
124
142
|
|
|
125
143
|
/** Reconstruct a payload from the wire, stamping the loop-guard sentinel. */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useEffect, useMemo, useRef } from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { UNSAFE_expValNoExposure } from '@atlaskit/platform-feature-experiments/unsafe-exp-val-no-exposure';
|
|
3
3
|
import { usePublish, useSubscribeAll } from '../../../main';
|
|
4
4
|
import { BRIDGE_CONTROL_REQUEST_CONTEXT, BRIDGE_MESSAGE_MARKER, BRIDGE_SOURCE, BRIDGE_TO_EXTENSION, BRIDGE_TO_PRODUCT } from './constants';
|
|
5
5
|
import { shouldRelayToProduct } from './policy';
|
|
@@ -52,8 +52,9 @@ export const ExtensionContextBridgeClient = ({
|
|
|
52
52
|
transport,
|
|
53
53
|
allowedOrigins
|
|
54
54
|
}) => {
|
|
55
|
-
// Self-gated. Matches `
|
|
56
|
-
|
|
55
|
+
// Self-gated. Matches `EXT_CONTEXT_BRIDGE_EXPERIMENT` in `./constants`. No-exposure read: the
|
|
56
|
+
// extension decides whether to mount this and owns the exposure call.
|
|
57
|
+
const active = UNSAFE_expValNoExposure('rovo-ext_context_bridge_exp', 'isEnabled', false) === true;
|
|
57
58
|
const publish = usePublish('ai-mate');
|
|
58
59
|
const lastKnownSelectionRef = useRef(null);
|
|
59
60
|
const activeTransport = useMemo(() => {
|
|
@@ -159,15 +160,15 @@ export const ExtensionContextBridgeClient = ({
|
|
|
159
160
|
if (!shouldRelayToProduct(payload.type)) {
|
|
160
161
|
return;
|
|
161
162
|
}
|
|
162
|
-
const
|
|
163
|
-
if (!
|
|
163
|
+
const result = serializePayload(payload);
|
|
164
|
+
if (!result.ok) {
|
|
164
165
|
return;
|
|
165
166
|
}
|
|
166
167
|
try {
|
|
167
168
|
activeTransport.send({
|
|
168
169
|
[BRIDGE_MESSAGE_MARKER]: true,
|
|
169
170
|
direction: BRIDGE_TO_PRODUCT,
|
|
170
|
-
payload:
|
|
171
|
+
payload: result.payload
|
|
171
172
|
});
|
|
172
173
|
} catch {
|
|
173
174
|
// Never let bridging break the chat iframe.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useEffect, useMemo, useRef } from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { UNSAFE_expValNoExposure } from '@atlaskit/platform-feature-experiments/unsafe-exp-val-no-exposure';
|
|
3
3
|
import { useSubscribeAll } from '../../../main';
|
|
4
|
+
import { getContextKey, reportRelayDecision } from './analytics';
|
|
4
5
|
import { BRIDGE_CONTROL_REQUEST_CONTEXT, BRIDGE_MESSAGE_MARKER, BRIDGE_SOURCE, BRIDGE_TO_EXTENSION, BRIDGE_TO_PRODUCT } from './constants';
|
|
5
6
|
import { getInboundDecision } from './policy';
|
|
6
7
|
import { serializePayload } from './registry';
|
|
@@ -31,14 +32,8 @@ const stampProductContentId = (serialized, targetContentId) => {
|
|
|
31
32
|
* else is keyed by payload type, of which there is one live instance at a time.
|
|
32
33
|
*/
|
|
33
34
|
const getContextSlotKey = payload => {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const contextKey = typeof data === 'object' && data !== null && 'contextKey' in data ? data.contextKey : undefined;
|
|
37
|
-
if (typeof contextKey === 'string') {
|
|
38
|
-
return `${payload.type}:${contextKey}`;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
return payload.type;
|
|
35
|
+
const contextKey = getContextKey(payload);
|
|
36
|
+
return contextKey === undefined ? payload.type : `${payload.type}:${contextKey}`;
|
|
42
37
|
};
|
|
43
38
|
/**
|
|
44
39
|
* Product-window half of the bridge. Observes the `ai-mate` bus and mirrors allowlisted context out
|
|
@@ -50,11 +45,16 @@ const getContextSlotKey = payload => {
|
|
|
50
45
|
export const ExtensionContextBridgeHost = ({
|
|
51
46
|
transport,
|
|
52
47
|
getContentId,
|
|
53
|
-
isAiEnabled
|
|
48
|
+
isAiEnabled,
|
|
49
|
+
onRelayDecision
|
|
54
50
|
}) => {
|
|
55
51
|
// Self-gated (not taken from the consumer) so this cannot mount by mistake wherever
|
|
56
|
-
// `ChatOpenerSubscriber` renders. Matches `
|
|
57
|
-
|
|
52
|
+
// `ChatOpenerSubscriber` renders. Matches `EXT_CONTEXT_BRIDGE_EXPERIMENT` in `./constants`.
|
|
53
|
+
//
|
|
54
|
+
// No-exposure read on purpose: this component mounts on every page of an opted-in product, so
|
|
55
|
+
// firing exposure here would measure product page loads instead of extension users. The
|
|
56
|
+
// extension logs the one exposure. See `./constants`.
|
|
57
|
+
const active = UNSAFE_expValNoExposure('rovo-ext_context_bridge_exp', 'isEnabled', false) === true;
|
|
58
58
|
const activeTransport = useMemo(() => {
|
|
59
59
|
if (!active) {
|
|
60
60
|
return undefined;
|
|
@@ -79,6 +79,9 @@ export const ExtensionContextBridgeHost = ({
|
|
|
79
79
|
*/
|
|
80
80
|
const lastRelayedRef = useRef(new Map());
|
|
81
81
|
|
|
82
|
+
/** Reported decisions, so a per-keystroke payload yields one event rather than thousands. */
|
|
83
|
+
const reportedDecisionsRef = useRef(new Set());
|
|
84
|
+
|
|
82
85
|
// Answers `request-context` from the chat, which cannot observe context published before it
|
|
83
86
|
// mounted. Products such as Jira publish once per navigation, so without this a chat opened after
|
|
84
87
|
// the page settled would describe the wrong view — or none at all.
|
|
@@ -116,17 +119,32 @@ export const ExtensionContextBridgeHost = ({
|
|
|
116
119
|
if (isAiEnabled === false) {
|
|
117
120
|
return;
|
|
118
121
|
}
|
|
119
|
-
// Loop guard.
|
|
122
|
+
// Loop guard. Not reported: our own echo, not a decision about the product.
|
|
120
123
|
if (payload.interactionSource === BRIDGE_SOURCE) {
|
|
121
124
|
return;
|
|
122
125
|
}
|
|
123
|
-
|
|
126
|
+
const report = decision => {
|
|
127
|
+
reportRelayDecision(reportedDecisionsRef.current, onRelayDecision, {
|
|
128
|
+
payloadType: payload.type,
|
|
129
|
+
contextKey: getContextKey(payload),
|
|
130
|
+
relayed: decision === 'allow',
|
|
131
|
+
decision,
|
|
132
|
+
publisherSource: payload.source,
|
|
133
|
+
payloadProduct: payload.product
|
|
134
|
+
});
|
|
135
|
+
};
|
|
136
|
+
const inboundDecision = getInboundDecision(payload.type);
|
|
137
|
+
if (inboundDecision !== 'allow') {
|
|
138
|
+
report(inboundDecision);
|
|
124
139
|
return;
|
|
125
140
|
}
|
|
126
|
-
const
|
|
127
|
-
if (!
|
|
141
|
+
const result = serializePayload(payload);
|
|
142
|
+
if (!result.ok) {
|
|
143
|
+
report(result.reason);
|
|
128
144
|
return;
|
|
129
145
|
}
|
|
146
|
+
report('allow');
|
|
147
|
+
const serialized = result.payload;
|
|
130
148
|
stampProductContentId(serialized, getContentId === null || getContentId === void 0 ? void 0 : getContentId());
|
|
131
149
|
const message = {
|
|
132
150
|
[BRIDGE_MESSAGE_MARKER]: true,
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inbound relay decision reporting: what the product asked the bridge to relay, and what the policy
|
|
3
|
+
* in `./policy` / `./registry` decided. Every refusal is otherwise silent, so the allowlist can only
|
|
4
|
+
* be widened from data with this.
|
|
5
|
+
*
|
|
6
|
+
* Product → extension only, hence no direction field. The outbound allowlist has one entry and is
|
|
7
|
+
* owned by the chat, so products cannot drift into it.
|
|
8
|
+
*
|
|
9
|
+
* Consumer-supplied callback rather than an event fired here, for the same reason as the Host's
|
|
10
|
+
* `getContentId`: this package has no analytics dependency.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Why `./registry` refused to serialise a payload. Kept distinct because the fixes differ:
|
|
15
|
+
* `context-key-not-allowlisted` is a list we can widen, the rest are publisher-side shape problems.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** Every outcome the bridge can reach for a payload it was asked to relay. */
|
|
19
|
+
|
|
20
|
+
/** A slot identifier by contract, capped in case a publisher ever interpolates an id into one. */
|
|
21
|
+
const MAX_CONTEXT_KEY_LENGTH = 80;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* (payload types × decisions) is finite and far below this; the cap is for `contextKey`, which is
|
|
25
|
+
* not. A publisher generating keys would otherwise grow the dedupe set for the life of the page.
|
|
26
|
+
*/
|
|
27
|
+
const MAX_REPORTED_DECISIONS = 50;
|
|
28
|
+
/**
|
|
29
|
+
* The context slot a `set-message-context` payload addresses. Shape-checked rather than cast: runs
|
|
30
|
+
* against the raw bus payload, the serialized form, and types carrying neither. The Host also keys
|
|
31
|
+
* its replay map on this.
|
|
32
|
+
*/
|
|
33
|
+
export const getContextKey = payload => {
|
|
34
|
+
if (payload.type !== 'set-message-context') {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
const data = payload.data;
|
|
38
|
+
const contextKey = typeof data === 'object' && data !== null && 'contextKey' in data ? data.contextKey : undefined;
|
|
39
|
+
return typeof contextKey === 'string' ? contextKey : undefined;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** Identity of a decision for deduplication purposes. */
|
|
43
|
+
const getDecisionKey = ({
|
|
44
|
+
payloadType,
|
|
45
|
+
contextKey,
|
|
46
|
+
decision
|
|
47
|
+
}) => `${payloadType}|${contextKey !== null && contextKey !== void 0 ? contextKey : ''}|${decision}`;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Report a relay decision at most once per distinct outcome. Deduplicated because the bridge sees
|
|
51
|
+
* the whole product bus and `editor-context-payload` recurs on roughly every keystroke; which keys
|
|
52
|
+
* exist and how they were decided is the question, not how often each recurred.
|
|
53
|
+
*
|
|
54
|
+
* `seen` is caller-owned (a ref in the Host), so the window is one mount. Never throws.
|
|
55
|
+
*/
|
|
56
|
+
export const reportRelayDecision = (seen, onRelayDecision, decision) => {
|
|
57
|
+
var _decision$contextKey;
|
|
58
|
+
if (!onRelayDecision) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const truncatedContextKey = (_decision$contextKey = decision.contextKey) === null || _decision$contextKey === void 0 ? void 0 : _decision$contextKey.slice(0, MAX_CONTEXT_KEY_LENGTH);
|
|
62
|
+
const reportable = {
|
|
63
|
+
...decision,
|
|
64
|
+
contextKey: truncatedContextKey
|
|
65
|
+
};
|
|
66
|
+
const key = getDecisionKey(reportable);
|
|
67
|
+
if (seen.has(key) || seen.size >= MAX_REPORTED_DECISIONS) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
seen.add(key);
|
|
71
|
+
try {
|
|
72
|
+
onRelayDecision(reportable);
|
|
73
|
+
} catch {
|
|
74
|
+
// Reporting is observability, never a reason for the relay to fail.
|
|
75
|
+
}
|
|
76
|
+
};
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* `
|
|
4
|
-
* tests resolve the
|
|
2
|
+
* Statsig experiment for the extension context bridge; both Host and Client self-gate on its
|
|
3
|
+
* boolean `isEnabled` parameter. Call sites inline this value rather than importing it, since the
|
|
4
|
+
* experiment runtime takes a static literal; tests resolve the experiment through this constant so
|
|
5
|
+
* the two cannot drift apart.
|
|
6
|
+
*
|
|
7
|
+
* Read here **without** logging exposure. Host mounts at the root of every page of an opted-in
|
|
8
|
+
* product, so an exposure-firing read would count product page loads rather than extension users.
|
|
9
|
+
* The extension owns the single exposure call, at the point the user is on a page the bridge can
|
|
10
|
+
* act on.
|
|
5
11
|
*/
|
|
6
|
-
export const
|
|
12
|
+
export const EXT_CONTEXT_BRIDGE_EXPERIMENT = 'rovo-ext_context_bridge_exp';
|
|
7
13
|
|
|
8
14
|
/** Marker on every bridge `postMessage`, so listeners can ignore unrelated messages. */
|
|
9
15
|
export const BRIDGE_MESSAGE_MARKER = '__rovoExtContextBridge';
|
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import { BRIDGE_SOURCE } from './constants';
|
|
2
2
|
import { isRelayableContextKey } from './policy';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Outcome of serialising a payload for the wire. A bare `| null` would collapse five distinct
|
|
6
|
+
* refusals into one, notably a non-allowlisted context key versus a merge-style publisher, which
|
|
7
|
+
* need different fixes. `reason` is reported as analytics (see `./analytics`).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const dropped = reason => ({
|
|
11
|
+
ok: false,
|
|
12
|
+
reason
|
|
13
|
+
});
|
|
3
14
|
const base = payload => ({
|
|
4
15
|
type: payload.type,
|
|
5
16
|
source: payload.source,
|
|
@@ -36,11 +47,11 @@ const SHIMS = {
|
|
|
36
47
|
serialize: payload => {
|
|
37
48
|
const data = payload.data;
|
|
38
49
|
if (!data || typeof data.setContext !== 'function') {
|
|
39
|
-
return
|
|
50
|
+
return dropped('missing-set-context');
|
|
40
51
|
}
|
|
41
52
|
// Checked before `setContext` runs, so a non-allowlisted key never invokes product code.
|
|
42
53
|
if (!isRelayableContextKey(data.contextKey)) {
|
|
43
|
-
return
|
|
54
|
+
return dropped('context-key-not-allowlisted');
|
|
44
55
|
}
|
|
45
56
|
let value;
|
|
46
57
|
try {
|
|
@@ -48,22 +59,25 @@ const SHIMS = {
|
|
|
48
59
|
[MERGE_PROBE_KEY]: true
|
|
49
60
|
});
|
|
50
61
|
} catch {
|
|
51
|
-
return
|
|
62
|
+
return dropped('set-context-threw');
|
|
52
63
|
}
|
|
53
64
|
// Sentinel survived: merge-style, cannot be resolved to a standalone value.
|
|
54
65
|
if (typeof value === 'object' && value !== null && MERGE_PROBE_KEY in value) {
|
|
55
|
-
return
|
|
66
|
+
return dropped('merge-style-publisher');
|
|
56
67
|
}
|
|
57
68
|
try {
|
|
58
69
|
JSON.stringify(value);
|
|
59
70
|
} catch {
|
|
60
|
-
return
|
|
71
|
+
return dropped('not-serializable');
|
|
61
72
|
}
|
|
62
73
|
return {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
74
|
+
ok: true,
|
|
75
|
+
payload: {
|
|
76
|
+
...base(payload),
|
|
77
|
+
data: {
|
|
78
|
+
contextKey: data.contextKey,
|
|
79
|
+
value
|
|
80
|
+
}
|
|
67
81
|
}
|
|
68
82
|
};
|
|
69
83
|
},
|
|
@@ -100,11 +114,14 @@ export const serializePayload = payload => {
|
|
|
100
114
|
try {
|
|
101
115
|
data = rawData === undefined ? undefined : JSON.parse(JSON.stringify(rawData));
|
|
102
116
|
} catch {
|
|
103
|
-
return
|
|
117
|
+
return dropped('not-serializable');
|
|
104
118
|
}
|
|
105
119
|
return {
|
|
106
|
-
|
|
107
|
-
|
|
120
|
+
ok: true,
|
|
121
|
+
payload: {
|
|
122
|
+
...base(payload),
|
|
123
|
+
data
|
|
124
|
+
}
|
|
108
125
|
};
|
|
109
126
|
};
|
|
110
127
|
|
|
@@ -2,7 +2,7 @@ import _defineProperty from "@babel/runtime/helpers/defineProperty";
|
|
|
2
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
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
4
|
import { useEffect, useMemo, useRef } from 'react';
|
|
5
|
-
import {
|
|
5
|
+
import { UNSAFE_expValNoExposure } from '@atlaskit/platform-feature-experiments/unsafe-exp-val-no-exposure';
|
|
6
6
|
import { usePublish, useSubscribeAll } from '../../../main';
|
|
7
7
|
import { BRIDGE_CONTROL_REQUEST_CONTEXT, BRIDGE_MESSAGE_MARKER, BRIDGE_SOURCE, BRIDGE_TO_EXTENSION, BRIDGE_TO_PRODUCT } from './constants';
|
|
8
8
|
import { shouldRelayToProduct } from './policy';
|
|
@@ -54,8 +54,9 @@ var isChatFocused = function isChatFocused() {
|
|
|
54
54
|
export var ExtensionContextBridgeClient = function ExtensionContextBridgeClient(_ref) {
|
|
55
55
|
var transport = _ref.transport,
|
|
56
56
|
allowedOrigins = _ref.allowedOrigins;
|
|
57
|
-
// Self-gated. Matches `
|
|
58
|
-
|
|
57
|
+
// Self-gated. Matches `EXT_CONTEXT_BRIDGE_EXPERIMENT` in `./constants`. No-exposure read: the
|
|
58
|
+
// extension decides whether to mount this and owns the exposure call.
|
|
59
|
+
var active = UNSAFE_expValNoExposure('rovo-ext_context_bridge_exp', 'isEnabled', false) === true;
|
|
59
60
|
var publish = usePublish('ai-mate');
|
|
60
61
|
var lastKnownSelectionRef = useRef(null);
|
|
61
62
|
var activeTransport = useMemo(function () {
|
|
@@ -155,12 +156,12 @@ export var ExtensionContextBridgeClient = function ExtensionContextBridgeClient(
|
|
|
155
156
|
if (!shouldRelayToProduct(payload.type)) {
|
|
156
157
|
return;
|
|
157
158
|
}
|
|
158
|
-
var
|
|
159
|
-
if (!
|
|
159
|
+
var result = serializePayload(payload);
|
|
160
|
+
if (!result.ok) {
|
|
160
161
|
return;
|
|
161
162
|
}
|
|
162
163
|
try {
|
|
163
|
-
activeTransport.send(_defineProperty(_defineProperty(_defineProperty({}, BRIDGE_MESSAGE_MARKER, true), "direction", BRIDGE_TO_PRODUCT), "payload",
|
|
164
|
+
activeTransport.send(_defineProperty(_defineProperty(_defineProperty({}, BRIDGE_MESSAGE_MARKER, true), "direction", BRIDGE_TO_PRODUCT), "payload", result.payload));
|
|
164
165
|
} catch (_unused3) {
|
|
165
166
|
// Never let bridging break the chat iframe.
|
|
166
167
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import _defineProperty from "@babel/runtime/helpers/defineProperty";
|
|
2
|
-
import _typeof from "@babel/runtime/helpers/typeof";
|
|
3
2
|
import { useEffect, useMemo, useRef } from 'react';
|
|
4
|
-
import {
|
|
3
|
+
import { UNSAFE_expValNoExposure } from '@atlaskit/platform-feature-experiments/unsafe-exp-val-no-exposure';
|
|
5
4
|
import { useSubscribeAll } from '../../../main';
|
|
5
|
+
import { getContextKey, reportRelayDecision } from './analytics';
|
|
6
6
|
import { BRIDGE_CONTROL_REQUEST_CONTEXT, BRIDGE_MESSAGE_MARKER, BRIDGE_SOURCE, BRIDGE_TO_EXTENSION, BRIDGE_TO_PRODUCT } from './constants';
|
|
7
7
|
import { getInboundDecision } from './policy';
|
|
8
8
|
import { serializePayload } from './registry';
|
|
@@ -33,14 +33,8 @@ var stampProductContentId = function stampProductContentId(serialized, targetCon
|
|
|
33
33
|
* else is keyed by payload type, of which there is one live instance at a time.
|
|
34
34
|
*/
|
|
35
35
|
var getContextSlotKey = function getContextSlotKey(payload) {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
var contextKey = _typeof(data) === 'object' && data !== null && 'contextKey' in data ? data.contextKey : undefined;
|
|
39
|
-
if (typeof contextKey === 'string') {
|
|
40
|
-
return "".concat(payload.type, ":").concat(contextKey);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
return payload.type;
|
|
36
|
+
var contextKey = getContextKey(payload);
|
|
37
|
+
return contextKey === undefined ? payload.type : "".concat(payload.type, ":").concat(contextKey);
|
|
44
38
|
};
|
|
45
39
|
/**
|
|
46
40
|
* Product-window half of the bridge. Observes the `ai-mate` bus and mirrors allowlisted context out
|
|
@@ -52,10 +46,15 @@ var getContextSlotKey = function getContextSlotKey(payload) {
|
|
|
52
46
|
export var ExtensionContextBridgeHost = function ExtensionContextBridgeHost(_ref) {
|
|
53
47
|
var transport = _ref.transport,
|
|
54
48
|
getContentId = _ref.getContentId,
|
|
55
|
-
isAiEnabled = _ref.isAiEnabled
|
|
49
|
+
isAiEnabled = _ref.isAiEnabled,
|
|
50
|
+
onRelayDecision = _ref.onRelayDecision;
|
|
56
51
|
// Self-gated (not taken from the consumer) so this cannot mount by mistake wherever
|
|
57
|
-
// `ChatOpenerSubscriber` renders. Matches `
|
|
58
|
-
|
|
52
|
+
// `ChatOpenerSubscriber` renders. Matches `EXT_CONTEXT_BRIDGE_EXPERIMENT` in `./constants`.
|
|
53
|
+
//
|
|
54
|
+
// No-exposure read on purpose: this component mounts on every page of an opted-in product, so
|
|
55
|
+
// firing exposure here would measure product page loads instead of extension users. The
|
|
56
|
+
// extension logs the one exposure. See `./constants`.
|
|
57
|
+
var active = UNSAFE_expValNoExposure('rovo-ext_context_bridge_exp', 'isEnabled', false) === true;
|
|
59
58
|
var activeTransport = useMemo(function () {
|
|
60
59
|
if (!active) {
|
|
61
60
|
return undefined;
|
|
@@ -80,6 +79,9 @@ export var ExtensionContextBridgeHost = function ExtensionContextBridgeHost(_ref
|
|
|
80
79
|
*/
|
|
81
80
|
var lastRelayedRef = useRef(new Map());
|
|
82
81
|
|
|
82
|
+
/** Reported decisions, so a per-keystroke payload yields one event rather than thousands. */
|
|
83
|
+
var reportedDecisionsRef = useRef(new Set());
|
|
84
|
+
|
|
83
85
|
// Answers `request-context` from the chat, which cannot observe context published before it
|
|
84
86
|
// mounted. Products such as Jira publish once per navigation, so without this a chat opened after
|
|
85
87
|
// the page settled would describe the wrong view — or none at all.
|
|
@@ -117,17 +119,32 @@ export var ExtensionContextBridgeHost = function ExtensionContextBridgeHost(_ref
|
|
|
117
119
|
if (isAiEnabled === false) {
|
|
118
120
|
return;
|
|
119
121
|
}
|
|
120
|
-
// Loop guard.
|
|
122
|
+
// Loop guard. Not reported: our own echo, not a decision about the product.
|
|
121
123
|
if (payload.interactionSource === BRIDGE_SOURCE) {
|
|
122
124
|
return;
|
|
123
125
|
}
|
|
124
|
-
|
|
126
|
+
var report = function report(decision) {
|
|
127
|
+
reportRelayDecision(reportedDecisionsRef.current, onRelayDecision, {
|
|
128
|
+
payloadType: payload.type,
|
|
129
|
+
contextKey: getContextKey(payload),
|
|
130
|
+
relayed: decision === 'allow',
|
|
131
|
+
decision: decision,
|
|
132
|
+
publisherSource: payload.source,
|
|
133
|
+
payloadProduct: payload.product
|
|
134
|
+
});
|
|
135
|
+
};
|
|
136
|
+
var inboundDecision = getInboundDecision(payload.type);
|
|
137
|
+
if (inboundDecision !== 'allow') {
|
|
138
|
+
report(inboundDecision);
|
|
125
139
|
return;
|
|
126
140
|
}
|
|
127
|
-
var
|
|
128
|
-
if (!
|
|
141
|
+
var result = serializePayload(payload);
|
|
142
|
+
if (!result.ok) {
|
|
143
|
+
report(result.reason);
|
|
129
144
|
return;
|
|
130
145
|
}
|
|
146
|
+
report('allow');
|
|
147
|
+
var serialized = result.payload;
|
|
131
148
|
stampProductContentId(serialized, getContentId === null || getContentId === void 0 ? void 0 : getContentId());
|
|
132
149
|
var message = _defineProperty(_defineProperty(_defineProperty({}, BRIDGE_MESSAGE_MARKER, true), "direction", BRIDGE_TO_EXTENSION), "payload", serialized);
|
|
133
150
|
// Keyed by context slot, not by message, so a re-publish replaces rather than accumulates.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import _defineProperty from "@babel/runtime/helpers/defineProperty";
|
|
2
|
+
import _typeof from "@babel/runtime/helpers/typeof";
|
|
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
|
+
/**
|
|
6
|
+
* Inbound relay decision reporting: what the product asked the bridge to relay, and what the policy
|
|
7
|
+
* in `./policy` / `./registry` decided. Every refusal is otherwise silent, so the allowlist can only
|
|
8
|
+
* be widened from data with this.
|
|
9
|
+
*
|
|
10
|
+
* Product → extension only, hence no direction field. The outbound allowlist has one entry and is
|
|
11
|
+
* owned by the chat, so products cannot drift into it.
|
|
12
|
+
*
|
|
13
|
+
* Consumer-supplied callback rather than an event fired here, for the same reason as the Host's
|
|
14
|
+
* `getContentId`: this package has no analytics dependency.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Why `./registry` refused to serialise a payload. Kept distinct because the fixes differ:
|
|
19
|
+
* `context-key-not-allowlisted` is a list we can widen, the rest are publisher-side shape problems.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** Every outcome the bridge can reach for a payload it was asked to relay. */
|
|
23
|
+
|
|
24
|
+
/** A slot identifier by contract, capped in case a publisher ever interpolates an id into one. */
|
|
25
|
+
var MAX_CONTEXT_KEY_LENGTH = 80;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* (payload types × decisions) is finite and far below this; the cap is for `contextKey`, which is
|
|
29
|
+
* not. A publisher generating keys would otherwise grow the dedupe set for the life of the page.
|
|
30
|
+
*/
|
|
31
|
+
var MAX_REPORTED_DECISIONS = 50;
|
|
32
|
+
/**
|
|
33
|
+
* The context slot a `set-message-context` payload addresses. Shape-checked rather than cast: runs
|
|
34
|
+
* against the raw bus payload, the serialized form, and types carrying neither. The Host also keys
|
|
35
|
+
* its replay map on this.
|
|
36
|
+
*/
|
|
37
|
+
export var getContextKey = function getContextKey(payload) {
|
|
38
|
+
if (payload.type !== 'set-message-context') {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
var data = payload.data;
|
|
42
|
+
var contextKey = _typeof(data) === 'object' && data !== null && 'contextKey' in data ? data.contextKey : undefined;
|
|
43
|
+
return typeof contextKey === 'string' ? contextKey : undefined;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/** Identity of a decision for deduplication purposes. */
|
|
47
|
+
var getDecisionKey = function getDecisionKey(_ref) {
|
|
48
|
+
var payloadType = _ref.payloadType,
|
|
49
|
+
contextKey = _ref.contextKey,
|
|
50
|
+
decision = _ref.decision;
|
|
51
|
+
return "".concat(payloadType, "|").concat(contextKey !== null && contextKey !== void 0 ? contextKey : '', "|").concat(decision);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Report a relay decision at most once per distinct outcome. Deduplicated because the bridge sees
|
|
56
|
+
* the whole product bus and `editor-context-payload` recurs on roughly every keystroke; which keys
|
|
57
|
+
* exist and how they were decided is the question, not how often each recurred.
|
|
58
|
+
*
|
|
59
|
+
* `seen` is caller-owned (a ref in the Host), so the window is one mount. Never throws.
|
|
60
|
+
*/
|
|
61
|
+
export var reportRelayDecision = function reportRelayDecision(seen, onRelayDecision, decision) {
|
|
62
|
+
var _decision$contextKey;
|
|
63
|
+
if (!onRelayDecision) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
var truncatedContextKey = (_decision$contextKey = decision.contextKey) === null || _decision$contextKey === void 0 ? void 0 : _decision$contextKey.slice(0, MAX_CONTEXT_KEY_LENGTH);
|
|
67
|
+
var reportable = _objectSpread(_objectSpread({}, decision), {}, {
|
|
68
|
+
contextKey: truncatedContextKey
|
|
69
|
+
});
|
|
70
|
+
var key = getDecisionKey(reportable);
|
|
71
|
+
if (seen.has(key) || seen.size >= MAX_REPORTED_DECISIONS) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
seen.add(key);
|
|
75
|
+
try {
|
|
76
|
+
onRelayDecision(reportable);
|
|
77
|
+
} catch (_unused) {
|
|
78
|
+
// Reporting is observability, never a reason for the relay to fail.
|
|
79
|
+
}
|
|
80
|
+
};
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* `
|
|
4
|
-
* tests resolve the
|
|
2
|
+
* Statsig experiment for the extension context bridge; both Host and Client self-gate on its
|
|
3
|
+
* boolean `isEnabled` parameter. Call sites inline this value rather than importing it, since the
|
|
4
|
+
* experiment runtime takes a static literal; tests resolve the experiment through this constant so
|
|
5
|
+
* the two cannot drift apart.
|
|
6
|
+
*
|
|
7
|
+
* Read here **without** logging exposure. Host mounts at the root of every page of an opted-in
|
|
8
|
+
* product, so an exposure-firing read would count product page loads rather than extension users.
|
|
9
|
+
* The extension owns the single exposure call, at the point the user is on a page the bridge can
|
|
10
|
+
* act on.
|
|
5
11
|
*/
|
|
6
|
-
export var
|
|
12
|
+
export var EXT_CONTEXT_BRIDGE_EXPERIMENT = 'rovo-ext_context_bridge_exp';
|
|
7
13
|
|
|
8
14
|
/** Marker on every bridge `postMessage`, so listeners can ignore unrelated messages. */
|
|
9
15
|
export var BRIDGE_MESSAGE_MARKER = '__rovoExtContextBridge';
|
|
@@ -4,6 +4,19 @@ function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbol
|
|
|
4
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
5
|
import { BRIDGE_SOURCE } from './constants';
|
|
6
6
|
import { isRelayableContextKey } from './policy';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Outcome of serialising a payload for the wire. A bare `| null` would collapse five distinct
|
|
10
|
+
* refusals into one, notably a non-allowlisted context key versus a merge-style publisher, which
|
|
11
|
+
* need different fixes. `reason` is reported as analytics (see `./analytics`).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
var dropped = function dropped(reason) {
|
|
15
|
+
return {
|
|
16
|
+
ok: false,
|
|
17
|
+
reason: reason
|
|
18
|
+
};
|
|
19
|
+
};
|
|
7
20
|
var base = function base(payload) {
|
|
8
21
|
return {
|
|
9
22
|
type: payload.type,
|
|
@@ -43,33 +56,36 @@ var SHIMS = {
|
|
|
43
56
|
serialize: function serialize(payload) {
|
|
44
57
|
var data = payload.data;
|
|
45
58
|
if (!data || typeof data.setContext !== 'function') {
|
|
46
|
-
return
|
|
59
|
+
return dropped('missing-set-context');
|
|
47
60
|
}
|
|
48
61
|
// Checked before `setContext` runs, so a non-allowlisted key never invokes product code.
|
|
49
62
|
if (!isRelayableContextKey(data.contextKey)) {
|
|
50
|
-
return
|
|
63
|
+
return dropped('context-key-not-allowlisted');
|
|
51
64
|
}
|
|
52
65
|
var value;
|
|
53
66
|
try {
|
|
54
67
|
value = data.setContext(_defineProperty({}, MERGE_PROBE_KEY, true));
|
|
55
68
|
} catch (_unused) {
|
|
56
|
-
return
|
|
69
|
+
return dropped('set-context-threw');
|
|
57
70
|
}
|
|
58
71
|
// Sentinel survived: merge-style, cannot be resolved to a standalone value.
|
|
59
72
|
if (_typeof(value) === 'object' && value !== null && MERGE_PROBE_KEY in value) {
|
|
60
|
-
return
|
|
73
|
+
return dropped('merge-style-publisher');
|
|
61
74
|
}
|
|
62
75
|
try {
|
|
63
76
|
JSON.stringify(value);
|
|
64
77
|
} catch (_unused2) {
|
|
65
|
-
return
|
|
78
|
+
return dropped('not-serializable');
|
|
66
79
|
}
|
|
67
|
-
return
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
80
|
+
return {
|
|
81
|
+
ok: true,
|
|
82
|
+
payload: _objectSpread(_objectSpread({}, base(payload)), {}, {
|
|
83
|
+
data: {
|
|
84
|
+
contextKey: data.contextKey,
|
|
85
|
+
value: value
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
};
|
|
73
89
|
},
|
|
74
90
|
deserialize: function deserialize(serialized) {
|
|
75
91
|
var _serialized$source;
|
|
@@ -108,11 +124,14 @@ export var serializePayload = function serializePayload(payload) {
|
|
|
108
124
|
try {
|
|
109
125
|
data = rawData === undefined ? undefined : JSON.parse(JSON.stringify(rawData));
|
|
110
126
|
} catch (_unused3) {
|
|
111
|
-
return
|
|
127
|
+
return dropped('not-serializable');
|
|
112
128
|
}
|
|
113
|
-
return
|
|
114
|
-
|
|
115
|
-
|
|
129
|
+
return {
|
|
130
|
+
ok: true,
|
|
131
|
+
payload: _objectSpread(_objectSpread({}, base(payload)), {}, {
|
|
132
|
+
data: data
|
|
133
|
+
})
|
|
134
|
+
};
|
|
116
135
|
};
|
|
117
136
|
|
|
118
137
|
/** Reconstruct a payload from the wire, stamping the loop-guard sentinel. */
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type OnBridgeRelayDecision } from './analytics';
|
|
1
2
|
import { type Transport } from './transport';
|
|
2
3
|
type Props = {
|
|
3
4
|
/** Override the transport (tests). Defaults to same-window `postMessage`. */
|
|
@@ -14,6 +15,12 @@ type Props = {
|
|
|
14
15
|
* mount passes it explicitly, so that only covers bespoke embeddings.
|
|
15
16
|
*/
|
|
16
17
|
isAiEnabled?: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Called with what the relay policy decided for each payload this product published, at most once
|
|
20
|
+
* per distinct outcome per mount. Consumer-supplied because the analytics client lives outside
|
|
21
|
+
* this package. Omitted, nothing is reported. See `./analytics`.
|
|
22
|
+
*/
|
|
23
|
+
onRelayDecision?: OnBridgeRelayDecision;
|
|
17
24
|
};
|
|
18
25
|
/**
|
|
19
26
|
* Product-window half of the bridge. Observes the `ai-mate` bus and mirrors allowlisted context out
|
|
@@ -22,5 +29,5 @@ type Props = {
|
|
|
22
29
|
* The reverse direction is handled by the sibling `RovoPostMessagePubsubListener` in
|
|
23
30
|
* `ChatOpenerSubscriber`, not here. Headless; mounts at product root via `ChatOpenerSubscriber`.
|
|
24
31
|
*/
|
|
25
|
-
export declare const ExtensionContextBridgeHost: ({ transport, getContentId, isAiEnabled, }: Props) => null;
|
|
32
|
+
export declare const ExtensionContextBridgeHost: ({ transport, getContentId, isAiEnabled, onRelayDecision, }: Props) => null;
|
|
26
33
|
export {};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inbound relay decision reporting: what the product asked the bridge to relay, and what the policy
|
|
3
|
+
* in `./policy` / `./registry` decided. Every refusal is otherwise silent, so the allowlist can only
|
|
4
|
+
* be widened from data with this.
|
|
5
|
+
*
|
|
6
|
+
* Product → extension only, hence no direction field. The outbound allowlist has one entry and is
|
|
7
|
+
* owned by the chat, so products cannot drift into it.
|
|
8
|
+
*
|
|
9
|
+
* Consumer-supplied callback rather than an event fired here, for the same reason as the Host's
|
|
10
|
+
* `getContentId`: this package has no analytics dependency.
|
|
11
|
+
*/
|
|
12
|
+
import { type Payload } from '../../../types';
|
|
13
|
+
import { type InboundDecision } from './policy';
|
|
14
|
+
/**
|
|
15
|
+
* Why `./registry` refused to serialise a payload. Kept distinct because the fixes differ:
|
|
16
|
+
* `context-key-not-allowlisted` is a list we can widen, the rest are publisher-side shape problems.
|
|
17
|
+
*/
|
|
18
|
+
export type SerializationDropReason = 'missing-set-context' | 'context-key-not-allowlisted' | 'set-context-threw' | 'merge-style-publisher' | 'not-serializable';
|
|
19
|
+
/** Every outcome the bridge can reach for a payload it was asked to relay. */
|
|
20
|
+
export type RelayDecision = InboundDecision | SerializationDropReason;
|
|
21
|
+
export type BridgeRelayDecision = {
|
|
22
|
+
/** The bus payload type, e.g. `editor-context-payload`. */
|
|
23
|
+
payloadType: Payload['type'];
|
|
24
|
+
/** Context slot for `set-message-context`; undefined for every other type. */
|
|
25
|
+
contextKey?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Whether the policy admitted it. Reported before the `postMessage`, so a throwing transport
|
|
28
|
+
* reads as a broken channel elsewhere rather than a policy refusal here.
|
|
29
|
+
*/
|
|
30
|
+
relayed: boolean;
|
|
31
|
+
/** Why, in the policy's own vocabulary. */
|
|
32
|
+
decision: RelayDecision;
|
|
33
|
+
/** The surface that published it, i.e. the payload's own `source`. */
|
|
34
|
+
publisherSource?: string;
|
|
35
|
+
/** The product the publisher stamped on the payload. */
|
|
36
|
+
payloadProduct?: string;
|
|
37
|
+
};
|
|
38
|
+
export type OnBridgeRelayDecision = (decision: BridgeRelayDecision) => void;
|
|
39
|
+
/**
|
|
40
|
+
* The context slot a `set-message-context` payload addresses. Shape-checked rather than cast: runs
|
|
41
|
+
* against the raw bus payload, the serialized form, and types carrying neither. The Host also keys
|
|
42
|
+
* its replay map on this.
|
|
43
|
+
*/
|
|
44
|
+
export declare const getContextKey: (payload: {
|
|
45
|
+
type: string;
|
|
46
|
+
data?: unknown;
|
|
47
|
+
}) => string | undefined;
|
|
48
|
+
/**
|
|
49
|
+
* Report a relay decision at most once per distinct outcome. Deduplicated because the bridge sees
|
|
50
|
+
* the whole product bus and `editor-context-payload` recurs on roughly every keystroke; which keys
|
|
51
|
+
* exist and how they were decided is the question, not how often each recurred.
|
|
52
|
+
*
|
|
53
|
+
* `seen` is caller-owned (a ref in the Host), so the window is one mount. Never throws.
|
|
54
|
+
*/
|
|
55
|
+
export declare const reportRelayDecision: (seen: Set<string>, onRelayDecision: OnBridgeRelayDecision | undefined, decision: BridgeRelayDecision) => void;
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* `
|
|
4
|
-
* tests resolve the
|
|
2
|
+
* Statsig experiment for the extension context bridge; both Host and Client self-gate on its
|
|
3
|
+
* boolean `isEnabled` parameter. Call sites inline this value rather than importing it, since the
|
|
4
|
+
* experiment runtime takes a static literal; tests resolve the experiment through this constant so
|
|
5
|
+
* the two cannot drift apart.
|
|
6
|
+
*
|
|
7
|
+
* Read here **without** logging exposure. Host mounts at the root of every page of an opted-in
|
|
8
|
+
* product, so an exposure-firing read would count product page loads rather than extension users.
|
|
9
|
+
* The extension owns the single exposure call, at the point the user is on a page the bridge can
|
|
10
|
+
* act on.
|
|
5
11
|
*/
|
|
6
|
-
export declare const
|
|
12
|
+
export declare const EXT_CONTEXT_BRIDGE_EXPERIMENT = "rovo-ext_context_bridge_exp";
|
|
7
13
|
/** Marker on every bridge `postMessage`, so listeners can ignore unrelated messages. */
|
|
8
14
|
export declare const BRIDGE_MESSAGE_MARKER: '__rovoExtContextBridge';
|
|
9
15
|
/** Read context flows product -> extension; actions flow extension -> product. */
|
|
@@ -2,9 +2,8 @@ import { type Payload } from '../../../types';
|
|
|
2
2
|
/** True if a `set-message-context` payload carrying this `contextKey` may cross to the extension. */
|
|
3
3
|
export declare const isRelayableContextKey: (contextKey: string) => boolean;
|
|
4
4
|
/** Why a product-bus payload was or was not relayed. */
|
|
5
|
-
type InboundDecision = 'allow' | 'not-allowlisted' | 'denylisted';
|
|
5
|
+
export type InboundDecision = 'allow' | 'not-allowlisted' | 'denylisted';
|
|
6
6
|
/** Classify a product-bus payload. Relay iff `allow`. */
|
|
7
7
|
export declare const getInboundDecision: (type: Payload['type']) => InboundDecision;
|
|
8
8
|
/** True if a chat-bus payload of this type may be relayed back to the product. */
|
|
9
9
|
export declare const shouldRelayToProduct: (type: Payload['type']) => boolean;
|
|
10
|
-
export {};
|
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
import { type Payload } from '../../../types';
|
|
2
|
+
import { type SerializationDropReason } from './analytics';
|
|
2
3
|
import { type SerializedPayload } from './transport';
|
|
4
|
+
/**
|
|
5
|
+
* Outcome of serialising a payload for the wire. A bare `| null` would collapse five distinct
|
|
6
|
+
* refusals into one, notably a non-allowlisted context key versus a merge-style publisher, which
|
|
7
|
+
* need different fixes. `reason` is reported as analytics (see `./analytics`).
|
|
8
|
+
*/
|
|
9
|
+
export type SerializationResult = {
|
|
10
|
+
ok: true;
|
|
11
|
+
payload: SerializedPayload;
|
|
12
|
+
} | {
|
|
13
|
+
ok: false;
|
|
14
|
+
reason: SerializationDropReason;
|
|
15
|
+
};
|
|
3
16
|
/** Serialise a payload for the wire, using a shim if one exists, else a generic JSON round-trip. */
|
|
4
|
-
export declare const serializePayload: (payload: Payload) =>
|
|
17
|
+
export declare const serializePayload: (payload: Payload) => SerializationResult;
|
|
5
18
|
/** Reconstruct a payload from the wire, stamping the loop-guard sentinel. */
|
|
6
19
|
export declare const deserializePayload: (serialized: SerializedPayload) => Payload | null;
|
package/dist/types/types.d.ts
CHANGED
|
@@ -100,7 +100,7 @@ export type ChatCreationContextParams = {
|
|
|
100
100
|
source?: string;
|
|
101
101
|
shouldUseExistingContent?: boolean;
|
|
102
102
|
isViewMode?: boolean;
|
|
103
|
-
experience?: 'cwr' | 'cwr_type' | 'cwr_edit' | 'cwr_existing' | 'inline_edit' | 'remix' | 'remix_edit' | 'remix_object' | 'remix_custom' | 'chat_edit' | 'chat_view' | 'keep_existing_page_structure';
|
|
103
|
+
experience?: 'cwr' | 'cwr_type' | 'cwr_edit' | 'cwr_existing' | 'inline_edit' | 'remix' | 'remix_edit' | 'remix_object' | 'remix_custom' | 'remix_custom_edit' | 'chat_edit' | 'chat_view' | 'keep_existing_page_structure';
|
|
104
104
|
contentTypes?: string[];
|
|
105
105
|
blocks?: string[];
|
|
106
106
|
spaceKey?: string;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@atlaskit/rovo-triggers/extension-context-bridge/analytics",
|
|
3
|
+
"main": "../../dist/cjs/common/utils/extension-context-bridge/analytics.js",
|
|
4
|
+
"module": "../../dist/esm/common/utils/extension-context-bridge/analytics.js",
|
|
5
|
+
"module:es2019": "../../dist/es2019/common/utils/extension-context-bridge/analytics.js",
|
|
6
|
+
"sideEffects": [
|
|
7
|
+
"*.compiled.css"
|
|
8
|
+
],
|
|
9
|
+
"types": "../../dist/types/common/utils/extension-context-bridge/analytics.d.ts"
|
|
10
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atlaskit/rovo-triggers",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "11.0.0",
|
|
4
4
|
"description": "Provides various trigger events to drive Rovo Chat functionality, such as a publish-subscribe and URL parameter hooks",
|
|
5
5
|
"author": "Atlassian Pty Ltd",
|
|
6
6
|
"publishConfig": {
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"atlaskit:src": "src/index.ts",
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@atlaskit/adf-schema": "^57.4.0",
|
|
32
|
+
"@atlaskit/platform-feature-experiments": "^0.3.0",
|
|
32
33
|
"@atlaskit/platform-feature-flags": "^2.2.0",
|
|
33
34
|
"@atlaskit/react-compiler-gating": "^0.2.0",
|
|
34
35
|
"@babel/runtime": "^7.0.0",
|
|
@@ -44,7 +45,8 @@
|
|
|
44
45
|
"@atlaskit/primitives": "^22.5.0",
|
|
45
46
|
"@atlaskit/ssr": "workspace:^",
|
|
46
47
|
"@atlassian/a11y-jest-testing": "^0.17.0",
|
|
47
|
-
"@atlassian/conversation-assistant-service-api": "^
|
|
48
|
+
"@atlassian/conversation-assistant-service-api": "^18.0.0",
|
|
49
|
+
"@atlassian/experiment-test-utils": "^0.2.0",
|
|
48
50
|
"@atlassian/feature-flags-test-utils": "^1.3.0",
|
|
49
51
|
"@atlassian/testing-library": "^0.11.0",
|
|
50
52
|
"@atlassian/workbench": "workspace:*",
|
|
@@ -54,9 +56,6 @@
|
|
|
54
56
|
"wait-for-expect": "^1.2.0"
|
|
55
57
|
},
|
|
56
58
|
"platform-feature-flags": {
|
|
57
|
-
"rovo-ext_context_bridge": {
|
|
58
|
-
"type": "boolean"
|
|
59
|
-
},
|
|
60
59
|
"rovo_chat_replace_url_param_history": {
|
|
61
60
|
"type": "boolean"
|
|
62
61
|
}
|