@ankorcs/protocol 0.0.0-stage → 0.6.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.
@@ -0,0 +1,21 @@
1
+ import type { z } from 'zod';
2
+ import { AgentConfigSchema, type AgentConfig } from './agent-config.js';
3
+ /**
4
+ * The keys the controller owns. An absent key is left alone, and `null` removes it from the file.
5
+ */
6
+ export type AgentYmlFields = Partial<Pick<AgentConfig, 'token' | 'controller' | 'cs2_image' | 'agent_image' | 'pinned_ip' | 'auto_update' | 'demo_upload' | 'slots'>> & {
7
+ /** Null removes the cap. */
8
+ memory_mb?: number | null;
9
+ };
10
+ /** The given fields under their `agent.yml` keys, in file order. */
11
+ export declare function agentYmlEntries(fields: AgentYmlFields): [string, unknown][];
12
+ /**
13
+ * The keys the controller owns, as `agent.yml` takes them. A key that the file may leave out may
14
+ * be left out here too.
15
+ */
16
+ export type AgentYmlInput = Omit<z.input<typeof AgentConfigSchema>, 'official_images_only'>;
17
+ /**
18
+ * Renders the keys the controller owns as `agent.yml`, leaving out a key at its default. It throws
19
+ * on input that `agent.yml` would not accept.
20
+ */
21
+ export declare function renderAgentYml(input: AgentYmlInput): string;
@@ -0,0 +1,71 @@
1
+ /**
2
+ * The `agent.yml` that a controller renders for a node. Its layout is part of the contract,
3
+ * because `node-setup.sh` reads the images back out with `sed`, so those top-level scalars stay
4
+ * unindented and unquoted.
5
+ */
6
+ import { Document } from 'yaml';
7
+ import { AgentConfigSchema } from './agent-config.js';
8
+ import { DEFAULT_EVENTS_PATH, DEFAULT_SOCKET_PATH } from './paths.js';
9
+ /** The given fields under their `agent.yml` keys, in file order. */
10
+ export function agentYmlEntries(fields) {
11
+ const entries = [];
12
+ if (fields.token !== undefined) {
13
+ entries.push(['token', fields.token]);
14
+ }
15
+ if (fields.controller !== undefined) {
16
+ const controller = fields.controller;
17
+ entries.push(['controller', {
18
+ host: controller.host,
19
+ tls: controller.tls,
20
+ ...(controller.socket_path === DEFAULT_SOCKET_PATH ? {} : { socket_path: controller.socket_path }),
21
+ ...(controller.events_path === DEFAULT_EVENTS_PATH ? {} : { events_path: controller.events_path }),
22
+ }]);
23
+ }
24
+ if (fields.cs2_image !== undefined) {
25
+ entries.push(['cs2_image', fields.cs2_image]);
26
+ }
27
+ if (fields.agent_image !== undefined) {
28
+ entries.push(['agent_image', fields.agent_image]);
29
+ }
30
+ if (fields.pinned_ip !== undefined) {
31
+ entries.push(['pinned_ip', fields.pinned_ip]);
32
+ }
33
+ if (fields.memory_mb !== undefined) {
34
+ entries.push(['memory_mb', fields.memory_mb]);
35
+ }
36
+ // The default stays out of the file.
37
+ if (fields.auto_update !== undefined) {
38
+ entries.push(['auto_update', fields.auto_update ? null : false]);
39
+ }
40
+ if (fields.demo_upload !== undefined) {
41
+ const upload = fields.demo_upload;
42
+ entries.push(['demo_upload', upload === null ? null : {
43
+ url: upload.url,
44
+ ...(upload.header === null ? {} : { header: { name: upload.header.name, value: upload.header.value } }),
45
+ ...(upload.limit_mbit === null ? {} : { limit_mbit: upload.limit_mbit }),
46
+ }]);
47
+ }
48
+ if (fields.slots !== undefined) {
49
+ entries.push(['slots', fields.slots.map((slot) => ({
50
+ id: slot.id, port: slot.port, tv_port: slot.tv_port, gslt: slot.gslt,
51
+ }))]);
52
+ }
53
+ return entries;
54
+ }
55
+ /**
56
+ * Renders the keys the controller owns as `agent.yml`, leaving out a key at its default. It throws
57
+ * on input that `agent.yml` would not accept.
58
+ */
59
+ export function renderAgentYml(input) {
60
+ const config = AgentConfigSchema.parse(input);
61
+ const values = Object.fromEntries(agentYmlEntries(config).filter(([, value]) => value !== null));
62
+ const document = new Document(values);
63
+ document.commentBefore = [
64
+ ' agent.yml for this Ankor node.',
65
+ " Keep it secret: it carries this node's auth token and its slots' GSLTs.",
66
+ '',
67
+ ' The controller can manage some parts of this file remotely.',
68
+ ' See https://docs.ankor.sh for documentation and setup help.'
69
+ ].join('\n');
70
+ return document.toString();
71
+ }
@@ -0,0 +1,160 @@
1
+ {
2
+ "type": "object",
3
+ "properties": {
4
+ "token": {
5
+ "type": "string",
6
+ "pattern": "^[A-Za-z0-9]{32,128}$",
7
+ "description": "The node token from the controller, 32 to 128 letters and digits. [Node tokens](https://docs.ankor.sh/protocol/#node-tokens) explains how one is made. **Keep the file secret.**"
8
+ },
9
+ "controller": {
10
+ "type": "object",
11
+ "properties": {
12
+ "host": {
13
+ "type": "string",
14
+ "description": "The controller's host with an optional port, without scheme or path, such as `cs.example.com`, `192.168.0.10:3000` or `[fd00::1]`. The agent may dial over IPv6, but players need IPv4.\n\nRejected when it contains whitespace, a control character, `/`, `\\`, `?` or `#`. It is also rejected unless `http://` followed by the value parses as a URL that has a host, an optional port and nothing else, such as a user name."
15
+ },
16
+ "tls": {
17
+ "type": "boolean",
18
+ "description": "Whether the agent dials `wss://` and `https://` rather than `ws://` and `http://`."
19
+ },
20
+ "socket_path": {
21
+ "default": "/ws/agent",
22
+ "type": "string",
23
+ "maxLength": 255,
24
+ "pattern": "^\\/(?:[A-Za-z0-9\\-._~!$&'()*+,;=:@/]|%[0-9A-Fa-f]{2})*$",
25
+ "description": "The agent socket's path on `host`, a `/` followed by URL path characters."
26
+ },
27
+ "events_path": {
28
+ "default": "/api/node/events",
29
+ "type": "string",
30
+ "maxLength": 255,
31
+ "pattern": "^\\/(?:[A-Za-z0-9\\-._~!$&'()*+,;=:@/]|%[0-9A-Fa-f]{2})*$",
32
+ "description": "The path on `host` that event batches are posted to, a `/` followed by URL path characters."
33
+ }
34
+ },
35
+ "required": [
36
+ "host",
37
+ "tls"
38
+ ],
39
+ "description": "Where the agent dials the controller."
40
+ },
41
+ "pinned_ip": {
42
+ "type": "string",
43
+ "description": "The IPv4 address players connect to, in place of the one the agent looks up. Rejected unless it is four numbers from 0 to 255 joined by dots, each without leading zeros."
44
+ },
45
+ "cs2_image": {
46
+ "type": "string",
47
+ "pattern": "^[A-Za-z0-9][A-Za-z0-9._\\-/:@]{0,254}$",
48
+ "description": "The `ankor-cs2` image future game servers run, as a Docker image reference."
49
+ },
50
+ "agent_image": {
51
+ "type": "string",
52
+ "pattern": "^[A-Za-z0-9][A-Za-z0-9._\\-/:@]{0,254}$",
53
+ "description": "The agent's image, which `node-setup.sh --update` runs and the agent replaces itself from, as a Docker image reference."
54
+ },
55
+ "memory_mb": {
56
+ "type": "integer",
57
+ "minimum": 1024,
58
+ "maximum": 65536,
59
+ "description": "MiB of memory each game container may use. Without it, game containers are not capped. CS2 installs and updates are exempt.\n\nRejected when it is not a whole number from 1024 to 65536."
60
+ },
61
+ "auto_update": {
62
+ "default": true,
63
+ "type": "boolean",
64
+ "description": "Whether the agent keeps the node up to date on its own. It updates CS2 to the newest public build and refuses launches while it is behind that build. It also moves `cs2_image` and `agent_image` to the newest release or rebuild in their major. It reads their tags from the registry every 60 minutes, and leaves an image named by digest or by a custom tag alone. When false, only [`update_cs2`](asyncapi.yaml#/components/schemas/ControllerUpdateCs2), [`update_cs2_image`](asyncapi.yaml#/components/schemas/ControllerUpdateCs2Image) and [`update_agent`](asyncapi.yaml#/components/schemas/ControllerUpdateAgent) change the build and the images."
65
+ },
66
+ "official_images_only": {
67
+ "default": true,
68
+ "type": "boolean",
69
+ "description": "Whether the node runs only the official images: `agent_image` from `ghcr.io/ankorcs/ankor-agent` and `cs2_image` from `ghcr.io/ankorcs/ankor-cs2`, with any tag or digest. While it is on, the agent refuses an [`update_agent`](asyncapi.yaml#/components/schemas/ControllerUpdateAgent) or [`update_cs2_image`](asyncapi.yaml#/components/schemas/ControllerUpdateCs2Image) to any other image with `image_not_allowed`, and `node-setup.sh` refuses to run one.\n\nOnly the node's operator can turn it off, by setting it to false in the file, to run images of their own. [`set_config`](asyncapi.yaml#/components/schemas/ControllerSetConfig) cannot change it. If the `agent.yml` that `node-setup.sh` fetches from the controller turns it off, the script stops."
70
+ },
71
+ "slots": {
72
+ "maxItems": 64,
73
+ "type": "array",
74
+ "items": {
75
+ "type": "object",
76
+ "properties": {
77
+ "id": {
78
+ "type": "string",
79
+ "pattern": "^[A-Za-z0-9_-]{1,32}$",
80
+ "description": "The slot's id, 1 to 32 letters, digits, `_` and `-`. It is unique on its node."
81
+ },
82
+ "port": {
83
+ "type": "integer",
84
+ "minimum": 1,
85
+ "maximum": 65535,
86
+ "description": "The game port, UDP. Every `port` and `tv_port` on a node is different, free on the host, and not 4500, the port the agent listens on for plugins. [Host networking](https://docs.ankor.sh/how-it-works/#host-networking) explains why."
87
+ },
88
+ "tv_port": {
89
+ "type": "integer",
90
+ "minimum": 1,
91
+ "maximum": 65535,
92
+ "description": "The GOTV port, UDP, under the same rules as `port`."
93
+ },
94
+ "gslt": {
95
+ "type": "string",
96
+ "pattern": "^[A-Za-z0-9]{10,64}$",
97
+ "description": "The slot's Game Server Login Token, which every server in the slot logs in with. It is 10 to 64 letters and digits. [Game Server Login Tokens](https://docs.ankor.sh/requirements/#game-server-login-tokens) explains them."
98
+ }
99
+ },
100
+ "required": [
101
+ "id",
102
+ "port",
103
+ "tv_port",
104
+ "gslt"
105
+ ]
106
+ },
107
+ "description": "The node's game server slots. Empty for a node that runs no servers yet. Such a node connects and reports, but accepts no launch."
108
+ },
109
+ "demo_upload": {
110
+ "type": "object",
111
+ "properties": {
112
+ "url": {
113
+ "type": "string",
114
+ "maxLength": 255,
115
+ "format": "uri",
116
+ "description": "The `http` or `https` URL finished demos are posted to. Rejected when it carries a user name or a password, which belong in `header`."
117
+ },
118
+ "header": {
119
+ "type": "object",
120
+ "properties": {
121
+ "name": {
122
+ "type": "string",
123
+ "pattern": "^[A-Za-z0-9-]{1,64}$",
124
+ "description": "The header's name, up to 64 letters, digits and `-`. Rejected when it is, in any case, one of `host`, `content-type`, `content-length`, `transfer-encoding`, `connection`, `keep-alive`, `upgrade`, `expect`, `te`, `trailer`, `ankor-protocol`, `ankor-file-name`, `ankor-match-id` and `ankor-allocation-id`, which the agent sets itself or which change how the request is framed."
125
+ },
126
+ "value": {
127
+ "type": "string",
128
+ "maxLength": 1024,
129
+ "pattern": "^[^\\r\\n\\u0000]*$",
130
+ "description": "The header's value, sent verbatim, without line breaks."
131
+ }
132
+ },
133
+ "required": [
134
+ "name",
135
+ "value"
136
+ ],
137
+ "description": "One header sent with every upload."
138
+ },
139
+ "limit_mbit": {
140
+ "type": "integer",
141
+ "minimum": 1,
142
+ "maximum": 10000,
143
+ "description": "Megabits a second an upload may send while a game server runs on the node. Without it, uploads run at full speed."
144
+ }
145
+ },
146
+ "required": [
147
+ "url"
148
+ ],
149
+ "description": "Where finished demos are uploaded. Without it, demos stay on the node. [Demo uploads](https://docs.ankor.sh/protocol/demos/) describes the request."
150
+ }
151
+ },
152
+ "required": [
153
+ "token",
154
+ "controller",
155
+ "cs2_image",
156
+ "agent_image",
157
+ "slots"
158
+ ],
159
+ "description": "The node's `agent.yml` as the agent reads it. It is [`RawAgentConfigSchema`](https://docs.ankor.sh/configuration/#agentyml) with the checks that span slots, and it fills in `pinned_ip` and `demo_upload` as null when absent.\n\nRejected when two `slots` share an `id` or a `gslt`, or when two ports are equal. Every slot's `port` and `tv_port` count, so a slot's two ports differ too. It is also rejected when a port is 4500, where the agent listens for plugins."
160
+ }