@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,620 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { gitBranch, providerBaseUrl } from './common.js';
|
|
3
|
+
import { registryHost } from './service.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Zod schemas for management/admin endpoints that previously used ad-hoc
|
|
7
|
+
* `as { ... }` casts. Centralising them gives consistent 400 validation errors.
|
|
8
|
+
*
|
|
9
|
+
* The legacy `rolePatch` (admin/member) was removed when the global
|
|
10
|
+
* `users.role` column was dropped. Workspace role changes are now served by
|
|
11
|
+
* `workspaceMemberRoleUpdate` in workspaces.ts.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// ── Update check (admin) ───────────────────────────────────────────────────
|
|
15
|
+
export const updateCheckResult = z.object({
|
|
16
|
+
current: z.string(),
|
|
17
|
+
/** null when the feed was unreachable or checks are disabled. */
|
|
18
|
+
latest: z.string().nullable(),
|
|
19
|
+
/** null = unknown; otherwise true when latest > current. */
|
|
20
|
+
updateAvailable: z.boolean().nullable(),
|
|
21
|
+
notesUrl: z.string().nullable(),
|
|
22
|
+
checkedAt: z.string().datetime(),
|
|
23
|
+
/** Present only when updateAvailable is null: why no verdict exists. */
|
|
24
|
+
reason: z.enum(['disabled', 'unreachable']).optional(),
|
|
25
|
+
/** Why the live check failed (per-source reasons), when it did. */
|
|
26
|
+
detail: z.string().max(500).optional(),
|
|
27
|
+
/** Which source answered: 'github-api', 'release-page', 'git', … (+ ' via curl'). */
|
|
28
|
+
source: z.string().optional(),
|
|
29
|
+
/** True when every source failed and this is the last successful answer (checkedAt = its time). */
|
|
30
|
+
stale: z.boolean().optional(),
|
|
31
|
+
});
|
|
32
|
+
export type UpdateCheckResult = z.infer<typeof updateCheckResult>;
|
|
33
|
+
|
|
34
|
+
// ── Panel self-update (admin) ──────────────────────────────────────────────
|
|
35
|
+
/** Body of POST /v1/system/update-start: an exact release tag, e.g. "v0.3.4". */
|
|
36
|
+
export const selfUpdateStart = z.object({
|
|
37
|
+
version: z.string().regex(/^v\d+\.\d+\.\d+$/, 'version must be a release tag like v0.3.4'),
|
|
38
|
+
// r572: start even while deployments are executing (they are interrupted).
|
|
39
|
+
force: z.boolean().optional(),
|
|
40
|
+
});
|
|
41
|
+
export type SelfUpdateStart = z.infer<typeof selfUpdateStart>;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Live state of a panel-initiated upgrade. The updater deliberately runs
|
|
45
|
+
* outside the panel service's cgroup, so `running` and the terminal phases
|
|
46
|
+
* survive the panel restart that every upgrade performs mid-way; phase is
|
|
47
|
+
* resolved by reading marker files, not by process liveness.
|
|
48
|
+
*/
|
|
49
|
+
export const selfUpdateStatus = z.object({
|
|
50
|
+
/** False when this install cannot update itself (container mode, dev checkout, no install.sh). */
|
|
51
|
+
supported: z.boolean(),
|
|
52
|
+
phase: z.enum(['idle', 'running', 'success', 'failed', 'unsupported']),
|
|
53
|
+
currentVersion: z.string(),
|
|
54
|
+
targetVersion: z.string().nullable(),
|
|
55
|
+
startedAt: z.string().nullable(),
|
|
56
|
+
finishedAt: z.string().nullable(),
|
|
57
|
+
/** Short excerpt of the update log when phase === 'failed'. */
|
|
58
|
+
errorTail: z.string().nullable(),
|
|
59
|
+
/** Present when unsupported: why self-update is unavailable here. */
|
|
60
|
+
reason: z.string().optional(),
|
|
61
|
+
});
|
|
62
|
+
export type SelfUpdateStatus = z.infer<typeof selfUpdateStatus>;
|
|
63
|
+
|
|
64
|
+
export const webhookCreate = z.object({
|
|
65
|
+
// r473: same charset rule the service's own branch field uses — a branch
|
|
66
|
+
// this rejects could never match a push anyway. Empty/whitespace normalizes
|
|
67
|
+
// to ABSENT (the route then falls back to the service's own branch, exactly
|
|
68
|
+
// as it always did) instead of 400ing.
|
|
69
|
+
branch: z
|
|
70
|
+
.string()
|
|
71
|
+
.max(200)
|
|
72
|
+
.optional()
|
|
73
|
+
.transform((v) => v?.trim() || undefined)
|
|
74
|
+
.pipe(gitBranch.optional()),
|
|
75
|
+
/** Newline/comma-separated globs — deploy only when a changed file matches. */
|
|
76
|
+
watchPaths: z.string().max(1024).refine((raw) => {
|
|
77
|
+
const patterns = raw.split(/[\n,]/).map((pattern) => pattern.trim()).filter(Boolean);
|
|
78
|
+
return patterns.length <= 32 && patterns.every((pattern) => pattern.length <= 256 && (pattern.match(/\*\*/g) ?? []).length <= 4 && (pattern.match(/[?*]/g) ?? []).length <= 16);
|
|
79
|
+
}, 'watchPaths contains an unsafe glob pattern').optional(),
|
|
80
|
+
});
|
|
81
|
+
export type WebhookCreate = z.infer<typeof webhookCreate>;
|
|
82
|
+
|
|
83
|
+
export const sourcePatch = z.object({
|
|
84
|
+
name: z.string().min(1).max(100).optional(),
|
|
85
|
+
token: z.string().max(4096).optional(),
|
|
86
|
+
deployKey: z.string().max(16384).optional(),
|
|
87
|
+
registryUsername: z.string().max(255).optional(),
|
|
88
|
+
/** r512: replace the registry hosts this credential may be sent to. */
|
|
89
|
+
registryHosts: z.array(registryHost).max(20).optional(),
|
|
90
|
+
defaultBranch: z.string().max(255).optional(),
|
|
91
|
+
/** 0.13: set the self-hosted provider base URL (Gitea); null clears it. */
|
|
92
|
+
baseUrl: providerBaseUrl.nullable().optional(),
|
|
93
|
+
/**
|
|
94
|
+
* Multi-node (additive): allow this static credential (PAT, deploy key) to
|
|
95
|
+
* be sent to a node for one clone. Off for every existing source.
|
|
96
|
+
*/
|
|
97
|
+
allowOnNodes: z.boolean().optional(),
|
|
98
|
+
/**
|
|
99
|
+
* 0.15.8: pin the git host's SSH keys for deploy-key clones made by the
|
|
100
|
+
* panel. Switching it on only tightens; switching it off needs the password.
|
|
101
|
+
*/
|
|
102
|
+
pinHostKeys: z.boolean().optional(),
|
|
103
|
+
/** 0.15.8: forget the pinned keys so the next clone learns them again (needs the password). */
|
|
104
|
+
resetHostKeys: z.literal(true).optional(),
|
|
105
|
+
/** Step-up password, required when `allowOnNodes` turns on, pinning turns off or the keys are reset. */
|
|
106
|
+
password: z.string().min(1).max(1024).optional(),
|
|
107
|
+
});
|
|
108
|
+
export type SourcePatch = z.infer<typeof sourcePatch>;
|
|
109
|
+
|
|
110
|
+
export const notificationType = z.enum(['telegram', 'webhook', 'discord', 'slack', 'ntfy', 'gotify', 'pushover', 'lark', 'email', 'fcm', 'teams', 'resend']);
|
|
111
|
+
|
|
112
|
+
export const notificationChannelCreate = z.object({
|
|
113
|
+
name: z.string().min(1).max(100),
|
|
114
|
+
type: notificationType,
|
|
115
|
+
target: z.string().min(1).max(2048),
|
|
116
|
+
eventFilter: z.string().max(1000).optional(),
|
|
117
|
+
// Per-provider configuration blob. Discord reads username / avatarUrl /
|
|
118
|
+
// title / color for the embed; webhook (G-06) reads
|
|
119
|
+
// `secret` / `headerName` / `algorithm` / `template`; other
|
|
120
|
+
// channel types ignore it. Capped at 4KB so a misbehaving
|
|
121
|
+
// client can't bloat the row.
|
|
122
|
+
configJson: z.string().max(4096).optional(),
|
|
123
|
+
});
|
|
124
|
+
export type NotificationChannelCreate = z.infer<typeof notificationChannelCreate>;
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Webhook channel config (G-06). The dispatch path reads
|
|
128
|
+
* this from the `configJson` blob; the schema here is
|
|
129
|
+
* exported for the SDK and CLI to validate user input
|
|
130
|
+
* BEFORE the string is round-tripped to the database.
|
|
131
|
+
*/
|
|
132
|
+
export const webhookChannelConfig = z
|
|
133
|
+
.object({
|
|
134
|
+
/** HMAC secret. Required for the signature header to be emitted. */
|
|
135
|
+
secret: z.string().min(1).max(256).optional(),
|
|
136
|
+
/** Signature header name. Defaults to `X-NineDeploy-Signature`. */
|
|
137
|
+
headerName: z.string().min(1).max(64).regex(/^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/, 'invalid HTTP header name').optional(),
|
|
138
|
+
/** Hash algorithm. Defaults to `sha256`. */
|
|
139
|
+
algorithm: z.enum(['sha256', 'sha1']).optional(),
|
|
140
|
+
/**
|
|
141
|
+
* Body template. Either a JSON object (sent verbatim, with
|
|
142
|
+
* `${event}` / `${entity}` / `${message}` / `${ts}`
|
|
143
|
+
* placeholders expanded) or a string template that's
|
|
144
|
+
* JSON-encoded before send. The default — when this is
|
|
145
|
+
* absent — is the four-field envelope
|
|
146
|
+
* `{ event, entity, ts, message }`.
|
|
147
|
+
*/
|
|
148
|
+
template: z
|
|
149
|
+
.union([
|
|
150
|
+
z.record(z.string(), z.union([z.string(), z.number(), z.boolean(), z.null()])),
|
|
151
|
+
z.string().max(2048),
|
|
152
|
+
])
|
|
153
|
+
.optional(),
|
|
154
|
+
})
|
|
155
|
+
.strict();
|
|
156
|
+
export type WebhookChannelConfig = z.infer<typeof webhookChannelConfig>;
|
|
157
|
+
|
|
158
|
+
export const notificationChannelPatch = z.object({
|
|
159
|
+
name: z.string().min(1).max(100).optional(),
|
|
160
|
+
target: z.string().min(1).max(2048).optional(),
|
|
161
|
+
eventFilter: z.string().max(1000).optional(),
|
|
162
|
+
active: z.boolean().optional(),
|
|
163
|
+
// Empty string clears the channel's provider-specific config.
|
|
164
|
+
configJson: z.string().max(4096).optional(),
|
|
165
|
+
});
|
|
166
|
+
export type NotificationChannelPatch = z.infer<typeof notificationChannelPatch>;
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* cpu %, memory MiB, cert-expiry days remaining, disk % used (panel host and
|
|
170
|
+
* nodes, worst wins), server_offline minutes a node has gone unseen (0.12).
|
|
171
|
+
* `alert_rules.metric` is a free-text column, so new values need no migration.
|
|
172
|
+
*/
|
|
173
|
+
export const alertMetricEnum = z.enum(['cpu', 'memory', 'cert-expiry', 'disk', 'server_offline']);
|
|
174
|
+
export type AlertMetric = z.infer<typeof alertMetricEnum>;
|
|
175
|
+
/** Metrics sampled host-wide only — a service-scoped rule could never fire. */
|
|
176
|
+
export const HOST_ONLY_ALERT_METRICS: readonly AlertMetric[] = ['cert-expiry', 'disk', 'server_offline'];
|
|
177
|
+
const hostOnlyAlertRule = (r: { metric?: AlertMetric; serviceId?: number | null }, ctx: z.RefinementCtx) => {
|
|
178
|
+
if (r.metric && HOST_ONLY_ALERT_METRICS.includes(r.metric) && r.serviceId) {
|
|
179
|
+
ctx.addIssue({ code: 'custom', message: `${r.metric} rules are host-wide (omit serviceId)`, path: ['serviceId'] });
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
export const alertOperatorEnum = z.enum(['>', '<']);
|
|
183
|
+
|
|
184
|
+
export const alertRuleCreate = z
|
|
185
|
+
.object({
|
|
186
|
+
name: z.string().min(1).max(100),
|
|
187
|
+
serviceId: z.number().int().positive().nullable().optional(),
|
|
188
|
+
metric: alertMetricEnum,
|
|
189
|
+
operator: alertOperatorEnum.default('>'),
|
|
190
|
+
threshold: z.number().int(),
|
|
191
|
+
durationWindows: z.number().int().min(1).max(120).default(1),
|
|
192
|
+
enabled: z.boolean().default(true),
|
|
193
|
+
})
|
|
194
|
+
// Certificate expiry (the ACME store), disk and node liveness are tracked
|
|
195
|
+
// per HOST, so a service-scoped rule could never evaluate.
|
|
196
|
+
.superRefine(hostOnlyAlertRule);
|
|
197
|
+
export type AlertRuleCreate = z.infer<typeof alertRuleCreate>;
|
|
198
|
+
|
|
199
|
+
export const alertRulePatch = z
|
|
200
|
+
.object({
|
|
201
|
+
name: z.string().min(1).max(100).optional(),
|
|
202
|
+
serviceId: z.number().int().positive().nullable().optional(),
|
|
203
|
+
metric: alertMetricEnum.optional(),
|
|
204
|
+
operator: alertOperatorEnum.optional(),
|
|
205
|
+
threshold: z.number().int().optional(),
|
|
206
|
+
durationWindows: z.number().int().min(1).max(120).optional(),
|
|
207
|
+
enabled: z.boolean().optional(),
|
|
208
|
+
})
|
|
209
|
+
.superRefine(hostOnlyAlertRule);
|
|
210
|
+
export type AlertRulePatch = z.infer<typeof alertRulePatch>;
|
|
211
|
+
|
|
212
|
+
// ── Backup destinations (admin) ────────────────────────────────────────────
|
|
213
|
+
/**
|
|
214
|
+
* Blank region/prefix fall back to the documented defaults (pre-schema
|
|
215
|
+
* behaviour); the endpoint must be an http(s) URL.
|
|
216
|
+
*/
|
|
217
|
+
export const backupDestinationCreate = z.object({
|
|
218
|
+
name: z.string().trim().min(1).max(100),
|
|
219
|
+
endpoint: z.string().trim().regex(/^https?:\/\//, 'endpoint must be an http(s) URL'),
|
|
220
|
+
region: z.string().trim().optional().transform((v) => v || 'us-east-1'),
|
|
221
|
+
bucket: z.string().trim().min(1).max(255),
|
|
222
|
+
prefix: z.string().trim().optional().transform((v) => v || 'ninedeploy'),
|
|
223
|
+
accessKeyId: z.string().trim().min(1).max(255),
|
|
224
|
+
secretAccessKey: z.string().min(1).max(4096),
|
|
225
|
+
});
|
|
226
|
+
export type BackupDestinationCreate = z.infer<typeof backupDestinationCreate>;
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* All fields optional; blank strings are accepted here and skipped by the
|
|
230
|
+
* route (patching e.g. `name: " "` is a no-op, not an error).
|
|
231
|
+
*/
|
|
232
|
+
export const backupDestinationPatch = z.object({
|
|
233
|
+
name: z.string().max(100).optional(),
|
|
234
|
+
endpoint: z.string().max(2048).optional(),
|
|
235
|
+
region: z.string().max(100).optional(),
|
|
236
|
+
bucket: z.string().max(255).optional(),
|
|
237
|
+
prefix: z.string().max(255).optional(),
|
|
238
|
+
accessKeyId: z.string().max(255).optional(),
|
|
239
|
+
secretAccessKey: z.string().max(4096).optional(),
|
|
240
|
+
active: z.boolean().optional(),
|
|
241
|
+
});
|
|
242
|
+
export type BackupDestinationPatch = z.infer<typeof backupDestinationPatch>;
|
|
243
|
+
|
|
244
|
+
// ── Remote servers (admin) ─────────────────────────────────────────────────
|
|
245
|
+
/**
|
|
246
|
+
* `host` is a bare hostname (or IP) or host:port. A non-numeric port falls
|
|
247
|
+
* back to the default 4600 (pre-schema behaviour); an out-of-range one is
|
|
248
|
+
* rejected.
|
|
249
|
+
*/
|
|
250
|
+
export const serverCreate = z.object({
|
|
251
|
+
name: z.string().trim().min(1).max(100),
|
|
252
|
+
host: z
|
|
253
|
+
.string()
|
|
254
|
+
.trim()
|
|
255
|
+
.regex(/^[A-Za-z0-9][A-Za-z0-9.-]*(:\d+)?$/, 'host must be a hostname or host:port'),
|
|
256
|
+
port: z
|
|
257
|
+
.unknown()
|
|
258
|
+
.optional()
|
|
259
|
+
.transform((v) => Number(v ?? 4600) || 4600)
|
|
260
|
+
.pipe(z.number().int().min(1).max(65535)),
|
|
261
|
+
});
|
|
262
|
+
export type ServerCreate = z.infer<typeof serverCreate>;
|
|
263
|
+
|
|
264
|
+
export const serverAnnounce = z.object({
|
|
265
|
+
name: z.string().trim().min(1).max(100),
|
|
266
|
+
host: z
|
|
267
|
+
.string()
|
|
268
|
+
.trim()
|
|
269
|
+
.regex(/^[A-Za-z0-9][A-Za-z0-9.-]*(:\d+)?$/, 'host must be a hostname or host:port')
|
|
270
|
+
.optional(),
|
|
271
|
+
port: z
|
|
272
|
+
.unknown()
|
|
273
|
+
.optional()
|
|
274
|
+
.transform((v) => Number(v ?? 4600) || 4600)
|
|
275
|
+
.pipe(z.number().int().min(1).max(65535)),
|
|
276
|
+
token: z.string().trim().min(16).max(256),
|
|
277
|
+
});
|
|
278
|
+
export type ServerAnnounce = z.infer<typeof serverAnnounce>;
|
|
279
|
+
|
|
280
|
+
export const sshAuthType = z.enum(['key', 'password']);
|
|
281
|
+
export type SshAuthType = z.infer<typeof sshAuthType>;
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* SSH destination operands.
|
|
285
|
+
*
|
|
286
|
+
* These are joined as `${sshUser}@${host}` and handed to the `ssh` binary as a
|
|
287
|
+
* single argv element. OpenSSH parses any element that STARTS WITH `-` as an
|
|
288
|
+
* option, so an unconstrained `sshUser` of `-oProxyCommand=…` turns the
|
|
289
|
+
* destination into an option whose value runs through /bin/sh on the panel
|
|
290
|
+
* host. Constraining the charset (no leading dash, no whitespace, no `@`)
|
|
291
|
+
* keeps the element a destination.
|
|
292
|
+
*/
|
|
293
|
+
const sshUser = z
|
|
294
|
+
.string()
|
|
295
|
+
.trim()
|
|
296
|
+
.min(1)
|
|
297
|
+
.max(64)
|
|
298
|
+
.regex(/^[A-Za-z0-9_][A-Za-z0-9._-]*$/, 'invalid SSH username')
|
|
299
|
+
.default('root');
|
|
300
|
+
|
|
301
|
+
/** A hostname, IPv4 or bare IPv6 address — same reasoning as `sshUser`. */
|
|
302
|
+
const sshHost = z
|
|
303
|
+
.string()
|
|
304
|
+
.trim()
|
|
305
|
+
.min(1)
|
|
306
|
+
.max(255)
|
|
307
|
+
.regex(/^[A-Za-z0-9][A-Za-z0-9.:_-]*$/, 'invalid SSH host');
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* r661: the host key the operator expects, as OpenSSH prints it
|
|
311
|
+
* (`ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub` → `SHA256:…`).
|
|
312
|
+
* Optional and additive: without it the first key a host presents is
|
|
313
|
+
* recorded (trust on first use) and every later session must match it.
|
|
314
|
+
*/
|
|
315
|
+
const sshHostKeyFingerprint = z
|
|
316
|
+
.union([z.literal(''), z.string().trim().regex(/^SHA256:[A-Za-z0-9+/]{43}=?$/, 'expected an OpenSSH SHA256 fingerprint (SHA256:…)')])
|
|
317
|
+
.optional()
|
|
318
|
+
.transform((v) => (v ? v.replace(/=$/, '') : undefined));
|
|
319
|
+
|
|
320
|
+
export const serverSshTest = z.object({
|
|
321
|
+
host: sshHost,
|
|
322
|
+
sshPort: z
|
|
323
|
+
.unknown()
|
|
324
|
+
.optional()
|
|
325
|
+
.transform((v) => Number(v ?? 22) || 22)
|
|
326
|
+
.pipe(z.number().int().min(1).max(65535)),
|
|
327
|
+
sshUser,
|
|
328
|
+
authType: sshAuthType.default('key'),
|
|
329
|
+
sshKey: z.string().optional(),
|
|
330
|
+
sshPassword: z.string().optional(),
|
|
331
|
+
hostKeyFingerprint: sshHostKeyFingerprint,
|
|
332
|
+
});
|
|
333
|
+
export type ServerSshTest = z.infer<typeof serverSshTest>;
|
|
334
|
+
|
|
335
|
+
export const serverSshBootstrap = z.object({
|
|
336
|
+
name: z.string().trim().min(1).max(100),
|
|
337
|
+
host: sshHost,
|
|
338
|
+
sshPort: z
|
|
339
|
+
.unknown()
|
|
340
|
+
.optional()
|
|
341
|
+
.transform((v) => Number(v ?? 22) || 22)
|
|
342
|
+
.pipe(z.number().int().min(1).max(65535)),
|
|
343
|
+
sshUser,
|
|
344
|
+
authType: sshAuthType.default('key'),
|
|
345
|
+
sshKey: z.string().optional(),
|
|
346
|
+
sshPassword: z.string().optional(),
|
|
347
|
+
hostKeyFingerprint: sshHostKeyFingerprint,
|
|
348
|
+
installDocker: z.boolean().default(true),
|
|
349
|
+
agentPort: z
|
|
350
|
+
.unknown()
|
|
351
|
+
.optional()
|
|
352
|
+
.transform((v) => Number(v ?? 4600) || 4600)
|
|
353
|
+
.pipe(z.number().int().min(1).max(65535)),
|
|
354
|
+
});
|
|
355
|
+
export type ServerSshBootstrap = z.infer<typeof serverSshBootstrap>;
|
|
356
|
+
|
|
357
|
+
export interface ServerSshTestResult {
|
|
358
|
+
ok: boolean;
|
|
359
|
+
message: string;
|
|
360
|
+
os?: string;
|
|
361
|
+
dockerInstalled?: boolean;
|
|
362
|
+
dockerVersion?: string;
|
|
363
|
+
latencyMs?: number;
|
|
364
|
+
/** r661: the SSH host key the session was pinned to (`SHA256:…`). */
|
|
365
|
+
hostKeyFingerprint?: string;
|
|
366
|
+
/** r661: how that key was trusted — matched the operator's value, a key recorded earlier, or first use. */
|
|
367
|
+
hostKeyTrust?: 'verified' | 'pinned' | 'first-use';
|
|
368
|
+
/** r661: set on first use — the operator should compare the fingerprint out of band. */
|
|
369
|
+
warning?: string;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
export interface ServerBootstrapStep {
|
|
373
|
+
step: 'connecting' | 'os_detect' | 'docker_check' | 'docker_install' | 'agent_deploy' | 'verify' | 'done' | 'error';
|
|
374
|
+
status: 'pending' | 'running' | 'success' | 'failed';
|
|
375
|
+
message: string;
|
|
376
|
+
timestamp: string;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export interface ServerBootstrapResult {
|
|
380
|
+
ok: boolean;
|
|
381
|
+
serverId?: number;
|
|
382
|
+
serverName?: string;
|
|
383
|
+
steps: ServerBootstrapStep[];
|
|
384
|
+
logs: string[];
|
|
385
|
+
error?: string;
|
|
386
|
+
/** r661: the SSH host key the bootstrap was pinned to. */
|
|
387
|
+
hostKeyFingerprint?: string;
|
|
388
|
+
/** r661: e.g. the trust-on-first-use notice. */
|
|
389
|
+
warnings?: string[];
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// ── Scheduled jobs ─────────────────────────────────────────────────────────
|
|
393
|
+
/**
|
|
394
|
+
* The cron expression itself is validated with croner at the route (the
|
|
395
|
+
* schema package has no cron dependency); a non-string `command` is ignored
|
|
396
|
+
* (empty string) as before.
|
|
397
|
+
*/
|
|
398
|
+
export const jobCreate = z.object({
|
|
399
|
+
name: z.string().trim().min(1).max(100),
|
|
400
|
+
cron: z.string().trim().min(1).max(120),
|
|
401
|
+
kind: z.enum(['deploy', 'exec', 'backup']).default('deploy'),
|
|
402
|
+
command: z.unknown().optional().transform((v) => (typeof v === 'string' ? v.trim() : '')),
|
|
403
|
+
enabled: z.unknown().optional().transform((v) => v !== false),
|
|
404
|
+
});
|
|
405
|
+
export type JobCreate = z.infer<typeof jobCreate>;
|
|
406
|
+
|
|
407
|
+
/** Blank strings are accepted and treated as "no change"/cleared by the route. */
|
|
408
|
+
export const jobPatch = z.object({
|
|
409
|
+
name: z.string().optional(),
|
|
410
|
+
cron: z.string().optional(),
|
|
411
|
+
kind: z.enum(['deploy', 'exec', 'backup']).optional(),
|
|
412
|
+
command: z.string().optional(),
|
|
413
|
+
enabled: z.boolean().optional(),
|
|
414
|
+
});
|
|
415
|
+
export type JobPatch = z.infer<typeof jobPatch>;
|
|
416
|
+
|
|
417
|
+
// ── Metrics query ──────────────────────────────────────────────────────────
|
|
418
|
+
/**
|
|
419
|
+
* Query params arrive as strings: any `kind` other than "memory" reads as
|
|
420
|
+
* "cpu", and an unusable `minutes` falls back to 60 (clamped to 1..1440).
|
|
421
|
+
*/
|
|
422
|
+
export const metricQuery = z.object({
|
|
423
|
+
kind: z.string().optional().transform((k) => (k === 'memory' ? 'memory' : 'cpu')),
|
|
424
|
+
minutes: z
|
|
425
|
+
.unknown()
|
|
426
|
+
.optional()
|
|
427
|
+
.transform((v) => Math.min(Math.max(Number(v ?? 60) || 60, 1), 1440)),
|
|
428
|
+
});
|
|
429
|
+
export type MetricQuery = z.infer<typeof metricQuery>;
|
|
430
|
+
|
|
431
|
+
/** Serialized alert rule as returned by GET/PATCH /v1/alerts. */
|
|
432
|
+
export interface AlertRule {
|
|
433
|
+
id: number;
|
|
434
|
+
serviceId: number | null;
|
|
435
|
+
name: string;
|
|
436
|
+
metric: AlertMetric;
|
|
437
|
+
operator: '>' | '<';
|
|
438
|
+
threshold: number;
|
|
439
|
+
durationWindows: number;
|
|
440
|
+
enabled: boolean;
|
|
441
|
+
status: 'ok' | 'breaching' | 'firing';
|
|
442
|
+
lastValue: number | null;
|
|
443
|
+
/** When the collector last evaluated the rule (null = never evaluated). */
|
|
444
|
+
lastEvaluatedAt: string | null;
|
|
445
|
+
firedAt: string | null;
|
|
446
|
+
createdAt: string;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/** Input for creating an alert rule — defaults optional (zod applies them server-side). */
|
|
450
|
+
export type CreateAlertRuleInput = z.input<typeof alertRuleCreate>;
|
|
451
|
+
|
|
452
|
+
/** One audit-log entry (activity feed). `meta` carries request context (ip/ua). */
|
|
453
|
+
export interface ActivityEntry {
|
|
454
|
+
id: number;
|
|
455
|
+
userId: number | null;
|
|
456
|
+
userName?: string | null;
|
|
457
|
+
userEmail?: string | null;
|
|
458
|
+
action: string;
|
|
459
|
+
entity: string | null;
|
|
460
|
+
meta: Record<string, unknown> | null;
|
|
461
|
+
ts: string;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// ── Traefik proxy schemas & types ──────────────────────────────────────────
|
|
465
|
+
export interface TraefikStatus {
|
|
466
|
+
running: boolean;
|
|
467
|
+
version: string | null;
|
|
468
|
+
versionLatest: string | null;
|
|
469
|
+
outdated: boolean;
|
|
470
|
+
uptime: string | null;
|
|
471
|
+
ports: { http: number; https: number };
|
|
472
|
+
configDir: string;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
export interface TraefikCertificate {
|
|
476
|
+
domain: string;
|
|
477
|
+
expiresAt: string | null;
|
|
478
|
+
daysUntilExpiry: number | null;
|
|
479
|
+
issuer: string | null;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Richer certificate inventory (G-15). The basic
|
|
484
|
+
* `TraefikCertificate` shape is what the existing
|
|
485
|
+
* `/v1/traefik/certificates` returns; the inventory
|
|
486
|
+
* adds `sans` (parsed from the leaf cert's subject
|
|
487
|
+
* alt names when available), `notBefore`, `status`
|
|
488
|
+
* (valid / expiring-soon / expired), `autoRenew`
|
|
489
|
+
* (true when the host is owned by a Let's Encrypt
|
|
490
|
+
* solver), and `source` (`acme.json` | `static`).
|
|
491
|
+
*
|
|
492
|
+
* `daysToExpiry` is the same as `daysUntilExpiry`
|
|
493
|
+
* for backwards compat; new code should prefer
|
|
494
|
+
* `daysToExpiry` to match the column name in the
|
|
495
|
+
* alerting engine.
|
|
496
|
+
*/
|
|
497
|
+
export interface CertificateInventoryEntry {
|
|
498
|
+
host: string;
|
|
499
|
+
issuer: string | null;
|
|
500
|
+
subject: string | null;
|
|
501
|
+
sans: string[];
|
|
502
|
+
notBefore: string | null;
|
|
503
|
+
notAfter: string | null;
|
|
504
|
+
daysToExpiry: number | null;
|
|
505
|
+
status: 'valid' | 'expiring-soon' | 'expired' | 'unknown';
|
|
506
|
+
autoRenew: boolean;
|
|
507
|
+
source: 'acme.json' | 'static' | 'unknown';
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
export interface CertificateInventorySummary {
|
|
511
|
+
total: number;
|
|
512
|
+
valid: number;
|
|
513
|
+
expiringSoon: number;
|
|
514
|
+
expired: number;
|
|
515
|
+
/** Threshold used for `expiring-soon`; defaults to 30. */
|
|
516
|
+
expiringThresholdDays: number;
|
|
517
|
+
/** When the inventory was last rebuilt. */
|
|
518
|
+
fetchedAt: string;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
export interface CertificateInventoryReport {
|
|
522
|
+
certificates: CertificateInventoryEntry[];
|
|
523
|
+
summary: CertificateInventorySummary;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
export interface TraefikRouter {
|
|
527
|
+
name: string;
|
|
528
|
+
rule: string;
|
|
529
|
+
service: string;
|
|
530
|
+
entryPoints: string[];
|
|
531
|
+
tls: boolean;
|
|
532
|
+
middleware: string[];
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
export interface TraefikService {
|
|
536
|
+
name: string;
|
|
537
|
+
url: string;
|
|
538
|
+
loadBalancer: string;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
export interface TraefikMiddleware {
|
|
542
|
+
name: string;
|
|
543
|
+
type: string;
|
|
544
|
+
config: Record<string, unknown>;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
export interface TraefikInfo {
|
|
548
|
+
status: TraefikStatus;
|
|
549
|
+
certificates: TraefikCertificate[];
|
|
550
|
+
routers: TraefikRouter[];
|
|
551
|
+
services: TraefikService[];
|
|
552
|
+
middlewares: TraefikMiddleware[];
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// ── Database detail & credentials types ─────────────────────────────────────
|
|
556
|
+
export interface DatabaseDetail {
|
|
557
|
+
id: number;
|
|
558
|
+
projectId: number | null;
|
|
559
|
+
name: string;
|
|
560
|
+
slug: string;
|
|
561
|
+
engine: string;
|
|
562
|
+
version: string | null;
|
|
563
|
+
status: string;
|
|
564
|
+
host: string | null;
|
|
565
|
+
port: number | null;
|
|
566
|
+
username: string | null;
|
|
567
|
+
database: string | null;
|
|
568
|
+
connectionString: string | null;
|
|
569
|
+
containerName?: string | null;
|
|
570
|
+
containerId?: string | null;
|
|
571
|
+
volumeName?: string | null;
|
|
572
|
+
cpuShares?: number | null;
|
|
573
|
+
cpuLimitMilli?: number | null;
|
|
574
|
+
memLimitMb?: number | null;
|
|
575
|
+
webGuiEnabled?: boolean | null;
|
|
576
|
+
webGuiPort?: number | null;
|
|
577
|
+
extensions?: string[];
|
|
578
|
+
attachedServices: Array<{ id: number; name: string; slug: string }>;
|
|
579
|
+
/** 0.15.3: the node this database runs on; null or absent means the panel host. */
|
|
580
|
+
serverId?: number | null;
|
|
581
|
+
serverName?: string | null;
|
|
582
|
+
/** The node's last reachability probe; null when not probed or on the panel host. */
|
|
583
|
+
reachable?: boolean | null;
|
|
584
|
+
createdAt: string;
|
|
585
|
+
updatedAt: string;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
export interface DatabaseCredentials {
|
|
589
|
+
engine: string;
|
|
590
|
+
username: string | null;
|
|
591
|
+
password: string;
|
|
592
|
+
database: string | null;
|
|
593
|
+
internalHost: string | null;
|
|
594
|
+
internalPort: number | null;
|
|
595
|
+
connectionString: string;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
export const demoSeedResult = z.object({
|
|
599
|
+
ok: z.boolean(),
|
|
600
|
+
projectId: z.number(),
|
|
601
|
+
projectName: z.string(),
|
|
602
|
+
services: z.array(
|
|
603
|
+
z.object({
|
|
604
|
+
id: z.number(),
|
|
605
|
+
name: z.string(),
|
|
606
|
+
type: z.string(),
|
|
607
|
+
status: z.string(),
|
|
608
|
+
port: z.number().nullable(),
|
|
609
|
+
}),
|
|
610
|
+
),
|
|
611
|
+
database: z
|
|
612
|
+
.object({
|
|
613
|
+
id: z.number(),
|
|
614
|
+
name: z.string(),
|
|
615
|
+
engine: z.string(),
|
|
616
|
+
})
|
|
617
|
+
.nullable(),
|
|
618
|
+
});
|
|
619
|
+
export type DemoSeedResult = z.infer<typeof demoSeedResult>;
|
|
620
|
+
|