bitrix24-mcp-bridge 0.4.0 → 1.0.0

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.
@@ -2,44 +2,64 @@
2
2
  (() => {
3
3
  // extension/src/bridge-core.ts
4
4
  function encodeForm(params) {
5
- let out = new URLSearchParams(), add = (key, val) => {
6
- if (val != null)
7
- if (Array.isArray(val)) val.forEach((v, i) => add(`${key}[${i}]`, v));
8
- else if (typeof val == "object") for (let [k, v] of Object.entries(val)) add(`${key}[${k}]`, v);
9
- else out.set(key, String(val));
5
+ const out = new URLSearchParams();
6
+ const add = (key, val) => {
7
+ if (val === null || val === void 0) return;
8
+ if (Array.isArray(val)) val.forEach((v, i) => add(`${key}[${i}]`, v));
9
+ else if (typeof val === "object") for (const [k, v] of Object.entries(val)) add(`${key}[${k}]`, v);
10
+ else out.set(key, String(val));
10
11
  };
11
- for (let [k, v] of Object.entries(params)) add(k, v);
12
+ for (const [k, v] of Object.entries(params)) add(k, v);
12
13
  return out.toString();
13
14
  }
14
15
  function applyPathParams(endpoint, params) {
15
- let rest = { ...params };
16
- return { endpoint: endpoint.replace(/\{(\w+)\}/g, (_match, key) => {
17
- let value = rest[key];
18
- if (value == null || value === "")
16
+ const rest = { ...params };
17
+ const filled = endpoint.replace(/\{(\w+)\}/g, (_match, key) => {
18
+ const value = rest[key];
19
+ if (value === null || value === void 0 || value === "") {
19
20
  throw new Error(`missing path param "${key}" for endpoint ${endpoint}`);
20
- return delete rest[key], encodeURIComponent(String(value));
21
- }), rest };
21
+ }
22
+ delete rest[key];
23
+ return encodeURIComponent(String(value));
24
+ });
25
+ return { endpoint: filled, rest };
22
26
  }
23
27
  function buildRequest(origin, req, sessid) {
24
- let { endpoint, rest } = applyPathParams(req.endpoint, req.params), q = req.action ? `?action=${encodeURIComponent(req.action)}` : "", url = `${origin}${endpoint}${q}`;
25
- if (req.bodyType === "json")
28
+ const { endpoint, rest } = applyPathParams(req.endpoint, req.params);
29
+ const q = req.action ? `?action=${encodeURIComponent(req.action)}` : "";
30
+ const url = `${origin}${endpoint}${q}`;
31
+ if (req.bodyType === "json") {
26
32
  return { url, body: JSON.stringify(rest), contentType: "application/json" };
27
- let body = encodeForm({ ...rest, sessid });
33
+ }
34
+ const body = encodeForm({ ...rest, sessid });
28
35
  return { url, body, contentType: "application/x-www-form-urlencoded" };
29
36
  }
30
37
  function interpret(json) {
31
- let list = json && (Array.isArray(json.errors) ? json.errors : json.error ? [{ code: json.error }] : []), hasErrors = Array.isArray(list) && list.length > 0;
32
- if (json && (json.status === "error" || hasErrors)) {
33
- let first = list[0], code = first && (first.code || first.message) || json.error || "bitrix_error", description = json.error_description || first && first.message;
34
- return { ok: !1, error: description && description !== code ? `${code}: ${description}` : code, data: json };
38
+ if (!isRecord(json)) return { ok: true, data: json };
39
+ const list = Array.isArray(json.errors) ? json.errors : json.error ? [isRecord(json.error) ? json.error : { code: json.error }] : [];
40
+ if (json.status === "error" || list.length > 0) {
41
+ const first = isRecord(list[0]) ? list[0] : void 0;
42
+ const code = text(first?.code) || text(first?.message) || text(json.error) || "bitrix_error";
43
+ const description = text(json.error_description) || text(first?.message);
44
+ const error = description && description !== code ? `${code}: ${description}` : code;
45
+ return { ok: false, error, data: json };
35
46
  }
36
- return { ok: !0, data: json };
47
+ return { ok: true, data: json };
48
+ }
49
+ function isRecord(value) {
50
+ return typeof value === "object" && value !== null && !Array.isArray(value);
51
+ }
52
+ function text(value) {
53
+ if (typeof value === "number") return value ? String(value) : "";
54
+ return typeof value === "string" ? value : "";
37
55
  }
38
56
  var BINARY_CHUNK_BYTES = 1024 * 1024;
39
57
  function toBase64(bytes) {
40
58
  let binary = "";
41
- for (let i = 0; i < bytes.length; i += 32768)
42
- binary += String.fromCharCode(...bytes.subarray(i, i + 32768));
59
+ const window2 = 32768;
60
+ for (let i = 0; i < bytes.length; i += window2) {
61
+ binary += String.fromCharCode(...bytes.subarray(i, i + window2));
62
+ }
43
63
  return btoa(binary);
44
64
  }
45
65
  function looksLikeLoginPage(contentType, contentDisposition) {
@@ -47,17 +67,20 @@
47
67
  }
48
68
  function fileNameFromDisposition(contentDisposition) {
49
69
  if (!contentDisposition) return null;
50
- let encoded = /filename\*=(?:UTF-8'')?([^;]+)/i.exec(contentDisposition);
51
- if (encoded)
70
+ const encoded = /filename\*=(?:UTF-8'')?([^;]+)/i.exec(contentDisposition);
71
+ if (encoded) {
52
72
  try {
53
73
  return decodeURIComponent(encoded[1].trim().replace(/^"|"$/g, ""));
54
74
  } catch {
55
75
  }
56
- let plain = /filename="?([^";]+)"?/i.exec(contentDisposition);
76
+ }
77
+ const plain = /filename="?([^";]+)"?/i.exec(contentDisposition);
57
78
  return plain ? plain[1].trim() : null;
58
79
  }
59
- function interpretText(text, contentType, status) {
60
- return status >= 400 ? { ok: !1, error: `HTTP ${status}` } : text.trim() ? { ok: !0, data: { contentType, text } } : { ok: !1, error: "empty response" };
80
+ function interpretText(text2, contentType, status) {
81
+ if (status >= 400) return { ok: false, error: `HTTP ${status}` };
82
+ if (!text2.trim()) return { ok: false, error: "empty response" };
83
+ return { ok: true, data: { contentType, text: text2 } };
61
84
  }
62
85
 
63
86
  // extension/src/bridge-protocol.ts
@@ -68,86 +91,115 @@
68
91
  } catch {
69
92
  throw new Error("config.json is not valid JSON");
70
93
  }
71
- if (typeof doc != "object" || doc === null)
94
+ if (typeof doc !== "object" || doc === null) {
72
95
  throw new Error("config.json must be an object like { token, port }");
73
- let obj = doc, token = typeof obj.token == "string" ? obj.token.trim() : "";
96
+ }
97
+ const obj = doc;
98
+ const token = typeof obj.token === "string" ? obj.token.trim() : "";
74
99
  if (!token) throw new Error("config.json is missing a non-empty `token`");
75
- let port = Number(obj.port);
76
- if (!Number.isInteger(port) || port < 1 || port > 65535)
100
+ const port = Number(obj.port);
101
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
77
102
  throw new Error("config.json is missing a numeric `port`");
103
+ }
78
104
  return { token, port };
79
105
  }
80
106
  var BRIDGE_MSG_SOURCE = "bitrix24-mcp-bridge";
81
107
  function buildSessidRequest(nonce) {
82
108
  return { source: BRIDGE_MSG_SOURCE, kind: "sessid-request", nonce };
83
109
  }
