@tomflow/proflow-execution-browser-extension 0.1.28 → 0.1.30

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.
@@ -47,7 +47,7 @@ export declare const behaviorAdapter: {
47
47
  ok: true;
48
48
  status: "SUCCEEDED";
49
49
  moduleRef: "execution-browser-extension";
50
- moduleVersion: "0.1.28";
50
+ moduleVersion: "0.1.30";
51
51
  data: {
52
52
  loadDir: string;
53
53
  };
@@ -60,7 +60,7 @@ export declare const behaviorAdapter: {
60
60
  readonly ok: true;
61
61
  readonly status: "SUCCEEDED";
62
62
  readonly moduleRef: "execution-browser-extension";
63
- readonly moduleVersion: "0.1.28";
63
+ readonly moduleVersion: "0.1.30";
64
64
  };
65
65
  observedEffects: never[];
66
66
  }>;
@@ -70,7 +70,7 @@ export declare const behaviorAdapter: {
70
70
  ok: true;
71
71
  status: "SUCCEEDED";
72
72
  moduleRef: "execution-browser-extension";
73
- moduleVersion: "0.1.28";
73
+ moduleVersion: "0.1.30";
74
74
  data: {
75
75
  setupStatus: "ACTION_REQUIRED" | "READY";
76
76
  runtimeStatus: "NOT_APPLICABLE";
@@ -95,7 +95,7 @@ export declare const behaviorAdapter: {
95
95
  result: {
96
96
  contract: "deployment.result.v1";
97
97
  moduleRef: "execution-browser-extension";
98
- moduleVersion: "0.1.28";
98
+ moduleVersion: "0.1.30";
99
99
  ok: false;
100
100
  status: "ACTION_REQUIRED";
101
101
  data: {
@@ -127,14 +127,14 @@ export declare const behaviorAdapter: {
127
127
  readonly ok: true;
128
128
  readonly status: "SUCCEEDED";
129
129
  readonly moduleRef: "execution-browser-extension";
130
- readonly moduleVersion: "0.1.28";
130
+ readonly moduleVersion: "0.1.30";
131
131
  };
132
132
  observedEffects: string[];
133
133
  } | {
134
134
  result: {
135
135
  contract: "deployment.result.v1";
136
136
  moduleRef: "execution-browser-extension";
137
- moduleVersion: "0.1.28";
137
+ moduleVersion: "0.1.30";
138
138
  ok: false;
139
139
  status: "FAILED";
140
140
  error: {
@@ -167,7 +167,7 @@ export declare const behaviorAdapter: {
167
167
  ok: true;
168
168
  status: "SUCCEEDED";
169
169
  moduleRef: "execution-browser-extension";
170
- moduleVersion: "0.1.28";
170
+ moduleVersion: "0.1.30";
171
171
  data: {
172
172
  docs: string;
173
173
  };
@@ -180,11 +180,11 @@ export declare const behaviorAdapter: {
180
180
  readonly ok: true;
181
181
  readonly status: "SUCCEEDED";
182
182
  readonly moduleRef: "execution-browser-extension";
183
- readonly moduleVersion: "0.1.28";
183
+ readonly moduleVersion: "0.1.30";
184
184
  } | {
185
185
  contract: "deployment.result.v1";
186
186
  moduleRef: "execution-browser-extension";
187
- moduleVersion: "0.1.28";
187
+ moduleVersion: "0.1.30";
188
188
  ok: false;
189
189
  status: "FAILED";
190
190
  error: {
@@ -201,7 +201,7 @@ export declare const behaviorAdapter: {
201
201
  readonly ok: true;
202
202
  readonly status: "SUCCEEDED";
203
203
  readonly moduleRef: "execution-browser-extension";
204
- readonly moduleVersion: "0.1.28";
204
+ readonly moduleVersion: "0.1.30";
205
205
  };
206
206
  observedEffects: never[];
207
207
  }>;
@@ -3,7 +3,7 @@ export declare const descriptor: {
3
3
  readonly contractVersion: "1.0.0";
4
4
  readonly moduleRef: "execution-browser-extension";
5
5
  readonly packageName: "@tomflow/proflow-execution-browser-extension";
6
- readonly moduleVersion: "0.1.28";
6
+ readonly moduleVersion: "0.1.30";
7
7
  readonly kind: "browser-extension";
8
8
  readonly templateVersion: "1.0.0";
9
9
  readonly platformCompatibility: ">=1.0.0 <2.0.0";
@@ -3,7 +3,7 @@ export const descriptor = {
3
3
  contractVersion: "1.0.0",
4
4
  moduleRef: "execution-browser-extension",
5
5
  packageName: "@tomflow/proflow-execution-browser-extension",
6
- moduleVersion: "0.1.28",
6
+ moduleVersion: "0.1.30",
7
7
  kind: "browser-extension",
8
8
  templateVersion: "1.0.0",
9
9
  platformCompatibility: ">=1.0.0 <2.0.0",
@@ -14780,16 +14780,37 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
14780
14780
  }
14781
14781
  });
14782
14782
  async function openTaskPage() {
14783
- const taskPageUrl = chrome.runtime.getURL("extension/tasks.html");
14784
- const [existing] = await chrome.tabs.query({
14785
- url: taskPageUrl,
14783
+ const bridge = await bridgeConfig();
14784
+ if (bridge) {
14785
+ const webUrl = `${bridge.endpoint}/tasks`;
14786
+ const [existing] = await chrome.tabs.query({ url: webUrl, currentWindow: true });
14787
+ if (existing?.id !== void 0) {
14788
+ await chrome.tabs.update(existing.id, { active: true });
14789
+ return;
14790
+ }
14791
+ try {
14792
+ const response = await fetch(`${bridge.endpoint}/v1/tasks/session`, {
14793
+ method: "POST",
14794
+ headers: { authorization: `Bearer ${bridge.token}` }
14795
+ });
14796
+ const body = await response.json();
14797
+ if (response.ok && isRecord(body) && typeof body.url === "string" && body.url.startsWith(`${bridge.endpoint}/tasks/bootstrap/`)) {
14798
+ await chrome.tabs.create({ url: body.url, active: true });
14799
+ return;
14800
+ }
14801
+ } catch {
14802
+ }
14803
+ }
14804
+ const fallbackUrl = chrome.runtime.getURL("extension/tasks.html");
14805
+ const [fallback] = await chrome.tabs.query({
14806
+ url: fallbackUrl,
14786
14807
  currentWindow: true
14787
14808
  });
14788
- if (existing?.id !== void 0) {
14789
- await chrome.tabs.update(existing.id, { active: true });
14809
+ if (fallback?.id !== void 0) {
14810
+ await chrome.tabs.update(fallback.id, { active: true });
14790
14811
  return;
14791
14812
  }
14792
- await chrome.tabs.create({ url: taskPageUrl, active: true });
14813
+ await chrome.tabs.create({ url: fallbackUrl, active: true });
14793
14814
  }
14794
14815
  async function startBackgroundRuntime() {
14795
14816
  await bootstrapManagedRuntimeConfig();
@@ -1,4 +1,5 @@
1
1
  export {};
2
+ const extensionRuntime = typeof chrome !== "undefined" && chrome.runtime ? chrome.runtime : null;
2
3
  function element(selector) {
3
4
  const value = document.querySelector(selector);
4
5
  if (!value)
@@ -26,8 +27,21 @@ function record(value) {
26
27
  throw new Error("TASK_APPLICATION_RESPONSE_INVALID");
27
28
  return value;
28
29
  }
30
+ async function webApplication(path, operation, input) {
31
+ const response = await fetch(path, {
32
+ method: "POST",
33
+ headers: { "content-type": "application/json" },
34
+ body: JSON.stringify({ operation, input }),
35
+ });
36
+ const body = record(await response.json());
37
+ if (!response.ok || body.ok !== true)
38
+ throw new Error(typeof body.error === "string" ? body.error : "TASK_WEB_REQUEST_FAILED");
39
+ return body.value;
40
+ }
29
41
  async function taskApplication(operation, input) {
30
- const raw = await chrome.runtime.sendMessage({
42
+ if (!extensionRuntime)
43
+ return webApplication("/tasks/api/task", operation, input);
44
+ const raw = await extensionRuntime.sendMessage({
31
45
  type: "PROFLOW_TASK_APPLICATION",
32
46
  operation,
33
47
  input,
@@ -40,7 +54,9 @@ async function taskApplication(operation, input) {
40
54
  return response.value;
41
55
  }
42
56
  async function approvalApplication(operation, input) {
43
- const raw = await chrome.runtime.sendMessage({
57
+ if (!extensionRuntime)
58
+ return webApplication("/tasks/api/approval", operation, input);
59
+ const raw = await extensionRuntime.sendMessage({
44
60
  type: "PROFLOW_APPROVAL_APPLICATION",
45
61
  operation,
46
62
  input,
@@ -152,8 +168,17 @@ async function refreshTasks() {
152
168
  tasksTarget.append(row);
153
169
  }
154
170
  }
171
+ async function pageStatus() {
172
+ if (extensionRuntime)
173
+ return record(await extensionRuntime.sendMessage({ type: "PROFLOW_SIDE_PANEL_SNAPSHOT" }));
174
+ const response = await fetch("/tasks/api/status", { cache: "no-store" });
175
+ const body = record(await response.json());
176
+ if (!response.ok || body.ok !== true)
177
+ throw new Error(typeof body.error === "string" ? body.error : "TASK_WEB_STATUS_FAILED");
178
+ return record(body.value);
179
+ }
155
180
  async function refreshBrowserStatus() {
156
- const snapshot = record(await chrome.runtime.sendMessage({ type: "PROFLOW_SIDE_PANEL_SNAPSHOT" }));
181
+ const snapshot = await pageStatus();
157
182
  connection.textContent =
158
183
  snapshot.taskApplicationConfigured === true &&
159
184
  snapshot.approvalApplicationConfigured === true
@@ -1,4 +1,10 @@
1
1
  import type { BrowserRealityPort } from "./index.ts";
2
+ export interface BrowserRealityBridgeTaskWebOptions {
3
+ html: string;
4
+ script: string;
5
+ invokeTask(operation: string, input: Record<string, unknown>): Promise<unknown>;
6
+ invokeApproval(operation: string, input: Record<string, unknown>): Promise<unknown>;
7
+ }
2
8
  export interface BrowserRealityBridgeOptions {
3
9
  token: string;
4
10
  extensionId: string;
@@ -8,6 +14,7 @@ export interface BrowserRealityBridgeOptions {
8
14
  commandTimeoutMs?: number;
9
15
  now?: () => Date;
10
16
  idFactory?: () => string;
17
+ taskWeb?: BrowserRealityBridgeTaskWebOptions;
11
18
  }
12
19
  export declare class BrowserRealityBridgeError extends Error {
13
20
  readonly code: "BRIDGE_AUTH_INVALID" | "BRIDGE_INPUT_INVALID" | "BRIDGE_OFFLINE" | "BRIDGE_COMMAND_TIMEOUT" | "BRIDGE_COMMAND_FAILED";
@@ -82,6 +82,24 @@ function send(response, status, value) {
82
82
  response.writeHead(status, jsonHeaders);
83
83
  response.end(value === undefined ? "" : JSON.stringify(value));
84
84
  }
85
+ function sendText(response, status, contentType, value) {
86
+ response.writeHead(status, {
87
+ "content-type": contentType,
88
+ "cache-control": "no-store",
89
+ "content-security-policy": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self'; img-src 'self' data:",
90
+ "x-content-type-options": "nosniff",
91
+ "referrer-policy": "no-referrer",
92
+ });
93
+ response.end(value);
94
+ }
95
+ function cookieValue(request, name) {
96
+ for (const part of (request.headers.cookie ?? "").split(";")) {
97
+ const [key, ...rest] = part.trim().split("=");
98
+ if (key === name)
99
+ return rest.join("=");
100
+ }
101
+ return undefined;
102
+ }
85
103
  export async function createBrowserRealityBridgeServer(options) {
86
104
  if (options.token.length < 32)
87
105
  throw new TypeError("bridge token must contain at least 32 characters");
@@ -96,6 +114,26 @@ export async function createBrowserRealityBridgeServer(options) {
96
114
  const pending = new Map();
97
115
  let session;
98
116
  let closed = false;
117
+ let endpoint = "";
118
+ const taskBootstrap = new Map();
119
+ const taskSessions = new Map();
120
+ const taskCookie = "proflow_tasks_session";
121
+ const taskBootstrapTtlMs = 60_000;
122
+ const taskSessionTtlMs = 8 * 60 * 60_000;
123
+ const pruneTaskWebState = () => {
124
+ const current = now().getTime();
125
+ for (const [key, expiresAt] of taskBootstrap)
126
+ if (expiresAt <= current)
127
+ taskBootstrap.delete(key);
128
+ for (const [key, expiresAt] of taskSessions)
129
+ if (expiresAt <= current)
130
+ taskSessions.delete(key);
131
+ };
132
+ const taskSessionValid = (request) => {
133
+ pruneTaskWebState();
134
+ const value = cookieValue(request, taskCookie);
135
+ return value !== undefined && taskSessions.has(value);
136
+ };
99
137
  const authenticate = (request) => {
100
138
  const authorization = request.headers.authorization;
101
139
  const origin = request.headers.origin;
@@ -106,6 +144,70 @@ export async function createBrowserRealityBridgeServer(options) {
106
144
  };
107
145
  const server = createServer(async (request, response) => {
108
146
  try {
147
+ const url = new URL(request.url ?? "/", "http://127.0.0.1");
148
+ if (options.taskWeb && request.method === "GET" && url.pathname.startsWith("/tasks/bootstrap/")) {
149
+ pruneTaskWebState();
150
+ const bootstrap = decodeURIComponent(url.pathname.slice("/tasks/bootstrap/".length));
151
+ if (!taskBootstrap.has(bootstrap)) {
152
+ send(response, 401, { error: "TASK_WEB_SESSION_INVALID" });
153
+ return;
154
+ }
155
+ taskBootstrap.delete(bootstrap);
156
+ const sessionId = idFactory();
157
+ taskSessions.set(sessionId, now().getTime() + taskSessionTtlMs);
158
+ response.writeHead(302, {
159
+ location: "/tasks",
160
+ "cache-control": "no-store",
161
+ "set-cookie": `${taskCookie}=${sessionId}; HttpOnly; SameSite=Strict; Path=/tasks; Max-Age=${Math.floor(taskSessionTtlMs / 1000)}`,
162
+ });
163
+ response.end();
164
+ return;
165
+ }
166
+ if (options.taskWeb && url.pathname.startsWith("/tasks")) {
167
+ if (!taskSessionValid(request)) {
168
+ send(response, 401, { error: "TASK_WEB_SESSION_REQUIRED" });
169
+ return;
170
+ }
171
+ if (request.method === "GET" && url.pathname === "/tasks") {
172
+ sendText(response, 200, "text/html; charset=utf-8", options.taskWeb.html);
173
+ return;
174
+ }
175
+ if (request.method === "GET" && url.pathname === "/tasks.js") {
176
+ sendText(response, 200, "text/javascript; charset=utf-8", options.taskWeb.script);
177
+ return;
178
+ }
179
+ if (request.method === "GET" && url.pathname === "/tasks/api/status") {
180
+ send(response, 200, {
181
+ ok: true,
182
+ value: {
183
+ taskApplicationConfigured: true,
184
+ approvalApplicationConfigured: true,
185
+ systemObserver: null,
186
+ browserCarrier: {
187
+ online: session !== undefined && now().getTime() - session.lastHeartbeatAt <= freshnessMs,
188
+ extensionInstanceId: session?.extensionInstanceId ?? null,
189
+ },
190
+ },
191
+ });
192
+ return;
193
+ }
194
+ if (request.method === "POST" && (url.pathname === "/tasks/api/task" || url.pathname === "/tasks/api/approval")) {
195
+ if (request.headers.origin !== endpoint) {
196
+ send(response, 403, { error: "TASK_WEB_ORIGIN_INVALID" });
197
+ return;
198
+ }
199
+ const body = await readJson(request);
200
+ if (!isRecord(body) || typeof body.operation !== "string" || !isRecord(body.input))
201
+ throw new BrowserRealityBridgeError("BRIDGE_INPUT_INVALID", "task web request is invalid");
202
+ const value = url.pathname.endsWith("/task")
203
+ ? await options.taskWeb.invokeTask(body.operation, body.input)
204
+ : await options.taskWeb.invokeApproval(body.operation, body.input);
205
+ send(response, 200, { ok: true, value });
206
+ return;
207
+ }
208
+ send(response, 404, { error: "NOT_FOUND" });
209
+ return;
210
+ }
109
211
  response.setHeader("access-control-allow-origin", expectedOrigin);
110
212
  response.setHeader("vary", "origin");
111
213
  if (request.method === "OPTIONS") {
@@ -116,7 +218,13 @@ export async function createBrowserRealityBridgeServer(options) {
116
218
  return;
117
219
  }
118
220
  authenticate(request);
119
- const url = new URL(request.url ?? "/", "http://127.0.0.1");
221
+ if (options.taskWeb && request.method === "POST" && url.pathname === "/v1/tasks/session") {
222
+ pruneTaskWebState();
223
+ const bootstrap = idFactory();
224
+ taskBootstrap.set(bootstrap, now().getTime() + taskBootstrapTtlMs);
225
+ send(response, 200, { url: `${endpoint}/tasks/bootstrap/${encodeURIComponent(bootstrap)}` });
226
+ return;
227
+ }
120
228
  if (request.method === "POST" && url.pathname === "/v1/session/hello") {
121
229
  const body = await readJson(request);
122
230
  if (!isRecord(body) ||
@@ -200,7 +308,7 @@ export async function createBrowserRealityBridgeServer(options) {
200
308
  const address = server.address();
201
309
  if (!address || typeof address === "string")
202
310
  throw new Error("bridge address missing");
203
- const endpoint = `http://127.0.0.1:${address.port}`;
311
+ endpoint = `http://127.0.0.1:${address.port}`;
204
312
  const online = () => !closed &&
205
313
  session !== undefined &&
206
314
  now().getTime() - session.lastHeartbeatAt <= freshnessMs;
@@ -290,6 +398,8 @@ export async function createBrowserRealityBridgeServer(options) {
290
398
  item.reject(new BrowserRealityBridgeError("BRIDGE_OFFLINE", "bridge server closed"));
291
399
  }
292
400
  pending.clear();
401
+ taskBootstrap.clear();
402
+ taskSessions.clear();
293
403
  queue.length = 0;
294
404
  await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
295
405
  },
package/dist/src/index.js CHANGED
@@ -238,8 +238,8 @@ export function createExecutionBrowserExtension(options) {
238
238
  if (roleIdentity.roleRef !== request.input.roleRef ||
239
239
  roleIdentity.workerRef)
240
240
  throw new ExecutionBrowserError("PRECONDITION_FAILED", "ROLE_URL_MISMATCH");
241
- const precondition = await effectStarted(raw);
242
241
  const opened = await options.browser.open(request.input.roleUrl);
242
+ const precondition = await effectStarted(raw);
243
243
  const submitted = await options.browser.submit(opened.tabId, `WORKER_BIND ${request.input.bootstrapFingerprint}`, request.input.bootstrapFingerprint);
244
244
  if (!(await options.browser.hasMessage(submitted.tabId, request.input.bootstrapFingerprint)))
245
245
  throw new ExecutionBrowserError("UNKNOWN_SIDE_EFFECT", "CREATE_MESSAGE_REALITY_UNCONFIRMED");
@@ -1,7 +1,12 @@
1
1
  import { readFile, stat } from "node:fs/promises";
2
- import { dirname, resolve } from "node:path";
2
+ import { basename, dirname, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
3
4
  import { createBrowserRealityBridgeServer, } from "./bridge.js";
4
5
  import { createExecutionBrowserExtension, } from "./index.js";
6
+ function packageRoot() {
7
+ const candidate = dirname(dirname(fileURLToPath(import.meta.url)));
8
+ return basename(candidate) === "dist" ? dirname(candidate) : candidate;
9
+ }
5
10
  function record(value, name) {
6
11
  if (typeof value !== "object" || value === null || Array.isArray(value))
7
12
  throw new TypeError(`${name} must be an object`);
@@ -52,8 +57,8 @@ function applicationConfig(input) {
52
57
  throw new TypeError("platformHost token must contain at least 32 characters");
53
58
  return { endpoint: input.endpoint.replace(/\/$/, ""), token: input.token };
54
59
  }
55
- async function invokeApplication(config, operation, input) {
56
- const response = await fetch(`${config.endpoint}/application/observer`, {
60
+ async function invokeOwnerApplication(config, surface, operation, input) {
61
+ const response = await fetch(`${config.endpoint}/application/${surface}`, {
57
62
  method: "POST",
58
63
  headers: {
59
64
  authorization: `Bearer ${config.token}`,
@@ -71,6 +76,9 @@ async function invokeApplication(config, operation, input) {
71
76
  : "BROWSER_OWNER_TRANSPORT_FAILED");
72
77
  return body;
73
78
  }
79
+ async function invokeApplication(config, operation, input) {
80
+ return invokeOwnerApplication(config, "observer", operation, input);
81
+ }
74
82
  /**
75
83
  * Node-side Browser Executor composition. This deliberately does NOT create an
76
84
  * Execution Runtime process. The single formal execution-runtime binary owns
@@ -78,7 +86,18 @@ async function invokeApplication(config, operation, input) {
78
86
  */
79
87
  export async function createBrowserExecutorComposition(options) {
80
88
  const platformHost = applicationConfig(options.platformHost);
81
- const bridge = await createBrowserRealityBridgeServer(options.bridge);
89
+ const root = packageRoot();
90
+ const taskHtml = (await readFile(resolve(root, "extension/tasks.html"), "utf8")).replace("../dist/extension/tasks.js", "/tasks.js");
91
+ const taskScript = await readFile(resolve(root, "dist/extension/tasks.js"), "utf8");
92
+ const bridge = await createBrowserRealityBridgeServer({
93
+ ...options.bridge,
94
+ taskWeb: {
95
+ html: taskHtml,
96
+ script: taskScript,
97
+ invokeTask: (operation, input) => invokeOwnerApplication(platformHost, "task", operation, input),
98
+ invokeApproval: (operation, input) => invokeOwnerApplication(platformHost, "approval", operation, input),
99
+ },
100
+ });
82
101
  try {
83
102
  const browserExecutor = createExecutionBrowserExtension({
84
103
  browser: bridge.browser,
@@ -1318,16 +1318,43 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
1318
1318
  });
1319
1319
 
1320
1320
  async function openTaskPage(): Promise<void> {
1321
- const taskPageUrl = chrome.runtime.getURL("extension/tasks.html");
1322
- const [existing] = await chrome.tabs.query({
1323
- url: taskPageUrl,
1321
+ const bridge = await bridgeConfig();
1322
+ if (bridge) {
1323
+ const webUrl = `${bridge.endpoint}/tasks`;
1324
+ const [existing] = await chrome.tabs.query({ url: webUrl, currentWindow: true });
1325
+ if (existing?.id !== undefined) {
1326
+ await chrome.tabs.update(existing.id, { active: true });
1327
+ return;
1328
+ }
1329
+ try {
1330
+ const response = await fetch(`${bridge.endpoint}/v1/tasks/session`, {
1331
+ method: "POST",
1332
+ headers: { authorization: `Bearer ${bridge.token}` },
1333
+ });
1334
+ const body = (await response.json()) as unknown;
1335
+ if (
1336
+ response.ok &&
1337
+ isRecord(body) &&
1338
+ typeof body.url === "string" &&
1339
+ body.url.startsWith(`${bridge.endpoint}/tasks/bootstrap/`)
1340
+ ) {
1341
+ await chrome.tabs.create({ url: body.url, active: true });
1342
+ return;
1343
+ }
1344
+ } catch {
1345
+ /* bridge-unavailable fallback keeps the extension-owned Task UI reachable */
1346
+ }
1347
+ }
1348
+ const fallbackUrl = chrome.runtime.getURL("extension/tasks.html");
1349
+ const [fallback] = await chrome.tabs.query({
1350
+ url: fallbackUrl,
1324
1351
  currentWindow: true,
1325
1352
  });
1326
- if (existing?.id !== undefined) {
1327
- await chrome.tabs.update(existing.id, { active: true });
1353
+ if (fallback?.id !== undefined) {
1354
+ await chrome.tabs.update(fallback.id, { active: true });
1328
1355
  return;
1329
1356
  }
1330
- await chrome.tabs.create({ url: taskPageUrl, active: true });
1357
+ await chrome.tabs.create({ url: fallbackUrl, active: true });
1331
1358
  }
1332
1359
 
1333
1360
  async function startBackgroundRuntime(): Promise<void> {
@@ -5,6 +5,9 @@ type ChromePanel = {
5
5
  };
6
6
  declare const chrome: ChromePanel;
7
7
 
8
+ const extensionRuntime =
9
+ typeof chrome !== "undefined" && chrome.runtime ? chrome.runtime : null;
10
+
8
11
  type TaskSummary = {
9
12
  taskId: string;
10
13
  title: string;
@@ -76,11 +79,30 @@ function record(value: unknown): Record<string, unknown> {
76
79
  return value as Record<string, unknown>;
77
80
  }
78
81
 
82
+ async function webApplication(
83
+ path: "/tasks/api/task" | "/tasks/api/approval",
84
+ operation: string,
85
+ input: Record<string, unknown>,
86
+ ): Promise<unknown> {
87
+ const response = await fetch(path, {
88
+ method: "POST",
89
+ headers: { "content-type": "application/json" },
90
+ body: JSON.stringify({ operation, input }),
91
+ });
92
+ const body = record(await response.json());
93
+ if (!response.ok || body.ok !== true)
94
+ throw new Error(
95
+ typeof body.error === "string" ? body.error : "TASK_WEB_REQUEST_FAILED",
96
+ );
97
+ return body.value;
98
+ }
99
+
79
100
  async function taskApplication(
80
101
  operation: string,
81
102
  input: Record<string, unknown>,
82
103
  ): Promise<unknown> {
83
- const raw = await chrome.runtime.sendMessage({
104
+ if (!extensionRuntime) return webApplication("/tasks/api/task", operation, input);
105
+ const raw = await extensionRuntime.sendMessage({
84
106
  type: "PROFLOW_TASK_APPLICATION",
85
107
  operation,
86
108
  input,
@@ -99,7 +121,9 @@ async function approvalApplication(
99
121
  operation: string,
100
122
  input: Record<string, unknown>,
101
123
  ): Promise<unknown> {
102
- const raw = await chrome.runtime.sendMessage({
124
+ if (!extensionRuntime)
125
+ return webApplication("/tasks/api/approval", operation, input);
126
+ const raw = await extensionRuntime.sendMessage({
103
127
  type: "PROFLOW_APPROVAL_APPLICATION",
104
128
  operation,
105
129
  input,
@@ -227,10 +251,22 @@ async function refreshTasks() {
227
251
  }
228
252
  }
229
253
 
254
+ async function pageStatus(): Promise<Record<string, unknown>> {
255
+ if (extensionRuntime)
256
+ return record(
257
+ await extensionRuntime.sendMessage({ type: "PROFLOW_SIDE_PANEL_SNAPSHOT" }),
258
+ );
259
+ const response = await fetch("/tasks/api/status", { cache: "no-store" });
260
+ const body = record(await response.json());
261
+ if (!response.ok || body.ok !== true)
262
+ throw new Error(
263
+ typeof body.error === "string" ? body.error : "TASK_WEB_STATUS_FAILED",
264
+ );
265
+ return record(body.value);
266
+ }
267
+
230
268
  async function refreshBrowserStatus() {
231
- const snapshot = record(
232
- await chrome.runtime.sendMessage({ type: "PROFLOW_SIDE_PANEL_SNAPSHOT" }),
233
- );
269
+ const snapshot = await pageStatus();
234
270
  connection.textContent =
235
271
  snapshot.taskApplicationConfigured === true &&
236
272
  snapshot.approvalApplicationConfigured === true
package/manifest.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "ProFlow Execution Browser",
4
- "version": "0.1.28",
4
+ "version": "0.1.30",
5
5
  "permissions": [
6
6
  "tabs",
7
7
  "storage"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tomflow/proflow-execution-browser-extension",
3
- "version": "0.1.28",
3
+ "version": "0.1.30",
4
4
  "description": "Execution-owned MV3 Browser executor, evidence provider and browser application surface.",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -27,12 +27,12 @@
27
27
  "SETUP.md"
28
28
  ],
29
29
  "dependencies": {
30
- "@tomflow/proflow-module-contract": "^0.1.13",
31
- "@tomflow/proflow-execution-contracts": "^0.1.10"
30
+ "@tomflow/proflow-execution-contracts": "^0.1.10",
31
+ "@tomflow/proflow-module-contract": "^0.1.13"
32
32
  },
33
33
  "devDependencies": {
34
- "@tomflow/proflow-execution-runtime": "^0.1.15",
35
- "@tomflow/proflow-deployment-conformance": "^0.1.13"
34
+ "@tomflow/proflow-deployment-conformance": "^0.1.13",
35
+ "@tomflow/proflow-execution-runtime": "^0.1.15"
36
36
  },
37
37
  "keywords": [
38
38
  "proflow",
@@ -3,7 +3,7 @@
3
3
  "contractVersion": "1.0.0",
4
4
  "moduleRef": "execution-browser-extension",
5
5
  "packageName": "@tomflow/proflow-execution-browser-extension",
6
- "moduleVersion": "0.1.28",
6
+ "moduleVersion": "0.1.30",
7
7
  "kind": "browser-extension",
8
8
  "templateVersion": "1.0.0",
9
9
  "platformCompatibility": ">=1.0.0 <2.0.0",