@buzzni/saycode-cli 0.9.8 → 0.9.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -1
- package/channel-extension-host.mjs +382 -30
- package/index.mjs +12405 -5590
- package/official-extensions/buzzni.discord-1.3.0.saycode-extension.base64 +1 -0
- package/official-extensions/buzzni.slack-1.2.0.saycode-extension.base64 +1 -0
- package/official-extensions/buzzni.telegram-1.3.0.saycode-extension.base64 +1 -0
- package/official-extensions/catalog.v1.json +12 -12
- package/package.json +1 -1
- package/windows-host/saycode-windows-host.exe +0 -0
- package/official-extensions/buzzni.discord-1.1.0.saycode-extension.base64 +0 -1
- package/official-extensions/buzzni.slack-1.1.0.saycode-extension.base64 +0 -1
- package/official-extensions/buzzni.telegram-1.1.0.saycode-extension.base64 +0 -1
|
@@ -7,7 +7,7 @@ const ASSET_PERMISSIONS = ["assets.read", "assets.write", "files.select", "draft
|
|
|
7
7
|
function usesAssetWorkflow(permissions) {
|
|
8
8
|
return permissions.some((permission) => ASSET_PERMISSIONS.includes(permission));
|
|
9
9
|
}
|
|
10
|
-
const LONG_RUNNING_PERMISSIONS = ["localTools.inspect", "localTools.install", "localTools.control", "localBrowser.setup", "machine.run"];
|
|
10
|
+
const LONG_RUNNING_PERMISSIONS = ["localTools.inspect", "localTools.install", "localTools.control", "localBrowser.setup", "browser.runtimeSetup", "machine.run"];
|
|
11
11
|
function extensionRequestTimeoutMs(permissions, side) {
|
|
12
12
|
const long = usesAssetWorkflow(permissions) || permissions.some((permission) => LONG_RUNNING_PERMISSIONS.includes(permission));
|
|
13
13
|
if (!long) return void 0;
|
|
@@ -48,6 +48,15 @@ function contributionList(value, path2, allowedKeys, parse) {
|
|
|
48
48
|
return parse(entry, entryPath);
|
|
49
49
|
});
|
|
50
50
|
}
|
|
51
|
+
const ENTRYPOINT_PATTERN = /\.(?:cjs|mjs|js)$/;
|
|
52
|
+
function relativePath(value, path2, requireScript = false) {
|
|
53
|
+
const parsed = string(value, path2);
|
|
54
|
+
const segments = parsed.split("/");
|
|
55
|
+
if (parsed.startsWith("/") || parsed.includes("\\") || segments.some((segment) => segment === "" || segment === "." || segment === "..") || requireScript && !ENTRYPOINT_PATTERN.test(parsed)) {
|
|
56
|
+
fail$5(path2, "expected safe relative path");
|
|
57
|
+
}
|
|
58
|
+
return parsed;
|
|
59
|
+
}
|
|
51
60
|
function keys$1(value, allowed) {
|
|
52
61
|
const result = record$1(value, "widget");
|
|
53
62
|
assertOnlyKeys(result, new Set(allowed), "widget");
|
|
@@ -138,6 +147,23 @@ function assertExtensionFileViewers(extensionId2, viewers, panels, permissions)
|
|
|
138
147
|
if (!permissions.includes("filePreviews.read") || !viewer.id.startsWith(`${extensionId2}.`) || !viewer.panelId.startsWith(`${extensionId2}.`) || !panels.some((panel) => panel.id === viewer.panelId)) fail$5("manifest.contributes.fileViewers", "requires filePreviews.read and an owned panel");
|
|
139
148
|
}
|
|
140
149
|
}
|
|
150
|
+
const ELEMENT_PICK_INSPECT_PERMISSION = "browserPicks.inspect";
|
|
151
|
+
function parseExtensionElementPickInspectors(value, apiVersion) {
|
|
152
|
+
if (apiVersion < 4) fail$5("manifest.contributes.elementPickInspectors", "requires Extension API version 4");
|
|
153
|
+
return contributionList(value, "manifest.contributes.elementPickInspectors", ["id", "title", "script"], (entry, path2) => ({
|
|
154
|
+
id: id(entry.id, `${path2}.id`),
|
|
155
|
+
title: string(entry.title, `${path2}.title`),
|
|
156
|
+
script: relativePath(entry.script, `${path2}.script`, true)
|
|
157
|
+
}));
|
|
158
|
+
}
|
|
159
|
+
function assertExtensionElementPickInspectors(extensionId2, inspectors, permissions) {
|
|
160
|
+
if (inspectors.length > 0 !== permissions.includes(ELEMENT_PICK_INSPECT_PERMISSION)) {
|
|
161
|
+
fail$5("manifest.contributes.elementPickInspectors", `requires ${ELEMENT_PICK_INSPECT_PERMISSION} permission and an inspector that uses it`);
|
|
162
|
+
}
|
|
163
|
+
for (const inspector of inspectors) {
|
|
164
|
+
if (!inspector.id.startsWith(`${extensionId2}.`)) fail$5("manifest.contributes.elementPickInspectors", "expected an id declared by the same extension");
|
|
165
|
+
}
|
|
166
|
+
}
|
|
141
167
|
function parseMachineCommands(value, extensionId2, apiVersion) {
|
|
142
168
|
if (apiVersion < 3) fail$5("manifest.machineCommands", "requires Extension API version 3");
|
|
143
169
|
const root = record$1(value, "manifest.machineCommands");
|
|
@@ -443,8 +469,11 @@ function parseExtensionArtifactActions(value, path2 = "manifest.contributes.arti
|
|
|
443
469
|
};
|
|
444
470
|
});
|
|
445
471
|
}
|
|
446
|
-
const CHANNEL_API_VERSION =
|
|
472
|
+
const CHANNEL_API_VERSION = 2;
|
|
447
473
|
const CHANNEL_MINIMUM_API_VERSION = 1;
|
|
474
|
+
function isSupportedChannelApiVersion(value) {
|
|
475
|
+
return typeof value === "number" && Number.isInteger(value) && value >= CHANNEL_MINIMUM_API_VERSION && value <= CHANNEL_API_VERSION;
|
|
476
|
+
}
|
|
448
477
|
const CHANNEL_PROVIDERS = ["telegram", "slack", "discord"];
|
|
449
478
|
const CHANNEL_COMMAND_SCHEMA = Object.freeze({
|
|
450
479
|
pair: Object.freeze(["code!"]),
|
|
@@ -585,6 +614,7 @@ const EXTENSION_PERMISSIONS = [
|
|
|
585
614
|
"localTools.install",
|
|
586
615
|
"localTools.control",
|
|
587
616
|
"localBrowser.setup",
|
|
617
|
+
"browser.runtimeSetup",
|
|
588
618
|
"network.fetch",
|
|
589
619
|
"notifications.show",
|
|
590
620
|
"storage.read",
|
|
@@ -594,6 +624,7 @@ const EXTENSION_PERMISSIONS = [
|
|
|
594
624
|
"browserViewer.installChrome",
|
|
595
625
|
"artifacts.publishPublic",
|
|
596
626
|
"filePreviews.read",
|
|
627
|
+
"browserPicks.inspect",
|
|
597
628
|
// One permission per channel wire method (`src/domain/channelProtocol.ts`), deliberately not a
|
|
598
629
|
// single adapter grant. Receiving and acking messages is transport work; reading a session,
|
|
599
630
|
// driving it, or approving a tool call once are three larger and separately-refusable
|
|
@@ -606,7 +637,11 @@ const EXTENSION_PERMISSIONS = [
|
|
|
606
637
|
"channels.transport",
|
|
607
638
|
"sessions.read",
|
|
608
639
|
"sessions.control",
|
|
609
|
-
"sessions.approveOnce"
|
|
640
|
+
"sessions.approveOnce",
|
|
641
|
+
// Core → adapter, not a capability the adapter invokes: Core sends the current input and the
|
|
642
|
+
// thread's earlier messages (other people's words included) for display-text normalization
|
|
643
|
+
// (specs/slack-bot-improvements contracts §2). Channel API 2 only.
|
|
644
|
+
"channels.normalize"
|
|
610
645
|
];
|
|
611
646
|
const CONVERSATION_MODS_INTERNAL = typeof __SAYCODE_MODS_INTERNAL__ !== "undefined" && __SAYCODE_MODS_INTERNAL__ === true;
|
|
612
647
|
const SAYCODE_EXTENSION_API_VERSION = CONVERSATION_MODS_INTERNAL ? 5 : 4;
|
|
@@ -617,7 +652,7 @@ const SAYCODE_CHANNEL_API_VERSION = CHANNEL_API_VERSION;
|
|
|
617
652
|
const SAYCODE_CHANNEL_MINIMUM_API_VERSION = CHANNEL_MINIMUM_API_VERSION;
|
|
618
653
|
class UnsupportedChannelApiVersionError extends Error {
|
|
619
654
|
constructor(channelApiVersion) {
|
|
620
|
-
super(`manifest.channelApiVersion: channel API version ${String(channelApiVersion)} is unsupported; Desktop supports ${
|
|
655
|
+
super(`manifest.channelApiVersion: channel API version ${String(channelApiVersion)} is unsupported; Desktop supports ${`${SAYCODE_CHANNEL_MINIMUM_API_VERSION}-${SAYCODE_CHANNEL_API_VERSION}`}`);
|
|
621
656
|
this.channelApiVersion = channelApiVersion;
|
|
622
657
|
}
|
|
623
658
|
name = "UnsupportedChannelApiVersionError";
|
|
@@ -633,15 +668,14 @@ class UnsupportedExtensionApiVersionError extends Error {
|
|
|
633
668
|
}
|
|
634
669
|
const SEMVER_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
635
670
|
const ENGINE_RANGE_PATTERN = /^(?:\^|~|>=|<=|>|<)?\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
|
|
636
|
-
const ENTRYPOINT_PATTERN = /\.(?:cjs|mjs|js)$/;
|
|
637
671
|
const LOCALE_PATTERN = /^[a-z]{2,3}(?:-[A-Z]{2})?$/;
|
|
638
|
-
const CONTRIBUTION_KEYS = /* @__PURE__ */ new Set(["chatWidgets", "commands", "settings", "panels", "projectTemplates", "machineActions", "projectTools", "artifactActions", "channels", "fileViewers"]);
|
|
672
|
+
const CONTRIBUTION_KEYS = /* @__PURE__ */ new Set(["chatWidgets", "commands", "settings", "panels", "projectTemplates", "machineActions", "projectTools", "artifactActions", "channels", "fileViewers", "elementPickInspectors"]);
|
|
639
673
|
const V2_PERMISSIONS = /* @__PURE__ */ new Set([
|
|
640
674
|
"browserViewer.open",
|
|
641
675
|
"browserViewer.install",
|
|
642
676
|
"browserViewer.installChrome"
|
|
643
677
|
]);
|
|
644
|
-
const V3_PERMISSIONS = /* @__PURE__ */ new Set(["machine.run", "artifacts.publishPublic", "assets.read", "assets.write", "files.select", "drafts.prepare", "localTools.inspect", "localTools.install", "localTools.control", "localBrowser.setup"]);
|
|
678
|
+
const V3_PERMISSIONS = /* @__PURE__ */ new Set(["machine.run", "artifacts.publishPublic", "assets.read", "assets.write", "files.select", "drafts.prepare", "localTools.inspect", "localTools.install", "localTools.control", "localBrowser.setup", "browser.runtimeSetup"]);
|
|
645
679
|
const CHANNEL_PERMISSIONS = /* @__PURE__ */ new Set([
|
|
646
680
|
"channels.receive",
|
|
647
681
|
"channels.ack",
|
|
@@ -649,7 +683,8 @@ const CHANNEL_PERMISSIONS = /* @__PURE__ */ new Set([
|
|
|
649
683
|
"channels.transport",
|
|
650
684
|
"sessions.read",
|
|
651
685
|
"sessions.control",
|
|
652
|
-
"sessions.approveOnce"
|
|
686
|
+
"sessions.approveOnce",
|
|
687
|
+
"channels.normalize"
|
|
653
688
|
]);
|
|
654
689
|
const TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
|
|
655
690
|
"id",
|
|
@@ -707,14 +742,6 @@ function parseApiVersion(value, options) {
|
|
|
707
742
|
}
|
|
708
743
|
return value;
|
|
709
744
|
}
|
|
710
|
-
function relativePath(value, path2, requireScript = false) {
|
|
711
|
-
const parsed = string(value, path2);
|
|
712
|
-
const segments = parsed.split("/");
|
|
713
|
-
if (parsed.startsWith("/") || parsed.includes("\\") || segments.some((segment) => segment === "" || segment === "." || segment === "..") || requireScript && !ENTRYPOINT_PATTERN.test(parsed)) {
|
|
714
|
-
fail$5(path2, "expected safe relative path");
|
|
715
|
-
}
|
|
716
|
-
return parsed;
|
|
717
|
-
}
|
|
718
745
|
function parseChannelApiVersion(value, apiVersion) {
|
|
719
746
|
if (value === void 0) return void 0;
|
|
720
747
|
if (apiVersion < 3) fail$5("manifest.channelApiVersion", "requires Extension API version 3");
|
|
@@ -738,6 +765,7 @@ function parseContributions(value, apiVersion, channelApiVersion) {
|
|
|
738
765
|
}
|
|
739
766
|
const result = {};
|
|
740
767
|
if (parsed.fileViewers !== void 0) result.fileViewers = parseExtensionFileViewers(parsed.fileViewers, apiVersion);
|
|
768
|
+
if (parsed.elementPickInspectors !== void 0) result.elementPickInspectors = parseExtensionElementPickInspectors(parsed.elementPickInspectors, apiVersion);
|
|
741
769
|
if (parsed.commands !== void 0) {
|
|
742
770
|
result.commands = contributionList(
|
|
743
771
|
parsed.commands,
|
|
@@ -860,7 +888,8 @@ function parseContributions(value, apiVersion, channelApiVersion) {
|
|
|
860
888
|
...result.machineActions ?? [],
|
|
861
889
|
...result.projectTools ?? [],
|
|
862
890
|
...result.artifactActions ?? [],
|
|
863
|
-
...result.channels ?? []
|
|
891
|
+
...result.channels ?? [],
|
|
892
|
+
...result.elementPickInspectors ?? []
|
|
864
893
|
].map((entry) => entry.id);
|
|
865
894
|
if (new Set(ids).size !== ids.length) fail$5("manifest.contributes", "duplicate contribution id");
|
|
866
895
|
return result;
|
|
@@ -893,6 +922,7 @@ function parseExtensionManifest(value, options) {
|
|
|
893
922
|
const typed = parsed;
|
|
894
923
|
if (typed.startsWith("sessionContext.") && apiVersion !== 5) fail$5("manifest.permissions", "sessionContext requires API 5");
|
|
895
924
|
if (typed === "filePreviews.read" && apiVersion < 4) fail$5("manifest.permissions", "filePreviews.read requires Extension API version 4");
|
|
925
|
+
if (typed === "browserPicks.inspect" && apiVersion < 4) fail$5("manifest.permissions", "browserPicks.inspect requires Extension API version 4");
|
|
896
926
|
if (apiVersion < 2 && V2_PERMISSIONS.has(typed)) {
|
|
897
927
|
fail$5(`manifest.permissions[${index}]`, "requires Extension API version 2");
|
|
898
928
|
}
|
|
@@ -904,6 +934,9 @@ function parseExtensionManifest(value, options) {
|
|
|
904
934
|
if (channelApiVersion === void 0) {
|
|
905
935
|
fail$5(`manifest.permissions[${index}]`, "requires manifest.channelApiVersion");
|
|
906
936
|
}
|
|
937
|
+
if (typed === "channels.normalize" && channelApiVersion < 2) {
|
|
938
|
+
fail$5(`manifest.permissions[${index}]`, "channels.normalize requires channel API version 2");
|
|
939
|
+
}
|
|
907
940
|
}
|
|
908
941
|
return typed;
|
|
909
942
|
});
|
|
@@ -971,6 +1004,7 @@ function parseExtensionManifest(value, options) {
|
|
|
971
1004
|
}
|
|
972
1005
|
}
|
|
973
1006
|
assertExtensionFileViewers(parsedId, contributes.fileViewers ?? [], contributes.panels ?? [], permissions);
|
|
1007
|
+
assertExtensionElementPickInspectors(parsedId, contributes.elementPickInspectors ?? [], permissions);
|
|
974
1008
|
assertExtensionProjectToolTargets(parsedId, contributes.projectTools ?? [], contributes.commands ?? []);
|
|
975
1009
|
return {
|
|
976
1010
|
id: parsedId,
|
|
@@ -1051,6 +1085,246 @@ function parseChannelApprovalRequest(value) {
|
|
|
1051
1085
|
}
|
|
1052
1086
|
return { approvalHandle, approveLabel, denyLabel };
|
|
1053
1087
|
}
|
|
1088
|
+
const NORMALIZE_ADDITION_SOURCES = ["blocks", "quote", "forward", "table", "unfurl", "files"];
|
|
1089
|
+
const MAX_NORMALIZE_ITEMS = 200;
|
|
1090
|
+
const MAX_USER_LABELS = 200;
|
|
1091
|
+
const MAX_BLOCKS = 50;
|
|
1092
|
+
const MAX_ATTACHMENTS = 20;
|
|
1093
|
+
const MAX_FILES = 20;
|
|
1094
|
+
const MAX_RICH_TEXT_DEPTH = 6;
|
|
1095
|
+
const MAX_URL_LENGTH = 2048;
|
|
1096
|
+
const MAX_ADDITIONS_BYTES = 8192;
|
|
1097
|
+
const MAX_PRIMARY_BYTES = 65536;
|
|
1098
|
+
const ITEM_REF = /^i(0|[1-9]\d?|1\d\d)$/;
|
|
1099
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1100
|
+
const isString = (value) => typeof value === "string";
|
|
1101
|
+
function projectSlackMessage(raw) {
|
|
1102
|
+
const state = { truncated: false };
|
|
1103
|
+
const message = isRecord(raw) ? raw : {};
|
|
1104
|
+
const projection = {};
|
|
1105
|
+
const blocks = projectBlocks(message.blocks, state);
|
|
1106
|
+
if (blocks) projection.blocks = blocks;
|
|
1107
|
+
const attachments = capped(Array.isArray(message.attachments) ? message.attachments.filter(isRecord) : [], MAX_ATTACHMENTS, state).map((attachment) => projectAttachment(attachment, state));
|
|
1108
|
+
if (attachments.length > 0) projection.attachments = attachments;
|
|
1109
|
+
const files = capped(Array.isArray(message.files) ? message.files.filter(isRecord) : [], MAX_FILES, state).map(projectFile);
|
|
1110
|
+
if (files.length > 0) projection.files = files;
|
|
1111
|
+
if (isString(message.subtype)) projection.subtype = message.subtype;
|
|
1112
|
+
return { projection, truncated: state.truncated };
|
|
1113
|
+
}
|
|
1114
|
+
function capped(items, max, state) {
|
|
1115
|
+
if (items.length > max) state.truncated = true;
|
|
1116
|
+
return items.slice(0, max);
|
|
1117
|
+
}
|
|
1118
|
+
function projectBlocks(value, state) {
|
|
1119
|
+
if (!Array.isArray(value)) return void 0;
|
|
1120
|
+
const projected = value.map((block) => projectBlock(block, state)).filter((block) => block !== null);
|
|
1121
|
+
return projected.length > 0 ? capped(projected, MAX_BLOCKS, state) : void 0;
|
|
1122
|
+
}
|
|
1123
|
+
function projectBlock(block, state) {
|
|
1124
|
+
if (!isRecord(block)) return null;
|
|
1125
|
+
switch (block.type) {
|
|
1126
|
+
case "rich_text":
|
|
1127
|
+
return { type: "rich_text", elements: projectContainers(block.elements, 1, state) };
|
|
1128
|
+
case "section": {
|
|
1129
|
+
const section = { type: "section" };
|
|
1130
|
+
const text2 = projectTextObject(block.text);
|
|
1131
|
+
if (text2) section.text = text2;
|
|
1132
|
+
if (Array.isArray(block.fields)) section.fields = block.fields.map(projectTextObject).filter((field) => field !== null);
|
|
1133
|
+
return section;
|
|
1134
|
+
}
|
|
1135
|
+
case "header": {
|
|
1136
|
+
const text2 = projectTextObject(block.text);
|
|
1137
|
+
return text2 ? { type: "header", text: text2 } : null;
|
|
1138
|
+
}
|
|
1139
|
+
case "context":
|
|
1140
|
+
return { type: "context", elements: Array.isArray(block.elements) ? block.elements.map(projectTextObject).filter((element) => element !== null) : [] };
|
|
1141
|
+
case "table":
|
|
1142
|
+
return { type: "table", rows: Array.isArray(block.rows) ? block.rows.filter(Array.isArray).map((row2) => row2.flatMap((cell) => {
|
|
1143
|
+
if (isRecord(cell) && cell.type === "raw_text" && isString(cell.text)) return [{ type: "raw_text", text: cell.text }];
|
|
1144
|
+
if (isRecord(cell) && cell.type === "rich_text") return [{ type: "rich_text", elements: projectContainers(cell.elements, 1, state) }];
|
|
1145
|
+
return [];
|
|
1146
|
+
})) : [] };
|
|
1147
|
+
default:
|
|
1148
|
+
return null;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
function projectTextObject(value) {
|
|
1152
|
+
if (!isRecord(value) || value.type !== "mrkdwn" && value.type !== "plain_text" || !isString(value.text)) return null;
|
|
1153
|
+
return { type: value.type, text: value.text };
|
|
1154
|
+
}
|
|
1155
|
+
const CONTAINER_TYPES = /* @__PURE__ */ new Set(["rich_text_section", "rich_text_quote", "rich_text_preformatted", "rich_text_list"]);
|
|
1156
|
+
function projectContainers(value, depth, state) {
|
|
1157
|
+
if (!Array.isArray(value)) return [];
|
|
1158
|
+
return value.flatMap((node) => {
|
|
1159
|
+
const container = isRecord(node) && CONTAINER_TYPES.has(String(node.type)) ? projectContainer(node, depth, state) : null;
|
|
1160
|
+
return container ? [container] : [];
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
function projectContainer(node, depth, state) {
|
|
1164
|
+
if (depth > MAX_RICH_TEXT_DEPTH) {
|
|
1165
|
+
state.truncated = true;
|
|
1166
|
+
return null;
|
|
1167
|
+
}
|
|
1168
|
+
const elements = (Array.isArray(node.elements) ? node.elements : []).flatMap((child) => {
|
|
1169
|
+
if (isRecord(child) && CONTAINER_TYPES.has(String(child.type))) {
|
|
1170
|
+
const nested = projectContainer(child, depth + 1, state);
|
|
1171
|
+
return nested ? [nested] : [];
|
|
1172
|
+
}
|
|
1173
|
+
const leaf = projectLeaf(child);
|
|
1174
|
+
return leaf ? [leaf] : [];
|
|
1175
|
+
});
|
|
1176
|
+
const container = { type: node.type, elements };
|
|
1177
|
+
if (node.style === "bullet" || node.style === "ordered") container.style = node.style;
|
|
1178
|
+
return container;
|
|
1179
|
+
}
|
|
1180
|
+
function projectLeaf(node) {
|
|
1181
|
+
if (!isRecord(node)) return null;
|
|
1182
|
+
switch (node.type) {
|
|
1183
|
+
case "text": {
|
|
1184
|
+
if (!isString(node.text)) return null;
|
|
1185
|
+
const leaf = { type: "text", text: node.text };
|
|
1186
|
+
if (isRecord(node.style)) {
|
|
1187
|
+
const style = Object.fromEntries(["bold", "italic", "strike", "code"].filter((key) => typeof node.style[key] === "boolean").map((key) => [key, node.style[key]]));
|
|
1188
|
+
if (Object.keys(style).length > 0) leaf.style = style;
|
|
1189
|
+
}
|
|
1190
|
+
return leaf;
|
|
1191
|
+
}
|
|
1192
|
+
case "link": {
|
|
1193
|
+
const label = isString(node.text) ? node.text : void 0;
|
|
1194
|
+
if (!httpUrl(node.url)) return label === void 0 ? null : { type: "text", text: label };
|
|
1195
|
+
return label === void 0 ? { type: "link", url: node.url } : { type: "link", url: node.url, text: label };
|
|
1196
|
+
}
|
|
1197
|
+
case "user":
|
|
1198
|
+
return isString(node.user_id) ? { type: "user", user_id: node.user_id } : null;
|
|
1199
|
+
case "channel":
|
|
1200
|
+
return isString(node.channel_id) ? { type: "channel", channel_id: node.channel_id } : null;
|
|
1201
|
+
case "usergroup":
|
|
1202
|
+
return isString(node.usergroup_id) ? { type: "usergroup", usergroup_id: node.usergroup_id } : null;
|
|
1203
|
+
case "emoji":
|
|
1204
|
+
return isString(node.name) ? { type: "emoji", name: node.name } : null;
|
|
1205
|
+
case "broadcast":
|
|
1206
|
+
return node.range === "here" || node.range === "channel" || node.range === "everyone" ? { type: "broadcast", range: node.range } : null;
|
|
1207
|
+
default:
|
|
1208
|
+
return null;
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
const ATTACHMENT_TEXT_KEYS = ["text", "fallback", "pretext", "title", "author_name", "footer"];
|
|
1212
|
+
const ATTACHMENT_URL_KEYS = ["title_link", "from_url", "original_url"];
|
|
1213
|
+
function projectAttachment(attachment, state) {
|
|
1214
|
+
const projected = {};
|
|
1215
|
+
for (const key of ATTACHMENT_TEXT_KEYS) if (isString(attachment[key])) projected[key] = attachment[key];
|
|
1216
|
+
for (const key of ATTACHMENT_URL_KEYS) if (httpUrl(attachment[key])) projected[key] = attachment[key];
|
|
1217
|
+
if (typeof attachment.is_share === "boolean") projected.is_share = attachment.is_share;
|
|
1218
|
+
if (typeof attachment.is_msg_unfurl === "boolean") projected.is_msg_unfurl = attachment.is_msg_unfurl;
|
|
1219
|
+
if (Array.isArray(attachment.message_blocks)) {
|
|
1220
|
+
projected.message_blocks = attachment.message_blocks.filter(isRecord).map((forwarded) => ({
|
|
1221
|
+
message: { blocks: projectBlocks(isRecord(forwarded.message) ? forwarded.message.blocks : void 0, state) ?? [] }
|
|
1222
|
+
}));
|
|
1223
|
+
}
|
|
1224
|
+
return projected;
|
|
1225
|
+
}
|
|
1226
|
+
function projectFile(file) {
|
|
1227
|
+
const projected = {};
|
|
1228
|
+
for (const key of ["name", "title", "filetype", "mimetype"]) if (isString(file[key])) projected[key] = file[key];
|
|
1229
|
+
if (Number.isSafeInteger(file.size) && file.size >= 0) projected.size = file.size;
|
|
1230
|
+
return projected;
|
|
1231
|
+
}
|
|
1232
|
+
function httpUrl(value) {
|
|
1233
|
+
return isString(value) && value.length <= MAX_URL_LENGTH && /^https?:\/\//i.test(value);
|
|
1234
|
+
}
|
|
1235
|
+
function parseChannelNormalizeRequest(value) {
|
|
1236
|
+
const request = plainRecord(value, "normalize request");
|
|
1237
|
+
exactKeySet(request, ["apiVersion", "channelId", "connectionId", "provider", "connectionRevision", "requestId", "purpose", "userLabels", "items"], "normalize request");
|
|
1238
|
+
if (request.apiVersion !== 2 || request.provider !== "slack") fail("normalize request is channel API 2 Slack only");
|
|
1239
|
+
if (request.purpose !== "currentInput" && request.purpose !== "threadHistory") fail("invalid normalize purpose");
|
|
1240
|
+
if (!Number.isSafeInteger(request.connectionRevision) || request.connectionRevision < 1) fail("invalid normalize connection revision");
|
|
1241
|
+
const labels = plainRecord(request.userLabels, "normalize userLabels");
|
|
1242
|
+
if (Object.keys(labels).length > MAX_USER_LABELS || Object.entries(labels).some(([key, label]) => key.length === 0 || key.length > 64 || !isString(label) || label.length > 256)) {
|
|
1243
|
+
fail("invalid normalize userLabels");
|
|
1244
|
+
}
|
|
1245
|
+
if (!Array.isArray(request.items) || request.items.length < 1 || request.items.length > MAX_NORMALIZE_ITEMS) fail("invalid normalize items");
|
|
1246
|
+
const refs = /* @__PURE__ */ new Set();
|
|
1247
|
+
const items = request.items.map((value2) => {
|
|
1248
|
+
const item = plainRecord(value2, "normalize item");
|
|
1249
|
+
exactKeySet(item, ["itemRef", "primaryText", "projection", "maxAdditionsBytes"], "normalize item");
|
|
1250
|
+
if (!isString(item.itemRef) || !ITEM_REF.test(item.itemRef) || refs.has(item.itemRef)) fail("invalid normalize itemRef");
|
|
1251
|
+
refs.add(item.itemRef);
|
|
1252
|
+
if (!isString(item.primaryText) || utf8Bytes(item.primaryText) > MAX_PRIMARY_BYTES) fail("invalid normalize primaryText");
|
|
1253
|
+
if (!Number.isSafeInteger(item.maxAdditionsBytes) || item.maxAdditionsBytes < 1 || item.maxAdditionsBytes > MAX_ADDITIONS_BYTES) fail("invalid normalize maxAdditionsBytes");
|
|
1254
|
+
const projected = projectSlackMessage(item.projection);
|
|
1255
|
+
if (!isRecord(item.projection) || projected.truncated || !sameJson(projected.projection, item.projection)) fail("normalize projection is not a Core projection");
|
|
1256
|
+
return { itemRef: item.itemRef, primaryText: item.primaryText, projection: projected.projection, maxAdditionsBytes: item.maxAdditionsBytes };
|
|
1257
|
+
});
|
|
1258
|
+
return {
|
|
1259
|
+
apiVersion: 2,
|
|
1260
|
+
provider: "slack",
|
|
1261
|
+
purpose: request.purpose,
|
|
1262
|
+
channelId: boundedText(request.channelId, "channelId"),
|
|
1263
|
+
connectionId: boundedText(request.connectionId, "connectionId"),
|
|
1264
|
+
connectionRevision: request.connectionRevision,
|
|
1265
|
+
requestId: boundedText(request.requestId, "requestId"),
|
|
1266
|
+
userLabels: { ...labels },
|
|
1267
|
+
items
|
|
1268
|
+
};
|
|
1269
|
+
}
|
|
1270
|
+
function parseChannelNormalizeResult(request, value) {
|
|
1271
|
+
const result = plainRecord(value, "normalize result");
|
|
1272
|
+
exactKeySet(result, ["items"], "normalize result");
|
|
1273
|
+
if (!Array.isArray(result.items)) fail("invalid normalize result items");
|
|
1274
|
+
const wanted = new Map(request.items.map((item) => [item.itemRef, item]));
|
|
1275
|
+
const parsed = /* @__PURE__ */ new Map();
|
|
1276
|
+
for (const value2 of result.items) {
|
|
1277
|
+
const item = plainRecord(value2, "normalize result item");
|
|
1278
|
+
exactKeySet(item, ["itemRef", "additions", "truncated"], "normalize result item");
|
|
1279
|
+
const requested = isString(item.itemRef) ? wanted.get(item.itemRef) : void 0;
|
|
1280
|
+
if (!requested || parsed.has(requested.itemRef)) fail("normalize result items differ from the request");
|
|
1281
|
+
if (typeof item.truncated !== "boolean" || !Array.isArray(item.additions)) fail("invalid normalize result item");
|
|
1282
|
+
let bytes = 0;
|
|
1283
|
+
const additions = item.additions.map((value3) => {
|
|
1284
|
+
const addition = plainRecord(value3, "normalize addition");
|
|
1285
|
+
exactKeySet(addition, ["source", "text"], "normalize addition");
|
|
1286
|
+
if (!NORMALIZE_ADDITION_SOURCES.includes(addition.source) || !isString(addition.text) || addition.text.includes("\0") || hasLoneSurrogate(addition.text)) fail("invalid normalize addition");
|
|
1287
|
+
bytes += utf8Bytes(addition.text);
|
|
1288
|
+
return { source: addition.source, text: addition.text };
|
|
1289
|
+
});
|
|
1290
|
+
if (bytes > requested.maxAdditionsBytes) fail("normalize additions exceed their budget");
|
|
1291
|
+
parsed.set(requested.itemRef, { itemRef: requested.itemRef, additions: withoutRepeatedLines(requested.primaryText, additions), truncated: item.truncated });
|
|
1292
|
+
}
|
|
1293
|
+
if (parsed.size !== wanted.size) fail("normalize result items differ from the request");
|
|
1294
|
+
return request.items.map((item) => parsed.get(item.itemRef));
|
|
1295
|
+
}
|
|
1296
|
+
function withoutRepeatedLines(primaryText, additions) {
|
|
1297
|
+
const fold = (line) => line.replace(/\s+/gu, " ").trim();
|
|
1298
|
+
const known = new Set(primaryText.split("\n").map(fold).filter(Boolean));
|
|
1299
|
+
const kept = [];
|
|
1300
|
+
for (const addition of additions) {
|
|
1301
|
+
const lines2 = addition.text.split("\n");
|
|
1302
|
+
const remaining = lines2.filter((line) => fold(line) === "" || !known.has(fold(line)));
|
|
1303
|
+
for (const line of lines2) if (fold(line)) known.add(fold(line));
|
|
1304
|
+
if (remaining.some((line) => fold(line) !== "")) kept.push({ source: addition.source, text: remaining.join("\n") });
|
|
1305
|
+
}
|
|
1306
|
+
return kept;
|
|
1307
|
+
}
|
|
1308
|
+
function exactKeySet(record2, keys2, path2) {
|
|
1309
|
+
const actual = Object.keys(record2);
|
|
1310
|
+
if (actual.length !== keys2.length || !keys2.every((key) => actual.includes(key))) fail(`${path2} has unexpected keys`);
|
|
1311
|
+
}
|
|
1312
|
+
function boundedText(value, path2) {
|
|
1313
|
+
if (!isString(value) || value.length === 0 || value.length > 200) fail(`invalid normalize ${path2}`);
|
|
1314
|
+
return value;
|
|
1315
|
+
}
|
|
1316
|
+
function sameJson(a, b) {
|
|
1317
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
1318
|
+
return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((item, index) => sameJson(item, b[index]));
|
|
1319
|
+
}
|
|
1320
|
+
if (isRecord(a) && isRecord(b)) {
|
|
1321
|
+
const keys2 = Object.keys(a);
|
|
1322
|
+
return keys2.length === Object.keys(b).length && keys2.every((key) => Object.hasOwn(b, key) && sameJson(a[key], b[key]));
|
|
1323
|
+
}
|
|
1324
|
+
return Object.is(a, b);
|
|
1325
|
+
}
|
|
1326
|
+
const utf8Bytes = (text2) => new TextEncoder().encode(text2).length;
|
|
1327
|
+
const hasLoneSurrogate = (text2) => /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/.test(text2);
|
|
1054
1328
|
function finiteNumber(value, path2) {
|
|
1055
1329
|
if (typeof value !== "number" || !Number.isFinite(value)) fail(`${path2} must be a finite number`);
|
|
1056
1330
|
return value;
|
|
@@ -1120,6 +1394,13 @@ function parseRequest(message) {
|
|
|
1120
1394
|
connectionRevision: Number(params.connectionRevision)
|
|
1121
1395
|
} };
|
|
1122
1396
|
}
|
|
1397
|
+
case "channel.intake": {
|
|
1398
|
+
exactKeys(params, ["apiVersion", "channelId", "connectionId", "provider", "connectionRevision"], "request.params");
|
|
1399
|
+
const lifecycle = parseRequest({ kind: "request", id: id2, method: "channel.lifecycle", params: { ...params, state: "start" } });
|
|
1400
|
+
if (lifecycle.method !== "channel.lifecycle") fail("invalid channel intake");
|
|
1401
|
+
const { state: _state, ...identity } = lifecycle.params;
|
|
1402
|
+
return { kind: "request", id: id2, method, params: identity };
|
|
1403
|
+
}
|
|
1123
1404
|
case "channel.formatReply": {
|
|
1124
1405
|
const formatKeys = [
|
|
1125
1406
|
"apiVersion",
|
|
@@ -1133,13 +1414,15 @@ function parseRequest(message) {
|
|
|
1133
1414
|
"maxChunkUtf16Units",
|
|
1134
1415
|
"maxChunks"
|
|
1135
1416
|
];
|
|
1417
|
+
const v2 = params.apiVersion === 2;
|
|
1136
1418
|
exactKeys(
|
|
1137
1419
|
params,
|
|
1138
|
-
"approval" in params ? [...formatKeys, "approval"] : formatKeys,
|
|
1420
|
+
v2 ? [...formatKeys, "mode", "dialect"] : "approval" in params ? [...formatKeys, "approval"] : formatKeys,
|
|
1139
1421
|
"request.params"
|
|
1140
1422
|
);
|
|
1423
|
+
if (v2 && (params.mode !== "verbatim" && params.mode !== "rendered" || params.dialect !== "slack-mrkdwn" || params.kind === "approval")) fail("invalid channel API 2 formatter request");
|
|
1141
1424
|
const lifecycle = parseRequest({ kind: "request", id: id2, method: "channel.lifecycle", params: {
|
|
1142
|
-
apiVersion: params.apiVersion,
|
|
1425
|
+
apiVersion: v2 ? 1 : params.apiVersion,
|
|
1143
1426
|
channelId: params.channelId,
|
|
1144
1427
|
connectionId: params.connectionId,
|
|
1145
1428
|
provider: params.provider,
|
|
@@ -1155,16 +1438,26 @@ function parseRequest(message) {
|
|
|
1155
1438
|
}
|
|
1156
1439
|
const approval = params.approval === void 0 ? void 0 : parseChannelApprovalRequest(params.approval);
|
|
1157
1440
|
const { state: _state, ...identity } = lifecycle.params;
|
|
1441
|
+
const bounds = { requestId, text: body, maxChunks: Number(params.maxChunks), maxChunkUtf16Units: Number(params.maxChunkUtf16Units) };
|
|
1442
|
+
if (v2) {
|
|
1443
|
+
return { kind: "request", id: id2, method, params: {
|
|
1444
|
+
...identity,
|
|
1445
|
+
...bounds,
|
|
1446
|
+
apiVersion: 2,
|
|
1447
|
+
kind: params.kind,
|
|
1448
|
+
mode: params.mode,
|
|
1449
|
+
dialect: "slack-mrkdwn"
|
|
1450
|
+
} };
|
|
1451
|
+
}
|
|
1158
1452
|
return { kind: "request", id: id2, method, params: {
|
|
1159
1453
|
...identity,
|
|
1160
|
-
|
|
1161
|
-
text: body,
|
|
1454
|
+
...bounds,
|
|
1162
1455
|
kind: params.kind,
|
|
1163
|
-
maxChunks: Number(params.maxChunks),
|
|
1164
|
-
maxChunkUtf16Units: Number(params.maxChunkUtf16Units),
|
|
1165
1456
|
...approval ? { approval } : {}
|
|
1166
1457
|
} };
|
|
1167
1458
|
}
|
|
1459
|
+
case "channel.normalizeMessages":
|
|
1460
|
+
return { kind: "request", id: id2, method, params: parseChannelNormalizeRequest(params) };
|
|
1168
1461
|
case "invokeCommand": {
|
|
1169
1462
|
exactKeys(params, ["commandId", "args"], "request.params");
|
|
1170
1463
|
if (!Array.isArray(params.args)) fail("request.params.args must be an array");
|
|
@@ -1247,6 +1540,17 @@ function parseExtensionWireMessage(value) {
|
|
|
1247
1540
|
if (message.kind === "event") return parseEvent(message);
|
|
1248
1541
|
fail("message.kind is unknown");
|
|
1249
1542
|
}
|
|
1543
|
+
function parseChannelRenderedReplyResult(value) {
|
|
1544
|
+
const result = plainRecord(value, "channel rendered result");
|
|
1545
|
+
if (Object.keys(result).join() !== "rendered" || !Array.isArray(result.rendered)) fail("invalid channel rendered result");
|
|
1546
|
+
return {
|
|
1547
|
+
rendered: result.rendered.map((value2) => {
|
|
1548
|
+
const chunk = plainRecord(value2, "channel rendered chunk");
|
|
1549
|
+
if (Object.keys(chunk).sort().join() !== "sourceEnd,sourceStart,text" || typeof chunk.text !== "string" || !Number.isSafeInteger(chunk.sourceStart) || !Number.isSafeInteger(chunk.sourceEnd)) fail("invalid channel rendered chunk");
|
|
1550
|
+
return { text: chunk.text, sourceStart: chunk.sourceStart, sourceEnd: chunk.sourceEnd };
|
|
1551
|
+
})
|
|
1552
|
+
};
|
|
1553
|
+
}
|
|
1250
1554
|
const TIMEOUT = Symbol("extension-request-timeout");
|
|
1251
1555
|
function requestEnvelope(value) {
|
|
1252
1556
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
@@ -1268,6 +1572,8 @@ class ExtensionHostRuntime {
|
|
|
1268
1572
|
widgets = /* @__PURE__ */ new Map();
|
|
1269
1573
|
channelHandlers = /* @__PURE__ */ new Map();
|
|
1270
1574
|
channelFormatters = /* @__PURE__ */ new Map();
|
|
1575
|
+
channelNormalizers = /* @__PURE__ */ new Map();
|
|
1576
|
+
channelIntakeHandlers = /* @__PURE__ */ new Map();
|
|
1271
1577
|
channelStates = /* @__PURE__ */ new Map();
|
|
1272
1578
|
channelCalls = /* @__PURE__ */ new Map();
|
|
1273
1579
|
channelEpoch = 0;
|
|
@@ -1402,6 +1708,8 @@ class ExtensionHostRuntime {
|
|
|
1402
1708
|
this.commandHandlers.clear();
|
|
1403
1709
|
this.channelHandlers.clear();
|
|
1404
1710
|
this.channelFormatters.clear();
|
|
1711
|
+
this.channelNormalizers.clear();
|
|
1712
|
+
this.channelIntakeHandlers.clear();
|
|
1405
1713
|
this.channelStates.clear();
|
|
1406
1714
|
this.channelCalls.clear();
|
|
1407
1715
|
}
|
|
@@ -1444,6 +1752,8 @@ class ExtensionHostRuntime {
|
|
|
1444
1752
|
this.commandHandlers.clear();
|
|
1445
1753
|
this.channelHandlers.clear();
|
|
1446
1754
|
this.channelFormatters.clear();
|
|
1755
|
+
this.channelNormalizers.clear();
|
|
1756
|
+
this.channelIntakeHandlers.clear();
|
|
1447
1757
|
this.channelStates.clear();
|
|
1448
1758
|
this.channelCalls.clear();
|
|
1449
1759
|
this.deactivating = false;
|
|
@@ -1463,6 +1773,8 @@ class ExtensionHostRuntime {
|
|
|
1463
1773
|
}
|
|
1464
1774
|
if (request.method === "channel.lifecycle") return this.deliverChannel(request.params);
|
|
1465
1775
|
if (request.method === "channel.formatReply") return this.formatChannelReply(request.params);
|
|
1776
|
+
if (request.method === "channel.normalizeMessages") return this.normalizeChannelMessages(request.params);
|
|
1777
|
+
if (request.method === "channel.intake") return this.deliverChannelIntake(request.params);
|
|
1466
1778
|
const handler = this.commandHandlers.get(request.params.commandId);
|
|
1467
1779
|
if (!handler) {
|
|
1468
1780
|
throw { code: "COMMAND_NOT_REGISTERED", message: `extension command is not registered: ${request.params.commandId}` };
|
|
@@ -1474,7 +1786,7 @@ class ExtensionHostRuntime {
|
|
|
1474
1786
|
}
|
|
1475
1787
|
async deliverChannel(event) {
|
|
1476
1788
|
const contribution = this.options.manifest.contributes.channels?.find((channel) => channel.id === event.channelId);
|
|
1477
|
-
if (this.options.manifest.channelApiVersion
|
|
1789
|
+
if (!isSupportedChannelApiVersion(this.options.manifest.channelApiVersion) || !contribution || contribution.provider !== event.provider) {
|
|
1478
1790
|
throw { code: "CHANNEL_NOT_DECLARED", message: "channel contribution/provider is not declared" };
|
|
1479
1791
|
}
|
|
1480
1792
|
const key = JSON.stringify([event.channelId, event.connectionId]);
|
|
@@ -1511,17 +1823,38 @@ class ExtensionHostRuntime {
|
|
|
1511
1823
|
if (this.channelCalls.get(key) === next) this.channelCalls.delete(key);
|
|
1512
1824
|
}
|
|
1513
1825
|
}
|
|
1826
|
+
/** A hint for a live connection only; anything else is answered and dropped, never an error. */
|
|
1827
|
+
async deliverChannelIntake(input) {
|
|
1828
|
+
const current = this.channelStates.get(JSON.stringify([input.channelId, input.connectionId]));
|
|
1829
|
+
const handler = this.channelIntakeHandlers.get(input.channelId);
|
|
1830
|
+
if (!handler || this.deactivating || current?.state !== "start" || current.provider !== input.provider || current.connectionRevision !== input.connectionRevision) return null;
|
|
1831
|
+
await handler(Object.freeze({ ...input }));
|
|
1832
|
+
return null;
|
|
1833
|
+
}
|
|
1514
1834
|
async formatChannelReply(input) {
|
|
1515
|
-
const
|
|
1516
|
-
const current = this.channelStates.get(key);
|
|
1517
|
-
const epoch = this.channelEpoch;
|
|
1835
|
+
const isCurrent = this.channelCallIsCurrent(input);
|
|
1518
1836
|
const formatter = this.channelFormatters.get(input.channelId);
|
|
1519
|
-
const isCurrent = () => !this.disposed && !this.deactivating && this.active && epoch === this.channelEpoch && this.channelStates.get(key) === current && current?.state === "start" && current.provider === input.provider && current.connectionRevision === input.connectionRevision;
|
|
1520
1837
|
if (!isCurrent() || !formatter) throw { code: "CHANNEL_FORMAT_UNAVAILABLE", message: "channel formatter is not active" };
|
|
1521
1838
|
const result = await formatter(Object.freeze({ ...input }));
|
|
1522
1839
|
if (!isCurrent()) throw { code: "CHANNEL_FORMAT_WITHDRAWN", message: "channel formatter stopped" };
|
|
1840
|
+
if (input.apiVersion === 2 && input.mode === "rendered") return parseChannelRenderedReplyResult(result);
|
|
1523
1841
|
return parseChannelReplyFormatResult(result, input);
|
|
1524
1842
|
}
|
|
1843
|
+
async normalizeChannelMessages(input) {
|
|
1844
|
+
const isCurrent = this.channelCallIsCurrent(input);
|
|
1845
|
+
const normalizer = this.channelNormalizers.get(input.channelId);
|
|
1846
|
+
if (!isCurrent() || !normalizer) throw { code: "CHANNEL_NORMALIZE_UNAVAILABLE", message: "channel normalizer is not active" };
|
|
1847
|
+
const result = await normalizer(Object.freeze({ ...input }));
|
|
1848
|
+
if (!isCurrent()) throw { code: "CHANNEL_NORMALIZE_WITHDRAWN", message: "channel normalizer stopped" };
|
|
1849
|
+
return { items: parseChannelNormalizeResult(input, result) };
|
|
1850
|
+
}
|
|
1851
|
+
/** A formatter or normalizer answers only for the started connection generation it was asked about. */
|
|
1852
|
+
channelCallIsCurrent(input) {
|
|
1853
|
+
const key = JSON.stringify([input.channelId, input.connectionId]);
|
|
1854
|
+
const current = this.channelStates.get(key);
|
|
1855
|
+
const epoch = this.channelEpoch;
|
|
1856
|
+
return () => !this.disposed && !this.deactivating && this.active && epoch === this.channelEpoch && this.channelStates.get(key) === current && current?.state === "start" && current.provider === input.provider && current.connectionRevision === input.connectionRevision;
|
|
1857
|
+
}
|
|
1525
1858
|
async withTimeout(operation) {
|
|
1526
1859
|
let timer;
|
|
1527
1860
|
const timeout = new Promise((resolve2) => {
|
|
@@ -1555,6 +1888,8 @@ class ExtensionHostRuntime {
|
|
|
1555
1888
|
const registered = /* @__PURE__ */ new Map();
|
|
1556
1889
|
const registeredChannels = /* @__PURE__ */ new Map();
|
|
1557
1890
|
const registeredFormatters = /* @__PURE__ */ new Map();
|
|
1891
|
+
const registeredNormalizers = /* @__PURE__ */ new Map();
|
|
1892
|
+
const registeredIntake = /* @__PURE__ */ new Map();
|
|
1558
1893
|
await loaded.activate({
|
|
1559
1894
|
extensionId: this.options.manifest.id,
|
|
1560
1895
|
chatWidgets: { register: (id2, render) => {
|
|
@@ -1563,16 +1898,29 @@ class ExtensionHostRuntime {
|
|
|
1563
1898
|
} },
|
|
1564
1899
|
channels: {
|
|
1565
1900
|
register: (channelId, handler) => {
|
|
1566
|
-
if (!this.activating || this.options.manifest.channelApiVersion
|
|
1901
|
+
if (!this.activating || !isSupportedChannelApiVersion(this.options.manifest.channelApiVersion) || !this.options.manifest.contributes.channels?.some((channel) => channel.id === channelId) || typeof handler !== "function" || registeredChannels.has(channelId)) {
|
|
1567
1902
|
throw { code: "INVALID_CHANNEL_REGISTRATION", message: "channel must be declared and registered once" };
|
|
1568
1903
|
}
|
|
1569
1904
|
registeredChannels.set(channelId, handler);
|
|
1570
1905
|
},
|
|
1571
1906
|
registerFormatter: (channelId, formatter) => {
|
|
1572
|
-
if (!this.activating || this.options.manifest.channelApiVersion
|
|
1907
|
+
if (!this.activating || !isSupportedChannelApiVersion(this.options.manifest.channelApiVersion) || !this.options.manifest.contributes.channels?.some((channel) => channel.id === channelId) || typeof formatter !== "function" || registeredFormatters.has(channelId)) {
|
|
1573
1908
|
throw { code: "INVALID_CHANNEL_REGISTRATION", message: "formatter must be declared and registered once" };
|
|
1574
1909
|
}
|
|
1575
1910
|
registeredFormatters.set(channelId, formatter);
|
|
1911
|
+
},
|
|
1912
|
+
registerNormalizer: (channelId, normalizer) => {
|
|
1913
|
+
const manifest = this.options.manifest;
|
|
1914
|
+
if (!this.activating || (manifest.channelApiVersion ?? 0) < 2 || !manifest.permissions.includes("channels.normalize") || !manifest.contributes.channels?.some((channel) => channel.id === channelId) || typeof normalizer !== "function" || registeredNormalizers.has(channelId)) {
|
|
1915
|
+
throw { code: "INVALID_CHANNEL_REGISTRATION", message: "normalizer needs channel API 2, channels.normalize and one registration" };
|
|
1916
|
+
}
|
|
1917
|
+
registeredNormalizers.set(channelId, normalizer);
|
|
1918
|
+
},
|
|
1919
|
+
onIntake: (channelId, handler) => {
|
|
1920
|
+
if (!this.activating || !isSupportedChannelApiVersion(this.options.manifest.channelApiVersion) || !this.options.manifest.contributes.channels?.some((channel) => channel.id === channelId) || typeof handler !== "function" || registeredIntake.has(channelId)) {
|
|
1921
|
+
throw { code: "INVALID_CHANNEL_REGISTRATION", message: "intake handler must be declared and registered once" };
|
|
1922
|
+
}
|
|
1923
|
+
registeredIntake.set(channelId, handler);
|
|
1576
1924
|
}
|
|
1577
1925
|
},
|
|
1578
1926
|
commands: {
|
|
@@ -1591,7 +1939,11 @@ class ExtensionHostRuntime {
|
|
|
1591
1939
|
for (const [id2, render] of registeredWidgets) this.widgets.set(id2, render);
|
|
1592
1940
|
this.channelHandlers.clear();
|
|
1593
1941
|
this.channelFormatters.clear();
|
|
1942
|
+
this.channelNormalizers.clear();
|
|
1594
1943
|
for (const [id2, formatter] of registeredFormatters) this.channelFormatters.set(id2, formatter);
|
|
1944
|
+
for (const [id2, normalizer] of registeredNormalizers) this.channelNormalizers.set(id2, normalizer);
|
|
1945
|
+
this.channelIntakeHandlers.clear();
|
|
1946
|
+
for (const [id2, handler] of registeredIntake) this.channelIntakeHandlers.set(id2, handler);
|
|
1595
1947
|
for (const [id2, handler] of registeredChannels) this.channelHandlers.set(id2, handler);
|
|
1596
1948
|
this.commandHandlers.clear();
|
|
1597
1949
|
for (const [commandId, handler] of registered) this.commandHandlers.set(commandId, handler);
|