@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.
Files changed (113) hide show
  1. package/LICENSE +21 -21
  2. package/dist/accessGrants.d.ts +156 -0
  3. package/dist/accessGrants.d.ts.map +1 -0
  4. package/dist/accessGrants.js +100 -0
  5. package/dist/accessGrants.js.map +1 -0
  6. package/dist/agentCommand.d.ts.map +1 -1
  7. package/dist/agentCommand.js +11 -1
  8. package/dist/agentCommand.js.map +1 -1
  9. package/dist/auth.d.ts +53 -0
  10. package/dist/auth.d.ts.map +1 -1
  11. package/dist/auth.js +26 -0
  12. package/dist/auth.js.map +1 -1
  13. package/dist/backupPolicy.d.ts +37 -0
  14. package/dist/backupPolicy.d.ts.map +1 -0
  15. package/dist/backupPolicy.js +50 -0
  16. package/dist/backupPolicy.js.map +1 -0
  17. package/dist/common.d.ts +21 -5
  18. package/dist/common.d.ts.map +1 -1
  19. package/dist/common.js +35 -6
  20. package/dist/common.js.map +1 -1
  21. package/dist/databaseImport.d.ts +145 -0
  22. package/dist/databaseImport.d.ts.map +1 -0
  23. package/dist/databaseImport.js +115 -0
  24. package/dist/databaseImport.js.map +1 -0
  25. package/dist/doctor.d.ts +16 -0
  26. package/dist/doctor.d.ts.map +1 -1
  27. package/dist/doctor.js +25 -0
  28. package/dist/doctor.js.map +1 -1
  29. package/dist/githubApp.d.ts +236 -0
  30. package/dist/githubApp.d.ts.map +1 -0
  31. package/dist/githubApp.js +223 -0
  32. package/dist/githubApp.js.map +1 -0
  33. package/dist/index.d.ts +11 -0
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +11 -0
  36. package/dist/index.js.map +1 -1
  37. package/dist/kernel.d.ts +5 -2
  38. package/dist/kernel.d.ts.map +1 -1
  39. package/dist/kernel.js +11 -2
  40. package/dist/kernel.js.map +1 -1
  41. package/dist/management.d.ts +46 -2
  42. package/dist/management.d.ts.map +1 -1
  43. package/dist/management.js +68 -15
  44. package/dist/management.js.map +1 -1
  45. package/dist/multiNode.d.ts +326 -0
  46. package/dist/multiNode.d.ts.map +1 -0
  47. package/dist/multiNode.js +296 -0
  48. package/dist/multiNode.js.map +1 -0
  49. package/dist/ninedeployManifest.d.ts.map +1 -1
  50. package/dist/ninedeployManifest.js +6 -3
  51. package/dist/ninedeployManifest.js.map +1 -1
  52. package/dist/panelBackup.d.ts +73 -0
  53. package/dist/panelBackup.d.ts.map +1 -0
  54. package/dist/panelBackup.js +45 -0
  55. package/dist/panelBackup.js.map +1 -0
  56. package/dist/proxyConfig.d.ts +132 -0
  57. package/dist/proxyConfig.d.ts.map +1 -0
  58. package/dist/proxyConfig.js +105 -0
  59. package/dist/proxyConfig.js.map +1 -0
  60. package/dist/publicAccess.d.ts +64 -0
  61. package/dist/publicAccess.d.ts.map +1 -0
  62. package/dist/publicAccess.js +63 -0
  63. package/dist/publicAccess.js.map +1 -0
  64. package/dist/secretProviders.d.ts +119 -0
  65. package/dist/secretProviders.d.ts.map +1 -0
  66. package/dist/secretProviders.js +130 -0
  67. package/dist/secretProviders.js.map +1 -0
  68. package/dist/secretScan.js +1 -1
  69. package/dist/service.d.ts +235 -17
  70. package/dist/service.d.ts.map +1 -1
  71. package/dist/service.js +331 -26
  72. package/dist/service.js.map +1 -1
  73. package/dist/terminals.d.ts +284 -0
  74. package/dist/terminals.d.ts.map +1 -0
  75. package/dist/terminals.js +177 -0
  76. package/dist/terminals.js.map +1 -0
  77. package/dist/traffic.d.ts +276 -0
  78. package/dist/traffic.d.ts.map +1 -0
  79. package/dist/traffic.js +103 -0
  80. package/dist/traffic.js.map +1 -0
  81. package/dist/workspaces.d.ts +19 -1
  82. package/dist/workspaces.d.ts.map +1 -1
  83. package/dist/workspaces.js +26 -0
  84. package/dist/workspaces.js.map +1 -1
  85. package/package.json +5 -4
  86. package/src/accessGrants.ts +117 -0
  87. package/src/agentCommand.ts +67 -0
  88. package/src/ai.ts +32 -0
  89. package/src/auth.ts +323 -0
  90. package/src/backupPolicy.ts +55 -0
  91. package/src/common.ts +179 -0
  92. package/src/databaseImport.ts +138 -0
  93. package/src/doctor.ts +161 -0
  94. package/src/environments.ts +23 -0
  95. package/src/githubApp.ts +268 -0
  96. package/src/index.ts +30 -0
  97. package/src/insights.ts +78 -0
  98. package/src/kernel.ts +180 -0
  99. package/src/labels.ts +86 -0
  100. package/src/management.ts +620 -0
  101. package/src/multiNode.ts +356 -0
  102. package/src/ninedeployManifest.ts +363 -0
  103. package/src/panelBackup.ts +90 -0
  104. package/src/project.ts +42 -0
  105. package/src/proxyConfig.ts +139 -0
  106. package/src/publicAccess.ts +80 -0
  107. package/src/runtimeVersions.ts +232 -0
  108. package/src/secretProviders.ts +159 -0
  109. package/src/secretScan.ts +169 -0
  110. package/src/service.ts +1310 -0
  111. package/src/terminals.ts +212 -0
  112. package/src/traffic.ts +130 -0
  113. 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
+