@serverkgg/bridge 0.0.1

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.
Files changed (73) hide show
  1. package/LICENSE.md +22 -0
  2. package/README.md +52 -0
  3. package/package.json +26 -0
  4. package/src/cli/compile.ts +75 -0
  5. package/src/cli/directorySource.ts +48 -0
  6. package/src/cli/main.ts +44 -0
  7. package/src/cli/validate.ts +94 -0
  8. package/src/codecs/index.ts +3 -0
  9. package/src/codecs/json.ts +42 -0
  10. package/src/codecs/properties.ts +75 -0
  11. package/src/codecs/ueIni.ts +235 -0
  12. package/src/context/bridgeContext.ts +120 -0
  13. package/src/context/index.ts +1 -0
  14. package/src/core/bridgeAction.ts +13 -0
  15. package/src/core/bridgeId.ts +3 -0
  16. package/src/core/bridgeKind.ts +11 -0
  17. package/src/core/bridgePort.ts +4 -0
  18. package/src/core/bridgeRuntime.ts +13 -0
  19. package/src/core/bridgeSecretError.ts +10 -0
  20. package/src/core/bridgeStream.ts +4 -0
  21. package/src/core/bridgeValue.ts +19 -0
  22. package/src/core/index.ts +8 -0
  23. package/src/main.ts +5 -0
  24. package/src/manifest/assets.ts +102 -0
  25. package/src/manifest/category.ts +10 -0
  26. package/src/manifest/checks.ts +50 -0
  27. package/src/manifest/dimensions.ts +106 -0
  28. package/src/manifest/guide.ts +36 -0
  29. package/src/manifest/image.ts +1 -0
  30. package/src/manifest/index.ts +9 -0
  31. package/src/manifest/localized.ts +8 -0
  32. package/src/manifest/manifest.ts +170 -0
  33. package/src/manifest/source.ts +89 -0
  34. package/src/modules/bridge.ts +53 -0
  35. package/src/modules/bridgeDriver.ts +19 -0
  36. package/src/modules/bridgeModule.ts +114 -0
  37. package/src/modules/index.ts +3 -0
  38. package/src/panel/bridgeConfirm.ts +5 -0
  39. package/src/panel/bridgeControl.ts +7 -0
  40. package/src/panel/bridgeFormTarget.ts +4 -0
  41. package/src/panel/bridgeIcon.ts +17 -0
  42. package/src/panel/bridgeLayout.ts +6 -0
  43. package/src/panel/bridgePanel.ts +107 -0
  44. package/src/panel/index.ts +6 -0
  45. package/src/probes/a2s.ts +170 -0
  46. package/src/probes/index.ts +1 -0
  47. package/src/protocol/bridgeConfig.ts +13 -0
  48. package/src/protocol/bridgeFrame.ts +91 -0
  49. package/src/protocol/bridgeLine.ts +40 -0
  50. package/src/protocol/bridgeManifest.ts +19 -0
  51. package/src/protocol/bridgeVersion.ts +11 -0
  52. package/src/protocol/index.ts +7 -0
  53. package/src/runtime/bridgeCodec.ts +78 -0
  54. package/src/runtime/bridgeConfigReader.ts +119 -0
  55. package/src/runtime/bridgeContext.ts +96 -0
  56. package/src/runtime/bridgeDriver.ts +179 -0
  57. package/src/runtime/bridgeEvents.ts +71 -0
  58. package/src/runtime/bridgeExec.ts +143 -0
  59. package/src/runtime/bridgeFiles.ts +412 -0
  60. package/src/runtime/bridgeLog.ts +15 -0
  61. package/src/runtime/bridgeNet.ts +59 -0
  62. package/src/runtime/bridgePath.ts +132 -0
  63. package/src/runtime/bridgePrivilege.ts +99 -0
  64. package/src/runtime/bridgeProbe.ts +14 -0
  65. package/src/runtime/bridgeRegistry.ts +311 -0
  66. package/src/runtime/bridgeServer.ts +233 -0
  67. package/src/runtime/bridgeState.ts +40 -0
  68. package/src/runtime/bridgeSupervisor.ts +498 -0
  69. package/src/runtime/bridgeZip.ts +122 -0
  70. package/src/runtime/index.ts +18 -0
  71. package/src/runtime/serve.ts +294 -0
  72. package/src/utils/glob.ts +62 -0
  73. package/src/utils/index.ts +1 -0
