@tomflow/proflow-execution-browser-extension 0.1.29 → 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.
- package/dist/deployment/adapter.d.ts +10 -10
- package/dist/deployment/descriptor.d.ts +1 -1
- package/dist/deployment/descriptor.js +1 -1
- package/dist/extension/background.js +27 -6
- package/dist/extension/tasks.js +28 -3
- package/dist/src/bridge.d.ts +7 -0
- package/dist/src/bridge.js +112 -2
- package/dist/src/runtime-composition.js +23 -4
- package/extension/background.ts +33 -6
- package/extension/tasks.ts +41 -5
- package/manifest.json +1 -1
- package/package.json +1 -1
- package/proflow.module.json +1 -1
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
183
|
+
readonly moduleVersion: "0.1.30";
|
|
184
184
|
} | {
|
|
185
185
|
contract: "deployment.result.v1";
|
|
186
186
|
moduleRef: "execution-browser-extension";
|
|
187
|
-
moduleVersion: "0.1.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
14784
|
-
|
|
14785
|
-
|
|
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 (
|
|
14789
|
-
await chrome.tabs.update(
|
|
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:
|
|
14813
|
+
await chrome.tabs.create({ url: fallbackUrl, active: true });
|
|
14793
14814
|
}
|
|
14794
14815
|
async function startBackgroundRuntime() {
|
|
14795
14816
|
await bootstrapManagedRuntimeConfig();
|
package/dist/extension/tasks.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 =
|
|
181
|
+
const snapshot = await pageStatus();
|
|
157
182
|
connection.textContent =
|
|
158
183
|
snapshot.taskApplicationConfigured === true &&
|
|
159
184
|
snapshot.approvalApplicationConfigured === true
|
package/dist/src/bridge.d.ts
CHANGED
|
@@ -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";
|
package/dist/src/bridge.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
},
|
|
@@ -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
|
|
56
|
-
const response = await fetch(`${config.endpoint}/application
|
|
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
|
|
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,
|
package/extension/background.ts
CHANGED
|
@@ -1318,16 +1318,43 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|
|
1318
1318
|
});
|
|
1319
1319
|
|
|
1320
1320
|
async function openTaskPage(): Promise<void> {
|
|
1321
|
-
const
|
|
1322
|
-
|
|
1323
|
-
|
|
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 (
|
|
1327
|
-
await chrome.tabs.update(
|
|
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:
|
|
1357
|
+
await chrome.tabs.create({ url: fallbackUrl, active: true });
|
|
1331
1358
|
}
|
|
1332
1359
|
|
|
1333
1360
|
async function startBackgroundRuntime(): Promise<void> {
|
package/extension/tasks.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
package/package.json
CHANGED
package/proflow.module.json
CHANGED
|
@@ -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.
|
|
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",
|