@ninedeploy/schemas 0.10.6 → 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/LICENSE +21 -21
- package/dist/accessGrants.d.ts +156 -0
- package/dist/accessGrants.d.ts.map +1 -0
- package/dist/accessGrants.js +100 -0
- package/dist/accessGrants.js.map +1 -0
- package/dist/agentCommand.d.ts.map +1 -1
- package/dist/agentCommand.js +11 -1
- package/dist/agentCommand.js.map +1 -1
- package/dist/auth.d.ts +53 -0
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +26 -0
- package/dist/auth.js.map +1 -1
- package/dist/backupPolicy.d.ts +37 -0
- package/dist/backupPolicy.d.ts.map +1 -0
- package/dist/backupPolicy.js +50 -0
- package/dist/backupPolicy.js.map +1 -0
- package/dist/common.d.ts +21 -5
- package/dist/common.d.ts.map +1 -1
- package/dist/common.js +35 -6
- package/dist/common.js.map +1 -1
- package/dist/databaseImport.d.ts +145 -0
- package/dist/databaseImport.d.ts.map +1 -0
- package/dist/databaseImport.js +115 -0
- package/dist/databaseImport.js.map +1 -0
- package/dist/doctor.d.ts +16 -0
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +25 -0
- package/dist/doctor.js.map +1 -1
- package/dist/githubApp.d.ts +236 -0
- package/dist/githubApp.d.ts.map +1 -0
- package/dist/githubApp.js +223 -0
- package/dist/githubApp.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -1
- package/dist/kernel.d.ts +5 -2
- package/dist/kernel.d.ts.map +1 -1
- package/dist/kernel.js +11 -2
- package/dist/kernel.js.map +1 -1
- package/dist/management.d.ts +46 -2
- package/dist/management.d.ts.map +1 -1
- package/dist/management.js +68 -15
- package/dist/management.js.map +1 -1
- package/dist/multiNode.d.ts +326 -0
- package/dist/multiNode.d.ts.map +1 -0
- package/dist/multiNode.js +296 -0
- package/dist/multiNode.js.map +1 -0
- package/dist/ninedeployManifest.d.ts.map +1 -1
- package/dist/ninedeployManifest.js +6 -3
- package/dist/ninedeployManifest.js.map +1 -1
- package/dist/panelBackup.d.ts +73 -0
- package/dist/panelBackup.d.ts.map +1 -0
- package/dist/panelBackup.js +45 -0
- package/dist/panelBackup.js.map +1 -0
- package/dist/proxyConfig.d.ts +132 -0
- package/dist/proxyConfig.d.ts.map +1 -0
- package/dist/proxyConfig.js +105 -0
- package/dist/proxyConfig.js.map +1 -0
- package/dist/publicAccess.d.ts +64 -0
- package/dist/publicAccess.d.ts.map +1 -0
- package/dist/publicAccess.js +63 -0
- package/dist/publicAccess.js.map +1 -0
- package/dist/secretProviders.d.ts +119 -0
- package/dist/secretProviders.d.ts.map +1 -0
- package/dist/secretProviders.js +130 -0
- package/dist/secretProviders.js.map +1 -0
- package/dist/secretScan.js +1 -1
- package/dist/service.d.ts +235 -17
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +331 -26
- package/dist/service.js.map +1 -1
- package/dist/terminals.d.ts +284 -0
- package/dist/terminals.d.ts.map +1 -0
- package/dist/terminals.js +177 -0
- package/dist/terminals.js.map +1 -0
- package/dist/traffic.d.ts +276 -0
- package/dist/traffic.d.ts.map +1 -0
- package/dist/traffic.js +103 -0
- package/dist/traffic.js.map +1 -0
- package/dist/workspaces.d.ts +19 -1
- package/dist/workspaces.d.ts.map +1 -1
- package/dist/workspaces.js +26 -0
- package/dist/workspaces.js.map +1 -1
- package/package.json +5 -4
- package/src/accessGrants.ts +117 -0
- package/src/agentCommand.ts +67 -0
- package/src/ai.ts +32 -0
- package/src/auth.ts +323 -0
- package/src/backupPolicy.ts +55 -0
- package/src/common.ts +179 -0
- package/src/databaseImport.ts +138 -0
- package/src/doctor.ts +161 -0
- package/src/environments.ts +23 -0
- package/src/githubApp.ts +268 -0
- package/src/index.ts +30 -0
- package/src/insights.ts +78 -0
- package/src/kernel.ts +180 -0
- package/src/labels.ts +86 -0
- package/src/management.ts +620 -0
- package/src/multiNode.ts +356 -0
- package/src/ninedeployManifest.ts +363 -0
- package/src/panelBackup.ts +90 -0
- package/src/project.ts +42 -0
- package/src/proxyConfig.ts +139 -0
- package/src/publicAccess.ts +80 -0
- package/src/runtimeVersions.ts +232 -0
- package/src/secretProviders.ts +159 -0
- package/src/secretScan.ts +169 -0
- package/src/service.ts +1310 -0
- package/src/terminals.ts +212 -0
- package/src/traffic.ts +130 -0
- package/src/workspaces.ts +215 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
// ── Per-database backup policy (0.12) ──────────────────────────────────────
|
|
4
|
+
/** Bounds for both retention counts. At least one dump is always kept, so a
|
|
5
|
+
* policy can never prune a database's last good backup. */
|
|
6
|
+
export const BACKUP_RETAIN_MIN = 1;
|
|
7
|
+
export const BACKUP_RETAIN_MAX = 365;
|
|
8
|
+
/** What a database WITHOUT a policy gets: the built-in schedule. */
|
|
9
|
+
export const DEFAULT_BACKUP_RETAIN_COUNT = 7;
|
|
10
|
+
|
|
11
|
+
const retain = z.number().int().min(BACKUP_RETAIN_MIN).max(BACKUP_RETAIN_MAX);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* PUT /v1/databases/:id/backup-policy — the whole policy (PUT replaces).
|
|
15
|
+
* The cron expression is validated with croner (5-part) at the route, the
|
|
16
|
+
* same rule scheduled jobs use; the schema package has no cron dependency.
|
|
17
|
+
*/
|
|
18
|
+
export const backupPolicyInput = z
|
|
19
|
+
.object({
|
|
20
|
+
enabled: z.boolean().default(true),
|
|
21
|
+
cron: z.string().trim().min(1).max(120),
|
|
22
|
+
retainCount: retain,
|
|
23
|
+
/** Remote copies kept; null/omitted = the remote copies of the newest `retainCount` dumps. */
|
|
24
|
+
retainRemoteCount: retain.nullable().optional().transform((v) => v ?? null),
|
|
25
|
+
/** A `backup_destinations` id; null/omitted = the active destination. */
|
|
26
|
+
destinationId: z.number().int().positive().nullable().optional().transform((v) => v ?? null),
|
|
27
|
+
/** true = keep this database's scheduled dumps on the panel host only. */
|
|
28
|
+
localOnly: z.boolean().optional().transform((v) => v ?? false),
|
|
29
|
+
})
|
|
30
|
+
.refine((p) => !(p.localOnly && p.destinationId != null), {
|
|
31
|
+
message: 'localOnly and destinationId are mutually exclusive',
|
|
32
|
+
path: ['destinationId'],
|
|
33
|
+
})
|
|
34
|
+
.refine((p) => !(p.localOnly && p.retainRemoteCount != null), {
|
|
35
|
+
message: 'retainRemoteCount has no effect on a local-only policy',
|
|
36
|
+
path: ['retainRemoteCount'],
|
|
37
|
+
});
|
|
38
|
+
export type BackupPolicyInput = z.input<typeof backupPolicyInput>;
|
|
39
|
+
|
|
40
|
+
/** GET/PUT response. `configured: false` = no policy row: the built-in
|
|
41
|
+
* schedule (daily, 7 kept, active destination) applies and `cron` is null. */
|
|
42
|
+
export const backupPolicy = z.object({
|
|
43
|
+
databaseId: z.number().int(),
|
|
44
|
+
configured: z.boolean(),
|
|
45
|
+
enabled: z.boolean(),
|
|
46
|
+
cron: z.string().nullable(),
|
|
47
|
+
retainCount: z.number().int(),
|
|
48
|
+
retainRemoteCount: z.number().int().nullable(),
|
|
49
|
+
destinationId: z.number().int().nullable(),
|
|
50
|
+
localOnly: z.boolean(),
|
|
51
|
+
/** Next scheduled run for a configured, enabled policy (ISO); null otherwise. */
|
|
52
|
+
nextRunAt: z.string().nullable(),
|
|
53
|
+
updatedAt: z.string().nullable(),
|
|
54
|
+
});
|
|
55
|
+
export type BackupPolicy = z.infer<typeof backupPolicy>;
|
package/src/common.ts
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
/** URL-safe slug used for projects, services, routes. */
|
|
4
|
+
export const slug = z
|
|
5
|
+
.string()
|
|
6
|
+
.min(2)
|
|
7
|
+
.max(63)
|
|
8
|
+
.regex(/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/, 'lowercase letters, digits and hyphens');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* POSIX environment-variable name, as accepted by `docker run --env-file` and
|
|
12
|
+
* shells: letters, digits and underscores, starting with a letter or
|
|
13
|
+
* underscore. Surrounding whitespace is trimmed so sloppy input is normalized;
|
|
14
|
+
* anything else (e.g. `MY VAR`) is rejected — such a key would otherwise break
|
|
15
|
+
* the deploy at `docker run --env-file` time.
|
|
16
|
+
*/
|
|
17
|
+
export const envVarName = z
|
|
18
|
+
.string()
|
|
19
|
+
.trim()
|
|
20
|
+
.regex(
|
|
21
|
+
/^[A-Za-z_][A-Za-z0-9_]*$/,
|
|
22
|
+
'must be a valid environment variable name (letters, digits, underscore; cannot start with a digit)',
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
export const id = z.number().int().positive();
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* An absolute HTTP path (health checks, probe targets).
|
|
29
|
+
*
|
|
30
|
+
* Health paths are concatenated onto `http://host:port` before the result is
|
|
31
|
+
* parsed as a URL, so an unconstrained string can rewrite the authority: the
|
|
32
|
+
* value `@evil.example.com/` turns `http://127.0.0.1:3000@evil.example.com/`
|
|
33
|
+
* into a request to evil.example.com with `127.0.0.1:3000` as userinfo. The
|
|
34
|
+
* leading slash plus the character allowlist (no `@`, no `\`) removes that.
|
|
35
|
+
*/
|
|
36
|
+
export const httpPath = z
|
|
37
|
+
.string()
|
|
38
|
+
.max(200)
|
|
39
|
+
.regex(
|
|
40
|
+
/^\/[A-Za-z0-9\-._~!$&'()*+,;=:%/?[\]]*$/,
|
|
41
|
+
'must be an absolute path beginning with "/" (no host, no "@")',
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* A git remote URL restricted to transports we actually support.
|
|
46
|
+
*
|
|
47
|
+
* `z.url()` alone accepts any parseable URL, including `file://` (local
|
|
48
|
+
* repository disclosure) and `ext::sh -c …`, git's arbitrary-command
|
|
49
|
+
* transport. Modern git refuses `ext::` on its own and simple-git blocks
|
|
50
|
+
* several related options, but neither is this application's control — so the
|
|
51
|
+
* scheme is pinned here rather than relying on the toolchain to stay strict.
|
|
52
|
+
*/
|
|
53
|
+
export const gitRepoUrl = z
|
|
54
|
+
.url()
|
|
55
|
+
.refine((value) => /^(?:https?|ssh):\/\//i.test(value), {
|
|
56
|
+
message: 'repoUrl must be an http(s) or ssh URL',
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 0.13: the base URL of an operator-supplied git provider API or web UI
|
|
61
|
+
* (a Gitea instance, GitHub Enterprise Server, `https://github.com`).
|
|
62
|
+
*
|
|
63
|
+
* `http:` is accepted HERE on purpose: whether plain http is allowed depends on
|
|
64
|
+
* the server's `NINEDEPLOY_ALLOW_PRIVATE_EGRESS` escape hatch, which this
|
|
65
|
+
* package cannot see. The server refuses `http:` unless that flag is set, and
|
|
66
|
+
* every call still goes through its egress guard. Rejecting http in the schema
|
|
67
|
+
* would make the documented escape hatch (an internal http-only Gitea)
|
|
68
|
+
* unusable. Userinfo, query and fragment are refused — a base URL never needs
|
|
69
|
+
* them, and userinfo would smuggle credentials into a URL that gets logged.
|
|
70
|
+
* Trailing slashes are dropped so `${base}/api/v1/…` joins cleanly.
|
|
71
|
+
*/
|
|
72
|
+
export const providerBaseUrl = z
|
|
73
|
+
.string()
|
|
74
|
+
.trim()
|
|
75
|
+
.max(2048)
|
|
76
|
+
.pipe(z.url({ protocol: /^https?$/, message: 'must be an http(s) URL' }))
|
|
77
|
+
// Everything after the scheme: no `@` (userinfo), `?` or `#`.
|
|
78
|
+
.refine((value) => !/[@?#]/.test(value.slice(value.indexOf(':') + 3)), {
|
|
79
|
+
message: 'must be a base URL without credentials, query or fragment',
|
|
80
|
+
})
|
|
81
|
+
.transform((value) => value.replace(/\/+$/, ''));
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* A git branch/ref name. Reaches `git checkout` / `git pull` as an argv
|
|
85
|
+
* element, so a value starting with `-` would be read as an option rather
|
|
86
|
+
* than a ref; the allowlist also rules out the `..` and whitespace forms git
|
|
87
|
+
* itself rejects in `check-ref-format`.
|
|
88
|
+
*/
|
|
89
|
+
export const gitBranch = z
|
|
90
|
+
.string()
|
|
91
|
+
.min(1)
|
|
92
|
+
.max(200)
|
|
93
|
+
.regex(/^[A-Za-z0-9._][A-Za-z0-9._/-]*$/, 'invalid branch name')
|
|
94
|
+
.refine((v) => !v.includes('..') && !v.endsWith('.lock'), { message: 'invalid branch name' });
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* A path interpreted relative to the checked-out repository.
|
|
98
|
+
*
|
|
99
|
+
* A LEADING SLASH IS ALLOWED and means "from the repo root" — that is the
|
|
100
|
+
* existing convention for `baseDir` (`/`, `/app`), so rejecting it here would
|
|
101
|
+
* break real configurations. What is rejected is anything that could climb out
|
|
102
|
+
* of the checkout once resolved: `..` segments, NUL, and Windows drive
|
|
103
|
+
* letters.
|
|
104
|
+
*
|
|
105
|
+
* This is a usability guard, not the security boundary. The sinks
|
|
106
|
+
* (`docker build -f` / its build context, `docker compose -f`) resolve these
|
|
107
|
+
* values against the work dir, so they re-anchor the path themselves — see
|
|
108
|
+
* `lib/repoPath.ts`.
|
|
109
|
+
*/
|
|
110
|
+
const insideRepo = (value: string): boolean =>
|
|
111
|
+
!/^[A-Za-z]:/.test(value) &&
|
|
112
|
+
!value.includes('\0') &&
|
|
113
|
+
!value.split(/[\\/]/).includes('..');
|
|
114
|
+
|
|
115
|
+
export const repoRelativePath = z
|
|
116
|
+
.string()
|
|
117
|
+
.trim()
|
|
118
|
+
.max(400)
|
|
119
|
+
.refine(insideRepo, { message: 'must be a path inside the repository (no "..")' });
|
|
120
|
+
|
|
121
|
+
/** Same rules as {@link repoRelativePath}; named for the build-context field. */
|
|
122
|
+
export const repoBaseDir = repoRelativePath;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Absolute path inside a container (e.g. `/data`, `/var/lib/uploads`).
|
|
126
|
+
* Rejected: empty, relative, paths containing NUL, parent-relative segments,
|
|
127
|
+
* or a leading double slash (Docker treats `//` as the named-volume named
|
|
128
|
+
* bind-mount form which is not what we want here).
|
|
129
|
+
*/
|
|
130
|
+
export const containerPath = z
|
|
131
|
+
.string()
|
|
132
|
+
.min(1)
|
|
133
|
+
.max(1024)
|
|
134
|
+
// Anchored on both ends: the old `^\/[^/].*` prefix-match let `/a/../b`,
|
|
135
|
+
// embedded NUL/newline and interior `//` through. Segments are non-empty,
|
|
136
|
+
// slash-free and control-char-free; a lone `/` stays valid.
|
|
137
|
+
.regex(/^\/(?:[^/\0\n\r]+\/?)*$/, 'must be an absolute path starting with a single "/"')
|
|
138
|
+
.refine((v) => !v.split('/').includes('..'), { message: 'must not contain ".." segments' });
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Docker named-volume name: letters (BOTH cases — the `i` flag is deliberate,
|
|
142
|
+
* r474: docker itself accepts uppercase volume names, and rejecting them
|
|
143
|
+
* would break attach flows against pre-existing out-of-band volumes), digits,
|
|
144
|
+
* underscores, hyphens and dots; must start with alnum; max 64 chars (the
|
|
145
|
+
* volume driver limit on most filesystems). The managed prefixes `nd-svc-` /
|
|
146
|
+
* `nd-db-` are lowercased by construction; a non-managed name attaches a
|
|
147
|
+
* pre-existing volume the client created out-of-band.
|
|
148
|
+
*/
|
|
149
|
+
export const dockerVolumeName = z
|
|
150
|
+
.string()
|
|
151
|
+
.min(1)
|
|
152
|
+
.max(64)
|
|
153
|
+
.regex(/^[a-z0-9][a-z0-9_.-]*$/i, 'invalid docker volume name');
|
|
154
|
+
|
|
155
|
+
/** Cursor-style pagination params. */
|
|
156
|
+
export const pagination = z.object({
|
|
157
|
+
limit: z.coerce.number().int().min(1).max(100).default(25),
|
|
158
|
+
cursor: z.coerce.number().int().positive().optional(),
|
|
159
|
+
});
|
|
160
|
+
export type Pagination = z.infer<typeof pagination>;
|
|
161
|
+
|
|
162
|
+
/** Standard error envelope returned by the API. */
|
|
163
|
+
export const errorResponse = z.object({
|
|
164
|
+
error: z.object({
|
|
165
|
+
code: z.string(),
|
|
166
|
+
message: z.string(),
|
|
167
|
+
details: z.unknown().optional(),
|
|
168
|
+
}),
|
|
169
|
+
});
|
|
170
|
+
export type ErrorResponse = z.infer<typeof errorResponse>;
|
|
171
|
+
|
|
172
|
+
/** A page of results. */
|
|
173
|
+
export function page<T extends z.ZodTypeAny>(item: T) {
|
|
174
|
+
return z.object({
|
|
175
|
+
items: z.array(item),
|
|
176
|
+
nextCursor: z.number().int().positive().nullable(),
|
|
177
|
+
total: z.number().int().nonnegative().optional(),
|
|
178
|
+
});
|
|
179
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { id } from './common.js';
|
|
3
|
+
|
|
4
|
+
// ── Database dump import (0.14) ────────────────────────────────────────────
|
|
5
|
+
// Request and response shapes for `/v1/databases/:id/imports` and
|
|
6
|
+
// `/v1/backup-destinations/:id/objects`. Uploads are chunked and resumable
|
|
7
|
+
// (Traefik's 60s read timeout would cut a single multi-GB request). The
|
|
8
|
+
// server detects the format by magic bytes, refuses engines and options that
|
|
9
|
+
// do not apply, and enforces size, disk and role rules. Design: DESIGN.md §3.
|
|
10
|
+
|
|
11
|
+
/** 8 MiB. Advertised by the server on every import; chunks arrive in order. */
|
|
12
|
+
export const DATABASE_IMPORT_CHUNK_SIZE = 8 * 1024 * 1024;
|
|
13
|
+
|
|
14
|
+
/** Mirrors `databaseImportSource` in `@ninedeploy/db`. */
|
|
15
|
+
export const databaseImportSource = z.enum(['upload', 's3']);
|
|
16
|
+
export type DatabaseImportSource = z.infer<typeof databaseImportSource>;
|
|
17
|
+
|
|
18
|
+
/** Mirrors `databaseImportStatus` in `@ninedeploy/db`. */
|
|
19
|
+
export const databaseImportStatus = z.enum([
|
|
20
|
+
'uploading',
|
|
21
|
+
'pending',
|
|
22
|
+
'running',
|
|
23
|
+
'completed',
|
|
24
|
+
'completed_with_warnings',
|
|
25
|
+
'failed',
|
|
26
|
+
'cancelled',
|
|
27
|
+
'expired',
|
|
28
|
+
]);
|
|
29
|
+
export type DatabaseImportStatus = z.infer<typeof databaseImportStatus>;
|
|
30
|
+
|
|
31
|
+
/** Mirrors `databaseImportFormat` in `@ninedeploy/db`. */
|
|
32
|
+
export const databaseImportFormat = z.enum(['pg_custom', 'pg_plain', 'mysql_sql', 'mongo_archive', 'rdb']);
|
|
33
|
+
export type DatabaseImportFormat = z.infer<typeof databaseImportFormat>;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Import options. Strict: an unknown key is refused, not dropped. The server
|
|
37
|
+
* further refuses keys that do not apply to the database's engine, and
|
|
38
|
+
* applies `singleTransaction: true` when it is omitted (postgres).
|
|
39
|
+
*/
|
|
40
|
+
export const databaseImportOptions = z
|
|
41
|
+
.object({
|
|
42
|
+
/** postgres custom format: `--clean --if-exists`. */
|
|
43
|
+
clean: z.boolean().optional(),
|
|
44
|
+
/** postgres: wrap the restore in one transaction (server default true). */
|
|
45
|
+
singleTransaction: z.boolean().optional(),
|
|
46
|
+
/** mongo: `--drop`. */
|
|
47
|
+
drop: z.boolean().optional(),
|
|
48
|
+
/** redis/valkey/keydb/dragonfly: required, the RDB replaces the whole dataset. */
|
|
49
|
+
confirmReplace: z.boolean().optional(),
|
|
50
|
+
/** Operator, or a database initialised in the last 10 minutes, only. */
|
|
51
|
+
skipSafetyBackup: z.boolean().optional(),
|
|
52
|
+
})
|
|
53
|
+
.strict();
|
|
54
|
+
export type DatabaseImportOptions = z.infer<typeof databaseImportOptions>;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The engines an import accepts, and the option keys each one applies. The
|
|
58
|
+
* server refuses (422) any other engine, and any key not listed for the
|
|
59
|
+
* database's engine. clickhouse, meilisearch and rabbitmq have no import.
|
|
60
|
+
*/
|
|
61
|
+
export const DATABASE_IMPORT_ENGINE_OPTIONS = {
|
|
62
|
+
postgres: ['clean', 'singleTransaction', 'skipSafetyBackup'],
|
|
63
|
+
mysql: ['skipSafetyBackup'],
|
|
64
|
+
mariadb: ['skipSafetyBackup'],
|
|
65
|
+
mongo: ['drop', 'skipSafetyBackup'],
|
|
66
|
+
redis: ['confirmReplace', 'skipSafetyBackup'],
|
|
67
|
+
valkey: ['confirmReplace', 'skipSafetyBackup'],
|
|
68
|
+
keydb: ['confirmReplace', 'skipSafetyBackup'],
|
|
69
|
+
dragonfly: ['confirmReplace', 'skipSafetyBackup'],
|
|
70
|
+
} as const satisfies Record<string, ReadonlyArray<keyof DatabaseImportOptions>>;
|
|
71
|
+
export type DatabaseImportEngine = keyof typeof DATABASE_IMPORT_ENGINE_OPTIONS;
|
|
72
|
+
|
|
73
|
+
const sha256Hex = z
|
|
74
|
+
.string()
|
|
75
|
+
.trim()
|
|
76
|
+
.regex(/^[A-Fa-f0-9]{64}$/, 'sha256 must be 64 hex characters')
|
|
77
|
+
.transform((v) => v.toLowerCase());
|
|
78
|
+
|
|
79
|
+
/** Display only; the staging path is always chosen by the server. */
|
|
80
|
+
const importFilename = z.string().trim().min(1).max(255);
|
|
81
|
+
|
|
82
|
+
const uploadSource = z.object({
|
|
83
|
+
source: z.literal('upload'),
|
|
84
|
+
sizeBytes: z.number().int().positive(),
|
|
85
|
+
sha256: sha256Hex.optional(),
|
|
86
|
+
filename: importFilename.optional(),
|
|
87
|
+
options: databaseImportOptions.default({}),
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
/** S3 imports are operator only (checked in the handler). */
|
|
91
|
+
const s3Source = z.object({
|
|
92
|
+
source: z.literal('s3'),
|
|
93
|
+
destinationId: id,
|
|
94
|
+
key: z.string().min(1).max(1024),
|
|
95
|
+
options: databaseImportOptions.default({}),
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
/** POST /v1/databases/:id/imports. */
|
|
99
|
+
export const databaseImportCreate = z.discriminatedUnion('source', [uploadSource, s3Source]);
|
|
100
|
+
export type DatabaseImportCreate = z.input<typeof databaseImportCreate>;
|
|
101
|
+
|
|
102
|
+
/** One import row as the API returns it. */
|
|
103
|
+
export const databaseImport = z.object({
|
|
104
|
+
id: z.number().int(),
|
|
105
|
+
databaseId: z.number().int(),
|
|
106
|
+
source: databaseImportSource,
|
|
107
|
+
status: databaseImportStatus,
|
|
108
|
+
format: databaseImportFormat.nullable(),
|
|
109
|
+
sizeBytes: z.number().int(),
|
|
110
|
+
receivedBytes: z.number().int(),
|
|
111
|
+
chunkSize: z.number().int(),
|
|
112
|
+
sha256: z.string().nullable(),
|
|
113
|
+
filename: z.string().nullable(),
|
|
114
|
+
destinationId: z.number().int().nullable(),
|
|
115
|
+
objectKey: z.string().nullable(),
|
|
116
|
+
options: databaseImportOptions,
|
|
117
|
+
safetyBackupId: z.number().int().nullable(),
|
|
118
|
+
error: z.string().nullable(),
|
|
119
|
+
createdByUserId: z.number().int().nullable(),
|
|
120
|
+
createdAt: z.string(),
|
|
121
|
+
updatedAt: z.string(),
|
|
122
|
+
startedAt: z.string().nullable(),
|
|
123
|
+
completedAt: z.string().nullable(),
|
|
124
|
+
});
|
|
125
|
+
export type DatabaseImport = z.infer<typeof databaseImport>;
|
|
126
|
+
|
|
127
|
+
/** GET /v1/backup-destinations/:id/objects?prefix= (operator). */
|
|
128
|
+
export const backupDestinationObjectsQuery = z.object({
|
|
129
|
+
prefix: z.string().max(1024).optional(),
|
|
130
|
+
});
|
|
131
|
+
export type BackupDestinationObjectsQuery = z.infer<typeof backupDestinationObjectsQuery>;
|
|
132
|
+
|
|
133
|
+
export const backupDestinationObject = z.object({
|
|
134
|
+
key: z.string(),
|
|
135
|
+
sizeBytes: z.number().int(),
|
|
136
|
+
lastModified: z.string().nullable(),
|
|
137
|
+
});
|
|
138
|
+
export type BackupDestinationObject = z.infer<typeof backupDestinationObject>;
|
package/src/doctor.ts
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
/** One actionable observation from a Doctor scan. `id` is deterministic
|
|
4
|
+
* (`kind:target`) so a fix request can re-scan, re-locate the finding against
|
|
5
|
+
* FRESH state and only then execute — a stale report can never drive a
|
|
6
|
+
* destructive action against a target that stopped qualifying in between. */
|
|
7
|
+
export const doctorSeverity = z.enum(['info', 'warn', 'critical']);
|
|
8
|
+
export type DoctorSeverity = z.infer<typeof doctorSeverity>;
|
|
9
|
+
|
|
10
|
+
export const doctorTargetType = z.enum([
|
|
11
|
+
'container',
|
|
12
|
+
'volume',
|
|
13
|
+
'network',
|
|
14
|
+
'image',
|
|
15
|
+
'database',
|
|
16
|
+
'service',
|
|
17
|
+
'deployment',
|
|
18
|
+
'host',
|
|
19
|
+
// Rows identified by a stored `slug` (see the invalid_slug finding).
|
|
20
|
+
'project',
|
|
21
|
+
'workspace',
|
|
22
|
+
'tunnel',
|
|
23
|
+
'oidc_provider',
|
|
24
|
+
]);
|
|
25
|
+
export type DoctorTargetType = z.infer<typeof doctorTargetType>;
|
|
26
|
+
|
|
27
|
+
export const doctorFindingKind = z.enum([
|
|
28
|
+
/** A Hub-family container sat exited with no live row claiming it. */
|
|
29
|
+
'exited_container',
|
|
30
|
+
/** A managed volume (nd-svc-/nd-db-) no row owns any more. */
|
|
31
|
+
'orphan_volume',
|
|
32
|
+
/** A per-slug bridge or compose-project network nothing uses any more. */
|
|
33
|
+
'orphan_network',
|
|
34
|
+
/** Dangling `<none>` images (safe, uncontroversial reclaim). */
|
|
35
|
+
'dangling_images',
|
|
36
|
+
/** Builder cache beyond the noise threshold. */
|
|
37
|
+
'build_cache',
|
|
38
|
+
/** Disk usage crossed the warning / critical line. */
|
|
39
|
+
'disk_pressure',
|
|
40
|
+
/** Database row says running, the container disagrees. */
|
|
41
|
+
'database_down',
|
|
42
|
+
/** Database stuck in `creating` — provisioning never finished. */
|
|
43
|
+
'database_stuck',
|
|
44
|
+
/** Service row says running, its runtime container is gone or exited. */
|
|
45
|
+
'service_runtime_desync',
|
|
46
|
+
/** A deploy row stuck in queued/building far past any live window. */
|
|
47
|
+
'stuck_deployment',
|
|
48
|
+
/**
|
|
49
|
+
* A stored `slug` that violates the canonical `slug` contract — legacy rows
|
|
50
|
+
* written before slugify was fixed (r028 stranded a trailing hyphen, r029 a
|
|
51
|
+
* one-character slug), or a hand-written `input.slug` the create route stored
|
|
52
|
+
* without re-validating. Such a row is a record its own API rejects on PATCH.
|
|
53
|
+
*/
|
|
54
|
+
'invalid_slug',
|
|
55
|
+
/**
|
|
56
|
+
* r575: the panel's own port listens on a non-loopback address over plain
|
|
57
|
+
* HTTP (bare-metal default 0.0.0.0:3000) — logins and API tokens crossing
|
|
58
|
+
* it are readable on the network. Advisory: changing the bind is the
|
|
59
|
+
* operator's call (it cuts off whoever reaches the panel at http://ip:3000).
|
|
60
|
+
*/
|
|
61
|
+
'panel_plaintext_exposure',
|
|
62
|
+
/**
|
|
63
|
+
* r600: enabled sandbox plugins on a Node whose permission model has no
|
|
64
|
+
* network scope (< 25) — their code can reach anything the host can.
|
|
65
|
+
* Advisory: they keep loading (upgrade-safe); upgrading Node closes it.
|
|
66
|
+
*/
|
|
67
|
+
'sandbox_plugin_network',
|
|
68
|
+
/**
|
|
69
|
+
* r620: a stored repository analysis holds a nodeVersion that is not a
|
|
70
|
+
* version — host file content captured through a symlinked .nvmrc before
|
|
71
|
+
* 0.10.41. Critical: the exposed secrets must be rotated.
|
|
72
|
+
*/
|
|
73
|
+
'repo_insights_leak',
|
|
74
|
+
/**
|
|
75
|
+
* r655: the Hub's remote template registry is fetched over plain http, so
|
|
76
|
+
* anyone on the path can swap the catalog (images, env defaults). Advisory:
|
|
77
|
+
* the source keeps loading (upgrade-safe); new http sources are refused.
|
|
78
|
+
*/
|
|
79
|
+
'templates_source_plaintext',
|
|
80
|
+
]);
|
|
81
|
+
export type DoctorFindingKind = z.infer<typeof doctorFindingKind>;
|
|
82
|
+
|
|
83
|
+
/** Executable repair actions. Every one re-validates its precondition against
|
|
84
|
+
* current state inside the fix path; the enum is the full surface. */
|
|
85
|
+
export const doctorActionKind = z.enum([
|
|
86
|
+
'remove_container',
|
|
87
|
+
'delete_volume',
|
|
88
|
+
'remove_network',
|
|
89
|
+
'prune_dangling_images',
|
|
90
|
+
'prune_build_cache',
|
|
91
|
+
'run_autoprune',
|
|
92
|
+
'start_database',
|
|
93
|
+
'mark_database_error',
|
|
94
|
+
'sync_service',
|
|
95
|
+
'cancel_deployment',
|
|
96
|
+
/**
|
|
97
|
+
* Rewrite a stored `slug` that violates the canonical contract. Only offered
|
|
98
|
+
* for tables whose slug is NOT also a live Docker identity — renaming a
|
|
99
|
+
* service/database/tunnel row alone would desync it from its bridge,
|
|
100
|
+
* container and volume, so those findings stay advisory with a manual step.
|
|
101
|
+
*/
|
|
102
|
+
'repair_slug',
|
|
103
|
+
]);
|
|
104
|
+
export type DoctorActionKind = z.infer<typeof doctorActionKind>;
|
|
105
|
+
|
|
106
|
+
export const doctorFinding = z.object({
|
|
107
|
+
id: z.string(),
|
|
108
|
+
kind: doctorFindingKind,
|
|
109
|
+
severity: doctorSeverity,
|
|
110
|
+
title: z.string(),
|
|
111
|
+
detail: z.string(),
|
|
112
|
+
target: z.object({
|
|
113
|
+
type: doctorTargetType,
|
|
114
|
+
name: z.string().nullable(),
|
|
115
|
+
id: z.number().nullable(),
|
|
116
|
+
}),
|
|
117
|
+
/** Null when the finding is advisory only (no automated repair). */
|
|
118
|
+
action: doctorActionKind.nullable(),
|
|
119
|
+
sizeBytes: z.number().nullable(),
|
|
120
|
+
});
|
|
121
|
+
export type DoctorFinding = z.infer<typeof doctorFinding>;
|
|
122
|
+
|
|
123
|
+
export const doctorHostFacts = z.object({
|
|
124
|
+
diskUsedPercent: z.number(),
|
|
125
|
+
diskTotalBytes: z.number(),
|
|
126
|
+
diskFreeBytes: z.number(),
|
|
127
|
+
dockerImagesBytes: z.number().nullable(),
|
|
128
|
+
dockerVolumesBytes: z.number().nullable(),
|
|
129
|
+
dockerBuildCacheBytes: z.number().nullable(),
|
|
130
|
+
});
|
|
131
|
+
export type DoctorHostFacts = z.infer<typeof doctorHostFacts>;
|
|
132
|
+
|
|
133
|
+
export const doctorReport = z.object({
|
|
134
|
+
generatedAt: z.string().datetime(),
|
|
135
|
+
healthy: z.boolean(),
|
|
136
|
+
totals: z.object({
|
|
137
|
+
findings: z.number(),
|
|
138
|
+
critical: z.number(),
|
|
139
|
+
warn: z.number(),
|
|
140
|
+
info: z.number(),
|
|
141
|
+
reclaimableBytes: z.number(),
|
|
142
|
+
}),
|
|
143
|
+
host: doctorHostFacts,
|
|
144
|
+
findings: z.array(doctorFinding),
|
|
145
|
+
});
|
|
146
|
+
export type DoctorReport = z.infer<typeof doctorReport>;
|
|
147
|
+
|
|
148
|
+
export const doctorFixRequest = z.object({
|
|
149
|
+
findingId: z.string().min(1),
|
|
150
|
+
});
|
|
151
|
+
export type DoctorFixRequest = z.infer<typeof doctorFixRequest>;
|
|
152
|
+
export type DoctorFixRequestInput = DoctorFixRequest;
|
|
153
|
+
|
|
154
|
+
export const doctorFixResponse = z.object({
|
|
155
|
+
fixed: z.boolean(),
|
|
156
|
+
id: z.string(),
|
|
157
|
+
action: doctorActionKind,
|
|
158
|
+
log: z.array(z.string()),
|
|
159
|
+
report: doctorReport,
|
|
160
|
+
});
|
|
161
|
+
export type DoctorFixResponse = z.infer<typeof doctorFixResponse>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Environment = a named deployment lane (production / staging / development)
|
|
5
|
+
* inside a workspace. Services opt in via `services.environment_id`; the
|
|
6
|
+
* promote flow builds on this grouping.
|
|
7
|
+
*/
|
|
8
|
+
export const environment = z.object({
|
|
9
|
+
id: z.number().int(),
|
|
10
|
+
workspaceId: z.number().int(),
|
|
11
|
+
name: z.string(),
|
|
12
|
+
slug: z.string(),
|
|
13
|
+
serviceCount: z.number().int(),
|
|
14
|
+
createdAt: z.string().datetime(),
|
|
15
|
+
updatedAt: z.string().datetime(),
|
|
16
|
+
});
|
|
17
|
+
export type Environment = z.infer<typeof environment>;
|
|
18
|
+
|
|
19
|
+
export const environmentCreate = z.object({
|
|
20
|
+
workspaceId: z.number().int().positive(),
|
|
21
|
+
name: z.string().min(1).max(80),
|
|
22
|
+
});
|
|
23
|
+
export type EnvironmentCreateInput = z.infer<typeof environmentCreate>;
|