84
- function isRecord(v) {
85
- return typeof v == "object" && v !== null;
110
+ function isRecord2(v) {
111
+ return typeof v === "object" && v !== null;
86
112
  }
87
113
  function parseSessidResponse(data, expectedNonce) {
88
- return !isRecord(data) || data.source !== BRIDGE_MSG_SOURCE || data.kind !== "sessid-response" || typeof data.nonce != "string" || data.nonce !== expectedNonce || typeof data.sessid != "string" ? null : { source: BRIDGE_MSG_SOURCE, kind: "sessid-response", nonce: data.nonce, sessid: data.sessid };
114
+ if (!isRecord2(data)) return null;
115
+ if (data.source !== BRIDGE_MSG_SOURCE || data.kind !== "sessid-response") return null;
116
+ if (typeof data.nonce !== "string" || data.nonce !== expectedNonce) return null;
117
+ if (typeof data.sessid !== "string") return null;
118
+ return { source: BRIDGE_MSG_SOURCE, kind: "sessid-response", nonce: data.nonce, sessid: data.sessid };
89
119
  }
90
120
 
91
121
  // extension/src/connector.ts
92
- var ORIGIN = location.origin, SESSID_TIMEOUT_MS = 2e3, RECONNECT_MS = 3e3, MAX_DOWNLOAD_BYTES = 512 * 1024 * 1024, socket = null, nonceSeq = 0;
122
+ var ORIGIN = location.origin;
123
+ var SESSID_TIMEOUT_MS = 2e3;
124
+ var RECONNECT_MS = 3e3;
125
+ var MAX_DOWNLOAD_BYTES = 512 * 1024 * 1024;
126
+ var socket = null;
127
+ var nonceSeq = 0;
93
128
  function freshSessid() {
94
- let nonce = `${Date.now()}-${nonceSeq++}-${Math.random().toString(36).slice(2)}`;
129
+ const nonce = `${Date.now()}-${nonceSeq++}-${Math.random().toString(36).slice(2)}`;
95
130
  return new Promise((resolve) => {
96
- let onMsg = (ev) => {
131
+ const onMsg = (ev) => {
97
132
  if (ev.source !== window) return;
98
- let res = parseSessidResponse(ev.data, nonce);
99
- res && (cleanup(), resolve(res.sessid));
100
- }, timer = setTimeout(() => {
101
- cleanup(), resolve("");
102
- }, SESSID_TIMEOUT_MS), cleanup = () => {
103
- clearTimeout(timer), window.removeEventListener("message", onMsg);
133
+ const res = parseSessidResponse(ev.data, nonce);
134
+ if (!res) return;
135
+ cleanup();
136
+ resolve(res.sessid);
137
+ };
138
+ const timer = setTimeout(() => {
139
+ cleanup();
140
+ resolve("");
141
+ }, SESSID_TIMEOUT_MS);
142
+ const cleanup = () => {
143
+ clearTimeout(timer);
144
+ window.removeEventListener("message", onMsg);
104
145
  };
105
- window.addEventListener("message", onMsg), window.postMessage(buildSessidRequest(nonce), ORIGIN);
146
+ window.addEventListener("message", onMsg);
147
+ window.postMessage(buildSessidRequest(nonce), ORIGIN);
106
148
  });
107
149
  }
108
150
  async function fetchPortal(req) {
109
- let sessid = await freshSessid();
151
+ const sessid = await freshSessid();
110
152
  if (!sessid) return { error: "session context not ready \u2014 open a normal Bitrix24 portal tab" };
111
- let { url, body, contentType } = buildRequest(ORIGIN, req, sessid), init = {
153
+ const { url, body, contentType } = buildRequest(ORIGIN, req, sessid);
154
+ const init = {
112
155
  method: req.method,
113
156
  credentials: "include",
114
157
  // sessid rides the CSRF header for all ajax (v2 form + json); form bodies also carry it inline.
115
158
  headers: { "Content-Type": contentType, "X-Bitrix-Csrf-Token": sessid }
116
- }, finalUrl = url;
117
- return req.method === "POST" ? init.body = body : finalUrl += (url.includes("?") ? "&" : "?") + body, fetch(finalUrl, init);
159
+ };
160
+ let finalUrl = url;
161
+ if (req.method === "POST") init.body = body;
162
+ else finalUrl += (url.includes("?") ? "&" : "?") + body;
163
+ return fetch(finalUrl, init);
118
164
  }
119
165
  async function handleCall(req) {
120
- let resp = await fetchPortal(req);
121
- if (!(resp instanceof Response)) return { ok: !1, error: resp.error };
122
- if (req.responseType === "text")
166
+ const resp = await fetchPortal(req);
167
+ if (!(resp instanceof Response)) return { ok: false, error: resp.error };
168
+ if (req.responseType === "text") {
123
169
  return interpretText(await resp.text(), resp.headers.get("content-type") ?? "", resp.status);
124
- let json = await resp.json();
170
+ }
171
+ const json = await resp.json();
125
172
  return interpret(json);
126
173
  }
127
174
  async function handleBinaryCall(req, ws) {
128
- let resp = await fetchPortal(req);
129
- if (!(resp instanceof Response)) return { ok: !1, error: resp.error };
130
- if (resp.status >= 400) return { ok: !1, error: `HTTP ${resp.status}` };
131
- let contentType = resp.headers.get("content-type") ?? "", disposition = resp.headers.get("content-disposition");
132
- if (looksLikeLoginPage(contentType, disposition))
133
- return { ok: !1, error: "portal answered with an HTML page, not a file \u2014 session likely expired" };
134
- let bytes = new Uint8Array(await resp.arrayBuffer());
135
- if (bytes.length > MAX_DOWNLOAD_BYTES)
136
- return { ok: !1, error: `file is ${bytes.length} bytes, over the ${MAX_DOWNLOAD_BYTES} limit` };
137
- let fileName = fileNameFromDisposition(disposition);
175
+ const resp = await fetchPortal(req);
176
+ if (!(resp instanceof Response)) return { ok: false, error: resp.error };
177
+ if (resp.status >= 400) return { ok: false, error: `HTTP ${resp.status}` };
178
+ const contentType = resp.headers.get("content-type") ?? "";
179
+ const disposition = resp.headers.get("content-disposition");
180
+ if (looksLikeLoginPage(contentType, disposition)) {
181
+ return { ok: false, error: "portal answered with an HTML page, not a file \u2014 session likely expired" };
182
+ }
183
+ const bytes = new Uint8Array(await resp.arrayBuffer());
184
+ if (bytes.length > MAX_DOWNLOAD_BYTES) {
185
+ return { ok: false, error: `file is ${bytes.length} bytes, over the ${MAX_DOWNLOAD_BYTES} limit` };
186
+ }
187
+ const fileName = fileNameFromDisposition(disposition);
138
188
  ws.send(JSON.stringify({ type: "binary-begin", id: req.id, contentType, bytes: bytes.length, fileName }));
139
189
  let seq = 0;
140
190
  for (let at = 0; at < bytes.length; at += BINARY_CHUNK_BYTES) {
141
- let slice = bytes.subarray(at, at + BINARY_CHUNK_BYTES);
191
+ const slice = bytes.subarray(at, at + BINARY_CHUNK_BYTES);
142
192
  ws.send(JSON.stringify({ type: "binary-chunk", id: req.id, seq: seq++, data: toBase64(slice) }));
143
193
  }
144
- return { ok: !0, data: { contentType, bytes: bytes.length, fileName, chunks: seq } };
194
+ return { ok: true, data: { contentType, bytes: bytes.length, fileName, chunks: seq } };
145
195
  }
146
196
  function connect(config) {
147
- let ws = new WebSocket(`ws://127.0.0.1:${config.port}`);
197
+ const ws = new WebSocket(`ws://127.0.0.1:${config.port}`);
148
198
  ws.addEventListener("open", () => {
149
- ws.send(JSON.stringify({ type: "auth", token: config.token, version: chrome.runtime.getManifest().version })), socket = ws;
150
- }), ws.addEventListener("message", async (ev) => {
199
+ ws.send(JSON.stringify({ type: "auth", token: config.token, version: chrome.runtime.getManifest().version }));
200
+ socket = ws;
201
+ });
202
+ ws.addEventListener("message", async (ev) => {
151
203
  let req;
152
204
  try {
153
205
  req = JSON.parse(ev.data);
@@ -155,21 +207,23 @@
155
207
  console.error("[bitrix-bridge] malformed WS frame, dropping:", e);
156
208
  return;
157
209
  }
158
- if (req.type === "call")
159
- try {
160
- let r = req.responseType === "binary" ? await handleBinaryCall(req, ws) : await handleCall(req);
161
- ws.send(JSON.stringify({ type: "result", id: req.id, ok: r.ok, data: r.data, error: r.error }));
162
- } catch (e) {
163
- ws.send(JSON.stringify({ type: "result", id: req.id, ok: !1, error: String(e) }));
164
- }
165
- }), ws.addEventListener("close", () => {
166
- socket === ws && (socket = null), setTimeout(() => connect(config), RECONNECT_MS);
210
+ if (req.type !== "call") return;
211
+ try {
212
+ const r = req.responseType === "binary" ? await handleBinaryCall(req, ws) : await handleCall(req);
213
+ ws.send(JSON.stringify({ type: "result", id: req.id, ok: r.ok, data: r.data, error: r.error }));
214
+ } catch (e) {
215
+ ws.send(JSON.stringify({ type: "result", id: req.id, ok: false, error: String(e) }));
216
+ }
217
+ });
218
+ ws.addEventListener("close", () => {
219
+ if (socket === ws) socket = null;
220
+ setTimeout(() => connect(config), RECONNECT_MS);
167
221
  });
168
222
  }
169
223
  async function start() {
170
224
  let config;
171
225
  try {
172
- let resp = await fetch(chrome.runtime.getURL("config.json"));
226
+ const resp = await fetch(chrome.runtime.getURL("config.json"));
173
227
  if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
174
228
  config = parseConfig(await resp.text());
175
229
  } catch (e) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/bridge-core.ts", "../src/bridge-protocol.ts", "../src/connector.ts"],
4
- "sourcesContent": ["// Pure, browser-agnostic \u2014 unit-tested.\n\n// Wire types come from the shared single source; re-exported so downstream\n// extension modules keep importing them from here.\nimport type { CallRequest } from \"../../shared/wire.ts\";\nexport type { CallRequest, CapturedEntry } from \"../../shared/wire.ts\";\n\n// Extension-only: the shape handleCall returns to the bridge (not a wire message).\nexport interface InterpretResult {\n ok: boolean;\n data?: unknown;\n error?: string;\n}\n\nexport function encodeForm(params: Record<string, unknown>): string {\n const out = new URLSearchParams();\n const add = (key: string, val: unknown): void => {\n if (val === null || val === undefined) return;\n if (Array.isArray(val)) val.forEach((v, i) => add(`${key}[${i}]`, v));\n else if (typeof val === \"object\") for (const [k, v] of Object.entries(val)) add(`${key}[${k}]`, v);\n else out.set(key, String(val));\n };\n for (const [k, v] of Object.entries(params)) add(k, v);\n return out.toString();\n}\n\n// Endpoints may carry `{name}` placeholders (e.g. \"/call/detail/{callId}\"); the value comes\n// from params and is consumed there, so it never rides along in the body/query as well.\nexport function applyPathParams(\n endpoint: string,\n params: Record<string, unknown>,\n): { endpoint: string; rest: Record<string, unknown> } {\n const rest = { ...params };\n const filled = endpoint.replace(/\\{(\\w+)\\}/g, (_match, key: string) => {\n const value = rest[key];\n if (value === null || value === undefined || value === \"\") {\n throw new Error(`missing path param \"${key}\" for endpoint ${endpoint}`);\n }\n delete rest[key];\n return encodeURIComponent(String(value));\n });\n return { endpoint: filled, rest };\n}\n\nexport function buildRequest(\n origin: string,\n req: CallRequest,\n sessid: string,\n): { url: string; body: string; contentType: string } {\n const { endpoint, rest } = applyPathParams(req.endpoint, req.params);\n const q = req.action ? `?action=${encodeURIComponent(req.action)}` : \"\";\n const url = `${origin}${endpoint}${q}`;\n // JSON actions (ui.entityselector.*, tasks.v2.*) send a JSON body; sessid rides the\n // X-Bitrix-Csrf-Token header (added by the caller), never the body.\n if (req.bodyType === \"json\") {\n return { url, body: JSON.stringify(rest), contentType: \"application/json\" };\n }\n const body = encodeForm({ ...rest, sessid });\n return { url, body, contentType: \"application/x-www-form-urlencoded\" };\n}\n\n// Bitrix wraps errors in HTTP 200: { status:\"error\", errors:[{code}] } or { error, error_description }.\n// NOTE: successful ajax responses carry an EMPTY `errors: []` array \u2014 a truthy value in JS \u2014 so we\n// must treat only a NON-EMPTY errors list (or status:\"error\"/a top-level error) as a real failure.\nexport function interpret(json: any): InterpretResult {\n const list = json && (Array.isArray(json.errors) ? json.errors : json.error ? [{ code: json.error }] : []);\n const hasErrors = Array.isArray(list) && list.length > 0;\n if (json && (json.status === \"error\" || hasErrors)) {\n const first = list[0];\n const code = (first && (first.code || first.message)) || json.error || \"bitrix_error\";\n const description = json.error_description || (first && first.message);\n const error = description && description !== code ? `${code}: ${description}` : code;\n return { ok: false, error, data: json };\n }\n return { ok: true, data: json };\n}\n\n// Chunk size for binary downloads: base64 inflates it by ~33%, so a 1 MiB slice stays a\n// comfortable WS frame while keeping the message count low for large videos.\nexport const BINARY_CHUNK_BYTES = 1024 * 1024;\n\n// `btoa` needs a binary string, and String.fromCharCode blows the call stack on big arrays \u2014\n// so the array is walked in small windows.\nexport function toBase64(bytes: Uint8Array): string {\n let binary = \"\";\n const window = 0x8000;\n for (let i = 0; i < bytes.length; i += window) {\n binary += String.fromCharCode(...bytes.subarray(i, i + window));\n }\n return btoa(binary);\n}\n\n/**\n * The portal answers an unauthenticated download with HTTP 200 and a login PAGE, so the status\n * code cannot be trusted. A genuine file download is marked as an attachment; an HTML body\n * without that marker is the login page, not a .html the user asked for.\n */\nexport function looksLikeLoginPage(contentType: string, contentDisposition: string | null): boolean {\n return /^\\s*text\\/html/i.test(contentType) && !/attachment/i.test(contentDisposition ?? \"\");\n}\n\n/** Server-suggested file name, e.g. `attachment; filename=\"image (29).png\"` (RFC 5987 aware). */\nexport function fileNameFromDisposition(contentDisposition: string | null): string | null {\n if (!contentDisposition) return null;\n const encoded = /filename\\*=(?:UTF-8'')?([^;]+)/i.exec(contentDisposition);\n if (encoded) {\n try {\n return decodeURIComponent(encoded[1].trim().replace(/^\"|\"$/g, \"\"));\n } catch {\n // fall through to the plain form\n }\n }\n const plain = /filename=\"?([^\";]+)\"?/i.exec(contentDisposition);\n return plain ? plain[1].trim() : null;\n}\n\n// HTML sub-domain (responseType \"text\"): no Bitrix envelope to read, so the HTTP status is the\n// only failure signal we get. A login redirect answers 200 with a page \u2014 that one is caught\n// downstream, where the parser knows what the payload was supposed to look like.\nexport function interpretText(text: string, contentType: string, status: number): InterpretResult {\n if (status >= 400) return { ok: false, error: `HTTP ${status}` };\n if (!text.trim()) return { ok: false, error: \"empty response\" };\n return { ok: true, data: { contentType, text } };\n}\n", "// Pure, browser-agnostic \u2014 unit-tested. No DOM / chrome / window.BX here.\n//\n// Two concerns live together because both are the *contract* between the extension's\n// two content scripts and the packaged config, and both are pure/testable:\n// 1) parseConfig \u2014 validate the per-user config.json ({ token, port }).\n// 2) the postMessage protocol \u2014 the envelopes the ISOLATED connector and the MAIN\n// script exchange over window.postMessage (sessid request/response, and \u2014 in the\n// capture build \u2014 MAIN-observed capture entries forwarded to the connector).\n\nimport type { CapturedEntry } from \"./bridge-core.ts\";\n\nexport interface BridgeConfig {\n token: string;\n port: number;\n}\n\n/**\n * Parse and validate the packaged config.json body. Throws with a clear,\n * user-facing message (mentioning config.json) on any malformed input so the\n * connector can surface why it failed to start.\n */\nexport function parseConfig(raw: string): BridgeConfig {\n let doc: unknown;\n try {\n doc = JSON.parse(raw);\n } catch {\n throw new Error(\"config.json is not valid JSON\");\n }\n if (typeof doc !== \"object\" || doc === null) {\n throw new Error(\"config.json must be an object like { token, port }\");\n }\n const obj = doc as Record<string, unknown>;\n const token = typeof obj.token === \"string\" ? obj.token.trim() : \"\";\n if (!token) throw new Error(\"config.json is missing a non-empty `token`\");\n const port = Number(obj.port);\n if (!Number.isInteger(port) || port < 1 || port > 65535) {\n throw new Error(\"config.json is missing a numeric `port`\");\n }\n return { token, port };\n}\n\n// --- sessid postMessage protocol -------------------------------------------\n//\n// window.postMessage is delivered to BOTH the MAIN and ISOLATED worlds of the same\n// frame (they share the DOM window), and each world also receives its own posts. So\n// every envelope is tagged with a fixed `source` and a `kind`, and each side ignores\n// anything that isn't addressed to it. Responses additionally carry the request nonce\n// so the connector can match a reply to the exact in-flight request it issued.\n\nexport const BRIDGE_MSG_SOURCE = \"bitrix24-mcp-bridge\";\n\nexport interface SessidRequestMsg {\n source: typeof BRIDGE_MSG_SOURCE;\n kind: \"sessid-request\";\n nonce: string;\n}\n\nexport interface SessidResponseMsg {\n source: typeof BRIDGE_MSG_SOURCE;\n kind: \"sessid-response\";\n nonce: string;\n sessid: string;\n}\n\nexport function buildSessidRequest(nonce: string): SessidRequestMsg {\n return { source: BRIDGE_MSG_SOURCE, kind: \"sessid-request\", nonce };\n}\n\nexport function buildSessidResponse(nonce: string, sessid: string): SessidResponseMsg {\n return { source: BRIDGE_MSG_SOURCE, kind: \"sessid-response\", nonce, sessid };\n}\n\nfunction isRecord(v: unknown): v is Record<string, unknown> {\n return typeof v === \"object\" && v !== null;\n}\n\n/** For the MAIN shim: a well-formed request from our connector, or null. */\nexport function parseSessidRequest(data: unknown): SessidRequestMsg | null {\n if (!isRecord(data)) return null;\n if (data.source !== BRIDGE_MSG_SOURCE || data.kind !== \"sessid-request\") return null;\n if (typeof data.nonce !== \"string\") return null;\n return { source: BRIDGE_MSG_SOURCE, kind: \"sessid-request\", nonce: data.nonce };\n}\n\n/** For the ISOLATED connector: a response matching `expectedNonce`, or null. */\nexport function parseSessidResponse(data: unknown, expectedNonce: string): SessidResponseMsg | null {\n if (!isRecord(data)) return null;\n if (data.source !== BRIDGE_MSG_SOURCE || data.kind !== \"sessid-response\") return null;\n if (typeof data.nonce !== \"string\" || data.nonce !== expectedNonce) return null;\n if (typeof data.sessid !== \"string\") return null;\n return { source: BRIDGE_MSG_SOURCE, kind: \"sessid-response\", nonce: data.nonce, sessid: data.sessid };\n}\n\n// --- capture forwarding (capture build only) -------------------------------\n//\n// The capture hooks must observe the PAGE's fetch/XHR, so they run in the MAIN world.\n// The socket lives only in the ISOLATED connector, so MAIN forwards each observed entry\n// to the connector over postMessage; the connector relays it to the daemon.\n\nexport interface CaptureForwardMsg {\n source: typeof BRIDGE_MSG_SOURCE;\n kind: \"capture\";\n call: CapturedEntry;\n}\n\nexport function buildCaptureForward(call: CapturedEntry): CaptureForwardMsg {\n return { source: BRIDGE_MSG_SOURCE, kind: \"capture\", call };\n}\n\n/** For the ISOLATED connector: a capture entry forwarded by the MAIN script, or null. */\nexport function parseCaptureForward(data: unknown): CaptureForwardMsg | null {\n if (!isRecord(data)) return null;\n if (data.source !== BRIDGE_MSG_SOURCE || data.kind !== \"capture\") return null;\n if (!isRecord(data.call)) return null;\n return { source: BRIDGE_MSG_SOURCE, kind: \"capture\", call: data.call as unknown as CapturedEntry };\n}\n", "// ISOLATED-world connector. This is where the token and the WebSocket live \u2014 never in\n// the page's MAIN world, so the page cannot read the token without knowing the dynamic\n// (`use_dynamic_url`) resource URL for config.json, and cannot hijack the socket. It has\n// `chrome.runtime`, so it loads the per-user config from the packaged config.json at\n// runtime (nothing per-user is baked into this JS \u2014 the file is static). The actual\n// boundary is `use_dynamic_url: true` on config.json in the manifest: it makes the\n// resource URL unguessable from the page while `chrome.runtime.getURL()` here still\n// resolves it.\n//\n// Per daemon call it asks the MAIN shim for a FRESH sessid over postMessage (nonce-matched),\n// then reuses the pure bridge-core helpers to build the request, fetch it with the page's\n// cookies, and interpret the response.\n\nimport {\n buildRequest,\n interpret,\n interpretText,\n toBase64,\n looksLikeLoginPage,\n fileNameFromDisposition,\n BINARY_CHUNK_BYTES,\n type CallRequest,\n type InterpretResult,\n} from \"./bridge-core.ts\";\nimport {\n parseConfig,\n buildSessidRequest,\n parseSessidResponse,\n parseCaptureForward,\n type BridgeConfig,\n} from \"./bridge-protocol.ts\";\n\n// Gates the capture relay listener; stripped from normal builds via esbuild `define`.\ndeclare const __BITRIX_CAPTURE__: boolean;\n\n// Minimal ambient for the one chrome API we use (no @types/chrome dependency).\ndeclare const chrome: { runtime: { getURL(path: string): string; getManifest(): { version?: string } } };\n\nconst ORIGIN = location.origin;\nconst SESSID_TIMEOUT_MS = 2000;\nconst RECONNECT_MS = 3000;\n// The whole body is buffered here before streaming, so this caps peak tab memory.\nconst MAX_DOWNLOAD_BYTES = 512 * 1024 * 1024;\n\nlet socket: WebSocket | null = null;\nlet nonceSeq = 0;\n\n/** Ask the MAIN shim for a fresh sessid; resolves \"\" if it doesn't answer in time. */\nfunction freshSessid(): Promise<string> {\n const nonce = `${Date.now()}-${nonceSeq++}-${Math.random().toString(36).slice(2)}`;\n return new Promise((resolve) => {\n const onMsg = (ev: MessageEvent): void => {\n if (ev.source !== window) return;\n const res = parseSessidResponse(ev.data, nonce);\n if (!res) return;\n cleanup();\n resolve(res.sessid);\n };\n const timer = setTimeout(() => {\n cleanup();\n resolve(\"\");\n }, SESSID_TIMEOUT_MS);\n const cleanup = (): void => {\n clearTimeout(timer);\n window.removeEventListener(\"message\", onMsg);\n };\n window.addEventListener(\"message\", onMsg);\n window.postMessage(buildSessidRequest(nonce), ORIGIN);\n });\n}\n\nasync function fetchPortal(req: CallRequest): Promise<Response | { error: string }> {\n const sessid = await freshSessid();\n if (!sessid) return { error: \"session context not ready \u2014 open a normal Bitrix24 portal tab\" }; // G5\n const { url, body, contentType } = buildRequest(ORIGIN, req, sessid);\n const init: RequestInit = {\n method: req.method,\n credentials: \"include\",\n // sessid rides the CSRF header for all ajax (v2 form + json); form bodies also carry it inline.\n headers: { \"Content-Type\": contentType, \"X-Bitrix-Csrf-Token\": sessid },\n };\n let finalUrl = url;\n if (req.method === \"POST\") init.body = body;\n else finalUrl += (url.includes(\"?\") ? \"&\" : \"?\") + body;\n return fetch(finalUrl, init);\n}\n\nasync function handleCall(req: CallRequest): Promise<InterpretResult> {\n const resp = await fetchPortal(req);\n if (!(resp instanceof Response)) return { ok: false, error: resp.error };\n if (req.responseType === \"text\") {\n return interpretText(await resp.text(), resp.headers.get(\"content-type\") ?? \"\", resp.status);\n }\n const json = await resp.json();\n return interpret(json); // G3\n}\n\n/** Streams the body to the daemon as base64 chunks; the daemon writes them to a file. */\nasync function handleBinaryCall(req: CallRequest, ws: WebSocket): Promise<InterpretResult> {\n const resp = await fetchPortal(req);\n if (!(resp instanceof Response)) return { ok: false, error: resp.error };\n if (resp.status >= 400) return { ok: false, error: `HTTP ${resp.status}` };\n\n const contentType = resp.headers.get(\"content-type\") ?? \"\";\n const disposition = resp.headers.get(\"content-disposition\");\n if (looksLikeLoginPage(contentType, disposition)) {\n return { ok: false, error: \"portal answered with an HTML page, not a file \u2014 session likely expired\" };\n }\n\n const bytes = new Uint8Array(await resp.arrayBuffer());\n if (bytes.length > MAX_DOWNLOAD_BYTES) {\n return { ok: false, error: `file is ${bytes.length} bytes, over the ${MAX_DOWNLOAD_BYTES} limit` };\n }\n const fileName = fileNameFromDisposition(disposition);\n ws.send(JSON.stringify({ type: \"binary-begin\", id: req.id, contentType, bytes: bytes.length, fileName }));\n let seq = 0;\n for (let at = 0; at < bytes.length; at += BINARY_CHUNK_BYTES) {\n const slice = bytes.subarray(at, at + BINARY_CHUNK_BYTES);\n ws.send(JSON.stringify({ type: \"binary-chunk\", id: req.id, seq: seq++, data: toBase64(slice) }));\n }\n return { ok: true, data: { contentType, bytes: bytes.length, fileName, chunks: seq } };\n}\n\nfunction connect(config: BridgeConfig): void {\n const ws = new WebSocket(`ws://127.0.0.1:${config.port}`);\n ws.addEventListener(\"open\", () => {\n ws.send(JSON.stringify({ type: \"auth\", token: config.token, version: chrome.runtime.getManifest().version }));\n socket = ws;\n });\n ws.addEventListener(\"message\", async (ev: MessageEvent) => {\n let req: CallRequest;\n try {\n req = JSON.parse(ev.data) as CallRequest;\n } catch (e) {\n console.error(\"[bitrix-bridge] malformed WS frame, dropping:\", e);\n return;\n }\n if (req.type !== \"call\") return;\n try {\n const r = req.responseType === \"binary\" ? await handleBinaryCall(req, ws) : await handleCall(req);\n ws.send(JSON.stringify({ type: \"result\", id: req.id, ok: r.ok, data: r.data, error: r.error }));\n } catch (e) {\n ws.send(JSON.stringify({ type: \"result\", id: req.id, ok: false, error: String(e) }));\n }\n });\n ws.addEventListener(\"close\", () => {\n if (socket === ws) socket = null;\n setTimeout(() => connect(config), RECONNECT_MS); // auto-reconnect\n });\n}\n\n// Capture build only: relay MAIN-observed capture entries to the daemon.\nif (__BITRIX_CAPTURE__) {\n window.addEventListener(\"message\", (ev: MessageEvent) => {\n if (ev.source !== window) return;\n const fwd = parseCaptureForward(ev.data);\n if (!fwd) return;\n if (socket && socket.readyState === WebSocket.OPEN) {\n socket.send(JSON.stringify({ type: \"capture\", call: fwd.call }));\n }\n });\n}\n\nasync function start(): Promise<void> {\n let config: BridgeConfig;\n try {\n const resp = await fetch(chrome.runtime.getURL(\"config.json\"));\n if (!resp.ok) throw new Error(`HTTP ${resp.status}`);\n config = parseConfig(await resp.text());\n } catch (e) {\n // Nothing to fall back to \u2014 surface why the bridge won't start and stop.\n console.error(\"[bitrix-bridge] cannot load config.json:\", e);\n return;\n }\n connect(config);\n}\n\nstart();\n"],
5
- "mappings": "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",
6
- "names": []
4
+ "sourcesContent": ["// Pure, browser-agnostic \u2014 unit-tested.\n\n// Wire types come from the shared single source; re-exported so downstream\n// extension modules keep importing them from here.\nimport type { CallRequest } from \"../../shared/wire.ts\";\nexport type { CallRequest } from \"../../shared/wire.ts\";\n\n// Extension-only: the shape handleCall returns to the bridge (not a wire message).\nexport interface InterpretResult {\n ok: boolean;\n data?: unknown;\n error?: string;\n}\n\nexport function encodeForm(params: Record<string, unknown>): string {\n const out = new URLSearchParams();\n const add = (key: string, val: unknown): void => {\n if (val === null || val === undefined) return;\n if (Array.isArray(val)) val.forEach((v, i) => add(`${key}[${i}]`, v));\n else if (typeof val === \"object\") for (const [k, v] of Object.entries(val)) add(`${key}[${k}]`, v);\n else out.set(key, String(val));\n };\n for (const [k, v] of Object.entries(params)) add(k, v);\n return out.toString();\n}\n\n// Endpoints may carry `{name}` placeholders (e.g. \"/call/detail/{callId}\"); the value comes\n// from params and is consumed there, so it never rides along in the body/query as well.\nexport function applyPathParams(\n endpoint: string,\n params: Record<string, unknown>,\n): { endpoint: string; rest: Record<string, unknown> } {\n const rest = { ...params };\n const filled = endpoint.replace(/\\{(\\w+)\\}/g, (_match, key: string) => {\n const value = rest[key];\n if (value === null || value === undefined || value === \"\") {\n throw new Error(`missing path param \"${key}\" for endpoint ${endpoint}`);\n }\n delete rest[key];\n return encodeURIComponent(String(value));\n });\n return { endpoint: filled, rest };\n}\n\nexport function buildRequest(\n origin: string,\n req: CallRequest,\n sessid: string,\n): { url: string; body: string; contentType: string } {\n const { endpoint, rest } = applyPathParams(req.endpoint, req.params);\n const q = req.action ? `?action=${encodeURIComponent(req.action)}` : \"\";\n const url = `${origin}${endpoint}${q}`;\n // JSON actions (ui.entityselector.*, tasks.v2.*) send a JSON body; sessid rides the\n // X-Bitrix-Csrf-Token header (added by the caller), never the body.\n if (req.bodyType === \"json\") {\n return { url, body: JSON.stringify(rest), contentType: \"application/json\" };\n }\n const body = encodeForm({ ...rest, sessid });\n return { url, body, contentType: \"application/x-www-form-urlencoded\" };\n}\n\n// Bitrix wraps errors in HTTP 200: { status:\"error\", errors:[{code}] } or { error, error_description }.\n// NOTE: successful ajax responses carry an EMPTY `errors: []` array \u2014 a truthy value in JS \u2014 so we\n// must treat only a NON-EMPTY errors list (or status:\"error\"/a top-level error) as a real failure.\nexport function interpret(json: unknown): InterpretResult {\n if (!isRecord(json)) return { ok: true, data: json };\n // Three envelopes: ajax `errors: [{code, message}]`, classic REST `error: \"CODE\"` (+ error_description),\n // REST 3.0 `error: {code, message}`.\n const list: unknown[] = Array.isArray(json.errors)\n ? json.errors\n : json.error\n ? [isRecord(json.error) ? json.error : { code: json.error }]\n : [];\n if (json.status === \"error\" || list.length > 0) {\n const first = isRecord(list[0]) ? list[0] : undefined;\n const code = text(first?.code) || text(first?.message) || text(json.error) || \"bitrix_error\";\n const description = text(json.error_description) || text(first?.message);\n const error = description && description !== code ? `${code}: ${description}` : code;\n return { ok: false, error, data: json };\n }\n return { ok: true, data: json };\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction text(value: unknown): string {\n if (typeof value === \"number\") return value ? String(value) : \"\";\n return typeof value === \"string\" ? value : \"\";\n}\n\n// Chunk size for binary downloads: base64 inflates it by ~33%, so a 1 MiB slice stays a\n// comfortable WS frame while keeping the message count low for large videos.\nexport const BINARY_CHUNK_BYTES = 1024 * 1024;\n\n// `btoa` needs a binary string, and String.fromCharCode blows the call stack on big arrays \u2014\n// so the array is walked in small windows.\nexport function toBase64(bytes: Uint8Array): string {\n let binary = \"\";\n const window = 0x8000;\n for (let i = 0; i < bytes.length; i += window) {\n binary += String.fromCharCode(...bytes.subarray(i, i + window));\n }\n return btoa(binary);\n}\n\n/**\n * The portal answers an unauthenticated download with HTTP 200 and a login PAGE, so the status\n * code cannot be trusted. A genuine file download is marked as an attachment; an HTML body\n * without that marker is the login page, not a .html the user asked for.\n */\nexport function looksLikeLoginPage(contentType: string, contentDisposition: string | null): boolean {\n return /^\\s*text\\/html/i.test(contentType) && !/attachment/i.test(contentDisposition ?? \"\");\n}\n\n/** Server-suggested file name, e.g. `attachment; filename=\"image (29).png\"` (RFC 5987 aware). */\nexport function fileNameFromDisposition(contentDisposition: string | null): string | null {\n if (!contentDisposition) return null;\n const encoded = /filename\\*=(?:UTF-8'')?([^;]+)/i.exec(contentDisposition);\n if (encoded) {\n try {\n return decodeURIComponent(encoded[1].trim().replace(/^\"|\"$/g, \"\"));\n } catch {\n // fall through to the plain form\n }\n }\n const plain = /filename=\"?([^\";]+)\"?/i.exec(contentDisposition);\n return plain ? plain[1].trim() : null;\n}\n\n// HTML sub-domain (responseType \"text\"): no Bitrix envelope to read, so the HTTP status is the\n// only failure signal we get. A login redirect answers 200 with a page \u2014 that one is caught\n// downstream, where the parser knows what the payload was supposed to look like.\nexport function interpretText(text: string, contentType: string, status: number): InterpretResult {\n if (status >= 400) return { ok: false, error: `HTTP ${status}` };\n if (!text.trim()) return { ok: false, error: \"empty response\" };\n return { ok: true, data: { contentType, text } };\n}\n", "// Pure, browser-agnostic \u2014 unit-tested. No DOM / chrome / window.BX here.\n//\n// Two concerns live together because both are the *contract* between the extension's\n// two content scripts and the packaged config, and both are pure/testable:\n// 1) parseConfig \u2014 validate the per-user config.json ({ token, port }).\n// 2) the postMessage protocol \u2014 the envelopes the ISOLATED connector and the MAIN\n// script exchange over window.postMessage (sessid request/response).\n\nexport interface BridgeConfig {\n token: string;\n port: number;\n}\n\n/**\n * Parse and validate the packaged config.json body. Throws with a clear,\n * user-facing message (mentioning config.json) on any malformed input so the\n * connector can surface why it failed to start.\n */\nexport function parseConfig(raw: string): BridgeConfig {\n let doc: unknown;\n try {\n doc = JSON.parse(raw);\n } catch {\n throw new Error(\"config.json is not valid JSON\");\n }\n if (typeof doc !== \"object\" || doc === null) {\n throw new Error(\"config.json must be an object like { token, port }\");\n }\n const obj = doc as Record<string, unknown>;\n const token = typeof obj.token === \"string\" ? obj.token.trim() : \"\";\n if (!token) throw new Error(\"config.json is missing a non-empty `token`\");\n const port = Number(obj.port);\n if (!Number.isInteger(port) || port < 1 || port > 65535) {\n throw new Error(\"config.json is missing a numeric `port`\");\n }\n return { token, port };\n}\n\n// --- sessid postMessage protocol -------------------------------------------\n//\n// window.postMessage is delivered to BOTH the MAIN and ISOLATED worlds of the same\n// frame (they share the DOM window), and each world also receives its own posts. So\n// every envelope is tagged with a fixed `source` and a `kind`, and each side ignores\n// anything that isn't addressed to it. Responses additionally carry the request nonce\n// so the connector can match a reply to the exact in-flight request it issued.\n\nexport const BRIDGE_MSG_SOURCE = \"bitrix24-mcp-bridge\";\n\nexport interface SessidRequestMsg {\n source: typeof BRIDGE_MSG_SOURCE;\n kind: \"sessid-request\";\n nonce: string;\n}\n\nexport interface SessidResponseMsg {\n source: typeof BRIDGE_MSG_SOURCE;\n kind: \"sessid-response\";\n nonce: string;\n sessid: string;\n}\n\nexport function buildSessidRequest(nonce: string): SessidRequestMsg {\n return { source: BRIDGE_MSG_SOURCE, kind: \"sessid-request\", nonce };\n}\n\nexport function buildSessidResponse(nonce: string, sessid: string): SessidResponseMsg {\n return { source: BRIDGE_MSG_SOURCE, kind: \"sessid-response\", nonce, sessid };\n}\n\nfunction isRecord(v: unknown): v is Record<string, unknown> {\n return typeof v === \"object\" && v !== null;\n}\n\n/** For the MAIN shim: a well-formed request from our connector, or null. */\nexport function parseSessidRequest(data: unknown): SessidRequestMsg | null {\n if (!isRecord(data)) return null;\n if (data.source !== BRIDGE_MSG_SOURCE || data.kind !== \"sessid-request\") return null;\n if (typeof data.nonce !== \"string\") return null;\n return { source: BRIDGE_MSG_SOURCE, kind: \"sessid-request\", nonce: data.nonce };\n}\n\n/** For the ISOLATED connector: a response matching `expectedNonce`, or null. */\nexport function parseSessidResponse(data: unknown, expectedNonce: string): SessidResponseMsg | null {\n if (!isRecord(data)) return null;\n if (data.source !== BRIDGE_MSG_SOURCE || data.kind !== \"sessid-response\") return null;\n if (typeof data.nonce !== \"string\" || data.nonce !== expectedNonce) return null;\n if (typeof data.sessid !== \"string\") return null;\n return { source: BRIDGE_MSG_SOURCE, kind: \"sessid-response\", nonce: data.nonce, sessid: data.sessid };\n}\n", "// ISOLATED-world connector. This is where the token and the WebSocket live \u2014 never in\n// the page's MAIN world, so the page cannot read the token without knowing the dynamic\n// (`use_dynamic_url`) resource URL for config.json, and cannot hijack the socket. It has\n// `chrome.runtime`, so it loads the per-user config from the packaged config.json at\n// runtime (nothing per-user is baked into this JS \u2014 the file is static). The actual\n// boundary is `use_dynamic_url: true` on config.json in the manifest: it makes the\n// resource URL unguessable from the page while `chrome.runtime.getURL()` here still\n// resolves it.\n//\n// Per daemon call it asks the MAIN shim for a FRESH sessid over postMessage (nonce-matched),\n// then reuses the pure bridge-core helpers to build the request, fetch it with the page's\n// cookies, and interpret the response.\n\nimport {\n buildRequest,\n interpret,\n interpretText,\n toBase64,\n looksLikeLoginPage,\n fileNameFromDisposition,\n BINARY_CHUNK_BYTES,\n type CallRequest,\n type InterpretResult,\n} from \"./bridge-core.ts\";\nimport {\n parseConfig,\n buildSessidRequest,\n parseSessidResponse,\n type BridgeConfig,\n} from \"./bridge-protocol.ts\";\n\n// Minimal ambient for the one chrome API we use (no @types/chrome dependency).\ndeclare const chrome: { runtime: { getURL(path: string): string; getManifest(): { version?: string } } };\n\nconst ORIGIN = location.origin;\nconst SESSID_TIMEOUT_MS = 2000;\nconst RECONNECT_MS = 3000;\n// The whole body is buffered here before streaming, so this caps peak tab memory.\nconst MAX_DOWNLOAD_BYTES = 512 * 1024 * 1024;\n\nlet socket: WebSocket | null = null;\nlet nonceSeq = 0;\n\n/** Ask the MAIN shim for a fresh sessid; resolves \"\" if it doesn't answer in time. */\nfunction freshSessid(): Promise<string> {\n const nonce = `${Date.now()}-${nonceSeq++}-${Math.random().toString(36).slice(2)}`;\n return new Promise((resolve) => {\n const onMsg = (ev: MessageEvent): void => {\n if (ev.source !== window) return;\n const res = parseSessidResponse(ev.data, nonce);\n if (!res) return;\n cleanup();\n resolve(res.sessid);\n };\n const timer = setTimeout(() => {\n cleanup();\n resolve(\"\");\n }, SESSID_TIMEOUT_MS);\n const cleanup = (): void => {\n clearTimeout(timer);\n window.removeEventListener(\"message\", onMsg);\n };\n window.addEventListener(\"message\", onMsg);\n window.postMessage(buildSessidRequest(nonce), ORIGIN);\n });\n}\n\nasync function fetchPortal(req: CallRequest): Promise<Response | { error: string }> {\n const sessid = await freshSessid();\n if (!sessid) return { error: \"session context not ready \u2014 open a normal Bitrix24 portal tab\" }; // G5\n const { url, body, contentType } = buildRequest(ORIGIN, req, sessid);\n const init: RequestInit = {\n method: req.method,\n credentials: \"include\",\n // sessid rides the CSRF header for all ajax (v2 form + json); form bodies also carry it inline.\n headers: { \"Content-Type\": contentType, \"X-Bitrix-Csrf-Token\": sessid },\n };\n let finalUrl = url;\n if (req.method === \"POST\") init.body = body;\n else finalUrl += (url.includes(\"?\") ? \"&\" : \"?\") + body;\n return fetch(finalUrl, init);\n}\n\nasync function handleCall(req: CallRequest): Promise<InterpretResult> {\n const resp = await fetchPortal(req);\n if (!(resp instanceof Response)) return { ok: false, error: resp.error };\n if (req.responseType === \"text\") {\n return interpretText(await resp.text(), resp.headers.get(\"content-type\") ?? \"\", resp.status);\n }\n const json = await resp.json();\n return interpret(json); // G3\n}\n\n/** Streams the body to the daemon as base64 chunks; the daemon writes them to a file. */\nasync function handleBinaryCall(req: CallRequest, ws: WebSocket): Promise<InterpretResult> {\n const resp = await fetchPortal(req);\n if (!(resp instanceof Response)) return { ok: false, error: resp.error };\n if (resp.status >= 400) return { ok: false, error: `HTTP ${resp.status}` };\n\n const contentType = resp.headers.get(\"content-type\") ?? \"\";\n const disposition = resp.headers.get(\"content-disposition\");\n if (looksLikeLoginPage(contentType, disposition)) {\n return { ok: false, error: \"portal answered with an HTML page, not a file \u2014 session likely expired\" };\n }\n\n const bytes = new Uint8Array(await resp.arrayBuffer());\n if (bytes.length > MAX_DOWNLOAD_BYTES) {\n return { ok: false, error: `file is ${bytes.length} bytes, over the ${MAX_DOWNLOAD_BYTES} limit` };\n }\n const fileName = fileNameFromDisposition(disposition);\n ws.send(JSON.stringify({ type: \"binary-begin\", id: req.id, contentType, bytes: bytes.length, fileName }));\n let seq = 0;\n for (let at = 0; at < bytes.length; at += BINARY_CHUNK_BYTES) {\n const slice = bytes.subarray(at, at + BINARY_CHUNK_BYTES);\n ws.send(JSON.stringify({ type: \"binary-chunk\", id: req.id, seq: seq++, data: toBase64(slice) }));\n }\n return { ok: true, data: { contentType, bytes: bytes.length, fileName, chunks: seq } };\n}\n\nfunction connect(config: BridgeConfig): void {\n const ws = new WebSocket(`ws://127.0.0.1:${config.port}`);\n ws.addEventListener(\"open\", () => {\n ws.send(JSON.stringify({ type: \"auth\", token: config.token, version: chrome.runtime.getManifest().version }));\n socket = ws;\n });\n ws.addEventListener(\"message\", async (ev: MessageEvent) => {\n let req: CallRequest;\n try {\n req = JSON.parse(ev.data) as CallRequest;\n } catch (e) {\n console.error(\"[bitrix-bridge] malformed WS frame, dropping:\", e);\n return;\n }\n if (req.type !== \"call\") return;\n try {\n const r = req.responseType === \"binary\" ? await handleBinaryCall(req, ws) : await handleCall(req);\n ws.send(JSON.stringify({ type: \"result\", id: req.id, ok: r.ok, data: r.data, error: r.error }));\n } catch (e) {\n ws.send(JSON.stringify({ type: \"result\", id: req.id, ok: false, error: String(e) }));\n }\n });\n ws.addEventListener(\"close\", () => {\n if (socket === ws) socket = null;\n setTimeout(() => connect(config), RECONNECT_MS); // auto-reconnect\n });\n}\n\nasync function start(): Promise<void> {\n let config: BridgeConfig;\n try {\n const resp = await fetch(chrome.runtime.getURL(\"config.json\"));\n if (!resp.ok) throw new Error(`HTTP ${resp.status}`);\n config = parseConfig(await resp.text());\n } catch (e) {\n // Nothing to fall back to \u2014 surface why the bridge won't start and stop.\n console.error(\"[bitrix-bridge] cannot load config.json:\", e);\n return;\n }\n connect(config);\n}\n\nstart();\n"],
5
+ "mappings": "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",
6
+ "names": ["window", "text", "isRecord", "isRecord"]
7
7
  }
@@ -6,18 +6,21 @@
6
6
  return { source: BRIDGE_MSG_SOURCE, kind: "sessid-response", nonce, sessid };
7
7
  }
8
8
  function isRecord(v) {
9
- return typeof v == "object" && v !== null;
9
+ return typeof v === "object" && v !== null;
10
10
  }
11
11
  function parseSessidRequest(data) {
12
- return !isRecord(data) || data.source !== BRIDGE_MSG_SOURCE || data.kind !== "sessid-request" || typeof data.nonce != "string" ? null : { source: BRIDGE_MSG_SOURCE, kind: "sessid-request", nonce: data.nonce };
12
+ if (!isRecord(data)) return null;
13
+ if (data.source !== BRIDGE_MSG_SOURCE || data.kind !== "sessid-request") return null;
14
+ if (typeof data.nonce !== "string") return null;
15
+ return { source: BRIDGE_MSG_SOURCE, kind: "sessid-request", nonce: data.nonce };
13
16
  }
14
17
 
15
18
  // extension/src/sessid-shim.ts
16
19
  window.addEventListener("message", (ev) => {
17
20
  if (ev.source !== window) return;
18
- let req = parseSessidRequest(ev.data);
21
+ const req = parseSessidRequest(ev.data);
19
22
  if (!req) return;
20
- let sessid = window.BX?.bitrix_sessid?.() || "";
23
+ const sessid = window.BX?.bitrix_sessid?.() || "";
21
24
  window.postMessage(buildSessidResponse(req.nonce, sessid), location.origin);
22
25
  });
23
26
  })();
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/bridge-protocol.ts", "../src/sessid-shim.ts"],
4
- "sourcesContent": ["// Pure, browser-agnostic \u2014 unit-tested. No DOM / chrome / window.BX here.\n//\n// Two concerns live together because both are the *contract* between the extension's\n// two content scripts and the packaged config, and both are pure/testable:\n// 1) parseConfig \u2014 validate the per-user config.json ({ token, port }).\n// 2) the postMessage protocol \u2014 the envelopes the ISOLATED connector and the MAIN\n// script exchange over window.postMessage (sessid request/response, and \u2014 in the\n// capture build \u2014 MAIN-observed capture entries forwarded to the connector).\n\nimport type { CapturedEntry } from \"./bridge-core.ts\";\n\nexport interface BridgeConfig {\n token: string;\n port: number;\n}\n\n/**\n * Parse and validate the packaged config.json body. Throws with a clear,\n * user-facing message (mentioning config.json) on any malformed input so the\n * connector can surface why it failed to start.\n */\nexport function parseConfig(raw: string): BridgeConfig {\n let doc: unknown;\n try {\n doc = JSON.parse(raw);\n } catch {\n throw new Error(\"config.json is not valid JSON\");\n }\n if (typeof doc !== \"object\" || doc === null) {\n throw new Error(\"config.json must be an object like { token, port }\");\n }\n const obj = doc as Record<string, unknown>;\n const token = typeof obj.token === \"string\" ? obj.token.trim() : \"\";\n if (!token) throw new Error(\"config.json is missing a non-empty `token`\");\n const port = Number(obj.port);\n if (!Number.isInteger(port) || port < 1 || port > 65535) {\n throw new Error(\"config.json is missing a numeric `port`\");\n }\n return { token, port };\n}\n\n// --- sessid postMessage protocol -------------------------------------------\n//\n// window.postMessage is delivered to BOTH the MAIN and ISOLATED worlds of the same\n// frame (they share the DOM window), and each world also receives its own posts. So\n// every envelope is tagged with a fixed `source` and a `kind`, and each side ignores\n// anything that isn't addressed to it. Responses additionally carry the request nonce\n// so the connector can match a reply to the exact in-flight request it issued.\n\nexport const BRIDGE_MSG_SOURCE = \"bitrix24-mcp-bridge\";\n\nexport interface SessidRequestMsg {\n source: typeof BRIDGE_MSG_SOURCE;\n kind: \"sessid-request\";\n nonce: string;\n}\n\nexport interface SessidResponseMsg {\n source: typeof BRIDGE_MSG_SOURCE;\n kind: \"sessid-response\";\n nonce: string;\n sessid: string;\n}\n\nexport function buildSessidRequest(nonce: string): SessidRequestMsg {\n return { source: BRIDGE_MSG_SOURCE, kind: \"sessid-request\", nonce };\n}\n\nexport function buildSessidResponse(nonce: string, sessid: string): SessidResponseMsg {\n return { source: BRIDGE_MSG_SOURCE, kind: \"sessid-response\", nonce, sessid };\n}\n\nfunction isRecord(v: unknown): v is Record<string, unknown> {\n return typeof v === \"object\" && v !== null;\n}\n\n/** For the MAIN shim: a well-formed request from our connector, or null. */\nexport function parseSessidRequest(data: unknown): SessidRequestMsg | null {\n if (!isRecord(data)) return null;\n if (data.source !== BRIDGE_MSG_SOURCE || data.kind !== \"sessid-request\") return null;\n if (typeof data.nonce !== \"string\") return null;\n return { source: BRIDGE_MSG_SOURCE, kind: \"sessid-request\", nonce: data.nonce };\n}\n\n/** For the ISOLATED connector: a response matching `expectedNonce`, or null. */\nexport function parseSessidResponse(data: unknown, expectedNonce: string): SessidResponseMsg | null {\n if (!isRecord(data)) return null;\n if (data.source !== BRIDGE_MSG_SOURCE || data.kind !== \"sessid-response\") return null;\n if (typeof data.nonce !== \"string\" || data.nonce !== expectedNonce) return null;\n if (typeof data.sessid !== \"string\") return null;\n return { source: BRIDGE_MSG_SOURCE, kind: \"sessid-response\", nonce: data.nonce, sessid: data.sessid };\n}\n\n// --- capture forwarding (capture build only) -------------------------------\n//\n// The capture hooks must observe the PAGE's fetch/XHR, so they run in the MAIN world.\n// The socket lives only in the ISOLATED connector, so MAIN forwards each observed entry\n// to the connector over postMessage; the connector relays it to the daemon.\n\nexport interface CaptureForwardMsg {\n source: typeof BRIDGE_MSG_SOURCE;\n kind: \"capture\";\n call: CapturedEntry;\n}\n\nexport function buildCaptureForward(call: CapturedEntry): CaptureForwardMsg {\n return { source: BRIDGE_MSG_SOURCE, kind: \"capture\", call };\n}\n\n/** For the ISOLATED connector: a capture entry forwarded by the MAIN script, or null. */\nexport function parseCaptureForward(data: unknown): CaptureForwardMsg | null {\n if (!isRecord(data)) return null;\n if (data.source !== BRIDGE_MSG_SOURCE || data.kind !== \"capture\") return null;\n if (!isRecord(data.call)) return null;\n return { source: BRIDGE_MSG_SOURCE, kind: \"capture\", call: data.call as unknown as CapturedEntry };\n}\n", "// MAIN-world shim. The ONLY reason it runs in the page's MAIN world is that\n// window.BX (and thus the rotating CSRF sessid) lives there. It holds NO token and\n// NO socket \u2014 it just answers the ISOLATED connector's sessid requests.\n//\n// In the capture build it additionally installs the page-traffic recorder (which must\n// run in MAIN to observe the page's own fetch/XHR) and forwards each observed entry to\n// the connector over postMessage; the connector owns the socket and relays to the daemon.\n\nimport { parseSessidRequest, buildSessidResponse, buildCaptureForward } from \"./bridge-protocol.ts\";\nimport { installCapture } from \"./capture.ts\";\n\n// Stripped from normal builds via esbuild `define` + dead-code elimination.\ndeclare const __BITRIX_CAPTURE__: boolean;\n\n// window.BX is provided by the Bitrix24 page itself (MAIN world).\ndeclare global {\n interface Window {\n BX?: { bitrix_sessid?: () => string };\n }\n}\n\nwindow.addEventListener(\"message\", (ev: MessageEvent) => {\n if (ev.source !== window) return; // same frame only \u2014 ignore other frames/windows\n const req = parseSessidRequest(ev.data);\n if (!req) return;\n const sessid = window.BX?.bitrix_sessid?.() || \"\";\n window.postMessage(buildSessidResponse(req.nonce, sessid), location.origin);\n});\n\nif (__BITRIX_CAPTURE__) {\n installCapture((call) => window.postMessage(buildCaptureForward(call), location.origin));\n}\n"],
5
- "mappings": ";;;AAiDO,MAAM,oBAAoB;AAmB1B,WAAS,oBAAoB,OAAe,QAAmC;AACpF,WAAO,EAAE,QAAQ,mBAAmB,MAAM,mBAAmB,OAAO,OAAO;AAAA,EAC7E;AAEA,WAAS,SAAS,GAA0C;AAC1D,WAAO,OAAO,KAAM,YAAY,MAAM;AAAA,EACxC;AAGO,WAAS,mBAAmB,MAAwC;AAGzE,WAFI,CAAC,SAAS,IAAI,KACd,KAAK,WAAW,qBAAqB,KAAK,SAAS,oBACnD,OAAO,KAAK,SAAU,WAAiB,OACpC,EAAE,QAAQ,mBAAmB,MAAM,kBAAkB,OAAO,KAAK,MAAM;AAAA,EAChF;;;AC7DA,SAAO,iBAAiB,WAAW,CAAC,OAAqB;AACvD,QAAI,GAAG,WAAW,OAAQ;AAC1B,QAAM,MAAM,mBAAmB,GAAG,IAAI;AACtC,QAAI,CAAC,IAAK;AACV,QAAM,SAAS,OAAO,IAAI,gBAAgB,KAAK;AAC/C,WAAO,YAAY,oBAAoB,IAAI,OAAO,MAAM,GAAG,SAAS,MAAM;AAAA,EAC5E,CAAC;",
4
+ "sourcesContent": ["// Pure, browser-agnostic \u2014 unit-tested. No DOM / chrome / window.BX here.\n//\n// Two concerns live together because both are the *contract* between the extension's\n// two content scripts and the packaged config, and both are pure/testable:\n// 1) parseConfig \u2014 validate the per-user config.json ({ token, port }).\n// 2) the postMessage protocol \u2014 the envelopes the ISOLATED connector and the MAIN\n// script exchange over window.postMessage (sessid request/response).\n\nexport interface BridgeConfig {\n token: string;\n port: number;\n}\n\n/**\n * Parse and validate the packaged config.json body. Throws with a clear,\n * user-facing message (mentioning config.json) on any malformed input so the\n * connector can surface why it failed to start.\n */\nexport function parseConfig(raw: string): BridgeConfig {\n let doc: unknown;\n try {\n doc = JSON.parse(raw);\n } catch {\n throw new Error(\"config.json is not valid JSON\");\n }\n if (typeof doc !== \"object\" || doc === null) {\n throw new Error(\"config.json must be an object like { token, port }\");\n }\n const obj = doc as Record<string, unknown>;\n const token = typeof obj.token === \"string\" ? obj.token.trim() : \"\";\n if (!token) throw new Error(\"config.json is missing a non-empty `token`\");\n const port = Number(obj.port);\n if (!Number.isInteger(port) || port < 1 || port > 65535) {\n throw new Error(\"config.json is missing a numeric `port`\");\n }\n return { token, port };\n}\n\n// --- sessid postMessage protocol -------------------------------------------\n//\n// window.postMessage is delivered to BOTH the MAIN and ISOLATED worlds of the same\n// frame (they share the DOM window), and each world also receives its own posts. So\n// every envelope is tagged with a fixed `source` and a `kind`, and each side ignores\n// anything that isn't addressed to it. Responses additionally carry the request nonce\n// so the connector can match a reply to the exact in-flight request it issued.\n\nexport const BRIDGE_MSG_SOURCE = \"bitrix24-mcp-bridge\";\n\nexport interface SessidRequestMsg {\n source: typeof BRIDGE_MSG_SOURCE;\n kind: \"sessid-request\";\n nonce: string;\n}\n\nexport interface SessidResponseMsg {\n source: typeof BRIDGE_MSG_SOURCE;\n kind: \"sessid-response\";\n nonce: string;\n sessid: string;\n}\n\nexport function buildSessidRequest(nonce: string): SessidRequestMsg {\n return { source: BRIDGE_MSG_SOURCE, kind: \"sessid-request\", nonce };\n}\n\nexport function buildSessidResponse(nonce: string, sessid: string): SessidResponseMsg {\n return { source: BRIDGE_MSG_SOURCE, kind: \"sessid-response\", nonce, sessid };\n}\n\nfunction isRecord(v: unknown): v is Record<string, unknown> {\n return typeof v === \"object\" && v !== null;\n}\n\n/** For the MAIN shim: a well-formed request from our connector, or null. */\nexport function parseSessidRequest(data: unknown): SessidRequestMsg | null {\n if (!isRecord(data)) return null;\n if (data.source !== BRIDGE_MSG_SOURCE || data.kind !== \"sessid-request\") return null;\n if (typeof data.nonce !== \"string\") return null;\n return { source: BRIDGE_MSG_SOURCE, kind: \"sessid-request\", nonce: data.nonce };\n}\n\n/** For the ISOLATED connector: a response matching `expectedNonce`, or null. */\nexport function parseSessidResponse(data: unknown, expectedNonce: string): SessidResponseMsg | null {\n if (!isRecord(data)) return null;\n if (data.source !== BRIDGE_MSG_SOURCE || data.kind !== \"sessid-response\") return null;\n if (typeof data.nonce !== \"string\" || data.nonce !== expectedNonce) return null;\n if (typeof data.sessid !== \"string\") return null;\n return { source: BRIDGE_MSG_SOURCE, kind: \"sessid-response\", nonce: data.nonce, sessid: data.sessid };\n}\n", "// MAIN-world shim. The ONLY reason it runs in the page's MAIN world is that\n// window.BX (and thus the rotating CSRF sessid) lives there. It holds NO token and\n// NO socket \u2014 it just answers the ISOLATED connector's sessid requests.\n\nimport { parseSessidRequest, buildSessidResponse } from \"./bridge-protocol.ts\";\n\n// window.BX is provided by the Bitrix24 page itself (MAIN world).\ndeclare global {\n interface Window {\n BX?: { bitrix_sessid?: () => string };\n }\n}\n\nwindow.addEventListener(\"message\", (ev: MessageEvent) => {\n if (ev.source !== window) return; // same frame only \u2014 ignore other frames/windows\n const req = parseSessidRequest(ev.data);\n if (!req) return;\n const sessid = window.BX?.bitrix_sessid?.() || \"\";\n window.postMessage(buildSessidResponse(req.nonce, sessid), location.origin);\n});\n"],
5
+ "mappings": ";;;AA8CO,MAAM,oBAAoB;AAmB1B,WAAS,oBAAoB,OAAe,QAAmC;AACpF,WAAO,EAAE,QAAQ,mBAAmB,MAAM,mBAAmB,OAAO,OAAO;AAAA,EAC7E;AAEA,WAAS,SAAS,GAA0C;AAC1D,WAAO,OAAO,MAAM,YAAY,MAAM;AAAA,EACxC;AAGO,WAAS,mBAAmB,MAAwC;AACzE,QAAI,CAAC,SAAS,IAAI,EAAG,QAAO;AAC5B,QAAI,KAAK,WAAW,qBAAqB,KAAK,SAAS,iBAAkB,QAAO;AAChF,QAAI,OAAO,KAAK,UAAU,SAAU,QAAO;AAC3C,WAAO,EAAE,QAAQ,mBAAmB,MAAM,kBAAkB,OAAO,KAAK,MAAM;AAAA,EAChF;;;AClEA,SAAO,iBAAiB,WAAW,CAAC,OAAqB;AACvD,QAAI,GAAG,WAAW,OAAQ;AAC1B,UAAM,MAAM,mBAAmB,GAAG,IAAI;AACtC,QAAI,CAAC,IAAK;AACV,UAAM,SAAS,OAAO,IAAI,gBAAgB,KAAK;AAC/C,WAAO,YAAY,oBAAoB,IAAI,OAAO,MAAM,GAAG,SAAS,MAAM;AAAA,EAC5E,CAAC;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "bitrix24-mcp-bridge",
4
- "version": "0.4.0",
5
- "description": "MCP bridge server for Bitrix24 — exposes REST API access, tasks, projects and chat to MCP clients like Claude Code.",
4
+ "version": "1.0.0",
5
+ "description": "MCP bridge server for Bitrix24 — exposes tasks, projects, chats and calls from your live browser session to MCP clients like Claude Code.",
6
6
  "license": "MIT",
