@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,363 @@
1
+ import { z } from 'zod';
2
+ import { envVarName, httpPath, repoBaseDir, repoRelativePath, slug } from './common.js';
3
+
4
+ /**
5
+ * `.ninedeploy` manifest schema — project-side declarative config for how
6
+ * NineDeploy should build, run and route a service. Committed to the repo,
7
+ * so it must NEVER carry secrets, tokens or connection strings; the loader
8
+ * enforces that with a regex scan (see apps/server/src/lib/secretScan.ts).
9
+ *
10
+ * Merge precedence at apply time: panel/DB > manifest > auto-detect. The
11
+ * manifest fills empty panel fields; an operator who explicitly set something
12
+ * in the panel wins.
13
+ */
14
+
15
+ // ── Runtime ────────────────────────────────────────────────────────────────
16
+ export const runtimeType = z.enum([
17
+ 'auto',
18
+ 'node',
19
+ 'python',
20
+ 'go',
21
+ 'ruby',
22
+ 'php',
23
+ 'java',
24
+ 'rust',
25
+ 'static',
26
+ ]);
27
+ export type RuntimeType = z.infer<typeof runtimeType>;
28
+
29
+ export const runtime = z
30
+ .object({
31
+ type: runtimeType.default('auto'),
32
+ /**
33
+ * Pinned language version. Format `<major>` or `<major>.<minor>` or
34
+ * `<major>.<minor>.<patch>`. For example `20`, `20.18`, `3.12`.
35
+ */
36
+ version: z
37
+ .string()
38
+ .regex(/^\d+(?:\.\d+){0,2}$/, 'must be a numeric version like 20 or 3.12')
39
+ .max(20)
40
+ .optional(),
41
+ })
42
+ .strict();
43
+ export type Runtime = z.infer<typeof runtime>;
44
+
45
+ // ── Build ──────────────────────────────────────────────────────────────────
46
+ export const build = z
47
+ .object({
48
+ install: z.string().min(1).max(500).optional(),
49
+ build: z.string().min(1).max(500).optional(),
50
+ start: z.string().min(1).max(500).optional(),
51
+ baseDir: repoBaseDir.optional(),
52
+ // F928: same "inside the repository" rule as baseDir above and the panel's
53
+ // service.build.dockerfilePath — a repo-committed `../../x` must not pass.
54
+ dockerfile: repoRelativePath.min(1).max(200).optional(),
55
+ })
56
+ .strict();
57
+ export type Build = z.infer<typeof build>;
58
+
59
+ // ── Run ────────────────────────────────────────────────────────────────────
60
+ export const restartPolicy = z
61
+ .union([
62
+ z.enum(['no', 'always', 'unless-stopped']),
63
+ z
64
+ .string()
65
+ .regex(
66
+ /^on-failure(?::[1-9]\d{0,2})?$/,
67
+ 'must be on-failure or on-failure:N (1-999, no leading zero)',
68
+ ),
69
+ ])
70
+ .optional();
71
+ /** Inferred type for the `run.restart` field, exported for consumers. */
72
+ export type RestartPolicy = z.infer<typeof restartPolicy>;
73
+
74
+ export const run = z
75
+ .object({
76
+ port: z.number().int().min(1).max(65535).optional(),
77
+ healthcheck: httpPath.optional(),
78
+ restart: restartPolicy,
79
+ })
80
+ .strict();
81
+ export type Run = z.infer<typeof run>;
82
+
83
+ // ── Static ─────────────────────────────────────────────────────────────────
84
+ const noTraversalSegments = (value: string): boolean =>
85
+ !value.split('/').some((segment) => segment === '..' || segment === '.');
86
+
87
+ const isRelativePath = (value: string): boolean => !value.startsWith('/');
88
+
89
+ export const staticConfig = z
90
+ .object({
91
+ spa: z.boolean().default(false),
92
+ root: z
93
+ .string()
94
+ .min(1)
95
+ .max(200)
96
+ .regex(/^[A-Za-z0-9._/-]+$/, 'must be a relative path with allowed characters')
97
+ .refine(noTraversalSegments, { message: 'path must not contain . or .. segments' })
98
+ .refine(isRelativePath, { message: 'path must be relative (no leading "/")' })
99
+ .optional(),
100
+ })
101
+ .strict();
102
+ export type StaticConfig = z.infer<typeof staticConfig>;
103
+
104
+ // ── Env ────────────────────────────────────────────────────────────────────
105
+ export const env = z
106
+ .object({
107
+ /** Required env var names. Values stay in the panel's env vault. */
108
+ required: z.array(envVarName.max(100)).max(100).default([]),
109
+ /** Managed-DB attach alias: env-key → attachment-env-key. The KEY side is
110
+ * validated too: free-form keys (`'MY VAR'`, `''`) could never be
111
+ * referenced by an attachment and corrupt the emitted YAML. */
112
+ aliases: z.record(envVarName, envVarName).optional(),
113
+ })
114
+ .strict();
115
+ export type Env = z.infer<typeof env>;
116
+
117
+ // ── Phases (Nixpacks ince ayar) ────────────────────────────────────────────
118
+ export const phases = z
119
+ .object({
120
+ setup: z
121
+ .object({
122
+ pkgs: z.array(z.string().min(1).max(100)).max(50).default([]),
123
+ })
124
+ .strict()
125
+ .optional(),
126
+ build: z
127
+ .object({
128
+ cmds: z.array(z.string().min(1).max(500)).max(20).default([]),
129
+ })
130
+ .strict()
131
+ .optional(),
132
+ })
133
+ .strict();
134
+ export type Phases = z.infer<typeof phases>;
135
+
136
+ // ── Resources ──────────────────────────────────────────────────────────────
137
+ export const resources = z
138
+ .object({
139
+ cpuShares: z.number().int().min(0).max(262144).optional(),
140
+ /** Hard CPU cap in millicores (500 = 0.5 cores); maps to docker --cpus. */
141
+ cpuLimitMilli: z.number().int().min(0).max(512_000).optional(),
142
+ memMb: z.number().int().min(0).max(1_048_576).optional(),
143
+ /** Horizontal replicas for docker services (Traefik load-balances). */
144
+ replicas: z.number().int().min(1).max(10).optional(),
145
+ })
146
+ .strict();
147
+ export type Resources = z.infer<typeof resources>;
148
+
149
+ // ── Hooks ──────────────────────────────────────────────────────────────────
150
+ export const hooks = z
151
+ .object({
152
+ preBuild: z.string().min(1).max(500).optional(),
153
+ postBuild: z.string().min(1).max(500).optional(),
154
+ preStop: z.string().min(1).max(500).optional(),
155
+ })
156
+ .strict();
157
+ export type Hooks = z.infer<typeof hooks>;
158
+
159
+ // ── Watch (monorepo) ──────────────────────────────────────────────────────
160
+ export const watch = z
161
+ .object({
162
+ paths: z.array(z.string().min(1).max(200)).max(50).default([]),
163
+ })
164
+ .strict();
165
+ export type Watch = z.infer<typeof watch>;
166
+
167
+ // ── Routes ─────────────────────────────────────────────────────────────────
168
+ export const rateLimit = z
169
+ .object({
170
+ average: z.number().int().min(0).max(100_000),
171
+ burst: z.number().int().min(0).max(100_000),
172
+ })
173
+ .strict();
174
+ export type RateLimit = z.infer<typeof rateLimit>;
175
+
176
+ export const route = z
177
+ .object({
178
+ host: z
179
+ .string()
180
+ .min(3)
181
+ .max(253)
182
+ .regex(
183
+ /^(?=.{1,253}$)(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)(?:\.(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?))*$/,
184
+ 'must be a valid hostname',
185
+ ),
186
+ path: z
187
+ .string()
188
+ .min(1)
189
+ .max(200)
190
+ .regex(/^\/.*$/, 'must start with "/"')
191
+ .default('/'),
192
+ ssl: z.boolean().default(true),
193
+ redirectWww: z.boolean().optional(),
194
+ headers: z.record(z.string().min(1).max(200), z.string().min(1).max(2000)).optional(),
195
+ ipAllowlist: z
196
+ .array(
197
+ z
198
+ .string()
199
+ .min(1)
200
+ .max(64)
201
+ .regex(/^[\d./]+$/, 'must be a CIDR like 1.2.3.4/32'),
202
+ )
203
+ .max(50)
204
+ .optional(),
205
+ rateLimit: rateLimit.optional(),
206
+ })
207
+ .strict();
208
+ export type Route = z.infer<typeof route>;
209
+
210
+ // ── Previews ──────────────────────────────────────────────────────────────
211
+ export const previews = z
212
+ .object({
213
+ enabled: z.boolean().default(false),
214
+ /**
215
+ * Hostname template. `{n}` is replaced by the PR number. Example:
216
+ * `pr-{n}-{{slug}}.{{domain}}` (the panel also requires `{{slug}}` and its own
217
+ * wildcard zone, r511). Must contain `{n}` when enabled.
218
+ */
219
+ pattern: z
220
+ .string()
221
+ .min(1)
222
+ .max(200)
223
+ .regex(
224
+ /^[A-Za-z0-9._/{}*-]+$/,
225
+ 'must be a hostname template (letters, digits, dot, dash, underscore, slash, brace, asterisk)',
226
+ )
227
+ .refine(noTraversalSegments, { message: 'pattern must not contain . or .. segments' })
228
+ .optional(),
229
+ maxActive: z.number().int().min(1).max(50).default(5),
230
+ autoDestroyOnClose: z.boolean().default(true),
231
+ })
232
+ .strict()
233
+ .refine((p) => !p.enabled || (p.pattern != null && p.pattern.includes('{n}')), {
234
+ message: 'previews.pattern must contain {n} when previews.enabled is true',
235
+ path: ['pattern'],
236
+ });
237
+ export type Previews = z.infer<typeof previews>;
238
+
239
+ // ── Volume ────────────────────────────────────────────────────────────────
240
+ export const volumeBackups = z
241
+ .object({
242
+ schedule: z
243
+ .string()
244
+ .min(1)
245
+ .max(100)
246
+ .regex(
247
+ /^[\d*/,\-A-Za-z\s]+$/,
248
+ 'must look like a cron expression (digits, *, /, comma, dash, letters, space)',
249
+ ),
250
+ retention: z.number().int().min(1).max(365).default(7),
251
+ })
252
+ .strict();
253
+ export type VolumeBackups = z.infer<typeof volumeBackups>;
254
+
255
+ export const volume = z
256
+ .object({
257
+ mount: z
258
+ .string()
259
+ .min(1)
260
+ .max(200)
261
+ .regex(/^[A-Za-z0-9._/-]+$/, 'must be a path inside the container')
262
+ .refine(noTraversalSegments, { message: 'mount must not contain . or .. segments' })
263
+ .optional(),
264
+ backups: volumeBackups.optional(),
265
+ })
266
+ .strict();
267
+ export type Volume = z.infer<typeof volume>;
268
+
269
+ // ── Database (managed DB attach hint) ─────────────────────────────────────
270
+ export const database = z
271
+ .object({
272
+ ref: slug,
273
+ env: envVarName,
274
+ })
275
+ .strict();
276
+ export type Database = z.infer<typeof database>;
277
+
278
+ // ── Network ───────────────────────────────────────────────────────────────
279
+ export const network = z
280
+ .object({
281
+ publishPort: z.number().int().min(1).max(65535).optional(),
282
+ aliases: z.array(z.string().min(1).max(100).regex(/^[A-Za-z0-9._-]+$/)).max(20).default([]),
283
+ })
284
+ .strict();
285
+ export type Network = z.infer<typeof network>;
286
+
287
+ // ── Notifications (channel-name references) ──────────────────────────────
288
+ export const notifications = z
289
+ .object({
290
+ onDeploy: z.array(z.string().min(1).max(100)).max(20).default([]),
291
+ onFailure: z.array(z.string().min(1).max(100)).max(20).default([]),
292
+ onAlert: z.array(z.string().min(1).max(100)).max(20).default([]),
293
+ })
294
+ .strict();
295
+ export type Notifications = z.infer<typeof notifications>;
296
+
297
+ // ── Alerts ────────────────────────────────────────────────────────────────
298
+ export const alertWhen = z.enum([
299
+ 'deployFailed',
300
+ 'restartLoop',
301
+ 'highMemory',
302
+ 'highCpu',
303
+ 'certExpiry',
304
+ ]);
305
+ /** Inferred type for the `alerts[].when` field, exported for consumers. */
306
+ export type AlertWhen = z.infer<typeof alertWhen>;
307
+
308
+ export const alert = z
309
+ .object({
310
+ when: alertWhen,
311
+ channel: z.string().min(1).max(100),
312
+ thresholdPct: z.number().int().min(1).max(100).optional(),
313
+ })
314
+ .strict()
315
+ .refine(
316
+ (a) =>
317
+ a.when !== 'highMemory' && a.when !== 'highCpu' ? true : a.thresholdPct != null,
318
+ {
319
+ message: 'thresholdPct is required for highMemory/highCpu alerts',
320
+ path: ['thresholdPct'],
321
+ },
322
+ );
323
+ export type Alert = z.infer<typeof alert>;
324
+
325
+ // ── Top-level manifest ────────────────────────────────────────────────────
326
+ export const ninedeployManifest = z
327
+ .object({
328
+ version: z.literal('1'),
329
+ runtime: runtime.optional(),
330
+ build: build.optional(),
331
+ run: run.optional(),
332
+ static: staticConfig.optional(),
333
+ env: env.optional(),
334
+ phases: phases.optional(),
335
+ resources: resources.optional(),
336
+ hooks: hooks.optional(),
337
+ watch: watch.optional(),
338
+ routes: z.array(route).max(50).optional(),
339
+ previews: previews.optional(),
340
+ volume: volume.optional(),
341
+ database: database.optional(),
342
+ network: network.optional(),
343
+ notifications: notifications.optional(),
344
+ alerts: z.array(alert).max(20).optional(),
345
+ })
346
+ .strict();
347
+ export type NinedeployManifest = z.infer<typeof ninedeployManifest>;
348
+
349
+ /**
350
+ * The list of filenames the loader checks, in priority order. The first one
351
+ * found in the repo root is used.
352
+ */
353
+ export const NINEDEPLOY_MANIFEST_FILENAMES = [
354
+ '.ninedeploy',
355
+ '.ninedeploy.yml',
356
+ '.ninedeploy.yaml',
357
+ 'ninedeploy.yml',
358
+ 'ninedeploy.yaml',
359
+ ] as const;
360
+ export type NinedeployManifestFilename = (typeof NINEDEPLOY_MANIFEST_FILENAMES)[number];
361
+
362
+ /** Maximum allowed size of a `.ninedeploy` file (bytes). Larger files are refused. */
363
+ export const NINEDEPLOY_MANIFEST_MAX_BYTES = 16 * 1024;
@@ -0,0 +1,90 @@
1
+ import { z } from 'zod';
2
+
3
+ // ── Panel self-backup (0.12) ───────────────────────────────────────────────
4
+ /** The recovery passphrase seals every panel backup; it is the ONE secret an
5
+ * operator must keep off this server to restore on a new one. */
6
+ export const PANEL_BACKUP_PASSPHRASE_MIN = 12;
7
+ export const PANEL_BACKUP_RETAIN_MIN = 1;
8
+ export const PANEL_BACKUP_RETAIN_MAX = 365;
9
+ export const PANEL_BACKUP_DEFAULT_CRON = '0 3 * * *';
10
+ export const PANEL_BACKUP_DEFAULT_RETAIN = 7;
11
+
12
+ /**
13
+ * PUT /v1/system/panel-backup — every field optional; omitted keeps the stored
14
+ * value. `passphrase` is write-only (omitted = keep the stored one). The cron
15
+ * expression is validated with croner (5-part) at the route, the same rule
16
+ * scheduled jobs use; the schema package has no cron dependency.
17
+ */
18
+ export const panelBackupSettingsPatch = z
19
+ .object({
20
+ enabled: z.boolean().optional(),
21
+ cron: z.string().trim().min(1).max(120).optional(),
22
+ /** A `backup_destinations` id; null clears it (and requires enabled=false). */
23
+ destinationId: z.number().int().positive().nullable().optional(),
24
+ retain: z.number().int().min(PANEL_BACKUP_RETAIN_MIN).max(PANEL_BACKUP_RETAIN_MAX).optional(),
25
+ passphrase: z
26
+ .string()
27
+ .min(PANEL_BACKUP_PASSPHRASE_MIN, `The recovery passphrase needs at least ${PANEL_BACKUP_PASSPHRASE_MIN} characters`)
28
+ .max(1024)
29
+ .optional(),
30
+ })
31
+ .strict();
32
+ export type PanelBackupSettingsPatch = z.infer<typeof panelBackupSettingsPatch>;
33
+
34
+ /**
35
+ * POST /v1/system/panel-backup/restore — destructive: replaces this panel's
36
+ * database, master key, .env and Traefik config. `confirm` must repeat the
37
+ * object's file name (the part of `key` after the last `/`).
38
+ */
39
+ export const panelBackupRestore = z
40
+ .object({
41
+ /** Destination to read from; omitted = the configured one. On a fresh
42
+ * install, add the old bucket as a destination and name it here. */
43
+ destinationId: z.number().int().positive().optional(),
44
+ key: z.string().min(1).max(1024),
45
+ passphrase: z.string().min(1).max(1024),
46
+ confirm: z.string().max(1024),
47
+ })
48
+ .strict();
49
+ export type PanelBackupRestore = z.infer<typeof panelBackupRestore>;
50
+
51
+ /** Outcome of the latest run, as `GET /v1/system/panel-backup` reports it. */
52
+ export interface PanelBackupRunState {
53
+ status: 'running' | 'completed' | 'failed';
54
+ trigger: 'schedule' | 'manual';
55
+ startedAt: string;
56
+ finishedAt: string | null;
57
+ key: string | null;
58
+ sizeBytes: number | null;
59
+ error: string | null;
60
+ /** Retention problems that did not fail the run (the new backup is safe). */
61
+ warning: string | null;
62
+ }
63
+
64
+ /** `GET /v1/system/panel-backup`. */
65
+ export interface PanelBackupStatus {
66
+ settings: {
67
+ enabled: boolean;
68
+ cron: string;
69
+ destinationId: number | null;
70
+ retain: number;
71
+ hasPassphrase: boolean;
72
+ };
73
+ /** True while a run (or a restore) is executing in this process. */
74
+ running: boolean;
75
+ lastRun: PanelBackupRunState | null;
76
+ lastSuccessAt: string | null;
77
+ /** Next scheduled run (ISO) when enabled and the cron is valid. */
78
+ nextRunAt: string | null;
79
+ /** The master key comes from NINEDEPLOY_MASTER_KEY(S), not the master.key
80
+ * file — backups then do NOT contain it and it must be kept separately. */
81
+ masterKeyFromEnv: boolean;
82
+ }
83
+
84
+ /** One panel backup object in the destination bucket. */
85
+ export interface PanelBackupObject {
86
+ key: string;
87
+ name: string;
88
+ sizeBytes: number;
89
+ lastModified: string;
90
+ }
package/src/project.ts ADDED
@@ -0,0 +1,42 @@
1
+ import { z } from 'zod';
2
+ import { slug } from './common.js';
3
+
4
+ /**
5
+ * Project contracts — the single-level grouping layer over services and
6
+ * databases (Easypanel-style). Projects own nothing directly; deleting one
7
+ * detaches its resources (projectId → null), it never cascades.
8
+ */
9
+
10
+ export const createProject = z.object({
11
+ name: z.string().trim().min(2).max(63),
12
+ slug: slug.optional(),
13
+ description: z.string().trim().max(500).nullable().optional(),
14
+ workspaceId: z.number().int().positive().nullable().optional(),
15
+ });
16
+ export type CreateProject = z.infer<typeof createProject>;
17
+ export type CreateProjectInput = CreateProject;
18
+
19
+ export const projectPatch = z
20
+ .object({
21
+ name: z.string().trim().min(2).max(63).optional(),
22
+ description: z.string().trim().max(500).nullable().optional(),
23
+ workspaceId: z.number().int().positive().nullable().optional(),
24
+ })
25
+ .refine((v) => Object.keys(v).length > 0, { message: 'nothing to update' });
26
+ export type ProjectPatch = z.infer<typeof projectPatch>;
27
+ export type ProjectPatchInput = ProjectPatch;
28
+
29
+ /** Serialized project as returned by /v1/projects. */
30
+ export interface ProjectEntry {
31
+ id: number;
32
+ workspaceId?: number | null;
33
+ /** Display name of the owning workspace. Resolved server-side. */
34
+ workspaceName?: string | null;
35
+ name: string;
36
+ slug: string;
37
+ description: string | null;
38
+ serviceCount: number;
39
+ databaseCount: number;
40
+ createdAt: string;
41
+ updatedAt: string;
42
+ }
@@ -0,0 +1,139 @@
1
+ import { z } from 'zod';
2
+
3
+ // ── Proxy management (0.14) ────────────────────────────────────────────────
4
+ // Request and response shapes for `/v1/traefik/custom-config` and
5
+ // `/v1/traefik/certificates/custom`. Operator only. These are SHAPE checks:
6
+ // the YAML rules (allowed sections, `custom-` names, entry points, refused
7
+ // keys) and the certificate proof (key matches leaf, key type, expiry) run on
8
+ // the server. No response shape carries a private key. Design: DESIGN.md §2.
9
+
10
+ /** 256 KiB. Measured in UTF-8 bytes on the server; this caps the string. */
11
+ export const TRAEFIK_CUSTOM_CONFIG_MAX_BYTES = 256 * 1024;
12
+ export const CUSTOM_CERTIFICATE_CHAIN_MAX_BYTES = 64 * 1024;
13
+ export const CUSTOM_CERTIFICATE_KEY_MAX_BYTES = 16 * 1024;
14
+ export const CUSTOM_CERTIFICATES_MAX = 100;
15
+
16
+ // ── Custom dynamic config ──────────────────────────────────────────────────
17
+
18
+ /** PUT /v1/traefik/custom-config and POST …/validate. */
19
+ export const traefikCustomConfigInput = z.object({
20
+ content: z.string().min(1).max(TRAEFIK_CUSTOM_CONFIG_MAX_BYTES),
21
+ });
22
+ export type TraefikCustomConfigInput = z.infer<typeof traefikCustomConfigInput>;
23
+
24
+ /** One validation finding; `path` is a dotted YAML path (`http.routers.custom-a.rule`). */
25
+ export const traefikCustomConfigIssue = z.object({
26
+ path: z.string(),
27
+ message: z.string(),
28
+ });
29
+ export type TraefikCustomConfigIssue = z.infer<typeof traefikCustomConfigIssue>;
30
+
31
+ /** POST /v1/traefik/custom-config/validate. No state change. */
32
+ export const traefikCustomConfigValidation = z.object({
33
+ ok: z.boolean(),
34
+ errors: z.array(traefikCustomConfigIssue),
35
+ warnings: z.array(traefikCustomConfigIssue),
36
+ });
37
+ export type TraefikCustomConfigValidation = z.infer<typeof traefikCustomConfigValidation>;
38
+
39
+ export const traefikCustomConfigState = z.enum(['none', 'applied', 'rejected']);
40
+ export type TraefikCustomConfigState = z.infer<typeof traefikCustomConfigState>;
41
+
42
+ /** GET /v1/traefik/custom-config. `content: null` = none saved. */
43
+ export const traefikCustomConfig = z.object({
44
+ content: z.string().nullable(),
45
+ sha256: z.string().nullable(),
46
+ updatedAt: z.string().nullable(),
47
+ /** User id of the last saver. */
48
+ updatedBy: z.number().int().nullable(),
49
+ status: traefikCustomConfigState,
50
+ lastError: z.string().nullable(),
51
+ });
52
+ export type TraefikCustomConfig = z.infer<typeof traefikCustomConfig>;
53
+
54
+ // ── Custom certificates ────────────────────────────────────────────────────
55
+
56
+ const certificatePem = z
57
+ .string()
58
+ .trim()
59
+ .min(1)
60
+ .max(CUSTOM_CERTIFICATE_CHAIN_MAX_BYTES)
61
+ .regex(/-----BEGIN CERTIFICATE-----[\s\S]+-----END CERTIFICATE-----/, 'certPem must be a PEM certificate chain');
62
+
63
+ /** Unencrypted PKCS#8, PKCS#1 or SEC1 key. Passphrase-protected keys are refused. */
64
+ const privateKeyPem = z
65
+ .string()
66
+ .trim()
67
+ .min(1)
68
+ .max(CUSTOM_CERTIFICATE_KEY_MAX_BYTES)
69
+ .regex(
70
+ /^-----BEGIN (RSA |EC )?PRIVATE KEY-----\r?\n[\s\S]+\r?\n-----END \1PRIVATE KEY-----$/,
71
+ 'keyPem must be an unencrypted PEM private key',
72
+ );
73
+
74
+ const certificateName = z.string().trim().min(1).max(100);
75
+
76
+ /** POST /v1/traefik/certificates/custom → 201. */
77
+ export const customCertificateUpload = z.object({
78
+ name: certificateName,
79
+ certPem: certificatePem,
80
+ keyPem: privateKeyPem,
81
+ });
82
+ export type CustomCertificateUpload = z.infer<typeof customCertificateUpload>;
83
+
84
+ /** PUT /v1/traefik/certificates/custom/:certId. An omitted name keeps the stored one. */
85
+ export const customCertificateReplace = z.object({
86
+ name: certificateName.optional(),
87
+ certPem: certificatePem,
88
+ keyPem: privateKeyPem,
89
+ });
90
+ export type CustomCertificateReplace = z.infer<typeof customCertificateReplace>;
91
+
92
+ /** One element of GET /v1/traefik/certificates/custom. */
93
+ export const customCertificate = z.object({
94
+ id: z.number().int(),
95
+ name: z.string(),
96
+ hostnames: z.array(z.string()),
97
+ subject: z.string().nullable(),
98
+ issuer: z.string().nullable(),
99
+ notBefore: z.string(),
100
+ notAfter: z.string(),
101
+ fingerprint: z.string(),
102
+ expired: z.boolean(),
103
+ coveredDomains: z.array(
104
+ z.object({
105
+ id: z.number().int(),
106
+ hostname: z.string(),
107
+ serviceId: z.number().int(),
108
+ }),
109
+ ),
110
+ });
111
+ export type CustomCertificate = z.infer<typeof customCertificate>;
112
+
113
+ // ── Write results (0.14 T2, additive) ──────────────────────────────────────
114
+
115
+ /** PUT /v1/traefik/custom-config → 200. Router-level findings come back as warnings. */
116
+ export const traefikCustomConfigApplied = z.object({
117
+ ok: z.literal(true),
118
+ status: z.literal('applied'),
119
+ sha256: z.string(),
120
+ warnings: z.array(traefikCustomConfigIssue),
121
+ });
122
+ export type TraefikCustomConfigApplied = z.infer<typeof traefikCustomConfigApplied>;
123
+
124
+ /**
125
+ * The 400 / 422 body of PUT /v1/traefik/custom-config: the error envelope
126
+ * plus the findings (`errors` empty only on a 503).
127
+ */
128
+ export const traefikCustomConfigRefusal = z.object({
129
+ error: z.object({ code: z.string(), message: z.string() }),
130
+ errors: z.array(traefikCustomConfigIssue).default([]),
131
+ warnings: z.array(traefikCustomConfigIssue).default([]),
132
+ });
133
+ export type TraefikCustomConfigRefusal = z.infer<typeof traefikCustomConfigRefusal>;
134
+
135
+ /** POST (201) and PUT of /v1/traefik/certificates/custom: the listing entry plus size warnings. */
136
+ export const customCertificateSaved = customCertificate.extend({
137
+ warnings: z.array(z.string()),
138
+ });
139
+ export type CustomCertificateSaved = z.infer<typeof customCertificateSaved>;