@atlaskit/rovo-triggers 10.36.0 → 10.38.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 +28 -0
- package/dist/cjs/chat-entry-point.js +2 -0
- package/dist/cjs/common/utils/extension-context-bridge/ExtensionContextBridgeClient.js +3 -3
- package/dist/cjs/common/utils/extension-context-bridge/ExtensionContextBridgeHost.js +27 -14
- package/dist/cjs/common/utils/extension-context-bridge/analytics.js +87 -0
- package/dist/cjs/common/utils/extension-context-bridge/registry.js +33 -15
- package/dist/es2019/chat-entry-point.js +2 -0
- package/dist/es2019/common/utils/extension-context-bridge/ExtensionContextBridgeClient.js +3 -3
- package/dist/es2019/common/utils/extension-context-bridge/ExtensionContextBridgeHost.js +27 -13
- package/dist/es2019/common/utils/extension-context-bridge/analytics.js +76 -0
- package/dist/es2019/common/utils/extension-context-bridge/registry.js +29 -12
- package/dist/esm/chat-entry-point.js +2 -0
- package/dist/esm/common/utils/extension-context-bridge/ExtensionContextBridgeClient.js +3 -3
- package/dist/esm/common/utils/extension-context-bridge/ExtensionContextBridgeHost.js +27 -14
- package/dist/esm/common/utils/extension-context-bridge/analytics.js +80 -0
- package/dist/esm/common/utils/extension-context-bridge/registry.js +34 -15
- package/dist/types/chat-entry-point.d.ts +2 -0
- 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/policy.d.ts +1 -2
- package/dist/types/common/utils/extension-context-bridge/registry.d.ts +14 -1
- package/extension-context-bridge/analytics/package.json +10 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# @atlaskit/rovo-triggers
|
|
2
2
|
|
|
3
|
+
## 10.38.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [`2124250692aba`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/2124250692aba) -
|
|
8
|
+
ASIMO-5048: report what the extension context bridge's inbound relay policy decided for each
|
|
9
|
+
payload a product publishes, so a refusal is observable instead of silent.
|
|
10
|
+
|
|
11
|
+
`ExtensionContextBridgeHost` takes a new optional `onRelayDecision` callback, with its contract on
|
|
12
|
+
a new `./extension-context-bridge/analytics` entrypoint, and the serializer now returns a
|
|
13
|
+
discriminated result so a refusal names its reason. `ChatOpenerSubscriber` wires the callback to
|
|
14
|
+
the analytics client it already holds, emitting a `rovoExtensionBridgePayload evaluated`
|
|
15
|
+
operational event into the host product's stream.
|
|
16
|
+
|
|
17
|
+
Product to extension direction only. Deduplicated to one report per distinct outcome per mount,
|
|
18
|
+
and only active while `rovo-ext_context_bridge` is on.
|
|
19
|
+
|
|
20
|
+
### Patch Changes
|
|
21
|
+
|
|
22
|
+
- Updated dependencies
|
|
23
|
+
|
|
24
|
+
## 10.37.0
|
|
25
|
+
|
|
26
|
+
### Minor Changes
|
|
27
|
+
|
|
28
|
+
- [`8181d8d675df9`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/8181d8d675df9) -
|
|
29
|
+
Add chat entry points for the JSM RFS activation Test now flow.
|
|
30
|
+
|
|
3
31
|
## 10.36.0
|
|
4
32
|
|
|
5
33
|
### Minor Changes
|
|
@@ -5,6 +5,8 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
5
5
|
});
|
|
6
6
|
exports.ChatEntryPoint = void 0;
|
|
7
7
|
var ChatEntryPoint = exports.ChatEntryPoint = /*#__PURE__*/function (ChatEntryPoint) {
|
|
8
|
+
ChatEntryPoint["JSM_ROVO_SETTINGS_TEST_NOW"] = "jsmRovoSettingsTestNow";
|
|
9
|
+
ChatEntryPoint["JSM_PORTAL_SETTINGS_TEST_NOW"] = "jsmPortalSettingsTestNow";
|
|
8
10
|
ChatEntryPoint["SEARCH_PAGE_AUTO_OPEN_CHAT"] = "searchPageAutoOpenChat";
|
|
9
11
|
ChatEntryPoint["SEARCH_PAGE_SUMMARIZE_IN_CHAT"] = "searchPageSummarizeInChat";
|
|
10
12
|
return ChatEntryPoint;
|
|
@@ -161,12 +161,12 @@ var ExtensionContextBridgeClient = exports.ExtensionContextBridgeClient = functi
|
|
|
161
161
|
if (!(0, _policy.shouldRelayToProduct)(payload.type)) {
|
|
162
162
|
return;
|
|
163
163
|
}
|
|
164
|
-
var
|
|
165
|
-
if (!
|
|
164
|
+
var result = (0, _registry.serializePayload)(payload);
|
|
165
|
+
if (!result.ok) {
|
|
166
166
|
return;
|
|
167
167
|
}
|
|
168
168
|
try {
|
|
169
|
-
activeTransport.send((0, _defineProperty2.default)((0, _defineProperty2.default)((0, _defineProperty2.default)({}, _constants.BRIDGE_MESSAGE_MARKER, true), "direction", _constants.BRIDGE_TO_PRODUCT), "payload",
|
|
169
|
+
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
170
|
} catch (_unused3) {
|
|
171
171
|
// Never let bridging break the chat iframe.
|
|
172
172
|
}
|
|
@@ -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
10
|
var _fg = require("@atlaskit/platform-feature-flags/fg");
|
|
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,7 +53,8 @@ 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
59
|
// `ChatOpenerSubscriber` renders. Matches `BRIDGE_FEATURE_GATE` in `./constants`.
|
|
65
60
|
var active = (0, _fg.fg)('rovo-ext_context_bridge');
|
|
@@ -87,6 +82,9 @@ var ExtensionContextBridgeHost = exports.ExtensionContextBridgeHost = function E
|
|
|
87
82
|
*/
|
|
88
83
|
var lastRelayedRef = (0, _react.useRef)(new Map());
|
|
89
84
|
|
|
85
|
+
/** Reported decisions, so a per-keystroke payload yields one event rather than thousands. */
|
|
86
|
+
var reportedDecisionsRef = (0, _react.useRef)(new Set());
|
|
87
|
+
|
|
90
88
|
// Answers `request-context` from the chat, which cannot observe context published before it
|
|
91
89
|
// mounted. Products such as Jira publish once per navigation, so without this a chat opened after
|
|
92
90
|
// the page settled would describe the wrong view — or none at all.
|
|
@@ -124,17 +122,32 @@ var ExtensionContextBridgeHost = exports.ExtensionContextBridgeHost = function E
|
|
|
124
122
|
if (isAiEnabled === false) {
|
|
125
123
|
return;
|
|
126
124
|
}
|
|
127
|
-
// Loop guard.
|
|
125
|
+
// Loop guard. Not reported: our own echo, not a decision about the product.
|
|
128
126
|
if (payload.interactionSource === _constants.BRIDGE_SOURCE) {
|
|
129
127
|
return;
|
|
130
128
|
}
|
|
131
|
-
|
|
129
|
+
var report = function report(decision) {
|
|
130
|
+
(0, _analytics.reportRelayDecision)(reportedDecisionsRef.current, onRelayDecision, {
|
|
131
|
+
payloadType: payload.type,
|
|
132
|
+
contextKey: (0, _analytics.getContextKey)(payload),
|
|
133
|
+
relayed: decision === 'allow',
|
|
134
|
+
decision: decision,
|
|
135
|
+
publisherSource: payload.source,
|
|
136
|
+
payloadProduct: payload.product
|
|
137
|
+
});
|
|
138
|
+
};
|
|
139
|
+
var inboundDecision = (0, _policy.getInboundDecision)(payload.type);
|
|
140
|
+
if (inboundDecision !== 'allow') {
|
|
141
|
+
report(inboundDecision);
|
|
132
142
|
return;
|
|
133
143
|
}
|
|
134
|
-
var
|
|
135
|
-
if (!
|
|
144
|
+
var result = (0, _registry.serializePayload)(payload);
|
|
145
|
+
if (!result.ok) {
|
|
146
|
+
report(result.reason);
|
|
136
147
|
return;
|
|
137
148
|
}
|
|
149
|
+
report('allow');
|
|
150
|
+
var serialized = result.payload;
|
|
138
151
|
stampProductContentId(serialized, getContentId === null || getContentId === void 0 ? void 0 : getContentId());
|
|
139
152
|
var message = (0, _defineProperty2.default)((0, _defineProperty2.default)((0, _defineProperty2.default)({}, _constants.BRIDGE_MESSAGE_MARKER, true), "direction", _constants.BRIDGE_TO_EXTENSION), "payload", serialized);
|
|
140
153
|
// 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
|
+
};
|
|
@@ -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,4 +1,6 @@
|
|
|
1
1
|
export let ChatEntryPoint = /*#__PURE__*/function (ChatEntryPoint) {
|
|
2
|
+
ChatEntryPoint["JSM_ROVO_SETTINGS_TEST_NOW"] = "jsmRovoSettingsTestNow";
|
|
3
|
+
ChatEntryPoint["JSM_PORTAL_SETTINGS_TEST_NOW"] = "jsmPortalSettingsTestNow";
|
|
2
4
|
ChatEntryPoint["SEARCH_PAGE_AUTO_OPEN_CHAT"] = "searchPageAutoOpenChat";
|
|
3
5
|
ChatEntryPoint["SEARCH_PAGE_SUMMARIZE_IN_CHAT"] = "searchPageSummarizeInChat";
|
|
4
6
|
return ChatEntryPoint;
|
|
@@ -159,15 +159,15 @@ export const ExtensionContextBridgeClient = ({
|
|
|
159
159
|
if (!shouldRelayToProduct(payload.type)) {
|
|
160
160
|
return;
|
|
161
161
|
}
|
|
162
|
-
const
|
|
163
|
-
if (!
|
|
162
|
+
const result = serializePayload(payload);
|
|
163
|
+
if (!result.ok) {
|
|
164
164
|
return;
|
|
165
165
|
}
|
|
166
166
|
try {
|
|
167
167
|
activeTransport.send({
|
|
168
168
|
[BRIDGE_MESSAGE_MARKER]: true,
|
|
169
169
|
direction: BRIDGE_TO_PRODUCT,
|
|
170
|
-
payload:
|
|
170
|
+
payload: result.payload
|
|
171
171
|
});
|
|
172
172
|
} catch {
|
|
173
173
|
// Never let bridging break the chat iframe.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useEffect, useMemo, useRef } from 'react';
|
|
2
2
|
import { fg } from '@atlaskit/platform-feature-flags/fg';
|
|
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,7 +45,8 @@ 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
52
|
// `ChatOpenerSubscriber` renders. Matches `BRIDGE_FEATURE_GATE` in `./constants`.
|
|
@@ -79,6 +75,9 @@ export const ExtensionContextBridgeHost = ({
|
|
|
79
75
|
*/
|
|
80
76
|
const lastRelayedRef = useRef(new Map());
|
|
81
77
|
|
|
78
|
+
/** Reported decisions, so a per-keystroke payload yields one event rather than thousands. */
|
|
79
|
+
const reportedDecisionsRef = useRef(new Set());
|
|
80
|
+
|
|
82
81
|
// Answers `request-context` from the chat, which cannot observe context published before it
|
|
83
82
|
// mounted. Products such as Jira publish once per navigation, so without this a chat opened after
|
|
84
83
|
// the page settled would describe the wrong view — or none at all.
|
|
@@ -116,17 +115,32 @@ export const ExtensionContextBridgeHost = ({
|
|
|
116
115
|
if (isAiEnabled === false) {
|
|
117
116
|
return;
|
|
118
117
|
}
|
|
119
|
-
// Loop guard.
|
|
118
|
+
// Loop guard. Not reported: our own echo, not a decision about the product.
|
|
120
119
|
if (payload.interactionSource === BRIDGE_SOURCE) {
|
|
121
120
|
return;
|
|
122
121
|
}
|
|
123
|
-
|
|
122
|
+
const report = decision => {
|
|
123
|
+
reportRelayDecision(reportedDecisionsRef.current, onRelayDecision, {
|
|
124
|
+
payloadType: payload.type,
|
|
125
|
+
contextKey: getContextKey(payload),
|
|
126
|
+
relayed: decision === 'allow',
|
|
127
|
+
decision,
|
|
128
|
+
publisherSource: payload.source,
|
|
129
|
+
payloadProduct: payload.product
|
|
130
|
+
});
|
|
131
|
+
};
|
|
132
|
+
const inboundDecision = getInboundDecision(payload.type);
|
|
133
|
+
if (inboundDecision !== 'allow') {
|
|
134
|
+
report(inboundDecision);
|
|
124
135
|
return;
|
|
125
136
|
}
|
|
126
|
-
const
|
|
127
|
-
if (!
|
|
137
|
+
const result = serializePayload(payload);
|
|
138
|
+
if (!result.ok) {
|
|
139
|
+
report(result.reason);
|
|
128
140
|
return;
|
|
129
141
|
}
|
|
142
|
+
report('allow');
|
|
143
|
+
const serialized = result.payload;
|
|
130
144
|
stampProductContentId(serialized, getContentId === null || getContentId === void 0 ? void 0 : getContentId());
|
|
131
145
|
const message = {
|
|
132
146
|
[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,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
|
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export var ChatEntryPoint = /*#__PURE__*/function (ChatEntryPoint) {
|
|
2
|
+
ChatEntryPoint["JSM_ROVO_SETTINGS_TEST_NOW"] = "jsmRovoSettingsTestNow";
|
|
3
|
+
ChatEntryPoint["JSM_PORTAL_SETTINGS_TEST_NOW"] = "jsmPortalSettingsTestNow";
|
|
2
4
|
ChatEntryPoint["SEARCH_PAGE_AUTO_OPEN_CHAT"] = "searchPageAutoOpenChat";
|
|
3
5
|
ChatEntryPoint["SEARCH_PAGE_SUMMARIZE_IN_CHAT"] = "searchPageSummarizeInChat";
|
|
4
6
|
return ChatEntryPoint;
|
|
@@ -155,12 +155,12 @@ export var ExtensionContextBridgeClient = function ExtensionContextBridgeClient(
|
|
|
155
155
|
if (!shouldRelayToProduct(payload.type)) {
|
|
156
156
|
return;
|
|
157
157
|
}
|
|
158
|
-
var
|
|
159
|
-
if (!
|
|
158
|
+
var result = serializePayload(payload);
|
|
159
|
+
if (!result.ok) {
|
|
160
160
|
return;
|
|
161
161
|
}
|
|
162
162
|
try {
|
|
163
|
-
activeTransport.send(_defineProperty(_defineProperty(_defineProperty({}, BRIDGE_MESSAGE_MARKER, true), "direction", BRIDGE_TO_PRODUCT), "payload",
|
|
163
|
+
activeTransport.send(_defineProperty(_defineProperty(_defineProperty({}, BRIDGE_MESSAGE_MARKER, true), "direction", BRIDGE_TO_PRODUCT), "payload", result.payload));
|
|
164
164
|
} catch (_unused3) {
|
|
165
165
|
// Never let bridging break the chat iframe.
|
|
166
166
|
}
|
|
@@ -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
3
|
import { fg } from '@atlaskit/platform-feature-flags/fg';
|
|
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,7 +46,8 @@ 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
52
|
// `ChatOpenerSubscriber` renders. Matches `BRIDGE_FEATURE_GATE` in `./constants`.
|
|
58
53
|
var active = fg('rovo-ext_context_bridge');
|
|
@@ -80,6 +75,9 @@ export var ExtensionContextBridgeHost = function ExtensionContextBridgeHost(_ref
|
|
|
80
75
|
*/
|
|
81
76
|
var lastRelayedRef = useRef(new Map());
|
|
82
77
|
|
|
78
|
+
/** Reported decisions, so a per-keystroke payload yields one event rather than thousands. */
|
|
79
|
+
var reportedDecisionsRef = useRef(new Set());
|
|
80
|
+
|
|
83
81
|
// Answers `request-context` from the chat, which cannot observe context published before it
|
|
84
82
|
// mounted. Products such as Jira publish once per navigation, so without this a chat opened after
|
|
85
83
|
// the page settled would describe the wrong view — or none at all.
|
|
@@ -117,17 +115,32 @@ export var ExtensionContextBridgeHost = function ExtensionContextBridgeHost(_ref
|
|
|
117
115
|
if (isAiEnabled === false) {
|
|
118
116
|
return;
|
|
119
117
|
}
|
|
120
|
-
// Loop guard.
|
|
118
|
+
// Loop guard. Not reported: our own echo, not a decision about the product.
|
|
121
119
|
if (payload.interactionSource === BRIDGE_SOURCE) {
|
|
122
120
|
return;
|
|
123
121
|
}
|
|
124
|
-
|
|
122
|
+
var report = function report(decision) {
|
|
123
|
+
reportRelayDecision(reportedDecisionsRef.current, onRelayDecision, {
|
|
124
|
+
payloadType: payload.type,
|
|
125
|
+
contextKey: getContextKey(payload),
|
|
126
|
+
relayed: decision === 'allow',
|
|
127
|
+
decision: decision,
|
|
128
|
+
publisherSource: payload.source,
|
|
129
|
+
payloadProduct: payload.product
|
|
130
|
+
});
|
|
131
|
+
};
|
|
132
|
+
var inboundDecision = getInboundDecision(payload.type);
|
|
133
|
+
if (inboundDecision !== 'allow') {
|
|
134
|
+
report(inboundDecision);
|
|
125
135
|
return;
|
|
126
136
|
}
|
|
127
|
-
var
|
|
128
|
-
if (!
|
|
137
|
+
var result = serializePayload(payload);
|
|
138
|
+
if (!result.ok) {
|
|
139
|
+
report(result.reason);
|
|
129
140
|
return;
|
|
130
141
|
}
|
|
142
|
+
report('allow');
|
|
143
|
+
var serialized = result.payload;
|
|
131
144
|
stampProductContentId(serialized, getContentId === null || getContentId === void 0 ? void 0 : getContentId());
|
|
132
145
|
var message = _defineProperty(_defineProperty(_defineProperty({}, BRIDGE_MESSAGE_MARKER, true), "direction", BRIDGE_TO_EXTENSION), "payload", serialized);
|
|
133
146
|
// 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
|
+
};
|
|
@@ -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,4 +1,6 @@
|
|
|
1
1
|
export declare enum ChatEntryPoint {
|
|
2
|
+
JSM_ROVO_SETTINGS_TEST_NOW = "jsmRovoSettingsTestNow",
|
|
3
|
+
JSM_PORTAL_SETTINGS_TEST_NOW = "jsmPortalSettingsTestNow",
|
|
2
4
|
SEARCH_PAGE_AUTO_OPEN_CHAT = "searchPageAutoOpenChat",
|
|
3
5
|
SEARCH_PAGE_SUMMARIZE_IN_CHAT = "searchPageSummarizeInChat"
|
|
4
6
|
}
|
|
@@ -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;
|
|
@@ -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;
|
|
@@ -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": "10.
|
|
3
|
+
"version": "10.38.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": {
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"@atlaskit/primitives": "^22.5.0",
|
|
45
45
|
"@atlaskit/ssr": "workspace:^",
|
|
46
46
|
"@atlassian/a11y-jest-testing": "^0.17.0",
|
|
47
|
-
"@atlassian/conversation-assistant-service-api": "^17.
|
|
47
|
+
"@atlassian/conversation-assistant-service-api": "^17.13.0",
|
|
48
48
|
"@atlassian/feature-flags-test-utils": "^1.3.0",
|
|
49
49
|
"@atlassian/testing-library": "^0.11.0",
|
|
50
50
|
"@atlassian/workbench": "workspace:*",
|