7
7
  "repository": {
8
8
  "type": "git",
@@ -15,21 +15,15 @@
15
15
  "files": [
16
16
  "dist",
17
17
  "extension/dist",
18
- "docs/api-notes.md",
19
- "actions.example.json"
18
+ "docs/api-notes.md"
20
19
  ],
21
20
  "scripts": {
22
21
  "test": "bun test",
23
22
  "dev": "bun run src/index.ts",
24
23
  "typecheck": "bunx tsc --noEmit && bunx tsc -p extension/tsconfig.json",
25
24
  "build:dist": "node scripts/build-dist.mjs",
26
- "build:ext": "bun run scripts/build-extension.ts",
27
- "build:ext:capture": "BITRIX_CAPTURE=1 bun run scripts/build-extension.ts",
28
25
  "build:ext:static": "node scripts/build-ext-static.mjs",
29
26
  "prepare": "node scripts/build-dist.mjs && node scripts/build-ext-static.mjs",
30
- "capture": "bun run src/capture-server.ts",
31
- "catalog:draft": "bun run src/catalog/build-catalog.ts captured.har > actions.draft.json",
32
- "catalog": "bun run src/catalog/build-catalog.ts",
33
27
  "sync:runtime": "bun run scripts/sync-runtime.ts"
34
28
  },
35
29
  "dependencies": {
@@ -1,32 +0,0 @@
1
- {
2
- "tasks.list": { "action": "tasks.task.list", "method": "POST" },
3
- "task.get": { "action": "tasks.task.get", "method": "POST" },
4
- "task.v2.get": { "action": "tasks.v2.Task.get", "method": "POST", "bodyType": "json" },
5
- "task.scrum.info": { "action": "tasks.v2.Scrum.getTaskInfo", "method": "POST", "bodyType": "json" },
6
- "task.files": { "action": "tasks.v2.File.listObjects", "method": "POST", "bodyType": "json" },
7
- "task.views.count": { "action": "tasks.v2.Task.View.User.count", "method": "POST", "bodyType": "json" },
8
-
9
- "projects.list": { "action": "socialnetwork.api.workgroup.list", "method": "POST" },
10
- "projects.get": { "action": "socialnetwork.api.workgroup.get", "method": "POST" },
11
-
12
- "chats.recent": { "endpoint": "/rest/im.recent.list.json", "method": "POST", "params": { "LIMIT": 50, "SKIP_OPENLINES": "Y", "GET_ORIGINAL_TEXT": "Y", "PARSE_TEXT": "Y", "WITH_COUNTERS": "N" } },
13
- "chat.load": { "action": "im.v2.Chat.load", "method": "POST", "params": { "messageLimit": 25 } },
14
- "chat.messages": { "action": "im.v2.Chat.Message.list", "method": "POST" },
15
- "chat.messages.tail": { "action": "im.v2.Chat.Message.tail", "method": "POST", "params": { "order[id]": "DESC", "limit": 25 } },
16
- "chat.message.read": { "action": "im.v2.Chat.Message.read", "method": "POST" },
17
-
18
- "entityselector.load": { "action": "ui.entityselector.load", "method": "POST", "bodyType": "json" },
19
- "entityselector.search": { "action": "ui.entityselector.doSearch", "method": "POST", "bodyType": "json" },
20
-
21
- "recent.load": { "action": "im.v2.Recent.load", "method": "POST" },
22
- "recent.tail": { "action": "im.v2.Recent.tail", "method": "POST" },
23
- "chat.dialogId": { "action": "im.v2.Chat.getDialogId", "method": "POST" },
24
- "chat.read.all": { "action": "im.v2.Chat.readAll", "method": "POST" },
25
- "im.user.get": { "endpoint": "/rest/im.user.get.json", "method": "POST" },
26
- "im.chat.get": { "endpoint": "/rest/im.chat.get.json", "method": "POST" },
27
-
28
- "task.subtasks": { "action": "tasks.v2.Task.Relation.Child.list", "method": "POST", "bodyType": "json" },
29
- "task.related": { "action": "tasks.v2.Task.Relation.Related.list", "method": "POST", "bodyType": "json" },
30
-
31
- "call.detail": { "endpoint": "/call/detail/{callId}", "method": "GET", "params": { "IFRAME": "Y", "IFRAME_TYPE": "SIDE_SLIDER" }, "responseType": "text" }
32
- }