@serverkgg/bridge 0.1.7 → 0.1.8

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@serverkgg/bridge",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "Serverk game bridge and driver runtime, wire protocol, manifest schema, and maintainer CLI",
5
5
  "homepage": "https://serverk.gg",
6
6
  "license": "SEE LICENSE IN LICENSE.md",
package/rules/package.md CHANGED
@@ -51,6 +51,7 @@ 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
 
@@ -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
+ });
@@ -6,8 +6,16 @@ import { z } from "zod";
6
6
  import { type ArticlePack, compileArticles } from "../blog";
7
7
  import { DETECT_TABLE_FILE, renderDetectTable } from "../bot/detect";
8
8
  import { BridgeKind, PortProtocol } from "../core";
9
- import { GuideOpenTab, type GuideOpenTarget, type GuidePack, readGuidePack, readGuidePackage } from "../guides";
10
9
  import {
10
+ GuideOpenTab,
11
+ type GuideOpenTarget,
12
+ type GuidePack,
13
+ type ParsedBlock,
14
+ readGuidePack,
15
+ readGuidePackage,
16
+ } from "../guides";
17
+ import {
18
+ compileGlob,
11
19
  declaredFiles,
12
20
  type GuidePackage,
13
21
  isBotManifest,
@@ -347,6 +355,206 @@ export const confirmReportLines = (driver: BridgeDriver): string[] => {
347
355
  return lines;
348
356
  };
349
357
 
358
+ const REACH_PROBE = "x";
359
+
360
+ const SERVERK_SEGMENT = ".serverk";
361
+
362
+ const WILDCARD = "*";
363
+
364
+ interface UploadStaging {
365
+ owner: string;
366
+ folder: string;
367
+ }
368
+
369
+ interface FilesOpen {
370
+ owner: string;
371
+ path: string;
372
+ }
373
+
374
+ const protectedReaches = (path: string, rule: string) => {
375
+ if (rule === path || path.startsWith(`${rule}/`)) {
376
+ return true;
377
+ }
378
+
379
+ if (!rule.includes(WILDCARD)) {
380
+ return false;
381
+ }
382
+
383
+ const pattern = compileGlob(rule);
384
+
385
+ return pattern.test(path) || pattern.test(`${path}/${REACH_PROBE}`);
386
+ };
387
+
388
+ const isServerkPath = (rule: string) => {
389
+ return rule.split("/").some((segment) => segment === SERVERK_SEGMENT || segment.startsWith(`${SERVERK_SEGMENT}-`));
390
+ };
391
+
392
+ export const protectedNameReportLines = (manifest: RunnableManifest): string[] => {
393
+ const lines: string[] = [];
394
+
395
+ for (const rule of manifest.files.protected) {
396
+ if (isServerkPath(rule)) {
397
+ continue;
398
+ }
399
+
400
+ lines.push(
401
+ ` 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`,
402
+ );
403
+ }
404
+
405
+ return lines;
406
+ };
407
+
408
+ export const uploadStagingReportLines = (manifest: RunnableManifest, driver: BridgeDriver): string[] => {
409
+ const stagings: UploadStaging[] = [];
410
+
411
+ for (const tab of driver.panel?.tabs ?? []) {
412
+ for (const section of tab.sections) {
413
+ if (!("upload" in section) || !section.upload) {
414
+ continue;
415
+ }
416
+
417
+ stagings.push({
418
+ owner: `tab "${tab.id}" section "${section.id}"`,
419
+ folder: section.upload.staging,
420
+ });
421
+ }
422
+ }
423
+
424
+ if (driver.pending) {
425
+ stagings.push({
426
+ owner: "the pending upload",
427
+ folder: driver.pending.staging,
428
+ });
429
+ }
430
+
431
+ const lines: string[] = [];
432
+
433
+ for (const staging of stagings) {
434
+ for (const rule of manifest.files.protected) {
435
+ if (!protectedReaches(staging.folder, rule)) {
436
+ continue;
437
+ }
438
+
439
+ lines.push(
440
+ ` error: upload staging "${staging.folder}" of ${staging.owner} is protected by "${rule}" — a protected path refuses the customer's upload, so take the folder off \`files.protected\``,
441
+ );
442
+ }
443
+ }
444
+
445
+ return lines;
446
+ };
447
+
448
+ const literalPrefixOf = (match: string) => {
449
+ const segments = match.split("/");
450
+ const wild = segments.findIndex((segment) => segment.includes(WILDCARD));
451
+
452
+ return (wild === -1 ? segments : segments.slice(0, wild)).join("/");
453
+ };
454
+
455
+ export const notableReportLines = (manifest: RunnableManifest): string[] => {
456
+ const lines: string[] = [];
457
+
458
+ for (const note of manifest.files.notable) {
459
+ const prefix = literalPrefixOf(note.match);
460
+
461
+ if (prefix === "") {
462
+ continue;
463
+ }
464
+
465
+ for (const rule of manifest.files.protected) {
466
+ if (!protectedReaches(prefix, rule)) {
467
+ continue;
468
+ }
469
+
470
+ lines.push(
471
+ ` error: files.notable "${note.match}" is protected by "${rule}" — the file manager hides a protected path before it annotates it, so the note never shows; take one of the two off`,
472
+ );
473
+ }
474
+ }
475
+
476
+ return lines;
477
+ };
478
+
479
+ const guideOpenTargetsOf = (blocks: ParsedBlock[]) => {
480
+ const leaves = blocks.flatMap((block) =>
481
+ block.type === "conditional"
482
+ ? [
483
+ ...block.when,
484
+ ...block.otherwise,
485
+ ]
486
+ : [
487
+ block,
488
+ ],
489
+ );
490
+
491
+ return leaves.flatMap((block) =>
492
+ block.type === "open"
493
+ ? [
494
+ block.target,
495
+ ]
496
+ : [],
497
+ );
498
+ };
499
+
500
+ const filesPathOf = (target: GuideOpenTarget) => {
501
+ return target.tab === GuideOpenTab.Files ? target.path : null;
502
+ };
503
+
504
+ export const openTargetReportLines = (
505
+ manifest: RunnableManifest,
506
+ driver: BridgeDriver,
507
+ pack: GuidePack | null,
508
+ ): string[] => {
509
+ const opens: FilesOpen[] = [];
510
+
511
+ const collect = (owner: string, targets: GuideOpenTarget[]) => {
512
+ for (const target of targets) {
513
+ const path = filesPathOf(target);
514
+
515
+ if (path === null) {
516
+ continue;
517
+ }
518
+
519
+ opens.push({
520
+ owner,
521
+ path,
522
+ });
523
+ }
524
+ };
525
+
526
+ for (const draft of pack?.drafts ?? []) {
527
+ collect(`guide "${draft.id}" (ar)`, guideOpenTargetsOf(draft.body.ar));
528
+ collect(`guide "${draft.id}" (en)`, guideOpenTargetsOf(draft.body.en));
529
+ }
530
+
531
+ for (const step of driver.setup?.steps ?? []) {
532
+ if (step.kind !== BridgeSetupStepKind.Open) {
533
+ continue;
534
+ }
535
+
536
+ collect(`setup step "${step.id}"`, [
537
+ step.target,
538
+ ]);
539
+ }
540
+
541
+ const lines: string[] = [];
542
+
543
+ for (const open of opens) {
544
+ for (const rule of manifest.files.protected) {
545
+ if (!protectedReaches(open.path, rule)) {
546
+ continue;
547
+ }
548
+
549
+ lines.push(
550
+ ` error: ${open.owner} opens files at "${open.path}", which "${rule}" protects — the panel refuses a protected path, so the link is dead; point it elsewhere or take the rule off \`files.protected\``,
551
+ );
552
+ }
553
+ }
554
+
555
+ return lines;
556
+ };
557
+
350
558
  const openTargetOf = (target: GuideOpenTarget) => {
351
559
  if (target.tab === GuideOpenTab.Panel) {
352
560
  return `panel tab "${target.tabId}"`;
@@ -451,6 +659,12 @@ const validateRunnablePackage = async (
451
659
  }
452
660
 
453
661
  const pack = readGuidePack(source, manifest);
662
+ const nameLines = protectedNameReportLines(manifest);
663
+
664
+ if (nameLines.length > 0) {
665
+ throw new Error(`files.protected names a path that is not platform state:\n${nameLines.join("\n")}`);
666
+ }
667
+
454
668
  const detectLines = botDetectLines(source, manifest);
455
669
  const driverPath = driverArg ? resolve(driverArg) : join(dir, "src/driver.ts");
456
670
 
@@ -476,6 +690,16 @@ const validateRunnablePackage = async (
476
690
  );
477
691
  }
478
692
 
693
+ const reachLines = [
694
+ ...uploadStagingReportLines(manifest, driver),
695
+ ...notableReportLines(manifest),
696
+ ...openTargetReportLines(manifest, driver, pack),
697
+ ];
698
+
699
+ if (reachLines.length > 0) {
700
+ throw new Error(`a protected path is where the customer is sent:\n${reachLines.join("\n")}`);
701
+ }
702
+
479
703
  const conditionalPorts = manifest.container.ports.filter((port) => port.activeWhen.length > 0).length;
480
704
  const pairedPorts = manifest.container.ports.filter((port) => port.protocol === PortProtocol.TcpUdp).length;
481
705
  const blockPorts = manifest.container.ports.filter((port) => port.count > 1).length;