@ninedeploy/schemas 0.16.7 → 0.16.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/dist/canary.d.ts +66 -0
- package/dist/canary.d.ts.map +1 -0
- package/dist/canary.js +61 -0
- package/dist/canary.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/service.d.ts +14 -0
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +5 -0
- package/dist/service.js.map +1 -1
- package/dist/uptime.d.ts +204 -0
- package/dist/uptime.d.ts.map +1 -0
- package/dist/uptime.js +180 -0
- package/dist/uptime.js.map +1 -0
- package/package.json +1 -1
- package/src/canary.ts +95 -0
- package/src/index.ts +2 -0
- package/src/service.ts +5 -0
- package/src/uptime.ts +273 -0
package/src/uptime.ts
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { id } from './common.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 0.16.8 uptime monitors and public status pages.
|
|
6
|
+
*
|
|
7
|
+
* A monitor probes a target from the panel host on an interval: an HTTP(S)
|
|
8
|
+
* URL (status code, optional keyword in the body) or a TCP `host:port`. Its
|
|
9
|
+
* state goes up / down after `failuresBeforeDown` failed checks in a row, and
|
|
10
|
+
* every transition is an `alert.monitor_down` / `alert.monitor_up` event on
|
|
11
|
+
* the existing notification channels. A status page shows a chosen set of
|
|
12
|
+
* monitors publicly, by name only (never the target).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export const MONITOR_KINDS = ['http', 'tcp'] as const;
|
|
16
|
+
export const monitorKind = z.enum(MONITOR_KINDS);
|
|
17
|
+
export type MonitorKind = z.infer<typeof monitorKind>;
|
|
18
|
+
|
|
19
|
+
export const MONITOR_METHODS = ['GET', 'HEAD'] as const;
|
|
20
|
+
export const monitorMethod = z.enum(MONITOR_METHODS);
|
|
21
|
+
|
|
22
|
+
export const MONITOR_STATUSES = ['pending', 'up', 'down', 'paused'] as const;
|
|
23
|
+
export type MonitorStatus = (typeof MONITOR_STATUSES)[number];
|
|
24
|
+
|
|
25
|
+
export const MONITOR_INTERVAL_MIN = 30;
|
|
26
|
+
export const MONITOR_INTERVAL_MAX = 3600;
|
|
27
|
+
export const MONITOR_TIMEOUT_MAX = 60;
|
|
28
|
+
export const MONITOR_FAILURES_MAX = 10;
|
|
29
|
+
export const MONITOR_KEYWORD_MAX = 200;
|
|
30
|
+
/** Status pages show at most this many monitors. */
|
|
31
|
+
export const STATUS_PAGE_MONITORS_MAX = 50;
|
|
32
|
+
/** Days of history a status page shows. */
|
|
33
|
+
export const STATUS_PAGE_DAYS = 90;
|
|
34
|
+
export const DEFAULT_EXPECTED_STATUS = '200-399';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The status codes an HTTP check accepts: comma-separated codes and ranges,
|
|
38
|
+
* each between 100 and 599 (`200-299`, `200,204,301-302`). Null when the
|
|
39
|
+
* text is not such a list.
|
|
40
|
+
*/
|
|
41
|
+
export function parseExpectedStatus(spec: string): Array<[number, number]> | null {
|
|
42
|
+
const parts = spec.split(',').map((p) => p.trim());
|
|
43
|
+
if (parts.length === 0 || parts.length > 20) return null;
|
|
44
|
+
const ranges: Array<[number, number]> = [];
|
|
45
|
+
for (const part of parts) {
|
|
46
|
+
const m = /^(\d{3})(?:-(\d{3}))?$/.exec(part);
|
|
47
|
+
if (!m) return null;
|
|
48
|
+
const lo = Number(m[1]);
|
|
49
|
+
const hi = m[2] === undefined ? lo : Number(m[2]);
|
|
50
|
+
if (lo < 100 || hi > 599 || lo > hi) return null;
|
|
51
|
+
ranges.push([lo, hi]);
|
|
52
|
+
}
|
|
53
|
+
return ranges;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Whether `code` is one of the codes `spec` accepts (an invalid spec accepts none). */
|
|
57
|
+
export function statusMatches(code: number, spec: string): boolean {
|
|
58
|
+
const ranges = parseExpectedStatus(spec);
|
|
59
|
+
return ranges !== null && ranges.some(([lo, hi]) => code >= lo && code <= hi);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** `host:port` for a TCP check: a hostname, an IPv4 address or a bracketed IPv6 address, and a port 1–65535. */
|
|
63
|
+
export function parseTcpTarget(target: string): { host: string; port: number } | null {
|
|
64
|
+
const m = /^(\[[0-9A-Fa-f:.]+\]|[A-Za-z0-9](?:[A-Za-z0-9.-]{0,251}[A-Za-z0-9])?):(\d{1,5})$/.exec(target);
|
|
65
|
+
if (!m) return null;
|
|
66
|
+
const port = Number(m[2]);
|
|
67
|
+
if (port < 1 || port > 65535) return null;
|
|
68
|
+
const host = m[1]!.startsWith('[') ? m[1]!.slice(1, -1) : m[1]!;
|
|
69
|
+
return { host, port };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Why `target` is not valid for `kind`, or null. */
|
|
73
|
+
export function monitorTargetProblem(kind: MonitorKind, target: string): string | null {
|
|
74
|
+
if (kind === 'tcp') return parseTcpTarget(target) ? null : 'a TCP target is host:port (an IPv6 address in brackets)';
|
|
75
|
+
if (!httpUrl.safeParse(target).success) return 'an HTTP target is an http:// or https:// URL';
|
|
76
|
+
// userinfo is everything between `//` and an `@` before the first `/`, `?` or `#`.
|
|
77
|
+
if (/^https?:\/\/[^/?#]*@/i.test(target)) return 'credentials in the URL are not allowed';
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const httpUrl = z.url({ protocol: /^https?$/ });
|
|
82
|
+
|
|
83
|
+
const monitorFields = {
|
|
84
|
+
name: z.string().trim().min(1).max(100),
|
|
85
|
+
/** The service the monitor belongs to (members who can see it see the monitor), or null for an operator-only monitor. */
|
|
86
|
+
serviceId: id.nullable(),
|
|
87
|
+
kind: monitorKind,
|
|
88
|
+
/** http: the URL; tcp: `host:port`. */
|
|
89
|
+
target: z.string().trim().min(1).max(2048),
|
|
90
|
+
method: monitorMethod,
|
|
91
|
+
/** http: accepted status codes, see {@link parseExpectedStatus}. */
|
|
92
|
+
expectedStatus: z
|
|
93
|
+
.string()
|
|
94
|
+
.trim()
|
|
95
|
+
.max(200)
|
|
96
|
+
.refine((v) => parseExpectedStatus(v) !== null, 'codes and ranges between 100 and 599, such as 200-299,301'),
|
|
97
|
+
/** http: text the body must contain (the first 1 MiB is read). */
|
|
98
|
+
keyword: z.string().max(MONITOR_KEYWORD_MAX).nullable(),
|
|
99
|
+
intervalSeconds: z.number().int().min(MONITOR_INTERVAL_MIN).max(MONITOR_INTERVAL_MAX),
|
|
100
|
+
timeoutSeconds: z.number().int().min(1).max(MONITOR_TIMEOUT_MAX),
|
|
101
|
+
failuresBeforeDown: z.number().int().min(1).max(MONITOR_FAILURES_MAX),
|
|
102
|
+
followRedirects: z.boolean(),
|
|
103
|
+
/** http: false accepts a self-signed or expired certificate. */
|
|
104
|
+
verifyTls: z.boolean(),
|
|
105
|
+
enabled: z.boolean(),
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
type TargetFields = { kind?: MonitorKind; target?: string; keyword?: string | null; method?: string };
|
|
109
|
+
|
|
110
|
+
function checkTarget(v: TargetFields, ctx: z.RefinementCtx): void {
|
|
111
|
+
if (v.kind !== undefined && v.target !== undefined) {
|
|
112
|
+
const problem = monitorTargetProblem(v.kind, v.target);
|
|
113
|
+
if (problem) ctx.addIssue({ code: 'custom', path: ['target'], message: problem });
|
|
114
|
+
}
|
|
115
|
+
if (v.kind === 'tcp' && v.keyword) ctx.addIssue({ code: 'custom', path: ['keyword'], message: 'a keyword applies to HTTP checks' });
|
|
116
|
+
if (v.method === 'HEAD' && v.keyword) ctx.addIssue({ code: 'custom', path: ['keyword'], message: 'a HEAD request has no body to search; use GET' });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** `POST /v1/monitors` (operator). */
|
|
120
|
+
export const monitorCreate = z
|
|
121
|
+
.object({
|
|
122
|
+
...monitorFields,
|
|
123
|
+
serviceId: monitorFields.serviceId.default(null),
|
|
124
|
+
method: monitorFields.method.default('GET'),
|
|
125
|
+
expectedStatus: monitorFields.expectedStatus.default(DEFAULT_EXPECTED_STATUS),
|
|
126
|
+
keyword: monitorFields.keyword.default(null),
|
|
127
|
+
intervalSeconds: monitorFields.intervalSeconds.default(60),
|
|
128
|
+
timeoutSeconds: monitorFields.timeoutSeconds.default(10),
|
|
129
|
+
failuresBeforeDown: monitorFields.failuresBeforeDown.default(2),
|
|
130
|
+
followRedirects: monitorFields.followRedirects.default(true),
|
|
131
|
+
verifyTls: monitorFields.verifyTls.default(true),
|
|
132
|
+
enabled: monitorFields.enabled.default(true),
|
|
133
|
+
})
|
|
134
|
+
.strict()
|
|
135
|
+
.superRefine(checkTarget);
|
|
136
|
+
export type MonitorCreateInput = z.input<typeof monitorCreate>;
|
|
137
|
+
export type MonitorCreate = z.infer<typeof monitorCreate>;
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* `PATCH /v1/monitors/:id` (operator). Partial; the route checks the merged
|
|
141
|
+
* kind, target, method and keyword with {@link monitorTargetProblem} again.
|
|
142
|
+
*/
|
|
143
|
+
export const monitorPatch = z.object(monitorFields).partial().strict().superRefine(checkTarget);
|
|
144
|
+
export type MonitorPatch = z.infer<typeof monitorPatch>;
|
|
145
|
+
|
|
146
|
+
/** A monitor as the API returns it. */
|
|
147
|
+
export interface MonitorView {
|
|
148
|
+
id: number;
|
|
149
|
+
name: string;
|
|
150
|
+
serviceId: number | null;
|
|
151
|
+
kind: MonitorKind;
|
|
152
|
+
target: string;
|
|
153
|
+
method: 'GET' | 'HEAD';
|
|
154
|
+
expectedStatus: string;
|
|
155
|
+
keyword: string | null;
|
|
156
|
+
intervalSeconds: number;
|
|
157
|
+
timeoutSeconds: number;
|
|
158
|
+
failuresBeforeDown: number;
|
|
159
|
+
followRedirects: boolean;
|
|
160
|
+
verifyTls: boolean;
|
|
161
|
+
enabled: boolean;
|
|
162
|
+
status: MonitorStatus;
|
|
163
|
+
/** When the status last changed. */
|
|
164
|
+
statusSince: string | null;
|
|
165
|
+
consecutiveFailures: number;
|
|
166
|
+
lastCheckedAt: string | null;
|
|
167
|
+
lastResponseMs: number | null;
|
|
168
|
+
lastStatusCode: number | null;
|
|
169
|
+
lastError: string | null;
|
|
170
|
+
/** https: when the certificate the last check saw expires. */
|
|
171
|
+
certExpiresAt: string | null;
|
|
172
|
+
/** Share of successful checks, 0–100 with two decimals; null without checks. */
|
|
173
|
+
uptime: { h24: number | null; d7: number | null; d30: number | null };
|
|
174
|
+
createdAt: string;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** One check result. */
|
|
178
|
+
export interface MonitorCheckView {
|
|
179
|
+
ts: string;
|
|
180
|
+
ok: boolean;
|
|
181
|
+
responseMs: number | null;
|
|
182
|
+
statusCode: number | null;
|
|
183
|
+
error: string | null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** A status page slug: lowercase letters, digits and dashes. */
|
|
187
|
+
export const statusPageSlug = z
|
|
188
|
+
.string()
|
|
189
|
+
.trim()
|
|
190
|
+
.min(1)
|
|
191
|
+
.max(64)
|
|
192
|
+
.regex(/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/, 'lowercase letters, digits and dashes, not starting or ending with a dash');
|
|
193
|
+
|
|
194
|
+
const statusPageFields = {
|
|
195
|
+
slug: statusPageSlug,
|
|
196
|
+
title: z.string().trim().min(1).max(100),
|
|
197
|
+
description: z.string().trim().max(500).nullable(),
|
|
198
|
+
/** A notice shown at the top (maintenance, an incident), or null. */
|
|
199
|
+
notice: z.string().trim().max(1000).nullable(),
|
|
200
|
+
/** Off: the page answers 404 to everyone. */
|
|
201
|
+
published: z.boolean(),
|
|
202
|
+
showResponseTimes: z.boolean(),
|
|
203
|
+
/** In display order, each once. */
|
|
204
|
+
monitorIds: z
|
|
205
|
+
.array(id)
|
|
206
|
+
.max(STATUS_PAGE_MONITORS_MAX)
|
|
207
|
+
.refine((ids) => new Set(ids).size === ids.length, 'each monitor once'),
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
/** `POST /v1/status-pages` (operator). */
|
|
211
|
+
export const statusPageCreate = z
|
|
212
|
+
.object({
|
|
213
|
+
...statusPageFields,
|
|
214
|
+
description: statusPageFields.description.default(null),
|
|
215
|
+
notice: statusPageFields.notice.default(null),
|
|
216
|
+
published: statusPageFields.published.default(false),
|
|
217
|
+
showResponseTimes: statusPageFields.showResponseTimes.default(true),
|
|
218
|
+
monitorIds: statusPageFields.monitorIds.default([]),
|
|
219
|
+
})
|
|
220
|
+
.strict();
|
|
221
|
+
export type StatusPageCreateInput = z.input<typeof statusPageCreate>;
|
|
222
|
+
|
|
223
|
+
/** `PATCH /v1/status-pages/:id` (operator). */
|
|
224
|
+
export const statusPagePatch = z.object(statusPageFields).partial().strict();
|
|
225
|
+
export type StatusPagePatch = z.infer<typeof statusPagePatch>;
|
|
226
|
+
|
|
227
|
+
export interface StatusPageView {
|
|
228
|
+
id: number;
|
|
229
|
+
slug: string;
|
|
230
|
+
title: string;
|
|
231
|
+
description: string | null;
|
|
232
|
+
notice: string | null;
|
|
233
|
+
published: boolean;
|
|
234
|
+
showResponseTimes: boolean;
|
|
235
|
+
monitorIds: number[];
|
|
236
|
+
/** Path of the public page on the panel. */
|
|
237
|
+
path: string;
|
|
238
|
+
createdAt: string;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** One day of a monitor's history on a public status page. */
|
|
242
|
+
export interface PublicStatusDay {
|
|
243
|
+
/** `YYYY-MM-DD`, UTC. */
|
|
244
|
+
day: string;
|
|
245
|
+
/** Null: no checks that day. */
|
|
246
|
+
uptime: number | null;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** `GET /v1/status/:slug`: what a public status page shows. Never a target. */
|
|
250
|
+
export interface PublicStatusPage {
|
|
251
|
+
title: string;
|
|
252
|
+
description: string | null;
|
|
253
|
+
notice: string | null;
|
|
254
|
+
/** `operational` when every monitor is up, `degraded` when some are down, `down` when all are. */
|
|
255
|
+
overall: 'operational' | 'degraded' | 'down' | 'unknown';
|
|
256
|
+
monitors: Array<{
|
|
257
|
+
name: string;
|
|
258
|
+
status: 'up' | 'down' | 'unknown';
|
|
259
|
+
uptime90d: number | null;
|
|
260
|
+
avgResponseMs: number | null;
|
|
261
|
+
days: PublicStatusDay[];
|
|
262
|
+
}>;
|
|
263
|
+
generatedAt: string;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** The overall state of a status page from its monitors' states. */
|
|
267
|
+
export function overallStatus(states: Array<'up' | 'down' | 'unknown'>): PublicStatusPage['overall'] {
|
|
268
|
+
const known = states.filter((s) => s !== 'unknown');
|
|
269
|
+
if (known.length === 0) return 'unknown';
|
|
270
|
+
const down = known.filter((s) => s === 'down').length;
|
|
271
|
+
if (down === 0) return 'operational';
|
|
272
|
+
return down === known.length ? 'down' : 'degraded';
|
|
273
|
+
}
|