@tomflow/proflow-execution-browser-extension 0.1.36 → 0.1.38

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.
@@ -1,94 +1,354 @@
1
- import { containsSubmittedFingerprint } from "../src/submitted-message.js";
2
- const contentInstanceId = `content:${crypto.randomUUID()}`;
3
- function pageState() {
4
- if (document.querySelector('[role="dialog"]'))
5
- return { pageState: "BLOCKED", activityKind: "ACTION_PERMISSION" };
6
- if (document.querySelector('[data-testid="stop-button"], button[aria-label*="Stop"]'))
7
- return { pageState: "BUSY", activityKind: "GENERATING" };
8
- if (document.querySelector('#prompt-textarea, textarea, [contenteditable="true"]'))
9
- return { pageState: "IDLE", activityKind: null };
1
+ "use strict";
2
+ (() => {
3
+ // packages/execution-browser-extension/src/carrier-permission.ts
4
+ var actionLabels = [
5
+ [/^(始终允许|always allow)$/i, "allowAlways"],
6
+ [/^(允许一次|allow once)$/i, "allowOnce"],
7
+ [/^(拒绝|deny)$/i, "deny"]
8
+ ];
9
+ function permissionSemanticAction(label) {
10
+ const normalized = label.trim();
11
+ for (const [pattern, action] of actionLabels)
12
+ if (pattern.test(normalized)) return action;
13
+ return null;
14
+ }
15
+ function normalizedText(value) {
16
+ return value.replace(/\s+/g, " ").trim().slice(0, 4096);
17
+ }
18
+ function hashFingerprint(value) {
19
+ let hash = 2166136261;
20
+ for (let index = 0; index < value.length; index += 1) {
21
+ hash ^= value.charCodeAt(index);
22
+ hash = Math.imul(hash, 16777619);
23
+ }
24
+ return (hash >>> 0).toString(16).padStart(8, "0");
25
+ }
26
+ function targetHost(text) {
27
+ const url = text.match(/https?:\/\/([a-z0-9.-]+)(?=[/:\s"'”]|$)/i)?.[1];
28
+ if (url) return url.toLowerCase();
29
+ const hosts = text.match(/[a-z0-9][a-z0-9-]*(?:\.[a-z0-9-]+){2,}/gi) ?? [];
30
+ return hosts.find((value) => value.includes("devtunnels.ms"))?.toLowerCase() ?? hosts[0]?.toLowerCase() ?? null;
31
+ }
32
+ function operationId(text) {
33
+ return text.match(
34
+ /(?:工具调用|tool call)\s*[::]\s*[^\s.]+(?:\.[^\s.]+)*\.([A-Za-z][A-Za-z0-9_]*)/i
35
+ )?.[1] ?? null;
36
+ }
37
+ function taskId(text) {
38
+ return text.match(/\btask-[A-Za-z0-9-]+\b/)?.[0] ?? null;
39
+ }
40
+ function detectActionPermission(candidates) {
41
+ for (const candidate of candidates) {
42
+ const actions = candidate.buttonLabels.map(permissionSemanticAction).filter((value) => value !== null);
43
+ if (!actions.includes("deny") || !actions.includes("allowAlways") && !actions.includes("allowOnce"))
44
+ continue;
45
+ const text = normalizedText(candidate.text);
46
+ const operation = operationId(text);
47
+ if (!operation) continue;
48
+ return {
49
+ kind: "ACTION_PERMISSION",
50
+ targetHost: targetHost(text),
51
+ operationId: operation,
52
+ taskId: taskId(text),
53
+ actions,
54
+ fingerprint: `permission:v1:${hashFingerprint(`${text}|${actions.join(",")}`)}`
55
+ };
56
+ }
57
+ return null;
58
+ }
59
+ function permissionActionAllowed(facts, expectedFingerprint, action) {
60
+ return facts.fingerprint === expectedFingerprint && facts.actions.includes(action);
61
+ }
62
+
63
+ // packages/execution-browser-extension/src/composer-submit.ts
64
+ async function submitAfterComposerCommit(port, expectedValue, maxFrames = 180) {
65
+ let stableFrames = 0;
66
+ for (let frame = 0; frame < maxFrames; frame += 1) {
67
+ await port.nextFrame();
68
+ if (port.readValue() === expectedValue && port.submitReady()) {
69
+ stableFrames += 1;
70
+ if (stableFrames >= 2) {
71
+ port.clickSubmit();
72
+ return;
73
+ }
74
+ } else {
75
+ stableFrames = 0;
76
+ }
77
+ }
78
+ throw new Error("COMPOSER_SUBMIT_NOT_READY");
79
+ }
80
+ async function submitControlledComposer(port, expectedValue, maxFrames = 180) {
81
+ port.write(expectedValue);
82
+ port.dispatchInput(expectedValue);
83
+ await submitAfterComposerCommit(port, expectedValue, maxFrames);
84
+ }
85
+
86
+ // packages/execution-browser-extension/src/chatgpt-runtime-adapter.ts
87
+ function classifyChatGptPageSignals(input) {
88
+ if (input.permission)
89
+ return {
90
+ pageState: "BLOCKED",
91
+ activityKind: "ACTION_PERMISSION",
92
+ blockerFacts: input.permission
93
+ };
94
+ if (input.hasDialog)
95
+ return { pageState: "BLOCKED", activityKind: "WAITING_HUMAN" };
96
+ if (input.isGenerating)
97
+ return { pageState: "BUSY", activityKind: "GENERATING" };
98
+ if (input.hasComposer) return { pageState: "IDLE", activityKind: null };
10
99
  return { pageState: "UNKNOWN", activityKind: null };
11
- }
12
- function observation() {
100
+ }
101
+ var composerSelector = '#prompt-textarea, textarea, [contenteditable="true"]';
102
+ var sendSelector = 'button[data-testid="send-button"], button[aria-label*="Send"], button[aria-label*="\u53D1\u9001"]';
103
+ function buttonLabel(button) {
104
+ return (button.textContent ?? "").replace(/\s+/g, " ").trim();
105
+ }
106
+ function actionPermissionDom(document2) {
107
+ const view = document2.defaultView;
108
+ if (!view) return null;
109
+ const semanticButtons = [...document2.querySelectorAll("button")].filter(
110
+ (button) => button instanceof view.HTMLButtonElement && permissionSemanticAction(buttonLabel(button)) !== null
111
+ );
112
+ for (const seed of semanticButtons) {
113
+ let root = seed.parentElement;
114
+ for (let depth = 0; root && depth < 8; depth += 1, root = root.parentElement) {
115
+ const buttons = [...root.querySelectorAll("button")].filter(
116
+ (button) => button instanceof view.HTMLButtonElement
117
+ );
118
+ const facts = detectActionPermission([
119
+ {
120
+ text: root.textContent ?? "",
121
+ buttonLabels: buttons.map(buttonLabel)
122
+ }
123
+ ]);
124
+ if (!facts) continue;
125
+ const mapped = /* @__PURE__ */ new Map();
126
+ for (const button of buttons) {
127
+ const action = permissionSemanticAction(buttonLabel(button));
128
+ if (action && !mapped.has(action)) mapped.set(action, button);
129
+ }
130
+ return { facts, buttons: mapped };
131
+ }
132
+ }
133
+ return null;
134
+ }
135
+ function observeChatGptPage(document2) {
136
+ const permission = actionPermissionDom(document2);
137
+ return classifyChatGptPageSignals({
138
+ permission: permission?.facts ?? null,
139
+ hasDialog: document2.querySelector('[role="dialog"]') !== null,
140
+ isGenerating: document2.querySelector(
141
+ '[data-testid="stop-button"], button[aria-label*="Stop"], button[aria-label*="\u505C\u6B62"]'
142
+ ) !== null,
143
+ hasComposer: document2.querySelector(composerSelector) !== null
144
+ });
145
+ }
146
+ function performChatGptPermissionAction(document2, expectedFingerprint, action) {
147
+ const permission = actionPermissionDom(document2);
148
+ if (!permission) throw new Error("ACTION_PERMISSION_NOT_FOUND");
149
+ if (!permissionActionAllowed(permission.facts, expectedFingerprint, action))
150
+ throw new Error("STALE_PERMISSION");
151
+ const button = permission.buttons.get(action);
152
+ if (!button || button.disabled || button.getAttribute("aria-disabled") === "true")
153
+ throw new Error("PERMISSION_ACTION_NOT_READY");
154
+ button.click();
155
+ return permission.facts;
156
+ }
157
+ function composerElement(document2) {
158
+ const view = document2.defaultView;
159
+ const element = document2.querySelector(composerSelector);
160
+ if (!view || !(element instanceof view.HTMLElement))
161
+ throw new Error("COMPOSER_NOT_FOUND");
162
+ return element;
163
+ }
164
+ function readElementValue(element) {
165
+ const view = element.ownerDocument.defaultView;
166
+ if (!view) return "";
167
+ if (element instanceof view.HTMLTextAreaElement || element instanceof view.HTMLInputElement)
168
+ return element.value;
169
+ return element.textContent ?? "";
170
+ }
171
+ function nativeWrite(element, value) {
172
+ const view = element.ownerDocument.defaultView;
173
+ if (!view) throw new Error("DOM_WINDOW_NOT_READY");
174
+ if (element instanceof view.HTMLTextAreaElement || element instanceof view.HTMLInputElement) {
175
+ const prototype = element instanceof view.HTMLTextAreaElement ? view.HTMLTextAreaElement.prototype : view.HTMLInputElement.prototype;
176
+ const setter = Object.getOwnPropertyDescriptor(prototype, "value")?.set;
177
+ if (!setter) throw new Error("COMPOSER_NATIVE_SETTER_MISSING");
178
+ setter.call(element, value);
179
+ return;
180
+ }
181
+ element.textContent = value;
182
+ }
183
+ function dispatchComposerInput(element, value) {
184
+ const view = element.ownerDocument.defaultView;
185
+ if (!view) throw new Error("DOM_WINDOW_NOT_READY");
186
+ element.dispatchEvent(
187
+ new view.InputEvent("input", {
188
+ bubbles: true,
189
+ inputType: "insertText",
190
+ data: value
191
+ })
192
+ );
193
+ }
194
+ function nextComposerFrame(document2) {
195
+ const view = document2.defaultView;
196
+ if (!view) return Promise.reject(new Error("DOM_WINDOW_NOT_READY"));
197
+ return new Promise((resolve) => {
198
+ let settled = false;
199
+ const finish = () => {
200
+ if (settled) return;
201
+ settled = true;
202
+ resolve();
203
+ };
204
+ view.requestAnimationFrame(finish);
205
+ view.setTimeout(finish, 50);
206
+ });
207
+ }
208
+ function sendButton(document2) {
209
+ const view = document2.defaultView;
210
+ const element = document2.querySelector(sendSelector);
211
+ return view && element instanceof view.HTMLButtonElement ? element : null;
212
+ }
213
+ async function submitChatGptComposer(document2, value) {
214
+ const composer = composerElement(document2);
215
+ composer.focus();
216
+ await submitControlledComposer(
217
+ {
218
+ write: (next) => nativeWrite(composer, next),
219
+ dispatchInput: (next) => dispatchComposerInput(composer, next),
220
+ readValue: () => readElementValue(composer),
221
+ submitReady: () => {
222
+ const button = sendButton(document2);
223
+ return Boolean(
224
+ button && !button.disabled && button.getAttribute("aria-disabled") !== "true"
225
+ );
226
+ },
227
+ nextFrame: () => nextComposerFrame(document2),
228
+ clickSubmit: () => {
229
+ const button = sendButton(document2);
230
+ if (!button || button.disabled || button.getAttribute("aria-disabled") === "true")
231
+ throw new Error("COMPOSER_SUBMIT_NOT_READY");
232
+ button.click();
233
+ }
234
+ },
235
+ value
236
+ );
237
+ }
238
+ function writeChatGptInput(document2, selector, value) {
239
+ if (!selector || selector.length > 512) throw new Error("SELECTOR_INVALID");
240
+ const view = document2.defaultView;
241
+ const element = document2.querySelector(selector);
242
+ if (!view || !(element instanceof view.HTMLElement))
243
+ throw new Error("ELEMENT_NOT_FOUND");
244
+ element.focus();
245
+ nativeWrite(element, value);
246
+ dispatchComposerInput(element, value);
247
+ }
248
+
249
+ // packages/execution-browser-extension/src/submitted-message.ts
250
+ function containsSubmittedFingerprint(candidates, fingerprint) {
251
+ if (!fingerprint) return false;
252
+ for (const candidate of candidates) {
253
+ if (candidate.authorRole === "user" && candidate.textContent?.includes(fingerprint))
254
+ return true;
255
+ }
256
+ return false;
257
+ }
258
+
259
+ // packages/execution-browser-extension/extension/content.ts
260
+ var contentInstanceId = `content:${crypto.randomUUID()}`;
261
+ function pageState() {
262
+ return observeChatGptPage(document);
263
+ }
264
+ function observation() {
13
265
  return {
14
- url: location.href,
15
- contentInstanceId,
16
- ...pageState(),
17
- observedAt: new Date().toISOString(),
266
+ url: location.href,
267
+ contentInstanceId,
268
+ ...pageState(),
269
+ observedAt: (/* @__PURE__ */ new Date()).toISOString()
18
270
  };
19
- }
20
- function safeElement(selector) {
21
- if (!selector || selector.length > 512)
22
- throw new Error("SELECTOR_INVALID");
271
+ }
272
+ function safeElement(selector) {
273
+ if (!selector || selector.length > 512) throw new Error("SELECTOR_INVALID");
23
274
  const element = document.querySelector(selector);
24
- if (!(element instanceof HTMLElement))
25
- throw new Error("ELEMENT_NOT_FOUND");
275
+ if (!(element instanceof HTMLElement)) throw new Error("ELEMENT_NOT_FOUND");
26
276
  return element;
27
- }
28
- function hasFingerprint(fingerprint) {
29
- return containsSubmittedFingerprint([...document.querySelectorAll('[data-message-author-role="user"]')].map((element) => ({
30
- authorRole: element.getAttribute("data-message-author-role"),
31
- textContent: element.textContent,
32
- })), fingerprint);
33
- }
34
- chrome.runtime.onMessage.addListener((command, _sender, sendResponse) => {
277
+ }
278
+ function hasFingerprint(fingerprint) {
279
+ return containsSubmittedFingerprint(
280
+ [...document.querySelectorAll('[data-message-author-role="user"]')].map(
281
+ (element) => ({
282
+ authorRole: element.getAttribute("data-message-author-role"),
283
+ textContent: element.textContent
284
+ })
285
+ ),
286
+ fingerprint
287
+ );
288
+ }
289
+ chrome.runtime.onMessage.addListener((command, _sender, sendResponse) => {
35
290
  void (async () => {
36
- if (command.type !== "PROFLOW_PAGE_COMMAND" ||
37
- command.contentInstanceId !== contentInstanceId ||
38
- command.expectedUrl !== location.href)
39
- throw new Error("STALE_CONTENT_SESSION");
40
- if (command.operation === "observe")
41
- return observation();
42
- if (command.operation === "verify")
43
- return {
44
- ...observation(),
45
- verified: hasFingerprint(command.fingerprint),
46
- };
47
- if (pageState().pageState === "BLOCKED")
48
- throw new Error("PAGE_PERMISSION_REQUIRES_HUMAN");
49
- if (command.operation === "click") {
50
- safeElement(command.selector).click();
51
- return observation();
52
- }
53
- const input = safeElement(command.selector ?? "#prompt-textarea");
54
- if (command.value === undefined || command.value.length > 4_096)
55
- throw new Error("INPUT_BUDGET_EXCEEDED");
56
- input.focus();
57
- if (input instanceof HTMLTextAreaElement ||
58
- input instanceof HTMLInputElement)
59
- input.value = command.value;
60
- else
61
- input.textContent = command.value;
62
- input.dispatchEvent(new InputEvent("input", {
63
- bubbles: true,
64
- inputType: "insertText",
65
- data: command.value,
66
- }));
67
- if (command.operation === "submit")
68
- safeElement('button[data-testid="send-button"], button[aria-label*="Send"]').click();
291
+ if (command.type === "PROFLOW_PAGE_SNAPSHOT_REQUEST") return observation();
292
+ if (command.type !== "PROFLOW_PAGE_COMMAND" || command.contentInstanceId !== contentInstanceId || command.expectedUrl !== location.href)
293
+ throw new Error("STALE_CONTENT_SESSION");
294
+ if (command.operation === "observe") return observation();
295
+ if (command.operation === "verify")
296
+ return {
297
+ ...observation(),
298
+ verified: hasFingerprint(command.fingerprint)
299
+ };
300
+ if (command.operation === "permissionAction") {
301
+ if (!command.permissionFingerprint || !command.permissionAction)
302
+ throw new Error("PERMISSION_ACTION_INVALID");
303
+ performChatGptPermissionAction(
304
+ document,
305
+ command.permissionFingerprint,
306
+ command.permissionAction
307
+ );
308
+ return observation();
309
+ }
310
+ if (pageState().pageState === "BLOCKED") throw new Error("PAGE_BLOCKED");
311
+ if (command.operation === "click") {
312
+ safeElement(command.selector).click();
313
+ return observation();
314
+ }
315
+ if (command.value === void 0 || command.value.length > 4096)
316
+ throw new Error("INPUT_BUDGET_EXCEEDED");
317
+ if (command.operation === "submit") {
318
+ await submitChatGptComposer(document, command.value);
69
319
  return observation();
70
- })().then((value) => sendResponse({ ok: true, value }), (error) => sendResponse({
320
+ }
321
+ writeChatGptInput(
322
+ document,
323
+ command.selector ?? "#prompt-textarea",
324
+ command.value
325
+ );
326
+ return observation();
327
+ })().then(
328
+ (value) => sendResponse({ ok: true, value }),
329
+ (error) => sendResponse({
71
330
  ok: false,
72
- error: error instanceof Error ? error.message : "PAGE_COMMAND_FAILED",
73
- }));
331
+ error: error instanceof Error ? error.message : "PAGE_COMMAND_FAILED"
332
+ })
333
+ );
74
334
  return true;
75
- });
76
- const publish = () => chrome.runtime.sendMessage({
335
+ });
336
+ var publish = () => chrome.runtime.sendMessage({
77
337
  type: "PROFLOW_CONTENT_OBSERVATION",
78
- observation: observation(),
79
- });
80
- void publish();
81
- let publishTimer;
82
- const observer = new MutationObserver(() => {
83
- if (publishTimer !== undefined)
84
- clearTimeout(publishTimer);
338
+ observation: observation()
339
+ });
340
+ void publish();
341
+ var publishTimer;
342
+ var observer = new MutationObserver(() => {
343
+ if (publishTimer !== void 0) clearTimeout(publishTimer);
85
344
  publishTimer = setTimeout(() => {
86
- publishTimer = undefined;
87
- void publish();
345
+ publishTimer = void 0;
346
+ void publish();
88
347
  }, 100);
89
- });
90
- observer.observe(document.documentElement, {
348
+ });
349
+ observer.observe(document.documentElement, {
91
350
  subtree: true,
92
351
  childList: true,
93
- attributes: true,
94
- });
352
+ attributes: true
353
+ });
354
+ })();
@@ -1,5 +1,5 @@
1
- export {};
2
- const extensionRuntime = typeof chrome !== "undefined" && chrome.runtime ? chrome.runtime : null;
1
+ import { parseCarrierAttentionViews } from "../src/carrier-attention-view.js";
2
+ const extensionRuntime = typeof chrome === "undefined" ? null : chrome.runtime;
3
3
  function element(selector) {
4
4
  const value = document.querySelector(selector);
5
5
  if (!value)
@@ -17,6 +17,7 @@ const startButton = element("#start-task");
17
17
  const ensureWorkersButton = element("#ensure-workers");
18
18
  const newTaskForm = element("#new-task-form");
19
19
  const approvalsTarget = element("#approvals");
20
+ const carrierAttentionsTarget = element("#carrier-attentions");
20
21
  const systemAssessmentTarget = element("#system-assessment");
21
22
  let selected = null;
22
23
  function requestId(prefix) {
@@ -68,6 +69,73 @@ async function approvalApplication(operation, input) {
68
69
  : "APPROVAL_APPLICATION_FAILED");
69
70
  return response.value;
70
71
  }
72
+ async function carrierAttentionAction(attentionRef, action) {
73
+ if (!extensionRuntime) {
74
+ const response = await fetch("/tasks/api/carrier-attention", {
75
+ method: "POST",
76
+ headers: { "content-type": "application/json" },
77
+ body: JSON.stringify({ attentionRef, action }),
78
+ });
79
+ const body = record(await response.json());
80
+ if (!response.ok || body.ok !== true)
81
+ throw new Error(typeof body.error === "string"
82
+ ? body.error
83
+ : "CARRIER_ATTENTION_ACTION_FAILED");
84
+ return;
85
+ }
86
+ const raw = await extensionRuntime.sendMessage({
87
+ type: "PROFLOW_CARRIER_ATTENTION_ACTION",
88
+ input: { attentionRef, action },
89
+ });
90
+ const response = record(raw);
91
+ if (response.ok !== true)
92
+ throw new Error(typeof response.error === "string"
93
+ ? response.error
94
+ : "CARRIER_ATTENTION_ACTION_FAILED");
95
+ }
96
+ function renderCarrierAttentions(snapshot) {
97
+ const attentions = parseCarrierAttentionViews(snapshot.carrierAttentions);
98
+ carrierAttentionsTarget.replaceChildren();
99
+ if (attentions.length === 0) {
100
+ carrierAttentionsTarget.textContent = "No carrier attention.";
101
+ return;
102
+ }
103
+ for (const attention of attentions) {
104
+ const row = document.createElement("div");
105
+ row.className = "task";
106
+ const label = document.createElement("div");
107
+ label.textContent = `${attention.operationId} · ${attention.targetHost ?? "unknown target"}`;
108
+ const detail = document.createElement("div");
109
+ detail.className = "meta";
110
+ detail.textContent = [
111
+ attention.reason,
112
+ attention.taskId ? `task ${attention.taskId}` : "task unknown",
113
+ attention.roleRef ? `role ${attention.roleRef}` : "role unknown",
114
+ ].join(" · ");
115
+ row.append(label, detail);
116
+ if (attention.actions.includes("allowOnce")) {
117
+ const allow = document.createElement("button");
118
+ allow.type = "button";
119
+ allow.textContent = "Allow once";
120
+ allow.addEventListener("click", () => void run(async () => {
121
+ await carrierAttentionAction(attention.attentionRef, "allowOnce");
122
+ await refreshBrowserStatus();
123
+ }));
124
+ row.append(allow);
125
+ }
126
+ if (attention.actions.includes("deny")) {
127
+ const deny = document.createElement("button");
128
+ deny.type = "button";
129
+ deny.textContent = "Deny";
130
+ deny.addEventListener("click", () => void run(async () => {
131
+ await carrierAttentionAction(attention.attentionRef, "deny");
132
+ await refreshBrowserStatus();
133
+ }));
134
+ row.append(deny);
135
+ }
136
+ carrierAttentionsTarget.append(row);
137
+ }
138
+ }
71
139
  async function refreshApprovals() {
72
140
  const value = record(await approvalApplication("approval.list", { status: "PENDING" }));
73
141
  const approvals = Array.isArray(value.approvals)
@@ -170,7 +238,9 @@ async function refreshTasks() {
170
238
  }
171
239
  async function pageStatus() {
172
240
  if (extensionRuntime)
173
- return record(await extensionRuntime.sendMessage({ type: "PROFLOW_SIDE_PANEL_SNAPSHOT" }));
241
+ return record(await extensionRuntime.sendMessage({
242
+ type: "PROFLOW_SIDE_PANEL_SNAPSHOT",
243
+ }));
174
244
  const response = await fetch("/tasks/api/status", { cache: "no-store" });
175
245
  const body = record(await response.json());
176
246
  if (!response.ok || body.ok !== true)
@@ -179,6 +249,7 @@ async function pageStatus() {
179
249
  }
180
250
  async function refreshBrowserStatus() {
181
251
  const snapshot = await pageStatus();
252
+ renderCarrierAttentions(snapshot);
182
253
  connection.textContent =
183
254
  snapshot.taskApplicationConfigured === true &&
184
255
  snapshot.approvalApplicationConfigured === true
@@ -1,5 +1,6 @@
1
1
  import { randomUUID, timingSafeEqual } from "node:crypto";
2
2
  import { createServer, } from "node:http";
3
+ import { parseCarrierAttentionViews, } from "./carrier-attention-view.js";
3
4
  export class BrowserRealityBridgeError extends Error {
4
5
  code;
5
6
  constructor(code, message) {
@@ -121,6 +122,8 @@ export async function createBrowserRealityBridgeServer(options) {
121
122
  let lastCommandConsumerAt = null;
122
123
  let closed = false;
123
124
  let endpoint = "";
125
+ let carrierAttentions = [];
126
+ let requestCommand = () => Promise.reject(new BrowserRealityBridgeError("BRIDGE_OFFLINE", "extension command consumer is not ready"));
124
127
  const taskBootstrap = new Map();
125
128
  const taskSessions = new Map();
126
129
  const taskCookie = "proflow_tasks_session";
@@ -162,7 +165,9 @@ export async function createBrowserRealityBridgeServer(options) {
162
165
  const server = createServer(async (request, response) => {
163
166
  try {
164
167
  const url = new URL(request.url ?? "/", "http://127.0.0.1");
165
- if (options.taskWeb && request.method === "GET" && url.pathname.startsWith("/tasks/bootstrap/")) {
168
+ if (options.taskWeb &&
169
+ request.method === "GET" &&
170
+ url.pathname.startsWith("/tasks/bootstrap/")) {
166
171
  pruneTaskWebState();
167
172
  const bootstrap = decodeURIComponent(url.pathname.slice("/tasks/bootstrap/".length));
168
173
  if (!taskBootstrap.has(bootstrap)) {
@@ -200,6 +205,7 @@ export async function createBrowserRealityBridgeServer(options) {
200
205
  taskApplicationConfigured: true,
201
206
  approvalApplicationConfigured: true,
202
207
  systemObserver: null,
208
+ carrierAttentions,
203
209
  browserCarrier: {
204
210
  online: commandConsumerReady(),
205
211
  sessionOnline: sessionOnline(),
@@ -215,13 +221,41 @@ export async function createBrowserRealityBridgeServer(options) {
215
221
  });
216
222
  return;
217
223
  }
218
- if (request.method === "POST" && (url.pathname === "/tasks/api/task" || url.pathname === "/tasks/api/approval")) {
224
+ if (request.method === "POST" &&
225
+ url.pathname === "/tasks/api/carrier-attention") {
219
226
  if (request.headers.origin !== endpoint) {
220
227
  send(response, 403, { error: "TASK_WEB_ORIGIN_INVALID" });
221
228
  return;
222
229
  }
223
230
  const body = await readJson(request);
224
- if (!isRecord(body) || typeof body.operation !== "string" || !isRecord(body.input))
231
+ if (!isRecord(body))
232
+ throw new BrowserRealityBridgeError("BRIDGE_INPUT_INVALID", "carrier attention action must be an object");
233
+ const attentionRef = stringField(body, "attentionRef");
234
+ const action = body.action;
235
+ if (action !== "allowOnce" && action !== "deny")
236
+ throw new BrowserRealityBridgeError("BRIDGE_INPUT_INVALID", "carrier attention action is invalid");
237
+ const attention = carrierAttentions.find((candidate) => candidate.attentionRef === attentionRef);
238
+ if (!attention?.actions.includes(action))
239
+ throw new BrowserRealityBridgeError("BRIDGE_INPUT_INVALID", "carrier attention reference is stale or denied");
240
+ const value = await requestCommand({
241
+ type: "CARRIER_ATTENTION_ACTION",
242
+ attentionRef,
243
+ action,
244
+ });
245
+ send(response, 200, { ok: true, value });
246
+ return;
247
+ }
248
+ if (request.method === "POST" &&
249
+ (url.pathname === "/tasks/api/task" ||
250
+ url.pathname === "/tasks/api/approval")) {
251
+ if (request.headers.origin !== endpoint) {
252
+ send(response, 403, { error: "TASK_WEB_ORIGIN_INVALID" });
253
+ return;
254
+ }
255
+ const body = await readJson(request);
256
+ if (!isRecord(body) ||
257
+ typeof body.operation !== "string" ||
258
+ !isRecord(body.input))
225
259
  throw new BrowserRealityBridgeError("BRIDGE_INPUT_INVALID", "task web request is invalid");
226
260
  const value = url.pathname.endsWith("/task")
227
261
  ? await options.taskWeb.invokeTask(body.operation, body.input)
@@ -242,11 +276,15 @@ export async function createBrowserRealityBridgeServer(options) {
242
276
  return;
243
277
  }
244
278
  authenticate(request);
245
- if (options.taskWeb && request.method === "POST" && url.pathname === "/v1/tasks/session") {
279
+ if (options.taskWeb &&
280
+ request.method === "POST" &&
281
+ url.pathname === "/v1/tasks/session") {
246
282
  pruneTaskWebState();
247
283
  const bootstrap = idFactory();
248
284
  taskBootstrap.set(bootstrap, now().getTime() + taskBootstrapTtlMs);
249
- send(response, 200, { url: `${endpoint}/tasks/bootstrap/${encodeURIComponent(bootstrap)}` });
285
+ send(response, 200, {
286
+ url: `${endpoint}/tasks/bootstrap/${encodeURIComponent(bootstrap)}`,
287
+ });
250
288
  return;
251
289
  }
252
290
  if (request.method === "POST" && url.pathname === "/v1/session/hello") {
@@ -289,6 +327,20 @@ export async function createBrowserRealityBridgeServer(options) {
289
327
  send(response, 200, { accepted: true });
290
328
  return;
291
329
  }
330
+ if (request.method === "POST" &&
331
+ url.pathname === "/v1/carrier/attentions") {
332
+ requireExtensionOrigin(request);
333
+ const body = await readJson(request);
334
+ if (!isRecord(body) || !Array.isArray(body.carrierAttentions))
335
+ throw new BrowserRealityBridgeError("BRIDGE_INPUT_INVALID", "carrier attentions must be an array");
336
+ const parsed = parseCarrierAttentionViews(body.carrierAttentions);
337
+ if (body.carrierAttentions.length > 128 ||
338
+ parsed.length !== body.carrierAttentions.length)
339
+ throw new BrowserRealityBridgeError("BRIDGE_INPUT_INVALID", "carrier attentions contain invalid entries");
340
+ carrierAttentions = parsed;
341
+ send(response, 200, { accepted: true });
342
+ return;
343
+ }
292
344
  if (request.method === "GET" && url.pathname === "/v1/commands/next") {
293
345
  const stamp = now();
294
346
  session.lastHeartbeatAt = stamp.getTime();
@@ -349,7 +401,7 @@ export async function createBrowserRealityBridgeServer(options) {
349
401
  if (!address || typeof address === "string")
350
402
  throw new Error("bridge address missing");
351
403
  endpoint = `http://127.0.0.1:${address.port}`;
352
- const requestCommand = (command) => {
404
+ requestCommand = (command) => {
353
405
  if (!online())
354
406
  return Promise.reject(new BrowserRealityBridgeError("BRIDGE_OFFLINE", "extension command consumer is not ready"));
355
407
  const commandId = `browser-command:${idFactory()}`;
@@ -435,6 +487,7 @@ export async function createBrowserRealityBridgeServer(options) {
435
487
  },
436
488
  async close() {
437
489
  closed = true;
490
+ carrierAttentions = [];
438
491
  for (const item of pending.values()) {
439
492
  clearTimeout(item.timer);
440
493
  item.reject(new BrowserRealityBridgeError("BRIDGE_OFFLINE", "bridge server closed"));