@tomflow/proflow-execution-browser-extension 0.1.37 → 0.1.39

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,5 +1,251 @@
1
1
  "use strict";
2
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 };
99
+ return { pageState: "UNKNOWN", activityKind: null };
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
+
3
249
  // packages/execution-browser-extension/src/submitted-message.ts
4
250
  function containsSubmittedFingerprint(candidates, fingerprint) {
5
251
  if (!fingerprint) return false;
@@ -13,17 +259,7 @@
13
259
  // packages/execution-browser-extension/extension/content.ts
14
260
  var contentInstanceId = `content:${crypto.randomUUID()}`;
15
261
  function pageState() {
16
- if (document.querySelector('[role="dialog"]'))
17
- return { pageState: "BLOCKED", activityKind: "ACTION_PERMISSION" };
18
- if (document.querySelector(
19
- '[data-testid="stop-button"], button[aria-label*="Stop"]'
20
- ))
21
- return { pageState: "BUSY", activityKind: "GENERATING" };
22
- if (document.querySelector(
23
- '#prompt-textarea, textarea, [contenteditable="true"]'
24
- ))
25
- return { pageState: "IDLE", activityKind: null };
26
- return { pageState: "UNKNOWN", activityKind: null };
262
+ return observeChatGptPage(document);
27
263
  }
28
264
  function observation() {
29
265
  return {
@@ -52,6 +288,7 @@
52
288
  }
53
289
  chrome.runtime.onMessage.addListener((command, _sender, sendResponse) => {
54
290
  void (async () => {
291
+ if (command.type === "PROFLOW_PAGE_SNAPSHOT_REQUEST") return observation();
55
292
  if (command.type !== "PROFLOW_PAGE_COMMAND" || command.contentInstanceId !== contentInstanceId || command.expectedUrl !== location.href)
56
293
  throw new Error("STALE_CONTENT_SESSION");
57
294
  if (command.operation === "observe") return observation();
@@ -60,30 +297,32 @@
60
297
  ...observation(),
61
298
  verified: hasFingerprint(command.fingerprint)
62
299
  };
63
- if (pageState().pageState === "BLOCKED")
64
- throw new Error("PAGE_PERMISSION_REQUIRES_HUMAN");
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");
65
311
  if (command.operation === "click") {
66
312
  safeElement(command.selector).click();
67
313
  return observation();
68
314
  }
69
- const input = safeElement(command.selector ?? "#prompt-textarea");
70
315
  if (command.value === void 0 || command.value.length > 4096)
71
316
  throw new Error("INPUT_BUDGET_EXCEEDED");
72
- input.focus();
73
- if (input instanceof HTMLTextAreaElement || input instanceof HTMLInputElement)
74
- input.value = command.value;
75
- else input.textContent = command.value;
76
- input.dispatchEvent(
77
- new InputEvent("input", {
78
- bubbles: true,
79
- inputType: "insertText",
80
- data: command.value
81
- })
317
+ if (command.operation === "submit") {
318
+ await submitChatGptComposer(document, command.value);
319
+ return observation();
320
+ }
321
+ writeChatGptInput(
322
+ document,
323
+ command.selector ?? "#prompt-textarea",
324
+ command.value
82
325
  );
83
- if (command.operation === "submit")
84
- safeElement(
85
- 'button[data-testid="send-button"], button[aria-label*="Send"]'
86
- ).click();
87
326
  return observation();
88
327
  })().then(
89
328
  (value) => sendResponse({ ok: true, value }),
@@ -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"));
@@ -0,0 +1,13 @@
1
+ export type CarrierAttentionView = {
2
+ attentionRef: string;
3
+ occurrenceRef: string;
4
+ taskId: string | null;
5
+ roleRef: string | null;
6
+ workerRef: string | null;
7
+ targetHost: string | null;
8
+ operationId: string;
9
+ reason: string;
10
+ actions: Array<"allowOnce" | "deny">;
11
+ observedAt: string;
12
+ };
13
+ export declare function parseCarrierAttentionViews(value: unknown): CarrierAttentionView[];
@@ -0,0 +1,54 @@
1
+ function nullableString(value) {
2
+ return value === null || typeof value === "string";
3
+ }
4
+ function parseCarrierAttentionView(value) {
5
+ if (typeof value !== "object" || value === null || Array.isArray(value))
6
+ return null;
7
+ const attentionRef = Reflect.get(value, "attentionRef");
8
+ const occurrenceRef = Reflect.get(value, "occurrenceRef");
9
+ const taskId = Reflect.get(value, "taskId");
10
+ const roleRef = Reflect.get(value, "roleRef");
11
+ const workerRef = Reflect.get(value, "workerRef");
12
+ const targetHost = Reflect.get(value, "targetHost");
13
+ const operationId = Reflect.get(value, "operationId");
14
+ const reason = Reflect.get(value, "reason");
15
+ const actions = Reflect.get(value, "actions");
16
+ const observedAt = Reflect.get(value, "observedAt");
17
+ if (typeof attentionRef !== "string" ||
18
+ attentionRef.length === 0 ||
19
+ typeof occurrenceRef !== "string" ||
20
+ occurrenceRef.length === 0 ||
21
+ !nullableString(taskId) ||
22
+ !nullableString(roleRef) ||
23
+ !nullableString(workerRef) ||
24
+ !nullableString(targetHost) ||
25
+ typeof operationId !== "string" ||
26
+ operationId.length === 0 ||
27
+ typeof reason !== "string" ||
28
+ reason.length === 0 ||
29
+ !Array.isArray(actions) ||
30
+ actions.some((action) => action !== "allowOnce" && action !== "deny") ||
31
+ typeof observedAt !== "string" ||
32
+ observedAt.length === 0)
33
+ return null;
34
+ return {
35
+ attentionRef,
36
+ occurrenceRef,
37
+ taskId,
38
+ roleRef,
39
+ workerRef,
40
+ targetHost,
41
+ operationId,
42
+ reason,
43
+ actions: [...actions],
44
+ observedAt,
45
+ };
46
+ }
47
+ export function parseCarrierAttentionViews(value) {
48
+ if (!Array.isArray(value))
49
+ return [];
50
+ return value
51
+ .slice(0, 128)
52
+ .map(parseCarrierAttentionView)
53
+ .filter((item) => item !== null);
54
+ }
@@ -0,0 +1,26 @@
1
+ export type CarrierAttentionAction = "allowOnce" | "deny";
2
+ export type CarrierAttention = {
3
+ attentionRef: string;
4
+ occurrenceRef: string;
5
+ tabId: number;
6
+ contentInstanceId: string;
7
+ url: string;
8
+ permissionFingerprint: string;
9
+ taskId: string | null;
10
+ roleRef: string | null;
11
+ workerRef: string | null;
12
+ targetHost: string | null;
13
+ operationId: string;
14
+ reason: string;
15
+ actions: CarrierAttentionAction[];
16
+ observedAt: string;
17
+ };
18
+ export type CarrierAttentionInput = Omit<CarrierAttention, "attentionRef" | "occurrenceRef">;
19
+ export declare function createCarrierAttentionRegistry(idFactory?: () => string): Readonly<{
20
+ derive(input: CarrierAttentionInput): CarrierAttention;
21
+ find(attentionRef: string): CarrierAttention | null;
22
+ current(tabId: number): CarrierAttention | null;
23
+ values(): CarrierAttention[];
24
+ delete(attentionRef: string): boolean;
25
+ removeTab(tabId: number): boolean;
26
+ }>;