@ninedeploy/schemas 0.16.0 → 0.16.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.
- package/dist/multiNode.d.ts +2 -1
- package/dist/multiNode.d.ts.map +1 -1
- package/dist/multiNode.js +2 -0
- package/dist/multiNode.js.map +1 -1
- package/dist/service.d.ts +211 -0
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +260 -0
- package/dist/service.js.map +1 -1
- package/package.json +1 -1
- package/src/multiNode.ts +2 -0
- package/src/service.ts +266 -0
package/src/service.ts
CHANGED
|
@@ -4,6 +4,212 @@ import { containerPath, dockerVolumeName, envVarName, gitBranch, gitRepoUrl, htt
|
|
|
4
4
|
export const serviceType = z.enum(['pm2', 'docker', 'compose']);
|
|
5
5
|
export const buildPack = z.enum(['auto', 'nixpacks', 'dockerfile', 'railpack', 'static']);
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* 0.16.1: names of the service's environment variables a Dockerfile build
|
|
9
|
+
* receives as `--build-arg` (the Dockerfile reads them with `ARG`). Opt-in per
|
|
10
|
+
* name: a build gets none unless listed. Nixpacks and Railpack builds already
|
|
11
|
+
* see the whole environment.
|
|
12
|
+
*/
|
|
13
|
+
export const buildArgNames = z.array(envVarName.max(100)).max(100);
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 0.16.1: capabilities a docker service may add. Deliberately short: the ones
|
|
17
|
+
* real workloads ask for (VPNs, packet capture, profilers, realtime audio,
|
|
18
|
+
* mlock). SYS_ADMIN, SYS_MODULE, DAC_READ_SEARCH and the like are left out —
|
|
19
|
+
* each is a known path out of the container.
|
|
20
|
+
*/
|
|
21
|
+
export const runtimeCapability = z.enum(['NET_ADMIN', 'NET_RAW', 'SYS_PTRACE', 'SYS_NICE', 'SYS_RESOURCE', 'IPC_LOCK', 'SYS_TIME']);
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 0.16.1 advanced container options for docker services (operator only):
|
|
25
|
+
* structured fields, never free-form `docker run` flags, so nothing outside
|
|
26
|
+
* this list (privileged, host mounts, host network…) can be asked for.
|
|
27
|
+
*/
|
|
28
|
+
export const runtimeOptions = z
|
|
29
|
+
.object({
|
|
30
|
+
/** `--gpus all` or `--gpus <n>` (needs the NVIDIA container toolkit on the host). */
|
|
31
|
+
gpus: z.union([z.literal('all'), z.number().int().min(1).max(16)]).nullable().optional(),
|
|
32
|
+
/** `--shm-size` in MiB; browsers and PyTorch data loaders outgrow Docker's 64. */
|
|
33
|
+
shmSizeMb: z.number().int().min(1).max(65_536).nullable().optional(),
|
|
34
|
+
/** `--init`: reaps zombie processes and forwards signals to the app. */
|
|
35
|
+
init: z.boolean().optional(),
|
|
36
|
+
/** `--ulimit nofile=N:N`. */
|
|
37
|
+
ulimitNofile: z.number().int().min(1024).max(1_048_576).nullable().optional(),
|
|
38
|
+
/** `--cap-add`. */
|
|
39
|
+
capAdd: z.array(runtimeCapability).max(7).optional(),
|
|
40
|
+
})
|
|
41
|
+
.strict();
|
|
42
|
+
export type RuntimeOptions = z.infer<typeof runtimeOptions>;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 0.16.1: the most host ports one service may publish through `portMappings`.
|
|
46
|
+
* Docker starts one userland proxy process per published port and protocol,
|
|
47
|
+
* so a 10 000-port range costs 10 000 processes on the host.
|
|
48
|
+
*/
|
|
49
|
+
export const MAX_MAPPED_PORTS = 1000;
|
|
50
|
+
/** Most port-mapping rules per service. */
|
|
51
|
+
export const MAX_PORT_MAPPINGS = 20;
|
|
52
|
+
|
|
53
|
+
const IPV4 = /^(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}$/;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* A literal IPv4 or IPv6 address a port can be bound to (no zone, no CIDR).
|
|
57
|
+
* The unspecified addresses are refused: an empty IP already means "every
|
|
58
|
+
* address", and two spellings of it would dodge the overlap check.
|
|
59
|
+
*/
|
|
60
|
+
export function isBindableIp(ip: string): boolean {
|
|
61
|
+
if (IPV4.test(ip)) return ip !== '0.0.0.0';
|
|
62
|
+
if (ip.length > 45) return false;
|
|
63
|
+
const halves = ip.split('::');
|
|
64
|
+
if (halves.length > 2) return false;
|
|
65
|
+
const groups = halves.flatMap((h) => (h === '' ? [] : h.split(':')));
|
|
66
|
+
let width = groups.length;
|
|
67
|
+
if (groups.length > 0 && IPV4.test(groups[groups.length - 1]!)) {
|
|
68
|
+
groups.pop();
|
|
69
|
+
width += 1; // an embedded IPv4 fills two groups
|
|
70
|
+
}
|
|
71
|
+
if (!groups.every((g) => /^[0-9A-Fa-f]{1,4}$/.test(g))) return false;
|
|
72
|
+
if (halves.length === 2 ? width > 7 : width !== 8) return false;
|
|
73
|
+
// `::`, `0::0`, `0:0:0:0:0:0:0:0`: the unspecified address.
|
|
74
|
+
return width !== groups.length || groups.some((g) => Number.parseInt(g, 16) !== 0);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 0.16.1 extra host-port mappings of a docker service: one port or a range,
|
|
79
|
+
* TCP or UDP, on every host address or one dedicated IP
|
|
80
|
+
* (`docker run -p 203.0.113.7:27015-27030:27015-27030/udp`). The container
|
|
81
|
+
* range starts at `containerPort` and is as long as the host range.
|
|
82
|
+
*/
|
|
83
|
+
export const portMapping = z
|
|
84
|
+
.object({
|
|
85
|
+
/** Host address to bind; null or omitted = every address of the host. */
|
|
86
|
+
hostIp: z
|
|
87
|
+
.string()
|
|
88
|
+
.max(45)
|
|
89
|
+
.refine(isBindableIp, 'hostIp must be one IPv4 or IPv6 address of the host (leave it empty for every address)')
|
|
90
|
+
.nullable()
|
|
91
|
+
.optional(),
|
|
92
|
+
hostPort: z.number().int().min(1).max(65535),
|
|
93
|
+
/** Last host port of a range; null or omitted = the single `hostPort`. */
|
|
94
|
+
hostPortEnd: z.number().int().min(1).max(65535).nullable().optional(),
|
|
95
|
+
containerPort: z.number().int().min(1).max(65535),
|
|
96
|
+
protocol: z.enum(['tcp', 'udp']).default('tcp'),
|
|
97
|
+
/**
|
|
98
|
+
* TCP, one port: only these addresses or CIDR ranges may connect. The port
|
|
99
|
+
* is then published by a small Traefik proxy in front of the service
|
|
100
|
+
* instead of Docker's `-p`, which a host firewall cannot restrict.
|
|
101
|
+
*/
|
|
102
|
+
allowFrom: z.array(z.string().trim().min(1).max(64)).min(1).max(100).nullable().optional(),
|
|
103
|
+
})
|
|
104
|
+
.strict();
|
|
105
|
+
export type PortMapping = z.infer<typeof portMapping>;
|
|
106
|
+
type PortMappingLike = Pick<PortMapping, 'hostPort' | 'hostPortEnd' | 'containerPort'> & {
|
|
107
|
+
hostIp?: string | null;
|
|
108
|
+
protocol?: 'tcp' | 'udp';
|
|
109
|
+
allowFrom?: readonly string[] | null;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/** How many host ports a mapping publishes. */
|
|
113
|
+
export const mappedPortCount = (m: Pick<PortMapping, 'hostPort' | 'hostPortEnd'>): number =>
|
|
114
|
+
Math.max(1, (m.hostPortEnd ?? m.hostPort) - m.hostPort + 1);
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Whether two mappings would bind the same host socket: same protocol,
|
|
118
|
+
* overlapping ports, and the same IP (or either on every address).
|
|
119
|
+
*/
|
|
120
|
+
export function portMappingsOverlap(a: PortMappingLike, b: PortMappingLike): boolean {
|
|
121
|
+
if ((a.protocol ?? 'tcp') !== (b.protocol ?? 'tcp')) return false;
|
|
122
|
+
if (a.hostIp && b.hostIp && a.hostIp.toLowerCase() !== b.hostIp.toLowerCase()) return false;
|
|
123
|
+
return a.hostPort <= (b.hostPortEnd ?? b.hostPort) && b.hostPort <= (a.hostPortEnd ?? a.hostPort);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Why a list of mappings cannot be published, or null. Shared by the server, the panel and the CLI. */
|
|
127
|
+
export function portMappingsProblem(list: readonly PortMappingLike[]): string | null {
|
|
128
|
+
let total = 0;
|
|
129
|
+
for (const [i, m] of list.entries()) {
|
|
130
|
+
const end = m.hostPortEnd ?? m.hostPort;
|
|
131
|
+
const label = formatPortMapping(m);
|
|
132
|
+
if (end < m.hostPort) return `Mapping ${i + 1} (${label}): the range ends before it starts.`;
|
|
133
|
+
if (m.containerPort + (end - m.hostPort) > 65535) return `Mapping ${i + 1} (${label}): the container range runs past 65535.`;
|
|
134
|
+
total += end - m.hostPort + 1;
|
|
135
|
+
if (m.allowFrom?.length && ((m.protocol ?? 'tcp') !== 'tcp' || end !== m.hostPort)) {
|
|
136
|
+
return `Mapping ${i + 1} (${label}): an allow list works on one TCP port (UDP and ranges cannot be filtered by source).`;
|
|
137
|
+
}
|
|
138
|
+
const other = list.slice(0, i).find((o) => portMappingsOverlap(o, m));
|
|
139
|
+
if (other) {
|
|
140
|
+
return `Mapping ${i + 1} (${label}) overlaps ${formatPortMapping(other)}: a host port can be published once per address and protocol.`;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (total > MAX_MAPPED_PORTS) {
|
|
144
|
+
return `${total} host ports requested; at most ${MAX_MAPPED_PORTS} per service (Docker runs one proxy process per published port).`;
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** The `docker run -p` operand: `27015-27030:27015-27030/udp`, `203.0.113.7:53:53/udp`, `[2001:db8::7]:53:53/udp`. */
|
|
150
|
+
export function formatPortMapping(m: PortMappingLike): string {
|
|
151
|
+
const span = (m.hostPortEnd ?? m.hostPort) - m.hostPort;
|
|
152
|
+
const range = (start: number) => (span > 0 ? `${start}-${start + span}` : String(start));
|
|
153
|
+
const ip = m.hostIp ? (m.hostIp.includes(':') ? `[${m.hostIp}]:` : `${m.hostIp}:`) : '';
|
|
154
|
+
return `${ip}${range(m.hostPort)}:${range(m.containerPort)}/${m.protocol ?? 'tcp'}`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Parse `27015-27030:27015/udp`, `8080:80`, `5432@10.0.0.0/8`, `53/udp`, `10000-10100/udp`
|
|
159
|
+
* (no colon = the same ports inside the container), optionally after an IP:
|
|
160
|
+
* `203.0.113.7:53/udp`, `[2001:db8::7]:8080:80`. A container range must be as
|
|
161
|
+
* long as the host range. Returns the mapping, or why the text is not one.
|
|
162
|
+
*/
|
|
163
|
+
export function parsePortMapping(text: string): PortMapping | string {
|
|
164
|
+
// `…@10.0.0.0/8,203.0.113.4`: the allow list of a TCP port.
|
|
165
|
+
const parts = text.trim().split('@');
|
|
166
|
+
const t = parts[0]!;
|
|
167
|
+
const allowText = parts[1];
|
|
168
|
+
if (parts.length > 2) return `"${text.trim()}" has more than one @.`;
|
|
169
|
+
const allowFrom = allowText === undefined ? null : allowText.split(',').map((e) => e.trim()).filter(Boolean);
|
|
170
|
+
if (allowFrom && allowFrom.length === 0) return `"${text.trim()}": list the addresses after @ (10.0.0.0/8,203.0.113.4).`;
|
|
171
|
+
const m = /^(?:(\d{1,3}(?:\.\d{1,3}){3})|\[([0-9A-Fa-f:.]+)\]):(\d{1,5})(?:-(\d{1,5}))?(?::(\d{1,5})(?:-(\d{1,5}))?)?(?:\/(tcp|udp))?$/i.exec(t)
|
|
172
|
+
?? /^()()(\d{1,5})(?:-(\d{1,5}))?(?::(\d{1,5})(?:-(\d{1,5}))?)?(?:\/(tcp|udp))?$/i.exec(t);
|
|
173
|
+
if (!m) return `"${t}" is not a port mapping (examples: 8080:80, 27015-27030:27015/udp, 203.0.113.7:53/udp).`;
|
|
174
|
+
const hostIp = m[1] || m[2] || null;
|
|
175
|
+
if (hostIp && !isBindableIp(hostIp)) return `"${t}": ${hostIp} is not an address a port can be bound to.`;
|
|
176
|
+
const hostPort = Number(m[3]);
|
|
177
|
+
const hostPortEnd = m[4] ? Number(m[4]) : null;
|
|
178
|
+
const containerPort = m[5] ? Number(m[5]) : hostPort;
|
|
179
|
+
const ports = [hostPort, hostPortEnd ?? hostPort, containerPort];
|
|
180
|
+
if (ports.some((p) => p < 1 || p > 65535)) return `"${t}": ports run from 1 to 65535.`;
|
|
181
|
+
if (hostPortEnd !== null && hostPortEnd < hostPort) return `"${t}": the range ends before it starts.`;
|
|
182
|
+
if (m[6] && Number(m[6]) - containerPort !== (hostPortEnd ?? hostPort) - hostPort) {
|
|
183
|
+
return `"${t}": the container range must be as long as the host range.`;
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
hostIp,
|
|
187
|
+
hostPort,
|
|
188
|
+
hostPortEnd: hostPortEnd !== null && hostPortEnd !== hostPort ? hostPortEnd : null,
|
|
189
|
+
containerPort,
|
|
190
|
+
protocol: (m[7]?.toLowerCase() ?? 'tcp') as 'tcp' | 'udp',
|
|
191
|
+
...(allowFrom ? { allowFrom } : {}),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** The PATCH field: rules checked together (ranges, overlaps, the port budget). */
|
|
196
|
+
export const portMappings = z
|
|
197
|
+
.array(portMapping)
|
|
198
|
+
.max(MAX_PORT_MAPPINGS)
|
|
199
|
+
.superRefine((list, ctx) => {
|
|
200
|
+
const problem = portMappingsProblem(list);
|
|
201
|
+
if (problem) ctx.addIssue({ code: 'custom', message: problem });
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* 0.16.1 deploy healthcheck: `http` probes the container port (an HTTP answer
|
|
206
|
+
* below 500, then a TCP connect); `running` only waits for the container to
|
|
207
|
+
* stay up, for services with no HTTP or TCP port to probe (UDP game servers,
|
|
208
|
+
* VoIP, queue workers).
|
|
209
|
+
*/
|
|
210
|
+
export const healthCheckMode = z.enum(['http', 'running']);
|
|
211
|
+
export type HealthCheckMode = z.infer<typeof healthCheckMode>;
|
|
212
|
+
|
|
7
213
|
/** Optional project/workspace/label tag IDs for new service creation. */
|
|
8
214
|
const tagIds = z.array(z.number().int().positive()).optional();
|
|
9
215
|
|
|
@@ -74,6 +280,8 @@ export const createService = z.object({
|
|
|
74
280
|
restartPolicy: z.string().regex(/^(no|always|unless-stopped|on-failure(?::\d{1,3})?)$/).optional(),
|
|
75
281
|
// Seconds between SIGTERM and SIGKILL on stop (docker stop -t).
|
|
76
282
|
stopGraceSeconds: z.number().int().min(0).max(300).optional(),
|
|
283
|
+
/** Env-var names passed to a Dockerfile build as `--build-arg`. */
|
|
284
|
+
buildArgs: buildArgNames.optional(),
|
|
77
285
|
})
|
|
78
286
|
.default({ buildPack: 'auto', baseDir: '/' }),
|
|
79
287
|
}).superRefine((value, ctx) => {
|
|
@@ -134,8 +342,16 @@ export const updateService = z.object({
|
|
|
134
342
|
/** Horizontal replica count for docker services (Traefik load-balances). */
|
|
135
343
|
replicas: z.number().int().min(1).max(10).optional(),
|
|
136
344
|
healthPath: httpPath.optional(),
|
|
345
|
+
/** Seconds a deploy waits for the healthcheck (10–3600); null restores the 300 s default. */
|
|
346
|
+
healthTimeoutSeconds: z.number().int().min(10).max(3600).nullable().optional(),
|
|
347
|
+
/** 'running' = the deploy only waits for the container to stay up (UDP and other non-HTTP services); 'http' or null = the HTTP/TCP probe. */
|
|
348
|
+
healthCheck: healthCheckMode.nullable().optional(),
|
|
349
|
+
/** Operator only: advanced container options; null clears them all. */
|
|
350
|
+
runtimeOptions: runtimeOptions.nullable().optional(),
|
|
137
351
|
port: z.number().int().min(1).max(65535).optional(),
|
|
138
352
|
publishedPort: z.number().int().min(1).max(65535).nullable().optional(),
|
|
353
|
+
/** Extra host ports, TCP or UDP, single or ranges (docker services on the panel host); null or [] clears them. */
|
|
354
|
+
portMappings: portMappings.nullable().optional(),
|
|
139
355
|
previewDeploymentsEnabled: z.boolean().optional(),
|
|
140
356
|
previewAutoDestroyOnClose: z.boolean().optional(),
|
|
141
357
|
previewDomainPattern: z.string().nullable().optional(),
|
|
@@ -159,6 +375,8 @@ export const updateService = z.object({
|
|
|
159
375
|
preStopCmd: z.string().nullable().optional(),
|
|
160
376
|
restartPolicy: z.string().regex(/^(no|always|unless-stopped|on-failure(?::\d{1,3})?)$/).optional(),
|
|
161
377
|
stopGraceSeconds: z.number().int().min(0).max(300).optional(),
|
|
378
|
+
/** Env-var names passed to a Dockerfile build as `--build-arg`; [] clears. */
|
|
379
|
+
buildArgs: buildArgNames.optional(),
|
|
162
380
|
})
|
|
163
381
|
.optional(),
|
|
164
382
|
});
|
|
@@ -209,9 +427,17 @@ export const service = z.object({
|
|
|
209
427
|
commitSha: z.string().nullable(),
|
|
210
428
|
runtimeId: z.string().nullable(),
|
|
211
429
|
healthPath: z.string(),
|
|
430
|
+
/** Healthcheck deadline in seconds; null = the 300 s default (0.16.1). */
|
|
431
|
+
healthTimeoutSeconds: z.number().int().nullable().optional(),
|
|
432
|
+
/** How a deploy decides the new container is ready (0.16.1). */
|
|
433
|
+
healthCheck: healthCheckMode.optional(),
|
|
434
|
+
/** Advanced container options (0.16.1); null = none. */
|
|
435
|
+
runtimeOptions: runtimeOptions.nullable().optional(),
|
|
212
436
|
autoUrl: z.string().nullable(),
|
|
213
437
|
port: z.number().int().nullable(),
|
|
214
438
|
publishedPort: z.number().int().nullable().optional(),
|
|
439
|
+
/** Extra host-port mappings, TCP or UDP (0.16.1); [] = none. */
|
|
440
|
+
portMappings: z.array(portMapping).optional(),
|
|
215
441
|
cpuShares: z.number().int(),
|
|
216
442
|
cpuLimitMilli: z.number().int(),
|
|
217
443
|
memLimitMb: z.number().int(),
|
|
@@ -242,6 +468,8 @@ export const service = z.object({
|
|
|
242
468
|
preStopCmd: z.string().nullable().optional(),
|
|
243
469
|
restartPolicy: z.string(),
|
|
244
470
|
stopGraceSeconds: z.number().int(),
|
|
471
|
+
/** Env-var names a Dockerfile build receives as `--build-arg` (0.16.1). */
|
|
472
|
+
buildArgs: z.array(z.string()).optional(),
|
|
245
473
|
})
|
|
246
474
|
.nullable(),
|
|
247
475
|
createdAt: z.string().datetime(),
|
|
@@ -381,6 +609,40 @@ export const createDomain = z.object({
|
|
|
381
609
|
});
|
|
382
610
|
export type CreateDomainInput = z.input<typeof createDomain>;
|
|
383
611
|
|
|
612
|
+
/**
|
|
613
|
+
* Why a redirect regex cannot go to Traefik, or null. Traefik compiles it
|
|
614
|
+
* with Go's RE2, which has no lookaround and no backreferences — and a regex
|
|
615
|
+
* Traefik refuses takes the whole dynamic config file down with it, so these
|
|
616
|
+
* are refused when saved. The rest is checked by compiling it here.
|
|
617
|
+
*/
|
|
618
|
+
export function redirectRegexProblem(regex: string): string | null {
|
|
619
|
+
if (/\(\?<?[=!]/.test(regex)) return 'lookahead / lookbehind are not supported (Traefik uses Go RE2)';
|
|
620
|
+
if (/\\[1-9]/.test(regex)) return 'backreferences such as \\1 are not supported (Traefik uses Go RE2)';
|
|
621
|
+
try {
|
|
622
|
+
new RegExp(regex);
|
|
623
|
+
} catch (err) {
|
|
624
|
+
// RegExp only ever throws a SyntaxError here.
|
|
625
|
+
return `not a valid regular expression (${(err as Error).message})`;
|
|
626
|
+
}
|
|
627
|
+
return null;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/** 0.16.1 one redirect rule of a domain (Dokploy's "Redirects"): Traefik `redirectRegex`. */
|
|
631
|
+
export const redirectRule = z
|
|
632
|
+
.object({
|
|
633
|
+
/** Matched against the full request URL, e.g. `^https://example.com/old/(.*)`. */
|
|
634
|
+
regex: z.string().min(1).max(1000),
|
|
635
|
+
/** The target; `${1}` inserts the first capture group. */
|
|
636
|
+
replacement: z.string().min(1).max(2000).regex(/^[^\r\n]*$/, 'one line'),
|
|
637
|
+
/** 301/308 when true, 302/307 when false. */
|
|
638
|
+
permanent: z.boolean().default(false),
|
|
639
|
+
})
|
|
640
|
+
.superRefine((r, ctx) => {
|
|
641
|
+
const problem = redirectRegexProblem(r.regex);
|
|
642
|
+
if (problem) ctx.addIssue({ code: 'custom', path: ['regex'], message: problem });
|
|
643
|
+
});
|
|
644
|
+
export type RedirectRule = z.infer<typeof redirectRule>;
|
|
645
|
+
|
|
384
646
|
/** PATCHable routing extras for an existing domain. */
|
|
385
647
|
export const domainPatch = z.object({
|
|
386
648
|
ssl: z.boolean().optional(),
|
|
@@ -390,6 +652,8 @@ export const domainPatch = z.object({
|
|
|
390
652
|
ipAllowlist: z.string().max(4000).optional().nullable(),
|
|
391
653
|
rateLimitAverage: z.number().int().min(0).max(100000).optional().nullable(),
|
|
392
654
|
rateLimitBurst: z.number().int().min(0).max(100000).optional().nullable(),
|
|
655
|
+
/** 0.16.1: redirect rules, applied in order; null or [] removes them. */
|
|
656
|
+
redirects: z.array(redirectRule).max(10).optional().nullable(),
|
|
393
657
|
});
|
|
394
658
|
export type DomainPatch = z.input<typeof domainPatch>;
|
|
395
659
|
|
|
@@ -405,6 +669,8 @@ export const domain = z.object({
|
|
|
405
669
|
ipAllowlist: z.string().nullable().optional(),
|
|
406
670
|
rateLimitAverage: z.number().nullable().optional(),
|
|
407
671
|
rateLimitBurst: z.number().nullable().optional(),
|
|
672
|
+
/** 0.16.1 redirect rules (absent on older panels). */
|
|
673
|
+
redirects: z.array(z.object({ regex: z.string(), replacement: z.string(), permanent: z.boolean() })).optional(),
|
|
408
674
|
status: z.string(),
|
|
409
675
|
createdAt: z.string().datetime(),
|
|
410
676
|
updatedAt: z.string().datetime(),
|