@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.
Files changed (23) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/cjs/chat-entry-point.js +2 -0
  3. package/dist/cjs/common/utils/extension-context-bridge/ExtensionContextBridgeClient.js +3 -3
  4. package/dist/cjs/common/utils/extension-context-bridge/ExtensionContextBridgeHost.js +27 -14
  5. package/dist/cjs/common/utils/extension-context-bridge/analytics.js +87 -0
  6. package/dist/cjs/common/utils/extension-context-bridge/registry.js +33 -15
  7. package/dist/es2019/chat-entry-point.js +2 -0
  8. package/dist/es2019/common/utils/extension-context-bridge/ExtensionContextBridgeClient.js +3 -3
  9. package/dist/es2019/common/utils/extension-context-bridge/ExtensionContextBridgeHost.js +27 -13
  10. package/dist/es2019/common/utils/extension-context-bridge/analytics.js +76 -0
  11. package/dist/es2019/common/utils/extension-context-bridge/registry.js +29 -12
  12. package/dist/esm/chat-entry-point.js +2 -0
  13. package/dist/esm/common/utils/extension-context-bridge/ExtensionContextBridgeClient.js +3 -3
  14. package/dist/esm/common/utils/extension-context-bridge/ExtensionContextBridgeHost.js +27 -14
  15. package/dist/esm/common/utils/extension-context-bridge/analytics.js +80 -0
  16. package/dist/esm/common/utils/extension-context-bridge/registry.js +34 -15
  17. package/dist/types/chat-entry-point.d.ts +2 -0
  18. package/dist/types/common/utils/extension-context-bridge/ExtensionContextBridgeHost.d.ts +8 -1
  19. package/dist/types/common/utils/extension-context-bridge/analytics.d.ts +55 -0
  20. package/dist/types/common/utils/extension-context-bridge/policy.d.ts +1 -2
  21. package/dist/types/common/utils/extension-context-bridge/registry.d.ts +14 -1
  22. package/extension-context-bridge/analytics/package.json +10 -0
  23. 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 serialized = (0, _registry.serializePayload)(payload);
165
- if (!serialized) {
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", serialized));
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
- if (payload.type === 'set-message-context') {
44
- var data = payload.data;
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
- if ((0, _policy.getInboundDecision)(payload.type) !== 'allow') {
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 serialized = (0, _registry.serializePayload)(payload);
135
- if (!serialized) {
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 null;
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 null;
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 null;
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 null;
79
+ return dropped('merge-style-publisher');
68
80
  }
69
81
  try {
70
82
  JSON.stringify(value);
71
83
  } catch (_unused2) {
72
- return null;
84
+ return dropped('not-serializable');
73
85
  }
74
- return _objectSpread(_objectSpread({}, base(payload)), {}, {
75
- data: {
76
- contextKey: data.contextKey,
77
- value: value
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 null;
133
+ return dropped('not-serializable');
119
134
  }
120
- return _objectSpread(_objectSpread({}, base(payload)), {}, {
121
- data: data
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 serialized = serializePayload(payload);
163
- if (!serialized) {
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: serialized
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
- if (payload.type === 'set-message-context') {
35
- const data = payload.data;
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
- if (getInboundDecision(payload.type) !== 'allow') {
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 serialized = serializePayload(payload);
127
- if (!serialized) {
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 null;
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 null;
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 null;
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 null;
66
+ return dropped('merge-style-publisher');
56
67
  }
57
68
  try {
58
69
  JSON.stringify(value);
59
70
  } catch {
60
- return null;
71
+ return dropped('not-serializable');
61
72
  }
62
73
  return {
63
- ...base(payload),
64
- data: {
65
- contextKey: data.contextKey,
66
- value
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 null;
117
+ return dropped('not-serializable');
104
118
  }
105
119
  return {
106
- ...base(payload),
107
- data
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 serialized = serializePayload(payload);
159
- if (!serialized) {
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", serialized));
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
- if (payload.type === 'set-message-context') {
37
- var data = payload.data;
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
- if (getInboundDecision(payload.type) !== 'allow') {
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 serialized = serializePayload(payload);
128
- if (!serialized) {
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 null;
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 null;
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 null;
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 null;
73
+ return dropped('merge-style-publisher');
61
74
  }
62
75
  try {
63
76
  JSON.stringify(value);
64
77
  } catch (_unused2) {
65
- return null;
78
+ return dropped('not-serializable');
66
79
  }
67
- return _objectSpread(_objectSpread({}, base(payload)), {}, {
68
- data: {
69
- contextKey: data.contextKey,
70
- value: value
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 null;
127
+ return dropped('not-serializable');
112
128
  }
113
- return _objectSpread(_objectSpread({}, base(payload)), {}, {
114
- data: data
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) => SerializedPayload | null;
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.36.0",
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.12.0",
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:*",