@tomflow/proflow-execution-browser-extension 0.1.11 → 0.1.13

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.
@@ -3,6 +3,7 @@ import { cp, mkdir, readFile, writeFile } from "node:fs/promises";
3
3
  import { basename, dirname, join, resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { deterministicLoopbackPort, ensureModuleSecretFile, moduleWorkspaceStateDirectory, readModuleSharedFacts, writeModuleSharedFacts, } from "@tomflow/proflow-module-contract";
6
+ import { createBrowserExtensionPairingServer } from "../src/pairing.js";
6
7
  import { descriptor } from "./descriptor.js";
7
8
  const base = {
8
9
  contract: "deployment.result.v1",
@@ -15,48 +16,27 @@ const setupPlan = {
15
16
  steps: [
16
17
  {
17
18
  id: "STEP-EXECUTION-BROWSER-EXTENSION-01",
18
- title: "准备扩展并打开 Chrome",
19
- description: "生成 unpacked 目录,复制路径并打开扩展管理页。",
19
+ title: "加载扩展并自动配对",
20
+ description: "准备 unpacked 目录和本地 Bridge,等待 Chrome Extension 的真实 hello + heartbeat。",
20
21
  state: "TODO",
21
22
  responsible: "USER",
22
23
  execution: {
23
- interactive: "pnpm exec -- proflow-execution-browser-extension setup 01",
24
- nonInteractive: "pnpm exec -- proflow-execution-browser-extension setup 01",
24
+ interactive: "pnpm exec -- proflow-execution-browser-extension setup",
25
+ nonInteractive: "pnpm exec -- proflow-execution-browser-extension setup",
25
26
  },
26
27
  requiredInputs: [],
27
- verify: "pnpm exec -- proflow-execution-browser-extension setup 01",
28
- successCondition: "扩展目录已生成并可选择",
29
- humanAction: "启用开发者模式并选择脚本复制的目录。",
30
- },
31
- {
32
- id: "STEP-EXECUTION-BROWSER-EXTENSION-02",
33
- title: "登记 Extension ID 并生成配置",
34
- description: "保存扩展 ID,生成 Bridge 配置并提示 Reload。",
35
- state: "TODO",
36
- responsible: "USER",
37
- execution: {
38
- interactive: "pnpm exec -- proflow-execution-browser-extension setup 02",
39
- nonInteractive: "pnpm exec -- proflow-execution-browser-extension setup 02 --extension-id <id>",
40
- },
41
- requiredInputs: [
42
- {
43
- name: "extensionId",
44
- description: "Chrome Extension ID",
45
- sensitive: false,
46
- },
47
- ],
48
28
  verify: "pnpm exec -- proflow-execution-browser-extension verify",
49
- successCondition: "Extension ID 和运行配置已保存",
50
- humanAction: "复制扩展 ID,并在配置生成后点击 Reload。",
29
+ successCondition: "Extension hello + heartbeat 已被本地 pairing listener 验证并持久化",
30
+ humanAction: "启用开发者模式并加载脚本准备的 unpacked 目录;其余身份发现与验证自动完成。",
51
31
  },
52
32
  {
53
- id: "STEP-EXECUTION-BROWSER-EXTENSION-03",
54
- title: "验证 Service Worker 与 Bridge",
55
- description: "重新观察扩展后台和本地 Bridge。",
33
+ id: "STEP-EXECUTION-BROWSER-EXTENSION-02",
34
+ title: "验证扩展部署状态",
35
+ description: "读取 heartbeat 形成的持久化部署证据。",
56
36
  state: "TODO",
57
37
  responsible: "AI",
58
38
  execution: {
59
- interactive: "pnpm exec -- proflow-execution-browser-extension setup 03",
39
+ interactive: "pnpm exec -- proflow-execution-browser-extension verify",
60
40
  nonInteractive: "pnpm exec -- proflow-execution-browser-extension verify",
61
41
  },
62
42
  requiredInputs: [],
@@ -68,18 +48,18 @@ const setupPlan = {
68
48
  const blockedSetupPlan = {
69
49
  steps: [
70
50
  {
71
- id: "STEP-EXECUTION-BROWSER-EXTENSION-04",
72
- title: "修复扩展配置",
51
+ id: "STEP-EXECUTION-BROWSER-EXTENSION-RECOVER",
52
+ title: "重新加载并自动配对扩展",
73
53
  state: "BLOCKED",
74
54
  responsible: "EXTERNAL",
75
55
  execution: {
76
56
  interactive: "pnpm exec -- proflow-execution-browser-extension setup",
77
- nonInteractive: "pnpm exec -- proflow-execution-browser-extension setup --extension-id <id>",
57
+ nonInteractive: "pnpm exec -- proflow-execution-browser-extension setup",
78
58
  },
79
59
  requiredInputs: [],
80
60
  verify: "pnpm exec -- proflow-execution-browser-extension verify",
81
61
  successCondition: "配置状态变为“已就绪”",
82
- blockedReason: "Extension ID 或生成配置无效",
62
+ blockedReason: "真实 Extension heartbeat 未到达或本地 pairing 失败",
83
63
  },
84
64
  ],
85
65
  };
@@ -176,7 +156,10 @@ async function readEvidence(context, loadDir) {
176
156
  raw.loadDir === loadDir &&
177
157
  typeof raw.extensionId === "string" &&
178
158
  /^[a-z]{32}$/.test(raw.extensionId) &&
159
+ typeof raw.extensionInstanceId === "string" &&
160
+ raw.extensionInstanceId.length > 0 &&
179
161
  raw.serviceWorker === "RUNNING" &&
162
+ raw.evidenceSource === "PAIRING_HEARTBEAT" &&
180
163
  typeof raw.observedAt === "string" &&
181
164
  !Number.isNaN(Date.parse(raw.observedAt))
182
165
  ? raw
@@ -186,28 +169,19 @@ async function readEvidence(context, loadDir) {
186
169
  return undefined;
187
170
  }
188
171
  }
189
- function input(context) {
190
- const value = context.input;
191
- if (typeof value !== "object" || value === null || Array.isArray(value))
192
- return {};
193
- const extensionId = Reflect.get(value, "extensionId"), serviceWorker = Reflect.get(value, "serviceWorker"), observedAt = Reflect.get(value, "observedAt");
194
- return {
195
- ...(typeof extensionId === "string" ? { extensionId } : {}),
196
- ...(serviceWorker === "RUNNING"
197
- ? { serviceWorker: "RUNNING" }
198
- : {}),
199
- ...(typeof observedAt === "string" ? { observedAt } : {}),
200
- };
201
- }
202
172
  async function ownFacts(context) {
203
173
  await mkdir(stateDir(context), { recursive: true, mode: 0o700 });
204
174
  const loadDir = browserExtensionLoadDir(context.workspaceRoot);
205
175
  const bridgeTokenFile = await ensureModuleSecretFile(context, descriptor.moduleRef, "bridge");
206
176
  const bridgePort = deterministicLoopbackPort(context, descriptor.moduleRef, "bridge");
177
+ const provisioningBridgeTokenFile = await ensureModuleSecretFile(context, descriptor.moduleRef, "provisioning");
178
+ const provisioningBridgePort = deterministicLoopbackPort(context, descriptor.moduleRef, "provisioning");
207
179
  const facts = {
208
180
  loadDir,
209
181
  bridgeTokenFile,
210
182
  bridgeEndpoint: `http://127.0.0.1:${bridgePort}`,
183
+ provisioningBridgeTokenFile,
184
+ provisioningBridgeEndpoint: `http://127.0.0.1:${provisioningBridgePort}`,
211
185
  verificationEvidenceFile: verificationFile(context),
212
186
  };
213
187
  const setup = await readSetup(context);
@@ -221,7 +195,7 @@ async function ownFacts(context) {
221
195
  await writeModuleSharedFacts(context, descriptor.moduleRef, facts);
222
196
  return facts;
223
197
  }
224
- async function materialize(context, setup) {
198
+ async function materializeRuntimeConfig(context) {
225
199
  const host = await readModuleSharedFacts(context, "platform-host");
226
200
  const endpoint = factString(host, "endpoint"), taskTokenFile = factString(host, "taskApplicationTokenFile"), approvalTokenFile = factString(host, "approvalApplicationTokenFile");
227
201
  if (!endpoint || !taskTokenFile || !approvalTokenFile)
@@ -230,11 +204,17 @@ async function materialize(context, setup) {
230
204
  const loadDir = String(facts.loadDir);
231
205
  const bridgeTokenFile = String(facts.bridgeTokenFile);
232
206
  const bridgeEndpoint = String(facts.bridgeEndpoint);
233
- const runtimeConfig = {
207
+ const provisioningBridgeTokenFile = String(facts.provisioningBridgeTokenFile);
208
+ const provisioningBridgeEndpoint = String(facts.provisioningBridgeEndpoint);
209
+ await writeFile(join(loadDir, "runtime-config.json"), `${JSON.stringify({
234
210
  proflowRuntimeBridge: {
235
211
  endpoint: bridgeEndpoint,
236
212
  token: await credential(bridgeTokenFile),
237
213
  },
214
+ proflowProvisioningBridge: {
215
+ endpoint: provisioningBridgeEndpoint,
216
+ token: await credential(provisioningBridgeTokenFile),
217
+ },
238
218
  proflowTaskApplication: {
239
219
  endpoint,
240
220
  token: await credential(taskTokenFile),
@@ -243,15 +223,91 @@ async function materialize(context, setup) {
243
223
  endpoint,
244
224
  token: await credential(approvalTokenFile),
245
225
  },
226
+ }, null, 2)}\n`, { mode: 0o600 });
227
+ return {
228
+ facts,
229
+ loadDir,
230
+ bridgeTokenFile,
231
+ bridgeEndpoint,
232
+ provisioningBridgeTokenFile,
233
+ provisioningBridgeEndpoint,
234
+ endpoint,
235
+ taskTokenFile,
246
236
  };
247
- await writeFile(join(loadDir, "runtime-config.json"), `${JSON.stringify(runtimeConfig, null, 2)}\n`, { mode: 0o600 });
248
- await writeFile(executorConfigFile(context), `${JSON.stringify({ platformHost: { endpoint, tokenFile: taskTokenFile }, bridge: { extensionId: setup.extensionId, tokenFile: bridgeTokenFile, host: "127.0.0.1", port: Number(new URL(bridgeEndpoint).port) } }, null, 2)}\n`, { mode: 0o600 });
237
+ }
238
+ async function materializeExecutorConfig(context, setup, prepared) {
239
+ const resolved = prepared ?? (await materializeRuntimeConfig(context));
240
+ await writeFile(executorConfigFile(context), `${JSON.stringify({ platformHost: { endpoint: resolved.endpoint, tokenFile: resolved.taskTokenFile }, bridge: { extensionId: setup.extensionId, tokenFile: resolved.bridgeTokenFile, host: "127.0.0.1", port: Number(new URL(resolved.bridgeEndpoint).port) } }, null, 2)}\n`, { mode: 0o600 });
249
241
  await writeModuleSharedFacts(context, descriptor.moduleRef, {
250
- ...facts,
242
+ ...resolved.facts,
251
243
  extensionId: setup.extensionId,
252
244
  browserExecutorConfigPath: executorConfigFile(context),
253
245
  });
254
246
  }
247
+ async function materialize(context, setup) {
248
+ const prepared = await materializeRuntimeConfig(context);
249
+ await materializeExecutorConfig(context, setup, prepared);
250
+ }
251
+ async function persistPairedBrowserExtension(context, prepared, identity) {
252
+ const setup = { extensionId: identity.extensionId };
253
+ await writeFile(setupFile(context), `${JSON.stringify(setup, null, 2)}\n`, {
254
+ mode: 0o600,
255
+ });
256
+ await materializeExecutorConfig(context, setup, prepared);
257
+ const evidence = {
258
+ contract: "proflow.browser-extension-verification.v1",
259
+ moduleVersion: descriptor.moduleVersion,
260
+ loadDir: prepared.loadDir,
261
+ extensionId: identity.extensionId,
262
+ extensionInstanceId: identity.extensionInstanceId,
263
+ serviceWorker: "RUNNING",
264
+ evidenceSource: "PAIRING_HEARTBEAT",
265
+ observedAt: new Date().toISOString(),
266
+ };
267
+ await writeFile(verificationFile(context), `${JSON.stringify(evidence, null, 2)}\n`, { mode: 0o600 });
268
+ return identity;
269
+ }
270
+ export async function pairBrowserExtensionSetup(context, options = {}) {
271
+ await mkdir(stateDir(context), { recursive: true, mode: 0o700 });
272
+ await installPackage(context);
273
+ const existingSetup = await readSetup(context);
274
+ const existingEvidence = await readEvidence(context, browserExtensionLoadDir(context.workspaceRoot));
275
+ const canRevalidateExisting = Boolean(existingSetup &&
276
+ existingEvidence &&
277
+ existingSetup.extensionId === existingEvidence.extensionId);
278
+ const prepared = await materializeRuntimeConfig(context);
279
+ const pairing = await createBrowserExtensionPairingServer({
280
+ token: await credential(prepared.bridgeTokenFile),
281
+ host: "127.0.0.1",
282
+ port: Number(new URL(prepared.bridgeEndpoint).port),
283
+ pairingTimeoutMs: options.timeoutMs ?? 120_000,
284
+ });
285
+ try {
286
+ const pairingPromise = pairing.waitForPairing();
287
+ if (canRevalidateExisting) {
288
+ const timeoutMs = options.timeoutMs ?? 120_000;
289
+ const revalidationGraceMs = Math.min(2_000, Math.max(25, Math.floor(timeoutMs / 4)));
290
+ const revalidated = await Promise.race([
291
+ pairingPromise.then((identity) => ({
292
+ kind: "PAIRED",
293
+ identity,
294
+ })),
295
+ new Promise((resolve) => setTimeout(() => resolve({ kind: "WAITING" }), revalidationGraceMs)),
296
+ ]);
297
+ if (revalidated.kind === "PAIRED") {
298
+ return persistPairedBrowserExtension(context, prepared, revalidated.identity);
299
+ }
300
+ }
301
+ await options.onWaiting?.({
302
+ loadDir: prepared.loadDir,
303
+ endpoint: pairing.endpoint,
304
+ });
305
+ return persistPairedBrowserExtension(context, prepared, await pairingPromise);
306
+ }
307
+ finally {
308
+ await pairing.close();
309
+ }
310
+ }
255
311
  const failed = (code, message, retryable = true) => ({
256
312
  ...base,
257
313
  ok: false,
@@ -274,14 +330,28 @@ export const behaviorAdapter = {
274
330
  const loadDir = browserExtensionLoadDir(context.workspaceRoot);
275
331
  const setup = await readSetup(context);
276
332
  const evidence = await readEvidence(context, loadDir);
333
+ const setupReady = Boolean(setup && evidence && setup.extensionId === evidence.extensionId);
277
334
  return {
278
335
  result: {
279
336
  ...base,
280
337
  data: {
281
- setupStatus: setup && evidence
338
+ setupStatus: setupReady
282
339
  ? "READY"
283
340
  : "ACTION_REQUIRED",
284
341
  runtimeStatus: evidence ? "RUNNING" : "STOPPED",
342
+ ...(setupReady
343
+ ? {}
344
+ : {
345
+ issues: [
346
+ {
347
+ scope: "SETUP",
348
+ code: "EXTENSION_LOAD_REQUIRED",
349
+ message: "Chrome 扩展尚未加载或缺少可验证的运行证据",
350
+ relatedModuleRefs: ["chrome-runtime"],
351
+ nextCommand: "platform setup --module execution-browser-extension",
352
+ },
353
+ ],
354
+ }),
285
355
  },
286
356
  },
287
357
  observedEffects: evidence
@@ -291,61 +361,27 @@ export const behaviorAdapter = {
291
361
  },
292
362
  setup: async (context) => {
293
363
  await mkdir(stateDir(context), { recursive: true, mode: 0o700 });
294
- const supplied = input(context);
295
- let setup = await readSetup(context);
296
- if (supplied.extensionId) {
297
- if (!/^[a-z]{32}$/.test(supplied.extensionId))
364
+ await installPackage(context);
365
+ const setup = await readSetup(context);
366
+ const evidence = await readEvidence(context, browserExtensionLoadDir(context.workspaceRoot));
367
+ if (setup && evidence && setup.extensionId === evidence.extensionId) {
368
+ try {
369
+ await materialize(context, setup);
370
+ }
371
+ catch (error) {
298
372
  return {
299
373
  result: {
300
- ...failed("SETUP_FAILED", "extensionId must be a canonical 32-character Chromium extension id", false),
374
+ ...failed("SETUP_FAILED", error instanceof Error
375
+ ? error.message
376
+ : "browser extension config materialization failed"),
301
377
  data: blockedSetupPlan,
302
378
  },
303
379
  observedEffects: [],
304
380
  };
305
- setup = { extensionId: supplied.extensionId };
306
- await writeFile(setupFile(context), `${JSON.stringify(setup, null, 2)}\n`, { mode: 0o600 });
307
- }
308
- if (!setup)
309
- return {
310
- result: {
311
- ...base,
312
- ok: false,
313
- status: "ACTION_REQUIRED",
314
- data: setupPlan,
315
- actionRequired: {
316
- action: "load-unpacked-extension",
317
- description: `Load ${browserExtensionLoadDir(context.workspaceRoot)} in Chrome, copy its extensionId, then run proflow-execution-browser-extension setup --extension-id <id>.`,
318
- },
319
- },
320
- observedEffects: [],
321
- };
322
- try {
323
- await materialize(context, setup);
324
- }
325
- catch (error) {
326
- return {
327
- result: {
328
- ...failed("SETUP_FAILED", error instanceof Error
329
- ? error.message
330
- : "browser extension config materialization failed"),
331
- data: blockedSetupPlan,
332
- },
333
- observedEffects: [],
334
- };
335
- }
336
- if (supplied.serviceWorker === "RUNNING") {
337
- const evidence = {
338
- contract: "proflow.browser-extension-verification.v1",
339
- moduleVersion: descriptor.moduleVersion,
340
- loadDir: browserExtensionLoadDir(context.workspaceRoot),
341
- extensionId: setup.extensionId,
342
- serviceWorker: "RUNNING",
343
- observedAt: supplied.observedAt ?? new Date().toISOString(),
344
- };
345
- await writeFile(verificationFile(context), `${JSON.stringify(evidence, null, 2)}\n`, { mode: 0o600 });
381
+ }
346
382
  return {
347
383
  result: base,
348
- observedEffects: ["Records real Chrome MV3 load evidence"],
384
+ observedEffects: ["Uses heartbeat-proven Chrome MV3 pairing evidence"],
349
385
  };
350
386
  }
351
387
  return {
@@ -355,8 +391,8 @@ export const behaviorAdapter = {
355
391
  status: "ACTION_REQUIRED",
356
392
  data: setupPlan,
357
393
  actionRequired: {
358
- action: "reload-and-verify-extension",
359
- description: "Reload the unpacked extension, confirm its MV3 service worker is RUNNING, then rerun proflow-execution-browser-extension setup --extension-id <id>.",
394
+ action: "load-unpacked-extension",
395
+ description: `Run pnpm exec -- proflow-execution-browser-extension setup --workspace ${context.workspaceRoot}; then enable Developer Mode and load ${browserExtensionLoadDir(context.workspaceRoot)}. The package CLI materializes bootstrap config, discovers the extension and verifies its heartbeat automatically.`,
360
396
  },
361
397
  },
362
398
  observedEffects: [],
@@ -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.11";
6
+ readonly moduleVersion: "0.1.13";
7
7
  readonly kind: "browser-extension";
8
8
  readonly templateVersion: "1.0.0";
9
9
  readonly platformCompatibility: ">=1.0.0 <2.0.0";
@@ -14,6 +14,9 @@ export declare const descriptor: {
14
14
  readonly provides: readonly [{
15
15
  readonly contractRef: "execution-browser-executor";
16
16
  readonly version: "1.0.0";
17
+ }, {
18
+ readonly contractRef: "custom-gpt-web-provisioning";
19
+ readonly version: "1.0.0";
17
20
  }];
18
21
  readonly requires: readonly [{
19
22
  readonly contractRef: "execution";
@@ -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.11",
6
+ moduleVersion: "0.1.13",
7
7
  kind: "browser-extension",
8
8
  templateVersion: "1.0.0",
9
9
  platformCompatibility: ">=1.0.0 <2.0.0",
@@ -16,6 +16,10 @@ export const descriptor = {
16
16
  contractRef: "execution-browser-executor",
17
17
  version: "1.0.0",
18
18
  },
19
+ {
20
+ contractRef: "custom-gpt-web-provisioning",
21
+ version: "1.0.0",
22
+ },
19
23
  ],
20
24
  requires: [
21
25
  {
@@ -13912,6 +13912,7 @@ async function bootstrapManagedRuntimeConfig() {
13912
13912
  if (!isRecord(raw)) return;
13913
13913
  const managed = raw;
13914
13914
  const bridge = parseConfig(managed.proflowRuntimeBridge);
13915
+ const provisioning = parseConfig(managed.proflowProvisioningBridge);
13915
13916
  const task = parseConfig(managed.proflowTaskApplication);
13916
13917
  const approval = parseConfig(managed.proflowApprovalApplication);
13917
13918
  if (!bridge || !task || !approval) {
@@ -13919,6 +13920,7 @@ async function bootstrapManagedRuntimeConfig() {
13919
13920
  }
13920
13921
  await chrome.storage.local.set({
13921
13922
  proflowRuntimeBridge: bridge,
13923
+ ...provisioning ? { proflowProvisioningBridge: provisioning } : {},
13922
13924
  proflowTaskApplication: task,
13923
13925
  proflowApprovalApplication: approval
13924
13926
  });
@@ -13927,6 +13929,10 @@ async function bridgeConfig() {
13927
13929
  const stored = await chrome.storage.local.get("proflowRuntimeBridge");
13928
13930
  return parseConfig(stored.proflowRuntimeBridge);
13929
13931
  }
13932
+ async function provisioningBridgeConfig() {
13933
+ const stored = await chrome.storage.local.get("proflowProvisioningBridge");
13934
+ return parseConfig(stored.proflowProvisioningBridge);
13935
+ }
13930
13936
  async function taskApplicationConfig() {
13931
13937
  const stored = await chrome.storage.local.get("proflowTaskApplication");
13932
13938
  return parseConfig(stored.proflowTaskApplication);
@@ -14379,6 +14385,68 @@ async function executeCommand(command) {
14379
14385
  }
14380
14386
  throw new Error("BROWSER_PRIMITIVE_UNAVAILABLE");
14381
14387
  }
14388
+ function missingProvisioningReceiver(error46) {
14389
+ const message = error46 instanceof Error ? error46.message : String(error46);
14390
+ return message.includes("Could not establish connection") || message.includes("Receiving end does not exist");
14391
+ }
14392
+ function isEditorUrl(url2) {
14393
+ return url2 === "https://chatgpt.com/gpts/editor" || Boolean(url2?.startsWith("https://chatgpt.com/gpts/editor/"));
14394
+ }
14395
+ async function provisioningContentCommand(tabId, operation, request) {
14396
+ let receiverReloaded = false;
14397
+ for (let attempt = 0; attempt < 60; attempt += 1) {
14398
+ let response;
14399
+ try {
14400
+ response = await chrome.tabs.sendMessage(tabId, {
14401
+ type: "PROFLOW_PROVISIONING_COMMAND",
14402
+ operation,
14403
+ request
14404
+ });
14405
+ } catch (error46) {
14406
+ if (!receiverReloaded && missingProvisioningReceiver(error46)) {
14407
+ try {
14408
+ const tab = await chrome.tabs.get(tabId);
14409
+ if (tab.status === "complete" && isEditorUrl(tab.url)) {
14410
+ await chrome.tabs.reload(tabId);
14411
+ receiverReloaded = true;
14412
+ }
14413
+ } catch {
14414
+ }
14415
+ }
14416
+ if (attempt === 59) throw error46;
14417
+ await sleep(250);
14418
+ continue;
14419
+ }
14420
+ if (!isRecord(response) || response.ok !== true) {
14421
+ const detail = isRecord(response) && typeof response.error === "string" ? response.error : "PROVISIONING_CONTENT_FAILED";
14422
+ if ((detail === "PROVISIONING_SURFACE_NOT_READY" || detail === "GPT_EDITOR_CONFIGURE_SURFACE_NOT_READY") && attempt < 59) {
14423
+ await sleep(250);
14424
+ continue;
14425
+ }
14426
+ throw new Error(detail);
14427
+ }
14428
+ return response.value;
14429
+ }
14430
+ throw new Error("PROVISIONING_CONTENT_TIMEOUT");
14431
+ }
14432
+ async function executeProvisioningCommand(command) {
14433
+ if (!isRecord(command.request))
14434
+ throw new Error("PROVISIONING_COMMAND_INVALID");
14435
+ let editorUrl = "https://chatgpt.com/gpts/editor";
14436
+ if (command.type === "FINALIZE_CUSTOM_GPT_AUTH") {
14437
+ const carrierUrl = new URL(text(command.request.carrierUrl, "CARRIER_URL"));
14438
+ const match = /^\/g\/(g-[A-Za-z0-9_-]+)$/.exec(carrierUrl.pathname);
14439
+ if (carrierUrl.origin !== "https://chatgpt.com" || carrierUrl.username !== "" || carrierUrl.password !== "" || carrierUrl.search !== "" || carrierUrl.hash !== "" || !match?.[1])
14440
+ throw new Error("PROVISIONING_CARRIER_URL_INVALID");
14441
+ editorUrl = `https://chatgpt.com/gpts/editor/${match[1]}`;
14442
+ }
14443
+ const tab = await chrome.tabs.create({ url: editorUrl, active: true });
14444
+ return provisioningContentCommand(
14445
+ numeric(tab.id, "TAB_ID"),
14446
+ command.type,
14447
+ command.request
14448
+ );
14449
+ }
14382
14450
  async function bridgeFetch(config2, path, init = {}) {
14383
14451
  return fetch(`${config2.endpoint}${path}`, {
14384
14452
  ...init,
@@ -14471,7 +14539,127 @@ async function runBridgeLoop() {
14471
14539
  }
14472
14540
  }
14473
14541
  }
14542
+ var provisioningBridgeLoopStarted = false;
14543
+ async function runProvisioningBridgeLoop() {
14544
+ if (provisioningBridgeLoopStarted) return;
14545
+ provisioningBridgeLoopStarted = true;
14546
+ while (true) {
14547
+ const config2 = await provisioningBridgeConfig();
14548
+ if (!config2) {
14549
+ await sleep(1e3);
14550
+ continue;
14551
+ }
14552
+ const query = `?extensionInstanceId=${encodeURIComponent(extensionInstanceId)}`;
14553
+ try {
14554
+ const hello = await bridgeFetch(
14555
+ config2,
14556
+ "/v1/provisioning/session/hello",
14557
+ {
14558
+ method: "POST",
14559
+ body: JSON.stringify({
14560
+ extensionId: chrome.runtime.id,
14561
+ extensionInstanceId
14562
+ })
14563
+ }
14564
+ );
14565
+ if (!hello.ok) throw new Error("PROVISIONING_BRIDGE_HELLO_REJECTED");
14566
+ let lastHeartbeatAt = 0;
14567
+ while (true) {
14568
+ if (Date.now() - lastHeartbeatAt >= 5e3) {
14569
+ const heartbeat = await bridgeFetch(
14570
+ config2,
14571
+ `/v1/provisioning/session/heartbeat${query}`,
14572
+ { method: "POST", body: "{}" }
14573
+ );
14574
+ if (!heartbeat.ok)
14575
+ throw new Error("PROVISIONING_BRIDGE_HEARTBEAT_REJECTED");
14576
+ lastHeartbeatAt = Date.now();
14577
+ }
14578
+ const response = await bridgeFetch(
14579
+ config2,
14580
+ `/v1/provisioning/commands/next${query}`
14581
+ );
14582
+ if (response.status === 204) {
14583
+ await sleep(250);
14584
+ continue;
14585
+ }
14586
+ if (!response.ok) throw new Error("PROVISIONING_BRIDGE_POLL_REJECTED");
14587
+ const command = await response.json();
14588
+ const commandHeartbeat = setInterval(() => {
14589
+ void bridgeFetch(
14590
+ config2,
14591
+ `/v1/provisioning/session/heartbeat${query}`,
14592
+ { method: "POST", body: "{}" }
14593
+ ).catch(() => void 0);
14594
+ }, 2e3);
14595
+ let result;
14596
+ try {
14597
+ result = {
14598
+ commandId: command.commandId,
14599
+ ok: true,
14600
+ value: await executeProvisioningCommand(command)
14601
+ };
14602
+ } catch (error46) {
14603
+ result = {
14604
+ commandId: command.commandId,
14605
+ ok: false,
14606
+ error: error46 instanceof Error ? error46.message : "PROVISIONING_EXTENSION_COMMAND_FAILED"
14607
+ };
14608
+ } finally {
14609
+ clearInterval(commandHeartbeat);
14610
+ }
14611
+ const reported = await bridgeFetch(
14612
+ config2,
14613
+ `/v1/provisioning/commands/result${query}`,
14614
+ { method: "POST", body: JSON.stringify(result) }
14615
+ );
14616
+ if (!reported.ok)
14617
+ throw new Error("PROVISIONING_BRIDGE_RESULT_REJECTED");
14618
+ }
14619
+ } catch {
14620
+ await sleep(1e3);
14621
+ }
14622
+ }
14623
+ }
14624
+ function provisioningRelayBase64(bytes) {
14625
+ let binary = "";
14626
+ for (let offset = 0; offset < bytes.length; offset += 32768)
14627
+ binary += String.fromCharCode(...bytes.subarray(offset, offset + 32768));
14628
+ return btoa(binary);
14629
+ }
14474
14630
  chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
14631
+ if (message.type === "PROFLOW_PROVISIONING_RELAY_FETCH") {
14632
+ const rawUrl = typeof message.url === "string" ? message.url : "";
14633
+ let parsed;
14634
+ try {
14635
+ parsed = new URL(rawUrl);
14636
+ } catch {
14637
+ sendResponse({ ok: false, error: "KNOWLEDGE_RELAY_URL_INVALID" });
14638
+ return;
14639
+ }
14640
+ if (parsed.protocol !== "http:" || parsed.hostname !== "127.0.0.1" || !parsed.pathname.startsWith("/v1/provisioning/files/") || parsed.username !== "" || parsed.password !== "" || parsed.search !== "" || parsed.hash !== "") {
14641
+ sendResponse({ ok: false, error: "KNOWLEDGE_RELAY_URL_INVALID" });
14642
+ return;
14643
+ }
14644
+ void fetch(parsed.toString(), { cache: "no-store" }).then(
14645
+ async (response) => {
14646
+ if (!response.ok) {
14647
+ sendResponse({
14648
+ ok: false,
14649
+ error: `KNOWLEDGE_RELAY_FETCH_FAILED:${response.status}`
14650
+ });
14651
+ return;
14652
+ }
14653
+ const bytes = new Uint8Array(await response.arrayBuffer());
14654
+ sendResponse({ ok: true, base64: provisioningRelayBase64(bytes) });
14655
+ },
14656
+ (error46) => sendResponse({
14657
+ ok: false,
14658
+ error: error46 instanceof Error ? `KNOWLEDGE_RELAY_FETCH_FAILED:${error46.message}` : "KNOWLEDGE_RELAY_FETCH_FAILED"
14659
+ })
14660
+ );
14661
+ return true;
14662
+ }
14475
14663
  if (message.type === "PROFLOW_CONTENT_OBSERVATION" && message.observation && sender.tab?.id !== void 0 && sender.tab.windowId !== void 0) {
14476
14664
  const observed = {
14477
14665
  ...message.observation,
@@ -14554,6 +14742,7 @@ async function startBackgroundRuntime() {
14554
14742
  await bootstrapManagedRuntimeConfig();
14555
14743
  await persistSnapshot();
14556
14744
  void runBridgeLoop();
14745
+ void runProvisioningBridgeLoop();
14557
14746
  void runObserverRecovery();
14558
14747
  }
14559
14748
  chrome.runtime.onInstalled.addListener(() => {
@@ -0,0 +1 @@
1
+ export {};