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

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.34";
50
+ moduleVersion: "0.1.36";
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.34";
63
+ readonly moduleVersion: "0.1.36";
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.34";
73
+ moduleVersion: "0.1.36";
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.34";
98
+ moduleVersion: "0.1.36";
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.34";
130
+ readonly moduleVersion: "0.1.36";
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.34";
137
+ moduleVersion: "0.1.36";
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.34";
170
+ moduleVersion: "0.1.36";
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.34";
183
+ readonly moduleVersion: "0.1.36";
184
184
  } | {
185
185
  contract: "deployment.result.v1";
186
186
  moduleRef: "execution-browser-extension";
187
- moduleVersion: "0.1.34";
187
+ moduleVersion: "0.1.36";
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.34";
204
+ readonly moduleVersion: "0.1.36";
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.34";
6
+ readonly moduleVersion: "0.1.36";
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.34",
6
+ moduleVersion: "0.1.36",
7
7
  kind: "browser-extension",
8
8
  templateVersion: "1.0.0",
9
9
  platformCompatibility: ">=1.0.0 <2.0.0",
@@ -13878,6 +13878,7 @@ var extensionInstanceId = `extension:${crypto.randomUUID()}`;
13878
13878
  var sessions = /* @__PURE__ */ new Map();
13879
13879
  var BROWSER_CARRIER_KEEPALIVE_KEY = "proflowBrowserSnapshot";
13880
13880
  var BROWSER_CARRIER_KEEPALIVE_MS = 2e4;
13881
+ var BROWSER_BRIDGE_FETCH_TIMEOUT_MS = 5e3;
13881
13882
  var sleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
13882
13883
  async function persistSnapshot() {
13883
13884
  await chrome.storage.session.set({
@@ -14491,6 +14492,7 @@ async function executeProvisioningCommand(command) {
14491
14492
  async function bridgeFetch(config2, path, init = {}) {
14492
14493
  return fetch(`${config2.endpoint}${path}`, {
14493
14494
  ...init,
14495
+ signal: init.signal ?? AbortSignal.timeout(BROWSER_BRIDGE_FETCH_TIMEOUT_MS),
14494
14496
  headers: {
14495
14497
  authorization: `Bearer ${config2.token}`,
14496
14498
  "content-type": "application/json",
@@ -14503,7 +14505,7 @@ async function runBridgeLoop() {
14503
14505
  if (bridgeLoopStarted) return;
14504
14506
  bridgeLoopStarted = true;
14505
14507
  while (true) {
14506
- const config2 = await bridgeConfig();
14508
+ const config2 = await bridgeConfig().catch(() => null);
14507
14509
  if (!config2) {
14508
14510
  await sleep(1e3);
14509
14511
  continue;
@@ -14518,8 +14520,8 @@ async function runBridgeLoop() {
14518
14520
  })
14519
14521
  });
14520
14522
  if (!hello.ok) throw new Error("BRIDGE_HELLO_REJECTED");
14521
- let lastHeartbeatAt = 0;
14522
- let lastExtensionKeepaliveAt = 0;
14523
+ let lastHeartbeatAt = Date.now();
14524
+ let lastExtensionKeepaliveAt = Date.now();
14523
14525
  while (true) {
14524
14526
  if (Date.now() - lastExtensionKeepaliveAt >= BROWSER_CARRIER_KEEPALIVE_MS) {
14525
14527
  await chrome.storage.session.get(BROWSER_CARRIER_KEEPALIVE_KEY).catch(() => ({}));
@@ -25,6 +25,8 @@ export declare function createBrowserRealityBridgeServer(options: BrowserReality
25
25
  browser: BrowserRealityPort;
26
26
  status(): {
27
27
  online: boolean;
28
+ sessionOnline: boolean;
29
+ commandConsumerReady: boolean;
28
30
  extensionInstanceId: string | null;
29
31
  queuedCommands: number;
30
32
  pendingCommands: number;
@@ -118,6 +118,7 @@ export async function createBrowserRealityBridgeServer(options) {
118
118
  let lastCommandPollAt = null;
119
119
  let lastCommandDeliveredAt = null;
120
120
  let lastCommandResultAt = null;
121
+ let lastCommandConsumerAt = null;
121
122
  let closed = false;
122
123
  let endpoint = "";
123
124
  const taskBootstrap = new Map();
@@ -125,6 +126,13 @@ export async function createBrowserRealityBridgeServer(options) {
125
126
  const taskCookie = "proflow_tasks_session";
126
127
  const taskBootstrapTtlMs = 60_000;
127
128
  const taskSessionTtlMs = 8 * 60 * 60_000;
129
+ const sessionOnline = () => !closed &&
130
+ session !== undefined &&
131
+ now().getTime() - session.lastHeartbeatAt <= freshnessMs;
132
+ const commandConsumerReady = () => sessionOnline() &&
133
+ lastCommandConsumerAt !== null &&
134
+ now().getTime() - lastCommandConsumerAt <= freshnessMs;
135
+ const online = commandConsumerReady;
128
136
  const pruneTaskWebState = () => {
129
137
  const current = now().getTime();
130
138
  for (const [key, expiresAt] of taskBootstrap)
@@ -144,9 +152,13 @@ export async function createBrowserRealityBridgeServer(options) {
144
152
  const origin = request.headers.origin;
145
153
  if (!authorization?.startsWith("Bearer ") ||
146
154
  !safeEqual(authorization.slice(7), options.token) ||
147
- origin !== expectedOrigin)
155
+ (origin !== undefined && origin !== expectedOrigin))
148
156
  throw new BrowserRealityBridgeError("BRIDGE_AUTH_INVALID", "bridge authentication failed");
149
157
  };
158
+ const requireExtensionOrigin = (request) => {
159
+ if (request.headers.origin !== expectedOrigin)
160
+ throw new BrowserRealityBridgeError("BRIDGE_AUTH_INVALID", "bridge extension origin is required");
161
+ };
150
162
  const server = createServer(async (request, response) => {
151
163
  try {
152
164
  const url = new URL(request.url ?? "/", "http://127.0.0.1");
@@ -189,7 +201,9 @@ export async function createBrowserRealityBridgeServer(options) {
189
201
  approvalApplicationConfigured: true,
190
202
  systemObserver: null,
191
203
  browserCarrier: {
192
- online: session !== undefined && now().getTime() - session.lastHeartbeatAt <= freshnessMs,
204
+ online: commandConsumerReady(),
205
+ sessionOnline: sessionOnline(),
206
+ commandConsumerReady: commandConsumerReady(),
193
207
  extensionInstanceId: session?.extensionInstanceId ?? null,
194
208
  queuedCommands: queue.length,
195
209
  pendingCommands: pending.size,
@@ -236,6 +250,7 @@ export async function createBrowserRealityBridgeServer(options) {
236
250
  return;
237
251
  }
238
252
  if (request.method === "POST" && url.pathname === "/v1/session/hello") {
253
+ requireExtensionOrigin(request);
239
254
  const body = await readJson(request);
240
255
  if (!isRecord(body) ||
241
256
  stringField(body, "extensionId") !== options.extensionId)
@@ -245,6 +260,7 @@ export async function createBrowserRealityBridgeServer(options) {
245
260
  lastCommandPollAt = null;
246
261
  lastCommandDeliveredAt = null;
247
262
  lastCommandResultAt = null;
263
+ lastCommandConsumerAt = null;
248
264
  }
249
265
  session = {
250
266
  extensionInstanceId,
@@ -254,10 +270,10 @@ export async function createBrowserRealityBridgeServer(options) {
254
270
  return;
255
271
  }
256
272
  if (request.method === "GET" && url.pathname === "/v1/session/status") {
257
- const online = session !== undefined &&
258
- now().getTime() - session.lastHeartbeatAt <= freshnessMs;
259
273
  send(response, 200, {
260
- online,
274
+ online: commandConsumerReady(),
275
+ sessionOnline: sessionOnline(),
276
+ commandConsumerReady: commandConsumerReady(),
261
277
  extensionInstanceId: session?.extensionInstanceId ?? null,
262
278
  });
263
279
  return;
@@ -276,6 +292,7 @@ export async function createBrowserRealityBridgeServer(options) {
276
292
  if (request.method === "GET" && url.pathname === "/v1/commands/next") {
277
293
  const stamp = now();
278
294
  session.lastHeartbeatAt = stamp.getTime();
295
+ lastCommandConsumerAt = stamp.getTime();
279
296
  lastCommandPollAt = stamp.toISOString();
280
297
  const command = queue.shift();
281
298
  if (command) {
@@ -297,7 +314,10 @@ export async function createBrowserRealityBridgeServer(options) {
297
314
  throw new BrowserRealityBridgeError("BRIDGE_INPUT_INVALID", "command result is stale or unknown");
298
315
  pending.delete(commandId);
299
316
  clearTimeout(command.timer);
300
- lastCommandResultAt = now().toISOString();
317
+ const resultStamp = now();
318
+ session.lastHeartbeatAt = resultStamp.getTime();
319
+ lastCommandConsumerAt = resultStamp.getTime();
320
+ lastCommandResultAt = resultStamp.toISOString();
301
321
  if (body.ok === true)
302
322
  command.resolve(body.value);
303
323
  else
@@ -329,12 +349,9 @@ export async function createBrowserRealityBridgeServer(options) {
329
349
  if (!address || typeof address === "string")
330
350
  throw new Error("bridge address missing");
331
351
  endpoint = `http://127.0.0.1:${address.port}`;
332
- const online = () => !closed &&
333
- session !== undefined &&
334
- now().getTime() - session.lastHeartbeatAt <= freshnessMs;
335
352
  const requestCommand = (command) => {
336
353
  if (!online())
337
- return Promise.reject(new BrowserRealityBridgeError("BRIDGE_OFFLINE", "extension heartbeat is not fresh"));
354
+ return Promise.reject(new BrowserRealityBridgeError("BRIDGE_OFFLINE", "extension command consumer is not ready"));
338
355
  const commandId = `browser-command:${idFactory()}`;
339
356
  return new Promise((resolve, reject) => {
340
357
  const timer = setTimeout(() => {
@@ -406,6 +423,8 @@ export async function createBrowserRealityBridgeServer(options) {
406
423
  status() {
407
424
  return {
408
425
  online: online(),
426
+ sessionOnline: sessionOnline(),
427
+ commandConsumerReady: commandConsumerReady(),
409
428
  extensionInstanceId: session?.extensionInstanceId ?? null,
410
429
  queuedCommands: queue.length,
411
430
  pendingCommands: pending.size,
@@ -34,9 +34,9 @@ function text(value) {
34
34
  }
35
35
  function extensionIdentity(request) {
36
36
  const origin = request.headers.origin;
37
- const match = typeof origin === "string"
38
- ? /^chrome-extension:\/\/([a-z]{32})$/.exec(origin)
39
- : null;
37
+ if (origin === undefined)
38
+ return undefined;
39
+ const match = /^chrome-extension:\/\/([a-z]{32})$/.exec(origin);
40
40
  if (!match)
41
41
  throw new Error("PAIRING_AUTH_INVALID");
42
42
  return match[1];
@@ -47,7 +47,6 @@ function authenticate(request, token) {
47
47
  !safeEqual(authorization.slice(7), token)) {
48
48
  throw new Error("PAIRING_AUTH_INVALID");
49
49
  }
50
- return extensionIdentity(request);
51
50
  }
52
51
  export async function createBrowserExtensionPairingServer(options) {
53
52
  if (options.token.length < 32) {
@@ -75,9 +74,13 @@ export async function createBrowserExtensionPairingServer(options) {
75
74
  const server = createServer(async (request, response) => {
76
75
  try {
77
76
  const originId = extensionIdentity(request);
78
- response.setHeader("access-control-allow-origin", `chrome-extension://${originId}`);
79
- response.setHeader("vary", "origin");
77
+ if (originId) {
78
+ response.setHeader("access-control-allow-origin", `chrome-extension://${originId}`);
79
+ response.setHeader("vary", "origin");
80
+ }
80
81
  if (request.method === "OPTIONS") {
82
+ if (!originId)
83
+ throw new Error("PAIRING_AUTH_INVALID");
81
84
  response.setHeader("access-control-allow-headers", "authorization, content-type");
82
85
  response.setHeader("access-control-allow-methods", "GET, POST, OPTIONS");
83
86
  response.writeHead(204);
@@ -90,7 +93,7 @@ export async function createBrowserExtensionPairingServer(options) {
90
93
  const body = await readJson(request);
91
94
  const extensionId = text(body.extensionId);
92
95
  const extensionInstanceId = text(body.extensionInstanceId);
93
- if (extensionId !== originId)
96
+ if (!originId || extensionId !== originId)
94
97
  throw new Error("PAIRING_AUTH_INVALID");
95
98
  if (identity && identity.extensionId !== extensionId) {
96
99
  throw new Error("PAIRING_AUTH_INVALID");
@@ -99,7 +102,8 @@ export async function createBrowserExtensionPairingServer(options) {
99
102
  send(response, 200, { accepted: true });
100
103
  return;
101
104
  }
102
- if (!identity || originId !== identity.extensionId) {
105
+ if (!identity ||
106
+ (originId !== undefined && originId !== identity.extensionId)) {
103
107
  throw new Error("PAIRING_AUTH_INVALID");
104
108
  }
105
109
  if (url.searchParams.get("extensionInstanceId") !==
@@ -88,6 +88,8 @@ export declare function createBrowserExecutorComposition(options: BrowserExecuto
88
88
  bridgeEndpoint: string;
89
89
  bridgeStatus: () => {
90
90
  online: boolean;
91
+ sessionOnline: boolean;
92
+ commandConsumerReady: boolean;
91
93
  extensionInstanceId: string | null;
92
94
  queuedCommands: number;
93
95
  pendingCommands: number;
@@ -143,6 +143,7 @@ const extensionInstanceId = `extension:${crypto.randomUUID()}`;
143
143
  const sessions = new Map<number, ContentObservation>();
144
144
  const BROWSER_CARRIER_KEEPALIVE_KEY = "proflowBrowserSnapshot";
145
145
  const BROWSER_CARRIER_KEEPALIVE_MS = 20_000;
146
+ const BROWSER_BRIDGE_FETCH_TIMEOUT_MS = 5_000;
146
147
 
147
148
  const sleep = (milliseconds: number) =>
148
149
  new Promise<void>((resolve) => setTimeout(resolve, milliseconds));
@@ -978,6 +979,7 @@ async function bridgeFetch(
978
979
  ): Promise<Response> {
979
980
  return fetch(`${config.endpoint}${path}`, {
980
981
  ...init,
982
+ signal: init.signal ?? AbortSignal.timeout(BROWSER_BRIDGE_FETCH_TIMEOUT_MS),
981
983
  headers: {
982
984
  authorization: `Bearer ${config.token}`,
983
985
  "content-type": "application/json",
@@ -991,7 +993,7 @@ async function runBridgeLoop() {
991
993
  if (bridgeLoopStarted) return;
992
994
  bridgeLoopStarted = true;
993
995
  while (true) {
994
- const config = await bridgeConfig();
996
+ const config = await bridgeConfig().catch(() => null);
995
997
  if (!config) {
996
998
  await sleep(1_000);
997
999
  continue;
@@ -1006,8 +1008,10 @@ async function runBridgeLoop() {
1006
1008
  }),
1007
1009
  });
1008
1010
  if (!hello.ok) throw new Error("BRIDGE_HELLO_REJECTED");
1009
- let lastHeartbeatAt = 0;
1010
- let lastExtensionKeepaliveAt = 0;
1011
+ // Hello only establishes a session. The first inner-loop action must be
1012
+ // a real command poll so Runtime readiness cannot be granted by hello alone.
1013
+ let lastHeartbeatAt = Date.now();
1014
+ let lastExtensionKeepaliveAt = Date.now();
1011
1015
  while (true) {
1012
1016
  if (
1013
1017
  Date.now() - lastExtensionKeepaliveAt >=
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.34",
4
+ "version": "0.1.36",
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.34",
3
+ "version": "0.1.36",
4
4
  "description": "Execution-owned MV3 Browser executor, evidence provider and browser application surface.",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -31,8 +31,8 @@
31
31
  "@tomflow/proflow-module-contract": "^0.1.13"
32
32
  },
33
33
  "devDependencies": {
34
- "@tomflow/proflow-deployment-conformance": "^0.1.13",
35
- "@tomflow/proflow-execution-runtime": "^0.1.16"
34
+ "@tomflow/proflow-execution-runtime": "^0.1.16",
35
+ "@tomflow/proflow-deployment-conformance": "^0.1.13"
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.34",
6
+ "moduleVersion": "0.1.36",
7
7
  "kind": "browser-extension",
8
8
  "templateVersion": "1.0.0",
9
9
  "platformCompatibility": ">=1.0.0 <2.0.0",