@serverkgg/bridge 0.1.13 → 0.1.15
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/package.json +1 -1
- package/rules/package.md +3 -0
- package/src/catalogs/curseforge.test.ts +46 -0
- package/src/catalogs/curseforge.ts +20 -0
- package/src/context/bridgeContext.ts +1 -0
- package/src/manifest/extension.ts +1 -0
- package/src/manifest/manifest.test.ts +18 -0
- package/src/modules/bridge.ts +13 -2
- package/src/modules/bridgeModule.ts +20 -4
- package/src/panel/bridgePanel.ts +2 -0
- package/src/protocol/bridgeCatalog.ts +7 -0
- package/src/protocol/bridgeCatalogSource.ts +9 -0
- package/src/protocol/bridgeConfig.ts +1 -0
- package/src/protocol/bridgeFrame.ts +1 -0
- package/src/protocol/bridgeLimits.ts +6 -0
- package/src/protocol/bridgeManifest.ts +2 -0
- package/src/protocol/index.ts +1 -0
- package/src/runtime/bridgeActions.ts +9 -2
- package/src/runtime/bridgeConfigReader.test.ts +31 -0
- package/src/runtime/bridgeConfigReader.ts +1 -0
- package/src/runtime/bridgeContext.test.ts +53 -0
- package/src/runtime/bridgeContext.ts +3 -0
- package/src/runtime/bridgeDriver.test.ts +347 -1
- package/src/runtime/bridgeDriver.ts +130 -18
- package/src/runtime/bridgeLiveConfig.test.ts +137 -1
- package/src/runtime/bridgeLiveConfig.ts +46 -2
- package/src/runtime/bridgeRegistry.test.ts +220 -0
- package/src/runtime/bridgeRegistry.ts +27 -1
- package/src/runtime/bridgeSample.test.ts +63 -0
- package/src/runtime/bridgeSample.ts +22 -0
- package/src/runtime/serve.ts +5 -12
- package/src/steam/workshop.test.ts +19 -1
- package/src/steam/workshop.ts +12 -0
- package/src/wire/panel.test.ts +177 -0
- package/src/wire/panel.ts +16 -0
|
@@ -4,10 +4,12 @@ import { BridgeKind, BridgeSetupStepKind, PortJoin, PortProtocol } from "../core
|
|
|
4
4
|
import { GuideOpenTab } from "../guides";
|
|
5
5
|
import type { ManifestPort, ManifestPortActivation } from "../manifest";
|
|
6
6
|
import {
|
|
7
|
+
type BridgeCatalogModule,
|
|
7
8
|
type BridgeDriver,
|
|
8
9
|
BridgeLifecycleSubject,
|
|
9
10
|
type BridgePendingModule,
|
|
10
11
|
type BridgeSetupModule,
|
|
12
|
+
type BridgeSourcedCatalogModule,
|
|
11
13
|
BridgeStopSignal,
|
|
12
14
|
} from "../modules";
|
|
13
15
|
import {
|
|
@@ -23,7 +25,7 @@ import {
|
|
|
23
25
|
BridgeUploadMode,
|
|
24
26
|
type BridgeVisibleWhen,
|
|
25
27
|
} from "../panel";
|
|
26
|
-
import type
|
|
28
|
+
import { type BridgeCatalogEntry, BridgeCatalogSourceKind, type BridgeSetupStep } from "../protocol";
|
|
27
29
|
import { validateDriver, validatePortActivation } from "./bridgeDriver";
|
|
28
30
|
|
|
29
31
|
const text = (value: string) => ({
|
|
@@ -1030,6 +1032,212 @@ describe("table uploads", () => {
|
|
|
1030
1032
|
});
|
|
1031
1033
|
});
|
|
1032
1034
|
|
|
1035
|
+
describe("catalog sources and uploads", () => {
|
|
1036
|
+
const entry = (id: string): BridgeCatalogEntry => ({
|
|
1037
|
+
id,
|
|
1038
|
+
provider: null,
|
|
1039
|
+
path: `Mods/${id}`,
|
|
1040
|
+
title: id,
|
|
1041
|
+
version: null,
|
|
1042
|
+
sizeBytes: 0,
|
|
1043
|
+
enabled: true,
|
|
1044
|
+
gameVersion: null,
|
|
1045
|
+
stale: false,
|
|
1046
|
+
pageUrl: null,
|
|
1047
|
+
icon: null,
|
|
1048
|
+
});
|
|
1049
|
+
|
|
1050
|
+
const sourced = (overrides: Partial<BridgeSourcedCatalogModule> = {}): BridgeSourcedCatalogModule => ({
|
|
1051
|
+
kind: BridgeKind.Catalog,
|
|
1052
|
+
source: {
|
|
1053
|
+
kind: BridgeCatalogSourceKind.Workshop,
|
|
1054
|
+
appId: 1_623_730,
|
|
1055
|
+
provided: [
|
|
1056
|
+
"UE4SS",
|
|
1057
|
+
],
|
|
1058
|
+
},
|
|
1059
|
+
installed: async () => [],
|
|
1060
|
+
install: async (_context, id) => entry(id),
|
|
1061
|
+
remove: async () => {
|
|
1062
|
+
return;
|
|
1063
|
+
},
|
|
1064
|
+
...overrides,
|
|
1065
|
+
});
|
|
1066
|
+
|
|
1067
|
+
const modsUpload: BridgeSectionUpload = {
|
|
1068
|
+
label: text("Upload a mod"),
|
|
1069
|
+
extensions: [
|
|
1070
|
+
"zip",
|
|
1071
|
+
],
|
|
1072
|
+
staging: ".serverk-staging/mods-upload",
|
|
1073
|
+
mode: BridgeUploadMode.Archive,
|
|
1074
|
+
};
|
|
1075
|
+
|
|
1076
|
+
const driverWithCatalog = (mods: BridgeCatalogModule, upload?: BridgeSectionUpload): BridgeDriver => ({
|
|
1077
|
+
install: {
|
|
1078
|
+
kind: BridgeKind.Install,
|
|
1079
|
+
run: async () => {
|
|
1080
|
+
return;
|
|
1081
|
+
},
|
|
1082
|
+
},
|
|
1083
|
+
lifecycle: {
|
|
1084
|
+
kind: BridgeKind.Lifecycle,
|
|
1085
|
+
command: async () => [
|
|
1086
|
+
"run",
|
|
1087
|
+
],
|
|
1088
|
+
},
|
|
1089
|
+
modules: {
|
|
1090
|
+
mods,
|
|
1091
|
+
},
|
|
1092
|
+
panel: {
|
|
1093
|
+
tabs: [
|
|
1094
|
+
{
|
|
1095
|
+
id: "mods",
|
|
1096
|
+
title: text("Mods"),
|
|
1097
|
+
sections: [
|
|
1098
|
+
{
|
|
1099
|
+
layout: BridgeLayout.Catalog,
|
|
1100
|
+
id: "mods",
|
|
1101
|
+
module: "mods",
|
|
1102
|
+
...(upload
|
|
1103
|
+
? {
|
|
1104
|
+
upload,
|
|
1105
|
+
}
|
|
1106
|
+
: {}),
|
|
1107
|
+
},
|
|
1108
|
+
],
|
|
1109
|
+
},
|
|
1110
|
+
],
|
|
1111
|
+
},
|
|
1112
|
+
});
|
|
1113
|
+
|
|
1114
|
+
it("accepts a sourced catalog that leaves search to the platform", () => {
|
|
1115
|
+
expect(() => validateDriver(driverWithCatalog(sourced()))).not.toThrow();
|
|
1116
|
+
});
|
|
1117
|
+
|
|
1118
|
+
it("rejects a catalog that has neither a source nor its own search", () => {
|
|
1119
|
+
const { source: _source, ...unsourced } = sourced();
|
|
1120
|
+
|
|
1121
|
+
expect(() => validateDriver(driverWithCatalog(unsourced as unknown as BridgeCatalogModule))).toThrow(
|
|
1122
|
+
'module "mods" declares no search — a catalog without a source answers its own search',
|
|
1123
|
+
);
|
|
1124
|
+
});
|
|
1125
|
+
|
|
1126
|
+
it("rejects a source the platform does not serve", () => {
|
|
1127
|
+
const mods = sourced({
|
|
1128
|
+
source: {
|
|
1129
|
+
kind: "nexus" as BridgeCatalogSourceKind,
|
|
1130
|
+
appId: 1_623_730,
|
|
1131
|
+
},
|
|
1132
|
+
});
|
|
1133
|
+
|
|
1134
|
+
expect(() => validateDriver(driverWithCatalog(mods))).toThrow(
|
|
1135
|
+
'module "mods" declares source "nexus" — the platform serves only workshop',
|
|
1136
|
+
);
|
|
1137
|
+
});
|
|
1138
|
+
|
|
1139
|
+
it("rejects a source app id that is not a positive whole number", () => {
|
|
1140
|
+
for (const appId of [
|
|
1141
|
+
0,
|
|
1142
|
+
-1,
|
|
1143
|
+
1.5,
|
|
1144
|
+
]) {
|
|
1145
|
+
const mods = sourced({
|
|
1146
|
+
source: {
|
|
1147
|
+
kind: BridgeCatalogSourceKind.Workshop,
|
|
1148
|
+
appId,
|
|
1149
|
+
},
|
|
1150
|
+
});
|
|
1151
|
+
|
|
1152
|
+
expect(() => validateDriver(driverWithCatalog(mods))).toThrow("it must be a positive whole number");
|
|
1153
|
+
}
|
|
1154
|
+
});
|
|
1155
|
+
|
|
1156
|
+
it("rejects a provided name that is not a non-empty string", () => {
|
|
1157
|
+
for (const name of [
|
|
1158
|
+
"",
|
|
1159
|
+
7,
|
|
1160
|
+
]) {
|
|
1161
|
+
const mods = sourced({
|
|
1162
|
+
source: {
|
|
1163
|
+
kind: BridgeCatalogSourceKind.Workshop,
|
|
1164
|
+
appId: 1_623_730,
|
|
1165
|
+
provided: [
|
|
1166
|
+
name as string,
|
|
1167
|
+
],
|
|
1168
|
+
},
|
|
1169
|
+
});
|
|
1170
|
+
|
|
1171
|
+
expect(() => validateDriver(driverWithCatalog(mods))).toThrow(
|
|
1172
|
+
"source declares a provided name that is not a non-empty string",
|
|
1173
|
+
);
|
|
1174
|
+
}
|
|
1175
|
+
});
|
|
1176
|
+
|
|
1177
|
+
it("rejects more provided names than the platform takes", () => {
|
|
1178
|
+
const mods = sourced({
|
|
1179
|
+
source: {
|
|
1180
|
+
kind: BridgeCatalogSourceKind.Workshop,
|
|
1181
|
+
appId: 1_623_730,
|
|
1182
|
+
provided: Array.from(
|
|
1183
|
+
{
|
|
1184
|
+
length: 17,
|
|
1185
|
+
},
|
|
1186
|
+
(_entry, index) => `Pack${index}`,
|
|
1187
|
+
),
|
|
1188
|
+
},
|
|
1189
|
+
});
|
|
1190
|
+
|
|
1191
|
+
expect(() => validateDriver(driverWithCatalog(mods))).toThrow("declares 17 provided names");
|
|
1192
|
+
});
|
|
1193
|
+
|
|
1194
|
+
it("rejects a sourced catalog that answers its own search", () => {
|
|
1195
|
+
const mods = {
|
|
1196
|
+
...sourced(),
|
|
1197
|
+
search: async () => ({
|
|
1198
|
+
hits: [],
|
|
1199
|
+
total: 0,
|
|
1200
|
+
providers: [],
|
|
1201
|
+
categories: [],
|
|
1202
|
+
sorts: [],
|
|
1203
|
+
}),
|
|
1204
|
+
} as unknown as BridgeCatalogModule;
|
|
1205
|
+
|
|
1206
|
+
expect(() => validateDriver(driverWithCatalog(mods))).toThrow(
|
|
1207
|
+
'module "mods" declares a source and its own search — the platform answers search for a sourced catalog',
|
|
1208
|
+
);
|
|
1209
|
+
});
|
|
1210
|
+
|
|
1211
|
+
it("accepts a catalog upload whose module installs it through add", () => {
|
|
1212
|
+
const mods = sourced({
|
|
1213
|
+
add: async () => entry("upload"),
|
|
1214
|
+
});
|
|
1215
|
+
|
|
1216
|
+
expect(() => validateDriver(driverWithCatalog(mods, modsUpload))).not.toThrow();
|
|
1217
|
+
});
|
|
1218
|
+
|
|
1219
|
+
it("rejects a catalog upload whose module has no add handler", () => {
|
|
1220
|
+
expect(() => validateDriver(driverWithCatalog(sourced(), modsUpload))).toThrow(
|
|
1221
|
+
'tab "mods" section "mods" declares an upload control but module "mods" has no add handler',
|
|
1222
|
+
);
|
|
1223
|
+
});
|
|
1224
|
+
|
|
1225
|
+
it("checks a catalog upload as it checks a table upload", () => {
|
|
1226
|
+
const mods = sourced({
|
|
1227
|
+
add: async () => entry("upload"),
|
|
1228
|
+
});
|
|
1229
|
+
|
|
1230
|
+
expect(() =>
|
|
1231
|
+
validateDriver(
|
|
1232
|
+
driverWithCatalog(mods, {
|
|
1233
|
+
...modsUpload,
|
|
1234
|
+
extensions: [],
|
|
1235
|
+
}),
|
|
1236
|
+
),
|
|
1237
|
+
).toThrow("declares an upload control with no extensions");
|
|
1238
|
+
});
|
|
1239
|
+
});
|
|
1240
|
+
|
|
1033
1241
|
describe("the pending module", () => {
|
|
1034
1242
|
const driverWithPending = (pending: BridgePendingModule): BridgeDriver => ({
|
|
1035
1243
|
install: {
|
|
@@ -1691,6 +1899,144 @@ describe("field patterns", () => {
|
|
|
1691
1899
|
});
|
|
1692
1900
|
});
|
|
1693
1901
|
|
|
1902
|
+
describe("field defaults", () => {
|
|
1903
|
+
it("takes a default on a boolean, a select and a text field of a variables form", () => {
|
|
1904
|
+
expect(() =>
|
|
1905
|
+
validateDriver(
|
|
1906
|
+
driverWithFields([
|
|
1907
|
+
{
|
|
1908
|
+
key: "WINDOWS_STEAM",
|
|
1909
|
+
control: BridgeControl.Boolean,
|
|
1910
|
+
label: text("Use Steam"),
|
|
1911
|
+
default: "true",
|
|
1912
|
+
},
|
|
1913
|
+
{
|
|
1914
|
+
key: "DIFFICULTY",
|
|
1915
|
+
control: BridgeControl.Select,
|
|
1916
|
+
label: text("Difficulty"),
|
|
1917
|
+
options: [
|
|
1918
|
+
{
|
|
1919
|
+
value: "normal",
|
|
1920
|
+
label: text("Normal"),
|
|
1921
|
+
},
|
|
1922
|
+
],
|
|
1923
|
+
default: "normal",
|
|
1924
|
+
},
|
|
1925
|
+
{
|
|
1926
|
+
key: "SERVER_NAME",
|
|
1927
|
+
control: BridgeControl.Text,
|
|
1928
|
+
label: text("Name"),
|
|
1929
|
+
maxLength: 8,
|
|
1930
|
+
default: "Serverk",
|
|
1931
|
+
},
|
|
1932
|
+
]),
|
|
1933
|
+
),
|
|
1934
|
+
).not.toThrow();
|
|
1935
|
+
});
|
|
1936
|
+
|
|
1937
|
+
it("rejects a boolean default other than true or false", () => {
|
|
1938
|
+
expect(() =>
|
|
1939
|
+
validateDriver(
|
|
1940
|
+
driverWithFields([
|
|
1941
|
+
{
|
|
1942
|
+
key: "WINDOWS_STEAM",
|
|
1943
|
+
control: BridgeControl.Boolean,
|
|
1944
|
+
label: text("Use Steam"),
|
|
1945
|
+
default: "yes",
|
|
1946
|
+
},
|
|
1947
|
+
]),
|
|
1948
|
+
),
|
|
1949
|
+
).toThrow('declares default "yes" on a boolean control — use "true" or "false"');
|
|
1950
|
+
});
|
|
1951
|
+
|
|
1952
|
+
it("rejects a select default that is not one of its options", () => {
|
|
1953
|
+
expect(() =>
|
|
1954
|
+
validateDriver(
|
|
1955
|
+
driverWithFields([
|
|
1956
|
+
{
|
|
1957
|
+
key: "DIFFICULTY",
|
|
1958
|
+
control: BridgeControl.Select,
|
|
1959
|
+
label: text("Difficulty"),
|
|
1960
|
+
options: [
|
|
1961
|
+
{
|
|
1962
|
+
value: "normal",
|
|
1963
|
+
label: text("Normal"),
|
|
1964
|
+
},
|
|
1965
|
+
],
|
|
1966
|
+
default: "hard",
|
|
1967
|
+
},
|
|
1968
|
+
]),
|
|
1969
|
+
),
|
|
1970
|
+
).toThrow('declares default "hard", which is not one of its options');
|
|
1971
|
+
});
|
|
1972
|
+
|
|
1973
|
+
it("rejects a default on a secret field", () => {
|
|
1974
|
+
expect(() =>
|
|
1975
|
+
validateDriver(
|
|
1976
|
+
driverWithFields([
|
|
1977
|
+
{
|
|
1978
|
+
key: "ADMIN_PASSWORD",
|
|
1979
|
+
control: BridgeControl.Secret,
|
|
1980
|
+
label: text("Admin password"),
|
|
1981
|
+
default: "letmein1",
|
|
1982
|
+
},
|
|
1983
|
+
]),
|
|
1984
|
+
),
|
|
1985
|
+
).toThrow("declares a default on a secret control — a secret is never preset");
|
|
1986
|
+
});
|
|
1987
|
+
|
|
1988
|
+
it("rejects a default longer than the field takes", () => {
|
|
1989
|
+
expect(() =>
|
|
1990
|
+
validateDriver(
|
|
1991
|
+
driverWithFields([
|
|
1992
|
+
{
|
|
1993
|
+
key: "SERVER_NAME",
|
|
1994
|
+
control: BridgeControl.Text,
|
|
1995
|
+
label: text("Name"),
|
|
1996
|
+
maxLength: 4,
|
|
1997
|
+
default: "Serverk",
|
|
1998
|
+
},
|
|
1999
|
+
]),
|
|
2000
|
+
),
|
|
2001
|
+
).toThrow("default is 7 characters — the platform takes at most 4");
|
|
2002
|
+
});
|
|
2003
|
+
|
|
2004
|
+
it("rejects a default longer than the platform takes, even when the field takes more", () => {
|
|
2005
|
+
expect(() =>
|
|
2006
|
+
validateDriver(
|
|
2007
|
+
driverWithFields([
|
|
2008
|
+
{
|
|
2009
|
+
key: "MOTD",
|
|
2010
|
+
control: BridgeControl.Text,
|
|
2011
|
+
label: text("Message"),
|
|
2012
|
+
maxLength: 1024,
|
|
2013
|
+
default: "a".repeat(257),
|
|
2014
|
+
},
|
|
2015
|
+
]),
|
|
2016
|
+
),
|
|
2017
|
+
).toThrow("default is 257 characters — the platform takes at most 256");
|
|
2018
|
+
});
|
|
2019
|
+
|
|
2020
|
+
it("rejects a default on an action field, which is no variable", () => {
|
|
2021
|
+
expect(() =>
|
|
2022
|
+
validateDriver(
|
|
2023
|
+
driverWithActions([
|
|
2024
|
+
{
|
|
2025
|
+
id: "say",
|
|
2026
|
+
label: text("Say"),
|
|
2027
|
+
fields: [
|
|
2028
|
+
{
|
|
2029
|
+
...field("message"),
|
|
2030
|
+
default: "hello",
|
|
2031
|
+
},
|
|
2032
|
+
],
|
|
2033
|
+
},
|
|
2034
|
+
]),
|
|
2035
|
+
),
|
|
2036
|
+
).toThrow("declares a default outside a form that targets variables");
|
|
2037
|
+
});
|
|
2038
|
+
});
|
|
2039
|
+
|
|
1694
2040
|
describe("section placement", () => {
|
|
1695
2041
|
const anyVisibleWhen: BridgeVisibleWhen = {
|
|
1696
2042
|
variable: "MODPACK",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
BRIDGE_ID_PATTERN,
|
|
3
3
|
BRIDGE_MODULE_ID_PATTERN,
|
|
4
|
+
BridgeAction,
|
|
4
5
|
BridgeKind,
|
|
5
6
|
BridgeSetupStepKind,
|
|
6
7
|
isReadAction,
|
|
@@ -9,6 +10,7 @@ import {
|
|
|
9
10
|
import { GuideOpenTab } from "../guides";
|
|
10
11
|
import type { ManifestPort } from "../manifest";
|
|
11
12
|
import {
|
|
13
|
+
type BridgeCatalogModule,
|
|
12
14
|
type BridgeDriver,
|
|
13
15
|
BridgeLifecycleSubject,
|
|
14
16
|
type BridgePanelModule,
|
|
@@ -24,12 +26,15 @@ import {
|
|
|
24
26
|
BridgePlace,
|
|
25
27
|
type BridgeSection,
|
|
26
28
|
type BridgeSectionAction,
|
|
29
|
+
type BridgeSectionUpload,
|
|
27
30
|
type BridgeTab,
|
|
28
31
|
BridgeUploadMode,
|
|
29
32
|
type BridgeVisibleWhen,
|
|
30
33
|
} from "../panel";
|
|
31
34
|
import {
|
|
32
35
|
BRIDGE_ACTION_FIELDS_LIMIT,
|
|
36
|
+
BRIDGE_CATALOG_PROVIDED_LIMIT,
|
|
37
|
+
BRIDGE_DEFAULT_LENGTH_LIMIT,
|
|
33
38
|
BRIDGE_DEPENDS_ON_LIMIT,
|
|
34
39
|
BRIDGE_EVENTS_LIMIT,
|
|
35
40
|
BRIDGE_EXTENSION_LENGTH_LIMIT,
|
|
@@ -44,6 +49,7 @@ import {
|
|
|
44
49
|
BRIDGE_PANEL_TABS_LIMIT,
|
|
45
50
|
BRIDGE_PATTERN_LENGTH_LIMIT,
|
|
46
51
|
BRIDGE_PLACEHOLDER_LENGTH_LIMIT,
|
|
52
|
+
BRIDGE_PROVIDED_NAME_LENGTH_LIMIT,
|
|
47
53
|
BRIDGE_ROW_ACTIONS_LIMIT,
|
|
48
54
|
BRIDGE_SECTION_ACTIONS_LIMIT,
|
|
49
55
|
BRIDGE_SETUP_STEPS_LIMIT,
|
|
@@ -53,11 +59,14 @@ import {
|
|
|
53
59
|
BRIDGE_TEXT_LENGTH_LIMIT,
|
|
54
60
|
BRIDGE_UPLOAD_EXTENSIONS_LIMIT,
|
|
55
61
|
BRIDGE_VISIBLE_VALUES_LIMIT,
|
|
62
|
+
BridgeCatalogSourceKind,
|
|
56
63
|
} from "../protocol";
|
|
57
64
|
import { actionsOf } from "./bridgeActions";
|
|
58
65
|
|
|
59
66
|
const UPLOAD_MODES = new Set<string>(Object.values(BridgeUploadMode));
|
|
60
67
|
|
|
68
|
+
const CATALOG_SOURCE_KINDS = new Set<string>(Object.values(BridgeCatalogSourceKind));
|
|
69
|
+
|
|
61
70
|
const READY_TIMEOUT_MIN_SECONDS = 1;
|
|
62
71
|
|
|
63
72
|
const READY_TIMEOUT_MAX_SECONDS = 3600;
|
|
@@ -256,7 +265,41 @@ const validateVisibleWhen = (visibleWhen: BridgeVisibleWhen | undefined, where:
|
|
|
256
265
|
countIn((visibleWhen.values ?? []).length, BRIDGE_VISIBLE_VALUES_LIMIT, "visible values", where);
|
|
257
266
|
};
|
|
258
267
|
|
|
259
|
-
const
|
|
268
|
+
const validateDefault = (field: BridgeField, variables: boolean, at: string) => {
|
|
269
|
+
if (field.default === undefined) {
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (!variables) {
|
|
274
|
+
fail(`${at} declares a default outside a form that targets variables — only a variable has one`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (field.control === BridgeControl.Secret) {
|
|
278
|
+
fail(`${at} declares a default on a secret control — a secret is never preset`);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (field.control === BridgeControl.Boolean && field.default !== "true" && field.default !== "false") {
|
|
282
|
+
fail(`${at} declares default "${field.default}" on a boolean control — use "true" or "false"`);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (Array.isArray(field.options) && !field.options.some((option) => option.value === field.default)) {
|
|
286
|
+
fail(`${at} declares default "${field.default}", which is not one of its options`);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
lengthIn(
|
|
290
|
+
field.default,
|
|
291
|
+
Math.min(field.maxLength ?? BRIDGE_DEFAULT_LENGTH_LIMIT, BRIDGE_DEFAULT_LENGTH_LIMIT),
|
|
292
|
+
"default",
|
|
293
|
+
at,
|
|
294
|
+
);
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
const validateFields = (
|
|
298
|
+
modules: Record<string, BridgePanelModule>,
|
|
299
|
+
fields: BridgeField[],
|
|
300
|
+
where: string,
|
|
301
|
+
variables = false,
|
|
302
|
+
) => {
|
|
260
303
|
const keys = new Set<string>();
|
|
261
304
|
|
|
262
305
|
for (const field of fields) {
|
|
@@ -274,6 +317,7 @@ const validateFields = (modules: Record<string, BridgePanelModule>, fields: Brid
|
|
|
274
317
|
validateText(field.warning, "warning", at);
|
|
275
318
|
validateText(field.patternHint, "pattern hint", at);
|
|
276
319
|
validateVisibleWhen(field.visibleWhen, at);
|
|
320
|
+
validateDefault(field, variables, at);
|
|
277
321
|
|
|
278
322
|
if (field.placeholder) {
|
|
279
323
|
lengthIn(field.placeholder, BRIDGE_PLACEHOLDER_LENGTH_LIMIT, "placeholder", at);
|
|
@@ -388,6 +432,25 @@ const validateOffline = (section: BridgeSection, where: string) => {
|
|
|
388
432
|
}
|
|
389
433
|
};
|
|
390
434
|
|
|
435
|
+
const validateUpload = (upload: BridgeSectionUpload, where: string) => {
|
|
436
|
+
validateText(upload.label, "upload label", where);
|
|
437
|
+
lengthIn(upload.staging, BRIDGE_STAGING_LENGTH_LIMIT, "upload staging path", where);
|
|
438
|
+
|
|
439
|
+
if (upload.extensions.length === 0) {
|
|
440
|
+
fail(`${where} declares an upload control with no extensions`);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
countIn(upload.extensions.length, BRIDGE_UPLOAD_EXTENSIONS_LIMIT, "upload extensions", where);
|
|
444
|
+
|
|
445
|
+
for (const extension of upload.extensions) {
|
|
446
|
+
lengthIn(extension, BRIDGE_EXTENSION_LENGTH_LIMIT, `upload extension "${extension}"`, where);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (upload.mode !== undefined && !UPLOAD_MODES.has(upload.mode)) {
|
|
450
|
+
fail(`${where} declares an unknown upload mode "${upload.mode}"`);
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
|
|
391
454
|
const validateSection = (modules: Record<string, BridgePanelModule>, section: BridgeSection, where: string) => {
|
|
392
455
|
validateVisibleWhen(section.visibleWhen, where);
|
|
393
456
|
validateText(section.title, "title", where);
|
|
@@ -462,7 +525,7 @@ const validateSection = (modules: Record<string, BridgePanelModule>, section: Br
|
|
|
462
525
|
}
|
|
463
526
|
|
|
464
527
|
countIn(section.fields.length, BRIDGE_FORM_FIELDS_LIMIT, "fields", where);
|
|
465
|
-
validateFields(modules, section.fields, where);
|
|
528
|
+
validateFields(modules, section.fields, where, section.target === BridgeFormTarget.Variables);
|
|
466
529
|
|
|
467
530
|
const clears = section.clears ?? [];
|
|
468
531
|
|
|
@@ -558,22 +621,7 @@ const validateSection = (modules: Record<string, BridgePanelModule>, section: Br
|
|
|
558
621
|
}
|
|
559
622
|
|
|
560
623
|
if (section.upload) {
|
|
561
|
-
|
|
562
|
-
lengthIn(section.upload.staging, BRIDGE_STAGING_LENGTH_LIMIT, "upload staging path", where);
|
|
563
|
-
|
|
564
|
-
if (section.upload.extensions.length === 0) {
|
|
565
|
-
fail(`${where} declares an upload control with no extensions`);
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
countIn(section.upload.extensions.length, BRIDGE_UPLOAD_EXTENSIONS_LIMIT, "upload extensions", where);
|
|
569
|
-
|
|
570
|
-
for (const extension of section.upload.extensions) {
|
|
571
|
-
lengthIn(extension, BRIDGE_EXTENSION_LENGTH_LIMIT, `upload extension "${extension}"`, where);
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
if (section.upload.mode !== undefined && !UPLOAD_MODES.has(section.upload.mode)) {
|
|
575
|
-
fail(`${where} declares an unknown upload mode "${section.upload.mode}"`);
|
|
576
|
-
}
|
|
624
|
+
validateUpload(section.upload, where);
|
|
577
625
|
}
|
|
578
626
|
}
|
|
579
627
|
|
|
@@ -598,15 +646,75 @@ const validateSection = (modules: Record<string, BridgePanelModule>, section: Br
|
|
|
598
646
|
|
|
599
647
|
if (module.kind !== BridgeKind.Catalog) {
|
|
600
648
|
fail(`${where} references "${section.module}", which is not a catalog module`);
|
|
649
|
+
|
|
650
|
+
return;
|
|
601
651
|
}
|
|
602
652
|
|
|
603
653
|
validateText(section.confirmText, "confirm text", where);
|
|
604
654
|
|
|
655
|
+
if (section.upload) {
|
|
656
|
+
if (!module.add) {
|
|
657
|
+
fail(`${where} declares an upload control but module "${section.module}" has no add handler`);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
validateUpload(section.upload, where);
|
|
661
|
+
}
|
|
662
|
+
|
|
605
663
|
return;
|
|
606
664
|
}
|
|
607
665
|
}
|
|
608
666
|
};
|
|
609
667
|
|
|
668
|
+
const validateCatalog = (module: BridgeCatalogModule, where: string) => {
|
|
669
|
+
if (module.source === undefined) {
|
|
670
|
+
if (typeof module.search !== "function") {
|
|
671
|
+
fail(`${where} declares no search — a catalog without a source answers its own search`);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
const { source } = module;
|
|
678
|
+
|
|
679
|
+
if (!CATALOG_SOURCE_KINDS.has(source.kind)) {
|
|
680
|
+
fail(
|
|
681
|
+
`${where} declares source "${String(source.kind)}" — the platform serves only ${[
|
|
682
|
+
...CATALOG_SOURCE_KINDS,
|
|
683
|
+
].join(" or ")}`,
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
if (!(Number.isSafeInteger(source.appId) && source.appId > 0)) {
|
|
688
|
+
fail(`${where} declares source app id ${String(source.appId)} — it must be a positive whole number`);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
const provided: unknown[] = source.provided ?? [];
|
|
692
|
+
|
|
693
|
+
countIn(provided.length, BRIDGE_CATALOG_PROVIDED_LIMIT, "provided names", `${where} source`);
|
|
694
|
+
|
|
695
|
+
for (const name of provided) {
|
|
696
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
697
|
+
fail(`${where} source declares a provided name that is not a non-empty string`);
|
|
698
|
+
|
|
699
|
+
continue;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
lengthIn(name, BRIDGE_PROVIDED_NAME_LENGTH_LIMIT, `provided name "${name}"`, `${where} source`);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
const platformAnswers: Record<string, unknown> = {
|
|
706
|
+
[BridgeAction.Search]: module.search,
|
|
707
|
+
[BridgeAction.Releases]: module.releases,
|
|
708
|
+
[BridgeAction.Preview]: module.preview,
|
|
709
|
+
};
|
|
710
|
+
|
|
711
|
+
for (const [action, handler] of Object.entries(platformAnswers)) {
|
|
712
|
+
if (handler !== undefined) {
|
|
713
|
+
fail(`${where} declares a source and its own ${action} — the platform answers ${action} for a sourced catalog`);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
};
|
|
717
|
+
|
|
610
718
|
const validateLifecycle = (driver: BridgeDriver) => {
|
|
611
719
|
const environment: unknown = driver.lifecycle.environment;
|
|
612
720
|
|
|
@@ -827,6 +935,10 @@ export const validateDriver = (driver: BridgeDriver) => {
|
|
|
827
935
|
countIn((module.dependsOn ?? []).length, BRIDGE_DEPENDS_ON_LIMIT, "dependencies", `module "${id}"`);
|
|
828
936
|
}
|
|
829
937
|
|
|
938
|
+
if (module.kind === BridgeKind.Catalog) {
|
|
939
|
+
validateCatalog(module, `module "${id}"`);
|
|
940
|
+
}
|
|
941
|
+
|
|
830
942
|
if (
|
|
831
943
|
module.kind === BridgeKind.Actions
|
|
832
944
|
|| module.kind === BridgeKind.Collection
|