@@ -0,0 +1,235 @@
1
+ interface TupleEntry {
2
+ key: string;
3
+ value: string;
4
+ quoted: boolean;
5
+ }
6
+
7
+ const BARE_VALUE = /^-?\d+(?:\.\d+)?$|^(?:True|False)$|^[A-Za-z0-9_.-]+$/;
8
+
9
+ const isSectionHeader = (line: string) => {
10
+ const trimmed = line.trim();
11
+
12
+ return trimmed.startsWith("[") && trimmed.endsWith("]");
13
+ };
14
+
15
+ const sectionName = (line: string) => {
16
+ return line.trim().slice(1, -1);
17
+ };
18
+
19
+ const splitTuple = (tuple: string) => {
20
+ const parts: string[] = [];
21
+
22
+ let depth = 0;
23
+ let quoted = false;
24
+ let current = "";
25
+
26
+ for (let index = 0; index < tuple.length; index++) {
27
+ const character = tuple[index];
28
+
29
+ if (character === '"' && tuple[index - 1] !== "\\") {
30
+ quoted = !quoted;
31
+ }
32
+
33
+ if (!quoted) {
34
+ if (character === "(") {
35
+ depth++;
36
+ }
37
+
38
+ if (character === ")") {
39
+ depth--;
40
+ }
41
+
42
+ if (character === "," && depth === 0) {
43
+ parts.push(current);
44
+ current = "";
45
+
46
+ continue;
47
+ }
48
+ }
49
+
50
+ current += character;
51
+ }
52
+
53
+ if (current.trim().length > 0) {
54
+ parts.push(current);
55
+ }
56
+
57
+ return parts;
58
+ };
59
+
60
+ const toEntry = (part: string): TupleEntry | null => {
61
+ const separator = part.indexOf("=");
62
+
63
+ if (separator === -1) {
64
+ return null;
65
+ }
66
+
67
+ const key = part.slice(0, separator).trim();
68
+ const raw = part.slice(separator + 1).trim();
69
+
70
+ if (key.length === 0) {
71
+ return null;
72
+ }
73
+
74
+ if (raw.startsWith('"') && raw.endsWith('"') && raw.length >= 2) {
75
+ return {
76
+ key,
77
+ value: raw.slice(1, -1).replaceAll('\\"', '"'),
78
+ quoted: true,
79
+ };
80
+ }
81
+
82
+ return {
83
+ key,
84
+ value: raw,
85
+ quoted: false,
86
+ };
87
+ };
88
+
89
+ const fromEntry = (entry: TupleEntry) => {
90
+ if (entry.quoted) {
91
+ return `${entry.key}="${entry.value.replaceAll('"', '\\"')}"`;
92
+ }
93
+
94
+ return `${entry.key}=${entry.value}`;
95
+ };
96
+
97
+ const normalizeRead = (entry: TupleEntry) => {
98
+ if (!entry.quoted && (entry.value === "True" || entry.value === "False")) {
99
+ return entry.value.toLowerCase();
100
+ }
101
+
102
+ return entry.value;
103
+ };
104
+
105
+ const normalizeWrite = (value: string, quoted: boolean) => {
106
+ if (!quoted && (value === "true" || value === "false")) {
107
+ return value === "true" ? "True" : "False";
108
+ }
109
+
110
+ return value;
111
+ };
112
+
113
+ const parseEntries = (contents: string, section: string, object: string) => {
114
+ const lines = contents.split("\n");
115
+ const prefix = `${object}=`;
116
+
117
+ let inSection = false;
118
+
119
+ for (let index = 0; index < lines.length; index++) {
120
+ const line = lines[index] ?? "";
121
+
122
+ if (isSectionHeader(line)) {
123
+ inSection = sectionName(line) === section;
124
+
125
+ continue;
126
+ }
127
+
128
+ if (!inSection) {
129
+ continue;
130
+ }
131
+
132
+ const trimmed = line.trim();
133
+
134
+ if (!trimmed.startsWith(prefix)) {
135
+ continue;
136
+ }
137
+
138
+ const raw = trimmed.slice(prefix.length).trim();
139
+ const tuple = raw.startsWith("(") && raw.endsWith(")") ? raw.slice(1, -1) : raw;
140
+
141
+ const entries: TupleEntry[] = [];
142
+
143
+ for (const part of splitTuple(tuple)) {
144
+ const entry = toEntry(part);
145
+
146
+ if (entry) {
147
+ entries.push(entry);
148
+ }
149
+ }
150
+
151
+ return {
152
+ entries,
153
+ lineIndex: index,
154
+ };
155
+ }
156
+
157
+ return {
158
+ entries: [] as TupleEntry[],
159
+ lineIndex: -1,
160
+ };
161
+ };
162
+
163
+ export const parseUeIni = (contents: string, section: string, object: string) => {
164
+ const values: Record<string, string> = {};
165
+
166
+ for (const entry of parseEntries(contents, section, object).entries) {
167
+ values[entry.key] = normalizeRead(entry);
168
+ }
169
+
170
+ return values;
171
+ };
172
+
173
+ export const mergeUeIni = (contents: string, section: string, object: string, values: Record<string, string>) => {
174
+ const pending = new Map(Object.entries(values));
175
+ const parsed = parseEntries(contents, section, object);
176
+
177
+ const entries = parsed.entries.map((entry) => {
178
+ const value = pending.get(entry.key);
179
+
180
+ if (value === undefined) {
181
+ return entry;
182
+ }
183
+
184
+ pending.delete(entry.key);
185
+
186
+ return {
187
+ ...entry,
188
+ value: normalizeWrite(value, entry.quoted),
189
+ };
190
+ });
191
+
192
+ for (const [key, value] of pending) {
193
+ const quoted = !BARE_VALUE.test(value);
194
+
195
+ entries.push({
196
+ key,
197
+ value: normalizeWrite(value, quoted),
198
+ quoted,
199
+ });
200
+ }
201
+
202
+ const objectLine = `${object}=(${entries.map(fromEntry).join(",")})`;
203
+
204
+ if (parsed.lineIndex !== -1) {
205
+ const lines = contents.split("\n");
206
+
207
+ lines[parsed.lineIndex] = objectLine;
208
+
209
+ return lines.join("\n");
210
+ }
211
+
212
+ const lines = contents.length === 0 ? [] : contents.split("\n");
213
+
214
+ while (lines.length > 0 && lines.at(-1)?.length === 0) {
215
+ lines.pop();
216
+ }
217
+
218
+ for (let index = 0; index < lines.length; index++) {
219
+ const line = lines[index] ?? "";
220
+
221
+ if (isSectionHeader(line) && sectionName(line) === section) {
222
+ lines.splice(index + 1, 0, objectLine);
223
+
224
+ return `${lines.join("\n")}\n`;
225
+ }
226
+ }
227
+
228
+ if (lines.length > 0) {
229
+ lines.push("");
230
+ }
231
+
232
+ lines.push(`[${section}]`, objectLine);
233
+
234
+ return `${lines.join("\n")}\n`;
235
+ };
@@ -0,0 +1,120 @@
1
+ import type { BridgeStream, BridgeValues } from "../core";
2
+
3
+ export interface BridgeServer {
4
+ code: string;
5
+ memoryMb: number;
6
+ cpuPercent: number;
7
+ running: boolean;
8
+ }
9
+
10
+ export interface BridgeFileEntry {
11
+ path: string;
12
+ name: string;
13
+ directory: boolean;
14
+ sizeBytes: number;
15
+ modifiedAt: string;
16
+ }
17
+
18
+ export interface BridgeDownloadOptions {
19
+ digest?: string;
20
+ sizeBytes?: number;
21
+ }
22
+
23
+ export interface BridgeFiles {
24
+ read(path: string): Promise<string>;
25
+ write(path: string, content: string): Promise<void>;
26
+ list(glob: string): Promise<BridgeFileEntry[]>;
27
+ remove(path: string): Promise<void>;
28
+ move(from: string, to: string): Promise<void>;
29
+ ensure(...paths: string[]): Promise<void>;
30
+ exists(path: string): Promise<boolean>;
31
+ size(path: string): Promise<number>;
32
+ download(path: string, url: string, options?: BridgeDownloadOptions): Promise<void>;
33
+ extract(path: string, destination: string, extensions?: string[]): Promise<string[]>;
34
+ }
35
+
36
+ export interface BridgeCodecOptions {
37
+ section?: string;
38
+ object?: string;
39
+ }
40
+
41
+ export interface BridgeCodec {
42
+ read(path: string, options?: BridgeCodecOptions): Promise<BridgeValues>;
43
+ merge(path: string, values: BridgeValues, options?: BridgeCodecOptions): Promise<void>;
44
+ }
45
+
46
+ export interface BridgeCodecs {
47
+ json: BridgeCodec;
48
+ properties: BridgeCodec;
49
+ ueIni: BridgeCodec;
50
+ }
51
+
52
+ export interface BridgeRequestOptions {
53
+ headers?: Record<string, string>;
54
+ timeoutMs?: number;
55
+ }
56
+
57
+ export interface BridgeNet {
58
+ json<Result>(url: string, options?: BridgeRequestOptions): Promise<Result>;
59
+ text(url: string, options?: BridgeRequestOptions): Promise<string>;
60
+ }
61
+
62
+ export interface BridgePingPlayers {
63
+ online: number;
64
+ max: number;
65
+ }
66
+
67
+ export interface BridgeA2sInfo {
68
+ players: BridgePingPlayers;
69
+ name: string | null;
70
+ map: string | null;
71
+ latencyMs: number;
72
+ }
73
+
74
+ export interface BridgeProbes {
75
+ a2s(port: number): Promise<BridgeA2sInfo>;
76
+ }
77
+
78
+ export interface BridgeCommandOptions {
79
+ expect?: RegExp;
80
+ timeoutMs?: number;
81
+ }
82
+
83
+ export interface BridgeReply {
84
+ sent: string;
85
+ line: string | null;
86
+ groups: Record<string, string>;
87
+ }
88
+
89
+ export interface BridgeExecOptions {
90
+ timeoutMs?: number;
91
+ environment?: Record<string, string>;
92
+ onOutput?(line: string, stream: BridgeStream): void;
93
+ }
94
+
95
+ export interface BridgeExecResult {
96
+ stdout: string;
97
+ stderr: string;
98
+ code: number;
99
+ }
100
+
101
+ export interface BridgeLogs {
102
+ tail(lines: number): Promise<string[]>;
103
+ watch(pattern: RegExp, timeoutMs: number): Promise<RegExpMatchArray | null>;
104
+ }
105
+
106
+ export interface BridgeContext {
107
+ readonly server: BridgeServer;
108
+ variable(key: string): string | null;
109
+ secret(key: string): string | null;
110
+ port(key: string): number;
111
+ files: BridgeFiles;
112
+ codec: BridgeCodecs;
113
+ net: BridgeNet;
114
+ probe: BridgeProbes;
115
+ command(input: string, options?: BridgeCommandOptions): Promise<BridgeReply>;
116
+ exec(command: string[], options?: BridgeExecOptions): Promise<BridgeExecResult>;
117
+ logs: BridgeLogs;
118
+ emit(event: string, payload?: BridgeValues): void;
119
+ log(message: string, fields?: BridgeValues): void;
120
+ }
@@ -0,0 +1 @@
1
+ export * from "./bridgeContext";
@@ -0,0 +1,13 @@
1
+ export enum BridgeAction {
2
+ Add = "add",
3
+ Install = "install",
4
+ Installed = "installed",
5
+ List = "list",
6
+ Quiesce = "quiesce",
7
+ Read = "read",
8
+ Release = "release",
9
+ Remove = "remove",
10
+ Sample = "sample",
11
+ Search = "search",
12
+ Write = "write",
13
+ }
@@ -0,0 +1,3 @@
1
+ export const BRIDGE_ID_PATTERN = /^[a-z][a-z0-9-]*$/;
2
+
3
+ export const BRIDGE_MODULE_ID_PATTERN = /^[a-z][a-zA-Z0-9]*$/;
@@ -0,0 +1,11 @@
1
+ export enum BridgeKind {
2
+ Backup = "backup",
3
+ Catalog = "catalog",
4
+ Collection = "collection",
5
+ Events = "events",
6
+ Install = "install",
7
+ Lifecycle = "lifecycle",
8
+ Options = "options",
9
+ Query = "query",
10
+ Settings = "settings",
11
+ }
@@ -0,0 +1,4 @@
1
+ export enum PortProtocol {
2
+ Tcp = "tcp",
3
+ Udp = "udp",
4
+ }
@@ -0,0 +1,13 @@
1
+ export enum BridgeRuntime {
2
+ Crashed = "crashed",
3
+ Installing = "installing",
4
+ Running = "running",
5
+ Starting = "starting",
6
+ Stopped = "stopped",
7
+ Stopping = "stopping",
8
+ }
9
+
10
+ export enum BridgeDesired {
11
+ Running = "running",
12
+ Stopped = "stopped",
13
+ }
@@ -0,0 +1,10 @@
1
+ export class BridgeSecretError extends Error {
2
+ readonly key: string;
3
+
4
+ constructor(key: string, message: string) {
5
+ super(message);
6
+
7
+ this.name = "BridgeSecretError";
8
+ this.key = key;
9
+ }
10
+ }
@@ -0,0 +1,4 @@
1
+ export enum BridgeStream {
2
+ Stderr = "stderr",
3
+ Stdout = "stdout",
4
+ }
@@ -0,0 +1,19 @@
1
+ export type BridgeValue = boolean | number | string | null;
2
+
3
+ export type BridgeValues = Record<string, BridgeValue>;
4
+
5
+ export type BridgeRow = BridgeValues & {
6
+ id: string;
7
+ };
8
+
9
+ export interface LocalizedText {
10
+ ar: string;
11
+ en: string;
12
+ }
13
+
14
+ export interface BridgeOption {
15
+ value: string;
16
+ label: LocalizedText;
17
+ help?: LocalizedText;
18
+ latest?: boolean;
19
+ }
@@ -0,0 +1,8 @@
1
+ export * from "./bridgeAction";
2
+ export * from "./bridgeId";
3
+ export * from "./bridgeKind";
4
+ export * from "./bridgePort";
5
+ export * from "./bridgeRuntime";
6
+ export * from "./bridgeSecretError";
7
+ export * from "./bridgeStream";
8
+ export * from "./bridgeValue";
package/src/main.ts ADDED
@@ -0,0 +1,5 @@
1
+ export * from "./context";
2
+ export * from "./core";
3
+ export * from "./modules";
4
+ export * from "./panel";
5
+ export { BridgeNetError, serve } from "./runtime";
@@ -0,0 +1,102 @@
1
+ import { ASSET_MAX_EDGE, type AssetDimensions, readDimensions } from "./dimensions";
2
+
3
+ export const ASSET_MAX_BYTES = 512 * 1024;
4
+
5
+ export const ASSET_EXTENSION = "webp";
6
+
7
+ export const ASSET_MEDIA_TYPE = "image/webp";
8
+
9
+ const RIFF_HEADER_BYTES = 12;
10
+
11
+ const startsWith = (bytes: Uint8Array, signature: number[]) => {
12
+ return signature.every((byte, index) => bytes[index] === byte);
13
+ };
14
+
15
+ const isWebp = (bytes: Uint8Array) => {
16
+ if (bytes.byteLength < RIFF_HEADER_BYTES) {
17
+ return false;
18
+ }
19
+
20
+ const riff = startsWith(
21
+ bytes,
22
+ [
23
+ 0x52,
24
+ 0x49,
25
+ 0x46,
26
+ 0x46,
27
+ ],
28
+ );
29
+
30
+ const webp = startsWith(
31
+ bytes.subarray(8),
32
+ [
33
+ 0x57,
34
+ 0x45,
35
+ 0x42,
36
+ 0x50,
37
+ ],
38
+ );
39
+
40
+ if (!riff || !webp) {
41
+ return false;
42
+ }
43
+
44
+ const first = bytes[4];
45
+ const second = bytes[5];
46
+ const third = bytes[6];
47
+ const fourth = bytes[7];
48
+
49
+ if (first === undefined || second === undefined || third === undefined || fourth === undefined) {
50
+ return false;
51
+ }
52
+
53
+ const declared = first + (second << 8) + (third << 16) + fourth * 0x1_00_00_00;
54
+
55
+ return declared + 8 === bytes.byteLength;
56
+ };
57
+
58
+ export const assetMediaType = (path: string) => {
59
+ const extension = path.slice(path.lastIndexOf(".") + 1).toLowerCase();
60
+
61
+ if (extension !== ASSET_EXTENSION) {
62
+ throw new Error(`asset "${path}" must be a .${ASSET_EXTENSION} file`);
63
+ }
64
+
65
+ return ASSET_MEDIA_TYPE;
66
+ };
67
+
68
+ export interface VerifiedAsset {
69
+ mediaType: string;
70
+ dimensions: AssetDimensions;
71
+ }
72
+
73
+ export const verifyAsset = (path: string, bytes: Uint8Array): VerifiedAsset => {
74
+ if (bytes.byteLength === 0) {
75
+ throw new Error(`asset "${path}" is empty`);
76
+ }
77
+
78
+ if (bytes.byteLength > ASSET_MAX_BYTES) {
79
+ throw new Error(`asset "${path}" exceeds ${ASSET_MAX_BYTES} bytes`);
80
+ }
81
+
82
+ const mediaType = assetMediaType(path);
83
+
84
+ if (!isWebp(bytes)) {
85
+ throw new Error(`asset "${path}" is not a well-formed webp file`);
86
+ }
87
+
88
+ const dimensions = readDimensions(bytes);
89
+
90
+ if (!dimensions) {
91
+ throw new Error(`asset "${path}" has unreadable dimensions`);
92
+ }
93
+
94
+ if (dimensions.width > ASSET_MAX_EDGE || dimensions.height > ASSET_MAX_EDGE) {
95
+ throw new Error(`asset "${path}" exceeds ${ASSET_MAX_EDGE}px on an edge`);
96
+ }
97
+
98
+ return {
99
+ mediaType,
100
+ dimensions,
101
+ };
102
+ };
@@ -0,0 +1,10 @@
1
+ export enum GameCategory {
2
+ Sandbox = "sandbox",
3
+ Survival = "survival",
4
+ Shooter = "shooter",
5
+ Racing = "racing",
6
+ Roleplay = "roleplay",
7
+ Strategy = "strategy",
8
+ Simulation = "simulation",
9
+ Other = "other",
10
+ }
@@ -0,0 +1,50 @@
1
+ import type { Manifest, ManifestPort } from "./manifest";
2
+
3
+ const overlaps = (a: ManifestPort, b: ManifestPort) => {
4
+ return a.host.min <= b.host.max && b.host.min <= a.host.max;
5
+ };
6
+
7
+ export const checkManifest = (manifest: Manifest) => {
8
+ const portKeys = manifest.runtime.ports.map((port) => port.key);
9
+
10
+ if (new Set(portKeys).size !== portKeys.length) {
11
+ throw new Error("runtime declares duplicate port keys");
12
+ }
13
+
14
+ const ports = manifest.runtime.ports;
15
+
16
+ for (let index = 0; index < ports.length; index += 1) {
17
+ for (let other = index + 1; other < ports.length; other += 1) {
18
+ const a = ports[index];
19
+ const b = ports[other];
20
+
21
+ if (!a || !b || a.protocol !== b.protocol) {
22
+ continue;
23
+ }
24
+
25
+ if (overlaps(a, b)) {
26
+ throw new Error(`ports "${a.key}" and "${b.key}" declare overlapping ${a.protocol} host ranges`);
27
+ }
28
+ }
29
+ }
30
+
31
+ const guideIds = manifest.guides.map((guide) => guide.id);
32
+
33
+ if (new Set(guideIds).size !== guideIds.length) {
34
+ throw new Error("guides declare duplicate ids");
35
+ }
36
+
37
+ const bodies: string[] = [];
38
+
39
+ for (const guide of manifest.guides) {
40
+ if (guide.body.ar === guide.body.en) {
41
+ throw new Error(`guide "${guide.id}" reuses one body file for both locales`);
42
+ }
43
+
44
+ bodies.push(guide.body.ar, guide.body.en);
45
+ }
46
+
47
+ if (new Set(bodies).size !== bodies.length) {
48
+ throw new Error("guides declare duplicate body paths");
49
+ }
50
+ };