@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.
@@ -36,10 +36,12 @@ type RuntimeMessage = {
36
36
  | "PROFLOW_CONTENT_OBSERVATION"
37
37
  | "PROFLOW_SIDE_PANEL_SNAPSHOT"
38
38
  | "PROFLOW_TASK_APPLICATION"
39
- | "PROFLOW_APPROVAL_APPLICATION";
39
+ | "PROFLOW_APPROVAL_APPLICATION"
40
+ | "PROFLOW_PROVISIONING_RELAY_FETCH";
40
41
  observation?: Omit<ContentObservation, "tabId" | "windowId">;
41
42
  operation?: string;
42
43
  input?: Record<string, unknown>;
44
+ url?: string;
43
45
  };
44
46
  type BridgeConfig = { endpoint: string; token: string };
45
47
  type BridgeCommand = {
@@ -67,7 +69,25 @@ type ContentCommand = {
67
69
  value?: string;
68
70
  fingerprint?: string;
69
71
  };
70
- type ChromeTab = { id?: number; windowId?: number; url?: string };
72
+ type ProvisioningOperation =
73
+ | "PROVISION_CUSTOM_GPT"
74
+ | "FINALIZE_CUSTOM_GPT_AUTH";
75
+ type ProvisioningBridgeCommand = {
76
+ commandId: string;
77
+ type: ProvisioningOperation;
78
+ request: Record<string, unknown>;
79
+ };
80
+ type ProvisioningContentCommand = {
81
+ type: "PROFLOW_PROVISIONING_COMMAND";
82
+ operation: ProvisioningOperation;
83
+ request: Record<string, unknown>;
84
+ };
85
+ type ChromeTab = {
86
+ id?: number;
87
+ windowId?: number;
88
+ url?: string;
89
+ status?: "loading" | "complete";
90
+ };
71
91
  type ChromeRuntime = {
72
92
  runtime: {
73
93
  id: string;
@@ -98,12 +118,17 @@ type ChromeRuntime = {
98
118
  };
99
119
  tabs: {
100
120
  query(query: { url?: string }): Promise<ChromeTab[]>;
121
+ get(tabId: number): Promise<ChromeTab>;
101
122
  create(create: { url: string; active: boolean }): Promise<ChromeTab>;
123
+ reload(tabId: number): Promise<void>;
102
124
  update(
103
125
  tabId: number,
104
126
  update: { url?: string; active?: boolean },
105
127
  ): Promise<ChromeTab>;
106
- sendMessage(tabId: number, message: ContentCommand): Promise<unknown>;
128
+ sendMessage(
129
+ tabId: number,
130
+ message: ContentCommand | ProvisioningContentCommand,
131
+ ): Promise<unknown>;
107
132
  captureVisibleTab(
108
133
  windowId: number,
109
134
  options: { format: "png" },
@@ -158,6 +183,7 @@ function parseConfig(value: unknown): BridgeConfig | null {
158
183
 
159
184
  type ManagedRuntimeConfig = {
160
185
  proflowRuntimeBridge?: unknown;
186
+ proflowProvisioningBridge?: unknown;
161
187
  proflowTaskApplication?: unknown;
162
188
  proflowApprovalApplication?: unknown;
163
189
  };
@@ -176,6 +202,7 @@ async function bootstrapManagedRuntimeConfig(): Promise<void> {
176
202
  if (!isRecord(raw)) return;
177
203
  const managed = raw as ManagedRuntimeConfig;
178
204
  const bridge = parseConfig(managed.proflowRuntimeBridge);
205
+ const provisioning = parseConfig(managed.proflowProvisioningBridge);
179
206
  const task = parseConfig(managed.proflowTaskApplication);
180
207
  const approval = parseConfig(managed.proflowApprovalApplication);
181
208
  if (!bridge || !task || !approval) {
@@ -183,6 +210,7 @@ async function bootstrapManagedRuntimeConfig(): Promise<void> {
183
210
  }
184
211
  await chrome.storage.local.set({
185
212
  proflowRuntimeBridge: bridge,
213
+ ...(provisioning ? { proflowProvisioningBridge: provisioning } : {}),
186
214
  proflowTaskApplication: task,
187
215
  proflowApprovalApplication: approval,
188
216
  });
@@ -193,6 +221,11 @@ async function bridgeConfig(): Promise<BridgeConfig | null> {
193
221
  return parseConfig(stored.proflowRuntimeBridge);
194
222
  }
195
223
 
224
+ async function provisioningBridgeConfig(): Promise<BridgeConfig | null> {
225
+ const stored = await chrome.storage.local.get("proflowProvisioningBridge");
226
+ return parseConfig(stored.proflowProvisioningBridge);
227
+ }
228
+
196
229
  async function taskApplicationConfig(): Promise<BridgeConfig | null> {
197
230
  const stored = await chrome.storage.local.get("proflowTaskApplication");
198
231
  return parseConfig(stored.proflowTaskApplication);
@@ -804,6 +837,96 @@ async function executeCommand(command: BridgeCommand): Promise<unknown> {
804
837
  throw new Error("BROWSER_PRIMITIVE_UNAVAILABLE");
805
838
  }
806
839
 
840
+ function missingProvisioningReceiver(error: unknown): boolean {
841
+ const message = error instanceof Error ? error.message : String(error);
842
+ return (
843
+ message.includes("Could not establish connection") ||
844
+ message.includes("Receiving end does not exist")
845
+ );
846
+ }
847
+
848
+ function isEditorUrl(url: string | undefined): boolean {
849
+ return (
850
+ url === "https://chatgpt.com/gpts/editor" ||
851
+ Boolean(url?.startsWith("https://chatgpt.com/gpts/editor/"))
852
+ );
853
+ }
854
+
855
+ async function provisioningContentCommand(
856
+ tabId: number,
857
+ operation: ProvisioningOperation,
858
+ request: Record<string, unknown>,
859
+ ): Promise<unknown> {
860
+ let receiverReloaded = false;
861
+ for (let attempt = 0; attempt < 60; attempt += 1) {
862
+ let response: unknown;
863
+ try {
864
+ response = await chrome.tabs.sendMessage(tabId, {
865
+ type: "PROFLOW_PROVISIONING_COMMAND",
866
+ operation,
867
+ request,
868
+ });
869
+ } catch (error) {
870
+ if (!receiverReloaded && missingProvisioningReceiver(error)) {
871
+ try {
872
+ const tab = await chrome.tabs.get(tabId);
873
+ if (tab.status === "complete" && isEditorUrl(tab.url)) {
874
+ await chrome.tabs.reload(tabId);
875
+ receiverReloaded = true;
876
+ }
877
+ } catch {}
878
+ }
879
+ if (attempt === 59) throw error;
880
+ await sleep(250);
881
+ continue;
882
+ }
883
+ if (!isRecord(response) || response.ok !== true) {
884
+ const detail =
885
+ isRecord(response) && typeof response.error === "string"
886
+ ? response.error
887
+ : "PROVISIONING_CONTENT_FAILED";
888
+ if (
889
+ (detail === "PROVISIONING_SURFACE_NOT_READY" ||
890
+ detail === "GPT_EDITOR_CONFIGURE_SURFACE_NOT_READY") &&
891
+ attempt < 59
892
+ ) {
893
+ await sleep(250);
894
+ continue;
895
+ }
896
+ throw new Error(detail);
897
+ }
898
+ return response.value;
899
+ }
900
+ throw new Error("PROVISIONING_CONTENT_TIMEOUT");
901
+ }
902
+
903
+ async function executeProvisioningCommand(
904
+ command: ProvisioningBridgeCommand,
905
+ ): Promise<unknown> {
906
+ if (!isRecord(command.request))
907
+ throw new Error("PROVISIONING_COMMAND_INVALID");
908
+ let editorUrl = "https://chatgpt.com/gpts/editor";
909
+ if (command.type === "FINALIZE_CUSTOM_GPT_AUTH") {
910
+ const carrierUrl = new URL(text(command.request.carrierUrl, "CARRIER_URL"));
911
+ const match = /^\/g\/(g-[A-Za-z0-9_-]+)$/.exec(carrierUrl.pathname);
912
+ if (
913
+ carrierUrl.origin !== "https://chatgpt.com" ||
914
+ carrierUrl.username !== "" ||
915
+ carrierUrl.password !== "" ||
916
+ carrierUrl.search !== "" ||
917
+ carrierUrl.hash !== "" ||
918
+ !match?.[1]
919
+ )
920
+ throw new Error("PROVISIONING_CARRIER_URL_INVALID");
921
+ editorUrl = `https://chatgpt.com/gpts/editor/${match[1]}`;
922
+ }
923
+ const tab = await chrome.tabs.create({ url: editorUrl, active: true });
924
+ return provisioningContentCommand(
925
+ numeric(tab.id, "TAB_ID"),
926
+ command.type,
927
+ command.request,
928
+ );
929
+ }
807
930
  async function bridgeFetch(
808
931
  config: BridgeConfig,
809
932
  path: string,
@@ -911,7 +1034,144 @@ async function runBridgeLoop() {
911
1034
  }
912
1035
  }
913
1036
 
1037
+ let provisioningBridgeLoopStarted = false;
1038
+ async function runProvisioningBridgeLoop() {
1039
+ if (provisioningBridgeLoopStarted) return;
1040
+ provisioningBridgeLoopStarted = true;
1041
+ while (true) {
1042
+ const config = await provisioningBridgeConfig();
1043
+ if (!config) {
1044
+ await sleep(1_000);
1045
+ continue;
1046
+ }
1047
+ const query = `?extensionInstanceId=${encodeURIComponent(extensionInstanceId)}`;
1048
+ try {
1049
+ const hello = await bridgeFetch(
1050
+ config,
1051
+ "/v1/provisioning/session/hello",
1052
+ {
1053
+ method: "POST",
1054
+ body: JSON.stringify({
1055
+ extensionId: chrome.runtime.id,
1056
+ extensionInstanceId,
1057
+ }),
1058
+ },
1059
+ );
1060
+ if (!hello.ok) throw new Error("PROVISIONING_BRIDGE_HELLO_REJECTED");
1061
+ let lastHeartbeatAt = 0;
1062
+ while (true) {
1063
+ if (Date.now() - lastHeartbeatAt >= 5_000) {
1064
+ const heartbeat = await bridgeFetch(
1065
+ config,
1066
+ `/v1/provisioning/session/heartbeat${query}`,
1067
+ { method: "POST", body: "{}" },
1068
+ );
1069
+ if (!heartbeat.ok)
1070
+ throw new Error("PROVISIONING_BRIDGE_HEARTBEAT_REJECTED");
1071
+ lastHeartbeatAt = Date.now();
1072
+ }
1073
+ const response = await bridgeFetch(
1074
+ config,
1075
+ `/v1/provisioning/commands/next${query}`,
1076
+ );
1077
+ if (response.status === 204) {
1078
+ await sleep(250);
1079
+ continue;
1080
+ }
1081
+ if (!response.ok) throw new Error("PROVISIONING_BRIDGE_POLL_REJECTED");
1082
+ const command = (await response.json()) as ProvisioningBridgeCommand;
1083
+ const commandHeartbeat = setInterval(() => {
1084
+ void bridgeFetch(
1085
+ config,
1086
+ `/v1/provisioning/session/heartbeat${query}`,
1087
+ { method: "POST", body: "{}" },
1088
+ ).catch(() => undefined);
1089
+ }, 2_000);
1090
+ let result: Record<string, unknown>;
1091
+ try {
1092
+ result = {
1093
+ commandId: command.commandId,
1094
+ ok: true,
1095
+ value: await executeProvisioningCommand(command),
1096
+ };
1097
+ } catch (error) {
1098
+ result = {
1099
+ commandId: command.commandId,
1100
+ ok: false,
1101
+ error:
1102
+ error instanceof Error
1103
+ ? error.message
1104
+ : "PROVISIONING_EXTENSION_COMMAND_FAILED",
1105
+ };
1106
+ } finally {
1107
+ clearInterval(commandHeartbeat);
1108
+ }
1109
+ const reported = await bridgeFetch(
1110
+ config,
1111
+ `/v1/provisioning/commands/result${query}`,
1112
+ { method: "POST", body: JSON.stringify(result) },
1113
+ );
1114
+ if (!reported.ok)
1115
+ throw new Error("PROVISIONING_BRIDGE_RESULT_REJECTED");
1116
+ }
1117
+ } catch {
1118
+ await sleep(1_000);
1119
+ }
1120
+ }
1121
+ }
1122
+
1123
+ function provisioningRelayBase64(bytes: Uint8Array): string {
1124
+ let binary = "";
1125
+ for (let offset = 0; offset < bytes.length; offset += 0x8000)
1126
+ binary += String.fromCharCode(...bytes.subarray(offset, offset + 0x8000));
1127
+ return btoa(binary);
1128
+ }
1129
+
914
1130
  chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
1131
+ if (message.type === "PROFLOW_PROVISIONING_RELAY_FETCH") {
1132
+ const rawUrl = typeof message.url === "string" ? message.url : "";
1133
+ let parsed: URL;
1134
+ try {
1135
+ parsed = new URL(rawUrl);
1136
+ } catch {
1137
+ sendResponse({ ok: false, error: "KNOWLEDGE_RELAY_URL_INVALID" });
1138
+ return;
1139
+ }
1140
+ if (
1141
+ parsed.protocol !== "http:" ||
1142
+ parsed.hostname !== "127.0.0.1" ||
1143
+ !parsed.pathname.startsWith("/v1/provisioning/files/") ||
1144
+ parsed.username !== "" ||
1145
+ parsed.password !== "" ||
1146
+ parsed.search !== "" ||
1147
+ parsed.hash !== ""
1148
+ ) {
1149
+ sendResponse({ ok: false, error: "KNOWLEDGE_RELAY_URL_INVALID" });
1150
+ return;
1151
+ }
1152
+ void fetch(parsed.toString(), { cache: "no-store" }).then(
1153
+ async (response) => {
1154
+ if (!response.ok) {
1155
+ sendResponse({
1156
+ ok: false,
1157
+ error: `KNOWLEDGE_RELAY_FETCH_FAILED:${response.status}`,
1158
+ });
1159
+ return;
1160
+ }
1161
+ const bytes = new Uint8Array(await response.arrayBuffer());
1162
+ sendResponse({ ok: true, base64: provisioningRelayBase64(bytes) });
1163
+ },
1164
+ (error: unknown) =>
1165
+ sendResponse({
1166
+ ok: false,
1167
+ error:
1168
+ error instanceof Error
1169
+ ? `KNOWLEDGE_RELAY_FETCH_FAILED:${error.message}`
1170
+ : "KNOWLEDGE_RELAY_FETCH_FAILED",
1171
+ }),
1172
+ );
1173
+ return true;
1174
+ }
915
1175
  if (
916
1176
  message.type === "PROFLOW_CONTENT_OBSERVATION" &&
917
1177
  message.observation &&
@@ -1020,6 +1280,7 @@ async function startBackgroundRuntime(): Promise<void> {
1020
1280
  await bootstrapManagedRuntimeConfig();
1021
1281
  await persistSnapshot();
1022
1282
  void runBridgeLoop();
1283
+ void runProvisioningBridgeLoop();
1023
1284
  void runObserverRecovery();
1024
1285
  }
1025
1286