@ninedeploy/schemas 0.10.6 → 0.16.0

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