@serverkgg/bridge 0.1.7 → 0.1.9
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 +2 -0
- package/src/bot/botCauses.test.ts +32 -0
- package/src/bot/botCauses.ts +4 -0
- package/src/bot/botDeploy.test.ts +32 -0
- package/src/bot/botDeploy.ts +6 -0
- package/src/bot/detect/corpus/mruniquehacker-knightbot-md.json +35 -0
- package/src/cli/validate.test.ts +429 -3
- package/src/cli/validate.ts +225 -1
- package/src/guides/markdown.test.ts +118 -0
- package/src/guides/markdown.ts +24 -2
- package/src/manifest/index.ts +1 -0
- package/src/manifest/manifest.test.ts +79 -0
- package/src/manifest/manifest.ts +5 -0
- package/src/manifest/steam.ts +14 -0
- package/src/protocol/bridgeCrash.ts +5 -0
- package/src/protocol/bridgeEvents.ts +1 -0
- package/src/steam/steamcmd.test.ts +367 -1
- package/src/steam/steamcmd.ts +154 -18
package/package.json
CHANGED
package/rules/package.md
CHANGED
|
@@ -51,10 +51,12 @@ One Serverk package (a game or a bot runtime) on `@serverkgg/bridge`, with no pi
|
|
|
51
51
|
- Write `ar:` before `en:` in every localized field.
|
|
52
52
|
- `secrets` are platform API keys the game can use, `required: false`; customer settings are a panel form, and a bot customer's own values go through `environment.vault`.
|
|
53
53
|
- Change an outcome by changing the declared value (`reset.keep: []`, `backup.only`), never by asking the platform to ignore a field.
|
|
54
|
+
- `files.protected` holds only `.serverk` paths: the stamp, sidecars, journals and driver scratch under `.serverk-staging/`. Never a drop point under `.serverk-uploads/`, a `files.notable` match, an `@[open](files:…)` target or a folder the customer collects a file from; `serverk-bridge validate` fails each but the last, a runtime path.
|
|
54
55
|
|
|
55
56
|
## Guides and image
|
|
56
57
|
|
|
57
58
|
- A guide is `guides/<id>/ar.md` and `en.md`, starting at `##` with no `#` title; its title and summary live in `serverk.yml`. Every package has a `getting-started` guide tagged `start`.
|
|
59
|
+
- A heading may end with `{#<id>}`, lowercase letters, digits and single hyphens, to fix its anchor. A bot package's `errors` guide gives each cause section its cause id, the same in both languages. Why: the panel links every named cause to `errors#<cause id>`.
|
|
58
60
|
- `image/Dockerfile` keeps the shared contract: `ARG BRIDGE_VERSION` with `LABEL gg.serverk.bridge.version`, a `container` user with uid 1000 and home `/home/container`, `COPY dist/bridge /serverk/bridge`, `ENTRYPOINT ["/usr/bin/tini", "--", "/serverk/bridge"]`, and no `USER` line. Game files download at install, a bot's code arrives with a deploy; neither is baked in.
|
|
59
61
|
- The outermost start wrapper ends in `exec`, with no `setsid`, `nohup` or double fork. Why: the bridge checks the uid of the pid it spawned, and stop escalation signals the process group, so an orphan outside it keeps the port.
|
|
60
62
|
|
|
@@ -29,6 +29,10 @@ const CASES: readonly {
|
|
|
29
29
|
line: "telegram.error.InvalidToken: The token `123:abc` was rejected by the server.",
|
|
30
30
|
cause: BridgeCrashCause.TokenInvalid,
|
|
31
31
|
},
|
|
32
|
+
{
|
|
33
|
+
line: "telegram.error.InvalidToken: You must pass the token you received from https://t.me/Botfather!",
|
|
34
|
+
cause: BridgeCrashCause.TokenInvalid,
|
|
35
|
+
},
|
|
32
36
|
{
|
|
33
37
|
line: "GrammyError: Call to 'deleteWebhook' failed! (404: Not Found)",
|
|
34
38
|
cause: BridgeCrashCause.TokenInvalid,
|
|
@@ -165,6 +169,10 @@ const CASES: readonly {
|
|
|
165
169
|
line: "ModuleNotFoundError: No module named 'audioop'",
|
|
166
170
|
cause: BridgeCrashCause.AudioopRemoved,
|
|
167
171
|
},
|
|
172
|
+
{
|
|
173
|
+
line: "KeyError: 'TELEGRAM_TOKEN'",
|
|
174
|
+
cause: BridgeCrashCause.VariableMissing,
|
|
175
|
+
},
|
|
168
176
|
{
|
|
169
177
|
line: "discord.errors.HTTPException: 429 Too Many Requests (error code: 0): You are being blocked from accessing our API temporarily due to exceeding our rate limits frequently.",
|
|
170
178
|
cause: BridgeCrashCause.RateLimited,
|
|
@@ -253,6 +261,30 @@ describe("BOT_CRASH_EVIDENCE", () => {
|
|
|
253
261
|
});
|
|
254
262
|
}
|
|
255
263
|
|
|
264
|
+
it("names the variable the code read from an empty vault", () => {
|
|
265
|
+
const { ring, print } = botRing();
|
|
266
|
+
|
|
267
|
+
print("KeyError: 'TELEGRAM_TOKEN'");
|
|
268
|
+
|
|
269
|
+
expect(ring.crash(recordOf())).toBe(BridgeCrashCause.VariableMissing);
|
|
270
|
+
expect(ring.drain(0).events.at(0)?.payload?.variable).toBe("TELEGRAM_TOKEN");
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
for (const line of [
|
|
274
|
+
"KeyError: 0",
|
|
275
|
+
"KeyError: 'user'",
|
|
276
|
+
"KeyError: 'X'",
|
|
277
|
+
]) {
|
|
278
|
+
it(`leaves a key that is not a variable name unexplained from "${line}"`, () => {
|
|
279
|
+
const { ring, print } = botRing();
|
|
280
|
+
|
|
281
|
+
print(line);
|
|
282
|
+
|
|
283
|
+
expect(ring.crash(recordOf())).toBe(BridgeCrashCause.Unknown);
|
|
284
|
+
expect(ring.drain(0).events.at(0)?.payload?.variable).toBeUndefined();
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
256
288
|
it("names the removed audioop module before any other missing module", () => {
|
|
257
289
|
const { ring, print } = botRing();
|
|
258
290
|
|
package/src/bot/botCauses.ts
CHANGED
|
@@ -48,6 +48,10 @@ export const BOT_CRASH_EVIDENCE: readonly BridgeEventPattern[] = [
|
|
|
48
48
|
match: /ffmpeg was not found|\bENOENT\b.*\bffmpeg\b|\bffmpeg\b.*\bENOENT\b/i,
|
|
49
49
|
emit: BridgeEventName.FfmpegMissing,
|
|
50
50
|
},
|
|
51
|
+
{
|
|
52
|
+
match: /^KeyError: '(?<variable>[A-Z][A-Z0-9_]{1,63})'\s*$/,
|
|
53
|
+
emit: BridgeEventName.VariableMissing,
|
|
54
|
+
},
|
|
51
55
|
{
|
|
52
56
|
match: /No module named 'audioop'/,
|
|
53
57
|
emit: BridgeEventName.AudioopRemoved,
|
|
@@ -8,8 +8,10 @@ import {
|
|
|
8
8
|
DEPLOY_MARKER_DRAFT,
|
|
9
9
|
DEPLOYMENT_EVENT_PATTERN,
|
|
10
10
|
DEPLOYMENT_LINE,
|
|
11
|
+
DEPLOYMENT_STAGE_LINE,
|
|
11
12
|
type DeployMarker,
|
|
12
13
|
deploymentLine,
|
|
14
|
+
deploymentStageLine,
|
|
13
15
|
deployPending,
|
|
14
16
|
pendingDeploymentOf,
|
|
15
17
|
requestDeploy,
|
|
@@ -216,6 +218,36 @@ describe("deploymentLine", () => {
|
|
|
216
218
|
});
|
|
217
219
|
});
|
|
218
220
|
|
|
221
|
+
describe("deploymentStageLine", () => {
|
|
222
|
+
it("names the deployment and the stage it entered", () => {
|
|
223
|
+
expect(deploymentStageLine("dep_a1b2c3d4", BotDeploymentStage.Install)).toBe(
|
|
224
|
+
"serverk: deployment dep_a1b2c3d4 stage install",
|
|
225
|
+
);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("round-trips through the stage pattern for every stage", () => {
|
|
229
|
+
for (const stage of Object.values(BotDeploymentStage)) {
|
|
230
|
+
expect(DEPLOYMENT_STAGE_LINE.exec(deploymentStageLine("dep_a1b2c3d4", stage))?.groups).toEqual({
|
|
231
|
+
deployment: "dep_a1b2c3d4",
|
|
232
|
+
stage,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("is never read as a deployment outcome, so it raises no event", () => {
|
|
238
|
+
const line = deploymentStageLine("dep_a1b2c3d4", BotDeploymentStage.Start);
|
|
239
|
+
|
|
240
|
+
expect(DEPLOYMENT_LINE.test(line)).toBe(false);
|
|
241
|
+
expect(DEPLOYMENT_EVENT_PATTERN.match.test(line)).toBe(false);
|
|
242
|
+
expect(DEPLOYMENT_STAGE_LINE.test("serverk: deployment dep_a1b2c3d4 ready")).toBe(false);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("reads only deployment ids of at most 64 characters", () => {
|
|
246
|
+
expect(DEPLOYMENT_STAGE_LINE.test(`serverk: deployment ${"d".repeat(64)} stage sync`)).toBe(true);
|
|
247
|
+
expect(DEPLOYMENT_STAGE_LINE.test(`serverk: deployment ${"d".repeat(65)} stage sync`)).toBe(false);
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
219
251
|
describe("requestDeploy", () => {
|
|
220
252
|
it("writes the request as JSON and restarts a running bot", async () => {
|
|
221
253
|
const { context, stored, events } = deployContext(true);
|
package/src/bot/botDeploy.ts
CHANGED
|
@@ -21,6 +21,8 @@ export const DEPLOYMENT_EVENT_KIND = "deployment";
|
|
|
21
21
|
export const DEPLOYMENT_LINE =
|
|
22
22
|
/^serverk: deployment (?<deployment>\S{1,64}) (?<outcome>ready|failed)(?: at (?<commit>[0-9a-f]{7,64}))?(?:: (?<stage>[a-z-]+): (?<detail>.{1,200}))?$/u;
|
|
23
23
|
|
|
24
|
+
export const DEPLOYMENT_STAGE_LINE = /^serverk: deployment (?<deployment>\S{1,64}) stage (?<stage>[a-z-]+)$/u;
|
|
25
|
+
|
|
24
26
|
export const DEPLOYMENT_DETAIL_LIMIT = 200;
|
|
25
27
|
|
|
26
28
|
export const DEPLOYMENT_COMMIT_LENGTH = 12;
|
|
@@ -64,6 +66,10 @@ export const deploymentLine = (input: DeploymentLineInput) => {
|
|
|
64
66
|
return `serverk: deployment ${input.deployment} failed: ${input.stage}: ${detail}`;
|
|
65
67
|
};
|
|
66
68
|
|
|
69
|
+
export const deploymentStageLine = (deployment: string, stage: BotDeploymentStage) => {
|
|
70
|
+
return `serverk: deployment ${deployment} stage ${stage}`;
|
|
71
|
+
};
|
|
72
|
+
|
|
67
73
|
export const DEPLOYMENT_EVENT_PATTERN: BridgeEventPattern = {
|
|
68
74
|
match: DEPLOYMENT_LINE,
|
|
69
75
|
emit: BridgeEventName.ServerUpdated,
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"source": {
|
|
3
|
+
"url": "https://github.com/mruniquehacker/Knightbot-MD",
|
|
4
|
+
"branch": "main",
|
|
5
|
+
"commit": "871bce832169c3a52a632dc100c1f235cb35977c",
|
|
6
|
+
"stars": 8128
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
".gitignore",
|
|
10
|
+
"README.md",
|
|
11
|
+
"config.js",
|
|
12
|
+
"index.js",
|
|
13
|
+
"main.js",
|
|
14
|
+
"package.json",
|
|
15
|
+
"settings.js"
|
|
16
|
+
],
|
|
17
|
+
"contents": {
|
|
18
|
+
"package.json": "{\n \"name\": \"knightbot\",\n \"version\": \"1.0.0\",\n \"description\": \"WhatsApp Bot\",\n \"main\": \"index.js\",\n \"scripts\": {\n \"start\": \"node index.js\",\n \"start:optimized\": \"node --max-old-space-size=512 --optimize-for-size --gc-interval=100 index.js\",\n \"cleanup\": \"node cleanup.js\",\n \"reset-session\": \"node reset-session.js\",\n \"start:clean\": \"npm run cleanup && npm run start:optimized\",\n \"start:fresh\": \"npm run reset-session && npm start\",\n \"install:panel\": \"npm install --legacy-peer-deps\",\n \"install:force\": \"npm install --force\",\n \"test\": \"echo \\\"Error: no test specified\\\" && exit 1\",\n \"docker:build\": \"docker build -t docker run -e SESSION_ID=$SESSION_ID knightbot\"\n },\n \"keywords\": [\n \"whatsapp-bot\"\n ],\n \"author\": \"\",\n \"license\": \"ISC\",\n \"dependencies\": {\n \"@adiwajshing/keyed-db\": \"^0.2.4\",\n \"@ffmpeg/ffmpeg\": \"^0.12.15\",\n \"@hapi/boom\": \"^10.0.1\",\n \"@types/node\": \"^18.0.6\",\n \"@whiskeysockets/baileys\": \"^v7.0.0-rc.9\",\n \"awesome-phonenumber\": \"^5.9.0\",\n \"axios\": \"^1.8.4\",\n \"chalk\": \"^4.1.2\",\n \"cheerio\": \"^1.0.0-rc.12\",\n \"cookie\": \"^0.5.0\",\n \"dotenv\": \"^16.4.5\",\n \"events\": \"^3.3.0\",\n \"file-type\": \"^16.5.4\",\n \"fluent-ffmpeg\": \"^2.1.3\",\n \"form-data\": \"^4.0.1\",\n \"fs-extra\": \"^11.2.0\",\n \"gtts\": \"^0.2.1\",\n \"human-readable\": \"^0.2.1\",\n \"jimp\": \"^1.6.0\",\n \"jsdom\": \"^22.1.0\",\n \"libphonenumber-js\": \"^1.11.18\",\n \"libsignal\": \"^2.0.1\",\n \"link-preview-js\": \"^3.0.5\",\n \"moment-timezone\": \"^0.5.43\",\n \"mumaker\": \"^2.0.0\",\n \"node-cache\": \"^5.1.2\",\n \"node-fetch\": \"^2.7.0\",\n \"node-id3\": \"^0.2.3\",\n \"node-webpmux\": \"^3.1.0\",\n \"node-youtube-music\": \"^0.8.3\",\n \"performance-now\": \"^2.1.0\",\n \"phin\": \"^3.7.1\",\n \"pino\": \"^8.21.0\",\n \"qrcode\": \"^1.5.4\",\n \"qrcode-reader\": \"^1.0.4\",\n \"qrcode-terminal\": \"^0.12.0\",\n \"request\": \"^2.88.2\",\n \"ruhend-scraper\": \"^8.3.0\",\n \"safe-stable-stringify\": \"^2.5.0\",\n \"set-cookie\": \"^0.0.4\",\n \"sharp\": \"^0.32.6\",\n \"tough-cookie\": \"^5.0.0\",\n \"translate-google-api\": \"^1.0.4\",\n \"ws\": \"^8.17.1\",\n \"yargs\": \"^17.6.0\",\n \"yargs-parser\": \"^21.1.1\",\n \"youtube-yts\": \"^2.0.0\",\n \"youtubedl-core\": \"^4.11.7\",\n \"yt-search\": \"^2.12.1\",\n \"ytdl-core\": \"^4.11.5\"\n },\n \"overrides\": {\n \"jimp\": \"^1.6.0\"\n },\n \"engines\": {\n \"node\": \">=18.0.0\"\n }\n}\n"
|
|
19
|
+
},
|
|
20
|
+
"expected": {
|
|
21
|
+
"runtime": "node",
|
|
22
|
+
"packageManager": "npm",
|
|
23
|
+
"framework": null,
|
|
24
|
+
"platform": null,
|
|
25
|
+
"install": "npm install",
|
|
26
|
+
"build": null,
|
|
27
|
+
"start": "node index.js",
|
|
28
|
+
"nodeVersion": "18",
|
|
29
|
+
"pythonVersion": null,
|
|
30
|
+
"tokenKey": null,
|
|
31
|
+
"warnings": [
|
|
32
|
+
"no-lockfile"
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
}
|
package/src/cli/validate.test.ts
CHANGED
|
@@ -4,9 +4,9 @@ import { tmpdir } from "node:os";
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { renderDetectTable } from "../bot/detect";
|
|
6
6
|
import { BridgeKind, BridgeSetupStepKind } from "../core";
|
|
7
|
-
import { GuideOpenTab } from "../guides";
|
|
8
|
-
import { MANIFEST_SCHEMA_VERSION, PackageKind } from "../manifest";
|
|
9
|
-
import type { BridgeDriver, BridgeSetupModule } from "../modules";
|
|
7
|
+
import { GuideOpenTab, type GuidePack, type ParsedBlock, type ParsedLeafBlock } from "../guides";
|
|
8
|
+
import { MANIFEST_SCHEMA_VERSION, PackageKind, type RunnableManifest } from "../manifest";
|
|
9
|
+
import type { BridgeDriver, BridgePendingModule, BridgeSetupModule } from "../modules";
|
|
10
10
|
import {
|
|
11
11
|
type BridgeCatalogSection,
|
|
12
12
|
BridgeConfirm,
|
|
@@ -23,9 +23,13 @@ import { RULES_FILE, readShippedRules, writeRules } from "./rules";
|
|
|
23
23
|
import {
|
|
24
24
|
availabilityReportLines,
|
|
25
25
|
confirmReportLines,
|
|
26
|
+
notableReportLines,
|
|
27
|
+
openTargetReportLines,
|
|
26
28
|
placementReportLines,
|
|
29
|
+
protectedNameReportLines,
|
|
27
30
|
runValidate,
|
|
28
31
|
setupReportLines,
|
|
32
|
+
uploadStagingReportLines,
|
|
29
33
|
} from "./validate";
|
|
30
34
|
|
|
31
35
|
const text = (value: string) => ({
|
|
@@ -822,3 +826,425 @@ describe("the confirm report", () => {
|
|
|
822
826
|
).toEqual([]);
|
|
823
827
|
});
|
|
824
828
|
});
|
|
829
|
+
|
|
830
|
+
const manifestWith = (rules: string[], notes: string[] = []) => {
|
|
831
|
+
return {
|
|
832
|
+
files: {
|
|
833
|
+
protected: rules,
|
|
834
|
+
notable: notes.map((match) => ({
|
|
835
|
+
match,
|
|
836
|
+
})),
|
|
837
|
+
},
|
|
838
|
+
} as unknown as RunnableManifest;
|
|
839
|
+
};
|
|
840
|
+
|
|
841
|
+
const worldsWith = (staging: string): BridgeTableSection => ({
|
|
842
|
+
layout: BridgeLayout.Table,
|
|
843
|
+
id: "worlds",
|
|
844
|
+
module: "worlds",
|
|
845
|
+
columns: [
|
|
846
|
+
{
|
|
847
|
+
key: "name",
|
|
848
|
+
label: text("Name"),
|
|
849
|
+
},
|
|
850
|
+
],
|
|
851
|
+
upload: {
|
|
852
|
+
label: text("Upload a world"),
|
|
853
|
+
extensions: [
|
|
854
|
+
"zip",
|
|
855
|
+
],
|
|
856
|
+
staging,
|
|
857
|
+
},
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
const pendingWith = (staging: string): BridgePendingModule => ({
|
|
861
|
+
kind: BridgeKind.Pending,
|
|
862
|
+
staging,
|
|
863
|
+
extensions: [
|
|
864
|
+
"jar",
|
|
865
|
+
],
|
|
866
|
+
list: async () => [],
|
|
867
|
+
add: async () => {
|
|
868
|
+
return;
|
|
869
|
+
},
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
const driverWith = (
|
|
873
|
+
sections: BridgeTab["sections"],
|
|
874
|
+
pending?: BridgePendingModule,
|
|
875
|
+
setup?: BridgeSetupModule,
|
|
876
|
+
): BridgeDriver => ({
|
|
877
|
+
install: {
|
|
878
|
+
kind: BridgeKind.Install,
|
|
879
|
+
run: async () => {
|
|
880
|
+
return;
|
|
881
|
+
},
|
|
882
|
+
},
|
|
883
|
+
lifecycle: {
|
|
884
|
+
kind: BridgeKind.Lifecycle,
|
|
885
|
+
command: async () => [
|
|
886
|
+
"run",
|
|
887
|
+
],
|
|
888
|
+
},
|
|
889
|
+
pending,
|
|
890
|
+
setup,
|
|
891
|
+
panel: {
|
|
892
|
+
tabs: [
|
|
893
|
+
{
|
|
894
|
+
id: "worlds",
|
|
895
|
+
title: text("Worlds"),
|
|
896
|
+
sections,
|
|
897
|
+
},
|
|
898
|
+
],
|
|
899
|
+
},
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
const packWith = (blocks: ParsedBlock[]): GuidePack => ({
|
|
903
|
+
drafts: [
|
|
904
|
+
{
|
|
905
|
+
id: "getting-started",
|
|
906
|
+
title: text("Getting started"),
|
|
907
|
+
summary: text("Start here"),
|
|
908
|
+
tags: [],
|
|
909
|
+
surfaces: [],
|
|
910
|
+
videoId: null,
|
|
911
|
+
body: {
|
|
912
|
+
ar: blocks,
|
|
913
|
+
en: blocks,
|
|
914
|
+
},
|
|
915
|
+
},
|
|
916
|
+
],
|
|
917
|
+
files: [],
|
|
918
|
+
images: [],
|
|
919
|
+
commands: [],
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
const filesOpenWith = (path: string | null): ParsedLeafBlock => ({
|
|
923
|
+
type: "open",
|
|
924
|
+
target: {
|
|
925
|
+
tab: GuideOpenTab.Files,
|
|
926
|
+
path,
|
|
927
|
+
},
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
const SERVERK_RULES = [
|
|
931
|
+
".serverk-install.json",
|
|
932
|
+
".serverk/**",
|
|
933
|
+
"plugins/.serverk-catalog.json",
|
|
934
|
+
".serverk-staging/**",
|
|
935
|
+
".serverk-wipe",
|
|
936
|
+
"Server/mods/.serverk-mods.json",
|
|
937
|
+
];
|
|
938
|
+
|
|
939
|
+
const FOREIGN_RULES = [
|
|
940
|
+
"bedrock_server",
|
|
941
|
+
"*.so",
|
|
942
|
+
"worlds/*/db/**",
|
|
943
|
+
"config",
|
|
944
|
+
".serverkfoo",
|
|
945
|
+
];
|
|
946
|
+
|
|
947
|
+
const JAVA_RULES = [
|
|
948
|
+
".serverk-install.json",
|
|
949
|
+
".serverk-crashscan.json",
|
|
950
|
+
".serverk-companions.json",
|
|
951
|
+
".serverk-modpack.json",
|
|
952
|
+
"plugins/.serverk-catalog.json",
|
|
953
|
+
"mods/.serverk-catalog.json",
|
|
954
|
+
];
|
|
955
|
+
|
|
956
|
+
describe("protectedNameReportLines", () => {
|
|
957
|
+
it("passes rules that each name a .serverk path", () => {
|
|
958
|
+
expect(protectedNameReportLines(manifestWith(SERVERK_RULES))).toEqual([]);
|
|
959
|
+
});
|
|
960
|
+
|
|
961
|
+
for (const rule of FOREIGN_RULES) {
|
|
962
|
+
it(`fails "${rule}", which names no .serverk path`, () => {
|
|
963
|
+
expect(
|
|
964
|
+
protectedNameReportLines(
|
|
965
|
+
manifestWith([
|
|
966
|
+
".serverk-install.json",
|
|
967
|
+
rule,
|
|
968
|
+
]),
|
|
969
|
+
),
|
|
970
|
+
).toEqual([
|
|
971
|
+
` error: files.protected "${rule}" is not a .serverk path — a package protects only platform state, so drop the rule or move the file under a .serverk name`,
|
|
972
|
+
]);
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
});
|
|
976
|
+
|
|
977
|
+
describe("uploadStagingReportLines", () => {
|
|
978
|
+
it("fails a section upload that stages under a protected folder", () => {
|
|
979
|
+
const manifest = manifestWith([
|
|
980
|
+
".serverk-download/**",
|
|
981
|
+
".serverk-worlds/**",
|
|
982
|
+
]);
|
|
983
|
+
|
|
984
|
+
expect(
|
|
985
|
+
uploadStagingReportLines(
|
|
986
|
+
manifest,
|
|
987
|
+
driverWith([
|
|
988
|
+
worldsWith(".serverk-worlds"),
|
|
989
|
+
]),
|
|
990
|
+
),
|
|
991
|
+
).toEqual([
|
|
992
|
+
' error: upload staging ".serverk-worlds" of tab "worlds" section "worlds" is protected by ".serverk-worlds/**" — a protected path refuses the customer\'s upload, so take the folder off `files.protected`',
|
|
993
|
+
]);
|
|
994
|
+
});
|
|
995
|
+
|
|
996
|
+
it("fails a pending upload that stages under a protected folder", () => {
|
|
997
|
+
const manifest = manifestWith([
|
|
998
|
+
".serverk-staging",
|
|
999
|
+
]);
|
|
1000
|
+
|
|
1001
|
+
expect(uploadStagingReportLines(manifest, driverWith([], pendingWith(".serverk-staging/pending-upload")))).toEqual([
|
|
1002
|
+
' error: upload staging ".serverk-staging/pending-upload" of the pending upload is protected by ".serverk-staging" — a protected path refuses the customer\'s upload, so take the folder off `files.protected`',
|
|
1003
|
+
]);
|
|
1004
|
+
});
|
|
1005
|
+
|
|
1006
|
+
it("passes when no protected rule reaches an upload staging folder", () => {
|
|
1007
|
+
const manifest = manifestWith([
|
|
1008
|
+
".serverk-download/**",
|
|
1009
|
+
".serverk-install.json",
|
|
1010
|
+
".serverk-staging/install/**",
|
|
1011
|
+
"*.json",
|
|
1012
|
+
]);
|
|
1013
|
+
|
|
1014
|
+
expect(
|
|
1015
|
+
uploadStagingReportLines(
|
|
1016
|
+
manifest,
|
|
1017
|
+
driverWith(
|
|
1018
|
+
[
|
|
1019
|
+
worldsWith(".serverk-staging/world-upload"),
|
|
1020
|
+
],
|
|
1021
|
+
pendingWith(".serverk-staging/pending-upload"),
|
|
1022
|
+
),
|
|
1023
|
+
),
|
|
1024
|
+
).toEqual([]);
|
|
1025
|
+
});
|
|
1026
|
+
|
|
1027
|
+
it("passes an upload staging folder that sits beside a protected sibling", () => {
|
|
1028
|
+
const manifest = manifestWith([
|
|
1029
|
+
".serverk-staging/**",
|
|
1030
|
+
]);
|
|
1031
|
+
|
|
1032
|
+
expect(
|
|
1033
|
+
uploadStagingReportLines(
|
|
1034
|
+
manifest,
|
|
1035
|
+
driverWith([
|
|
1036
|
+
worldsWith(".serverk-uploads/world"),
|
|
1037
|
+
]),
|
|
1038
|
+
),
|
|
1039
|
+
).toEqual([]);
|
|
1040
|
+
});
|
|
1041
|
+
|
|
1042
|
+
it("passes a staging folder that merely holds a protected sidecar", () => {
|
|
1043
|
+
const manifest = manifestWith([
|
|
1044
|
+
".serverk-staging/install/**",
|
|
1045
|
+
]);
|
|
1046
|
+
|
|
1047
|
+
expect(
|
|
1048
|
+
uploadStagingReportLines(
|
|
1049
|
+
manifest,
|
|
1050
|
+
driverWith([
|
|
1051
|
+
worldsWith(".serverk-staging"),
|
|
1052
|
+
]),
|
|
1053
|
+
),
|
|
1054
|
+
).toEqual([]);
|
|
1055
|
+
});
|
|
1056
|
+
});
|
|
1057
|
+
|
|
1058
|
+
describe("notableReportLines", () => {
|
|
1059
|
+
it("fails a note on a protected file", () => {
|
|
1060
|
+
const manifest = manifestWith(
|
|
1061
|
+
[
|
|
1062
|
+
".serverk-install.json",
|
|
1063
|
+
],
|
|
1064
|
+
[
|
|
1065
|
+
".serverk-install.json",
|
|
1066
|
+
],
|
|
1067
|
+
);
|
|
1068
|
+
|
|
1069
|
+
expect(notableReportLines(manifest)).toEqual([
|
|
1070
|
+
' error: files.notable ".serverk-install.json" is protected by ".serverk-install.json" — the file manager hides a protected path before it annotates it, so the note never shows; take one of the two off',
|
|
1071
|
+
]);
|
|
1072
|
+
});
|
|
1073
|
+
|
|
1074
|
+
it("fails a note on a file inside a protected folder", () => {
|
|
1075
|
+
const manifest = manifestWith(
|
|
1076
|
+
[
|
|
1077
|
+
".serverk/**",
|
|
1078
|
+
],
|
|
1079
|
+
[
|
|
1080
|
+
".serverk/install.json",
|
|
1081
|
+
],
|
|
1082
|
+
);
|
|
1083
|
+
|
|
1084
|
+
expect(notableReportLines(manifest)).toEqual([
|
|
1085
|
+
' error: files.notable ".serverk/install.json" is protected by ".serverk/**" — the file manager hides a protected path before it annotates it, so the note never shows; take one of the two off',
|
|
1086
|
+
]);
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
it("fails a wildcard note under a protected folder", () => {
|
|
1090
|
+
const manifest = manifestWith(
|
|
1091
|
+
[
|
|
1092
|
+
".serverk-staging",
|
|
1093
|
+
],
|
|
1094
|
+
[
|
|
1095
|
+
".serverk-staging/**",
|
|
1096
|
+
],
|
|
1097
|
+
);
|
|
1098
|
+
|
|
1099
|
+
expect(notableReportLines(manifest)).toEqual([
|
|
1100
|
+
' error: files.notable ".serverk-staging/**" is protected by ".serverk-staging" — the file manager hides a protected path before it annotates it, so the note never shows; take one of the two off',
|
|
1101
|
+
]);
|
|
1102
|
+
});
|
|
1103
|
+
|
|
1104
|
+
it("passes notes on folders that merely hold protected sidecars", () => {
|
|
1105
|
+
const manifest = manifestWith(JAVA_RULES, [
|
|
1106
|
+
"plugins",
|
|
1107
|
+
"mods",
|
|
1108
|
+
"world/**",
|
|
1109
|
+
"*.json",
|
|
1110
|
+
]);
|
|
1111
|
+
|
|
1112
|
+
expect(notableReportLines(manifest)).toEqual([]);
|
|
1113
|
+
});
|
|
1114
|
+
});
|
|
1115
|
+
|
|
1116
|
+
describe("openTargetReportLines", () => {
|
|
1117
|
+
it("fails a guide link that opens a protected file", () => {
|
|
1118
|
+
const manifest = manifestWith([
|
|
1119
|
+
".serverk-install.json",
|
|
1120
|
+
]);
|
|
1121
|
+
|
|
1122
|
+
expect(
|
|
1123
|
+
openTargetReportLines(
|
|
1124
|
+
manifest,
|
|
1125
|
+
driverWith([]),
|
|
1126
|
+
packWith([
|
|
1127
|
+
filesOpenWith(".serverk-install.json"),
|
|
1128
|
+
]),
|
|
1129
|
+
),
|
|
1130
|
+
).toEqual([
|
|
1131
|
+
' error: guide "getting-started" (ar) opens files at ".serverk-install.json", which ".serverk-install.json" protects — the panel refuses a protected path, so the link is dead; point it elsewhere or take the rule off `files.protected`',
|
|
1132
|
+
' error: guide "getting-started" (en) opens files at ".serverk-install.json", which ".serverk-install.json" protects — the panel refuses a protected path, so the link is dead; point it elsewhere or take the rule off `files.protected`',
|
|
1133
|
+
]);
|
|
1134
|
+
});
|
|
1135
|
+
|
|
1136
|
+
it("fails a guide link inside a conditional block", () => {
|
|
1137
|
+
const manifest = manifestWith([
|
|
1138
|
+
".serverk/**",
|
|
1139
|
+
]);
|
|
1140
|
+
|
|
1141
|
+
expect(
|
|
1142
|
+
openTargetReportLines(
|
|
1143
|
+
manifest,
|
|
1144
|
+
driverWith([]),
|
|
1145
|
+
packWith([
|
|
1146
|
+
{
|
|
1147
|
+
type: "conditional",
|
|
1148
|
+
key: "panel.modded",
|
|
1149
|
+
when: [],
|
|
1150
|
+
otherwise: [
|
|
1151
|
+
filesOpenWith(".serverk/install.json"),
|
|
1152
|
+
],
|
|
1153
|
+
},
|
|
1154
|
+
]),
|
|
1155
|
+
),
|
|
1156
|
+
).toEqual([
|
|
1157
|
+
' error: guide "getting-started" (ar) opens files at ".serverk/install.json", which ".serverk/**" protects — the panel refuses a protected path, so the link is dead; point it elsewhere or take the rule off `files.protected`',
|
|
1158
|
+
' error: guide "getting-started" (en) opens files at ".serverk/install.json", which ".serverk/**" protects — the panel refuses a protected path, so the link is dead; point it elsewhere or take the rule off `files.protected`',
|
|
1159
|
+
]);
|
|
1160
|
+
});
|
|
1161
|
+
|
|
1162
|
+
it("fails a setup step that opens a protected folder", () => {
|
|
1163
|
+
const manifest = manifestWith([
|
|
1164
|
+
".serverk-level/**",
|
|
1165
|
+
]);
|
|
1166
|
+
|
|
1167
|
+
const setup: BridgeSetupModule = {
|
|
1168
|
+
kind: BridgeKind.Setup,
|
|
1169
|
+
steps: [
|
|
1170
|
+
{
|
|
1171
|
+
kind: BridgeSetupStepKind.Open,
|
|
1172
|
+
id: "level",
|
|
1173
|
+
title: text("Level"),
|
|
1174
|
+
required: false,
|
|
1175
|
+
target: {
|
|
1176
|
+
tab: GuideOpenTab.Files,
|
|
1177
|
+
path: ".serverk-level",
|
|
1178
|
+
},
|
|
1179
|
+
},
|
|
1180
|
+
],
|
|
1181
|
+
submit: async () => {
|
|
1182
|
+
return;
|
|
1183
|
+
},
|
|
1184
|
+
};
|
|
1185
|
+
|
|
1186
|
+
expect(openTargetReportLines(manifest, driverWith([], undefined, setup), null)).toEqual([
|
|
1187
|
+
' error: setup step "level" opens files at ".serverk-level", which ".serverk-level/**" protects — the panel refuses a protected path, so the link is dead; point it elsewhere or take the rule off `files.protected`',
|
|
1188
|
+
]);
|
|
1189
|
+
});
|
|
1190
|
+
|
|
1191
|
+
it("passes files links with no path, other tabs and unprotected paths", () => {
|
|
1192
|
+
const manifest = manifestWith([
|
|
1193
|
+
".serverk-install.json",
|
|
1194
|
+
".serverk-staging/**",
|
|
1195
|
+
]);
|
|
1196
|
+
|
|
1197
|
+
const setup: BridgeSetupModule = {
|
|
1198
|
+
kind: BridgeKind.Setup,
|
|
1199
|
+
steps: [
|
|
1200
|
+
{
|
|
1201
|
+
kind: BridgeSetupStepKind.Open,
|
|
1202
|
+
id: "files",
|
|
1203
|
+
title: text("Files"),
|
|
1204
|
+
required: false,
|
|
1205
|
+
target: {
|
|
1206
|
+
tab: GuideOpenTab.Files,
|
|
1207
|
+
path: null,
|
|
1208
|
+
},
|
|
1209
|
+
},
|
|
1210
|
+
{
|
|
1211
|
+
kind: BridgeSetupStepKind.Open,
|
|
1212
|
+
id: "invite",
|
|
1213
|
+
title: text("Invite"),
|
|
1214
|
+
required: false,
|
|
1215
|
+
target: {
|
|
1216
|
+
tab: GuideOpenTab.Access,
|
|
1217
|
+
},
|
|
1218
|
+
},
|
|
1219
|
+
],
|
|
1220
|
+
submit: async () => {
|
|
1221
|
+
return;
|
|
1222
|
+
},
|
|
1223
|
+
};
|
|
1224
|
+
|
|
1225
|
+
expect(
|
|
1226
|
+
openTargetReportLines(
|
|
1227
|
+
manifest,
|
|
1228
|
+
driverWith([], undefined, setup),
|
|
1229
|
+
packWith([
|
|
1230
|
+
filesOpenWith(null),
|
|
1231
|
+
filesOpenWith("server.properties"),
|
|
1232
|
+
filesOpenWith(".serverk-uploads/world"),
|
|
1233
|
+
{
|
|
1234
|
+
type: "open",
|
|
1235
|
+
target: {
|
|
1236
|
+
tab: GuideOpenTab.Panel,
|
|
1237
|
+
tabId: "settings",
|
|
1238
|
+
},
|
|
1239
|
+
},
|
|
1240
|
+
{
|
|
1241
|
+
type: "open",
|
|
1242
|
+
target: {
|
|
1243
|
+
tab: GuideOpenTab.Console,
|
|
1244
|
+
},
|
|
1245
|
+
},
|
|
1246
|
+
]),
|
|
1247
|
+
),
|
|
1248
|
+
).toEqual([]);
|
|
1249
|
+
});
|
|
1250
|
+
});
|