@ninedeploy/schemas 0.10.5 → 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.
- 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 +325 -0
- package/dist/multiNode.d.ts.map +1 -0
- package/dist/multiNode.js +294 -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 +27 -17
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +76 -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 +354 -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 +1044 -0
- package/src/terminals.ts +212 -0
- package/src/traffic.ts +130 -0
- package/src/workspaces.ts +215 -0
package/src/githubApp.ts
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { id, providerBaseUrl } from './common.js';
|
|
3
|
+
import { webhookCreate } from './management.js';
|
|
4
|
+
|
|
5
|
+
// ── GitHub App (0.13) ──────────────────────────────────────────────────────
|
|
6
|
+
// Request and response shapes for `/v1/github-apps/*` and
|
|
7
|
+
// `/v1/services/:id/github*`. Secrets (private key, webhook secret, client
|
|
8
|
+
// secret) only ever appear in REQUEST shapes; no response shape has a field
|
|
9
|
+
// that could carry one.
|
|
10
|
+
|
|
11
|
+
export const GITHUB_WEB_BASE_URL = 'https://github.com';
|
|
12
|
+
export const GITHUB_API_BASE_URL = 'https://api.github.com';
|
|
13
|
+
|
|
14
|
+
/** Mirrors `githubTokenScope` in `@ninedeploy/db`. */
|
|
15
|
+
export const githubTokenScope = z.enum(['repository', 'installation']);
|
|
16
|
+
export type GithubTokenScope = z.infer<typeof githubTokenScope>;
|
|
17
|
+
|
|
18
|
+
/** Mirrors `githubRepositorySelection` in `@ninedeploy/db`. */
|
|
19
|
+
export const githubRepositorySelection = z.enum(['all', 'selected']);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A numeric GitHub id (App, installation, repository, account). GitHub ids
|
|
23
|
+
* are positive and well below 2^53; `int()` keeps them safe integers.
|
|
24
|
+
*/
|
|
25
|
+
export const githubNumericId = z.number().int().positive();
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A GitHub user or organization login. It is interpolated into the manifest
|
|
29
|
+
* `postUrl` path, so the character set is pinned to GitHub's own rule
|
|
30
|
+
* (alphanumerics and single inner hyphens, at most 39 characters).
|
|
31
|
+
*/
|
|
32
|
+
export const githubLogin = z
|
|
33
|
+
.string()
|
|
34
|
+
.trim()
|
|
35
|
+
.regex(/^[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}$/, 'invalid GitHub login');
|
|
36
|
+
|
|
37
|
+
/** `owner/repo`, as GitHub reports `full_name`. */
|
|
38
|
+
export const githubRepoFullName = z
|
|
39
|
+
.string()
|
|
40
|
+
.max(200)
|
|
41
|
+
.regex(/^[A-Za-z0-9][A-Za-z0-9-]*\/[A-Za-z0-9._-]+$/, 'expected owner/repo');
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* A PEM-encoded private key as GitHub hands it out (PKCS#1 `RSA PRIVATE KEY`)
|
|
45
|
+
* or re-encoded as PKCS#8 (`PRIVATE KEY`). This is a shape check only; the
|
|
46
|
+
* server proves the key by signing a JWT and calling `GET /app`.
|
|
47
|
+
*/
|
|
48
|
+
export const githubPrivateKeyPem = z
|
|
49
|
+
.string()
|
|
50
|
+
.trim()
|
|
51
|
+
.max(16384)
|
|
52
|
+
.regex(
|
|
53
|
+
/^-----BEGIN (RSA )?PRIVATE KEY-----\r?\n[\s\S]+\r?\n-----END \1PRIVATE KEY-----$/,
|
|
54
|
+
'privateKey must be a PEM private key (-----BEGIN RSA PRIVATE KEY-----)',
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const appName = z.string().trim().min(1).max(100);
|
|
58
|
+
const secretText = z.string().min(1).max(1024);
|
|
59
|
+
const clientId = z.string().trim().min(1).max(255);
|
|
60
|
+
|
|
61
|
+
const atLeastOneField = (value: Record<string, unknown>): boolean =>
|
|
62
|
+
Object.values(value).some((v) => v !== undefined);
|
|
63
|
+
|
|
64
|
+
// ── Manifest flow ──────────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* POST /v1/github-apps/manifest. `org` is required for an organization-owned
|
|
68
|
+
* App and refused for a personal one. `apiBaseUrl` may be omitted: the server
|
|
69
|
+
* derives it (`https://api.github.com` for github.com, `<web>/api/v3` for
|
|
70
|
+
* GitHub Enterprise Server).
|
|
71
|
+
*/
|
|
72
|
+
export const githubAppManifestRequest = z
|
|
73
|
+
.object({
|
|
74
|
+
target: z.enum(['user', 'org']),
|
|
75
|
+
org: githubLogin.optional(),
|
|
76
|
+
webBaseUrl: providerBaseUrl.default(GITHUB_WEB_BASE_URL),
|
|
77
|
+
apiBaseUrl: providerBaseUrl.optional(),
|
|
78
|
+
/** Also request `checks: write` (the Checks API itself is not used in 0.13). */
|
|
79
|
+
checks: z.boolean().default(false),
|
|
80
|
+
})
|
|
81
|
+
.refine((v) => v.target !== 'org' || v.org !== undefined, {
|
|
82
|
+
message: 'org is required when target is "org"',
|
|
83
|
+
path: ['org'],
|
|
84
|
+
})
|
|
85
|
+
.refine((v) => v.target !== 'user' || v.org === undefined, {
|
|
86
|
+
message: 'org is only valid when target is "org"',
|
|
87
|
+
path: ['org'],
|
|
88
|
+
});
|
|
89
|
+
export type GithubAppManifestRequest = z.input<typeof githubAppManifestRequest>;
|
|
90
|
+
|
|
91
|
+
/** The SPA submits `manifest` (JSON-encoded) as a form field to `postUrl`. */
|
|
92
|
+
export const githubAppManifestResponse = z.object({
|
|
93
|
+
postUrl: z.string(),
|
|
94
|
+
manifest: z.record(z.string(), z.unknown()),
|
|
95
|
+
state: z.string(),
|
|
96
|
+
});
|
|
97
|
+
export type GithubAppManifestResponse = z.infer<typeof githubAppManifestResponse>;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* POST /v1/github-apps/manifest/complete. `code` is interpolated into
|
|
101
|
+
* `/app-manifests/<code>/conversions`, so it is restricted to URL-path-safe
|
|
102
|
+
* characters.
|
|
103
|
+
*/
|
|
104
|
+
export const githubAppManifestComplete = z.object({
|
|
105
|
+
code: z
|
|
106
|
+
.string()
|
|
107
|
+
.min(1)
|
|
108
|
+
.max(256)
|
|
109
|
+
.regex(/^[A-Za-z0-9_-]+$/, 'invalid manifest code'),
|
|
110
|
+
state: z.string().min(1).max(4096),
|
|
111
|
+
});
|
|
112
|
+
export type GithubAppManifestComplete = z.infer<typeof githubAppManifestComplete>;
|
|
113
|
+
|
|
114
|
+
// ── Manual entry (GHES or an existing App) ─────────────────────────────────
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* POST /v1/github-apps. Omit both base URLs for github.com; for GitHub
|
|
118
|
+
* Enterprise Server give both. Giving only one is refused, so a GHES App's JWT
|
|
119
|
+
* is never sent to api.github.com by default. An omitted `webhookSecret` is
|
|
120
|
+
* generated by the server and registered on the App.
|
|
121
|
+
*/
|
|
122
|
+
export const githubAppCreate = z
|
|
123
|
+
.object({
|
|
124
|
+
name: appName,
|
|
125
|
+
appId: githubNumericId,
|
|
126
|
+
privateKey: githubPrivateKeyPem,
|
|
127
|
+
webhookSecret: secretText.optional(),
|
|
128
|
+
clientId: clientId.optional(),
|
|
129
|
+
clientSecret: secretText.optional(),
|
|
130
|
+
webBaseUrl: providerBaseUrl.optional(),
|
|
131
|
+
apiBaseUrl: providerBaseUrl.optional(),
|
|
132
|
+
})
|
|
133
|
+
.refine((v) => (v.webBaseUrl === undefined) === (v.apiBaseUrl === undefined), {
|
|
134
|
+
message: 'webBaseUrl and apiBaseUrl go together; omit both for github.com',
|
|
135
|
+
path: ['apiBaseUrl'],
|
|
136
|
+
})
|
|
137
|
+
.transform((v) => ({
|
|
138
|
+
...v,
|
|
139
|
+
webBaseUrl: v.webBaseUrl ?? GITHUB_WEB_BASE_URL,
|
|
140
|
+
apiBaseUrl: v.apiBaseUrl ?? GITHUB_API_BASE_URL,
|
|
141
|
+
}));
|
|
142
|
+
export type GithubAppCreate = z.input<typeof githubAppCreate>;
|
|
143
|
+
|
|
144
|
+
/** PATCH /v1/github-apps/:id. `null` clears the optional OAuth client fields. */
|
|
145
|
+
export const githubAppPatch = z
|
|
146
|
+
.object({
|
|
147
|
+
name: appName.optional(),
|
|
148
|
+
clientId: clientId.nullable().optional(),
|
|
149
|
+
clientSecret: secretText.nullable().optional(),
|
|
150
|
+
})
|
|
151
|
+
.refine(atLeastOneField, { message: 'nothing to update' });
|
|
152
|
+
export type GithubAppPatch = z.infer<typeof githubAppPatch>;
|
|
153
|
+
|
|
154
|
+
/** PUT /v1/github-apps/:id/private-key. */
|
|
155
|
+
export const githubAppPrivateKey = z.object({ privateKey: githubPrivateKeyPem });
|
|
156
|
+
export type GithubAppPrivateKey = z.infer<typeof githubAppPrivateKey>;
|
|
157
|
+
|
|
158
|
+
// ── Views (responses: metadata only, never a secret) ───────────────────────
|
|
159
|
+
|
|
160
|
+
export const githubApp = z.object({
|
|
161
|
+
id: z.number().int(),
|
|
162
|
+
name: z.string(),
|
|
163
|
+
appId: z.number().int(),
|
|
164
|
+
slug: z.string().nullable(),
|
|
165
|
+
clientId: z.string().nullable(),
|
|
166
|
+
ownerLogin: z.string().nullable(),
|
|
167
|
+
ownerType: z.string().nullable(),
|
|
168
|
+
webBaseUrl: z.string(),
|
|
169
|
+
apiBaseUrl: z.string(),
|
|
170
|
+
htmlUrl: z.string().nullable(),
|
|
171
|
+
permissions: z.record(z.string(), z.string()).nullable(),
|
|
172
|
+
events: z.array(z.string()).nullable(),
|
|
173
|
+
/** Where GitHub delivers this App's webhooks (`/v1/hooks/github-app/<key>`). */
|
|
174
|
+
webhookUrl: z.string(),
|
|
175
|
+
/** `${htmlUrl}/installations/new`; null until the App's html URL is known. */
|
|
176
|
+
installUrl: z.string().nullable(),
|
|
177
|
+
hasPrivateKey: z.boolean(),
|
|
178
|
+
hasClientSecret: z.boolean(),
|
|
179
|
+
createdAt: z.string(),
|
|
180
|
+
updatedAt: z.string(),
|
|
181
|
+
});
|
|
182
|
+
export type GithubApp = z.infer<typeof githubApp>;
|
|
183
|
+
|
|
184
|
+
export const githubAppInstallation = z.object({
|
|
185
|
+
id: z.number().int(),
|
|
186
|
+
githubAppId: z.number().int(),
|
|
187
|
+
installationId: z.number().int(),
|
|
188
|
+
accountLogin: z.string().nullable(),
|
|
189
|
+
accountType: z.string().nullable(),
|
|
190
|
+
accountId: z.number().int().nullable(),
|
|
191
|
+
repositorySelection: githubRepositorySelection,
|
|
192
|
+
permissions: z.record(z.string(), z.string()).nullable(),
|
|
193
|
+
/** The generated `github_app` source; null once that source was deleted. */
|
|
194
|
+
sourceId: z.number().int().nullable(),
|
|
195
|
+
suspendedAt: z.string().nullable(),
|
|
196
|
+
removedAt: z.string().nullable(),
|
|
197
|
+
/** GitHub's settings page for this installation. */
|
|
198
|
+
configureUrl: z.string().nullable(),
|
|
199
|
+
createdAt: z.string(),
|
|
200
|
+
updatedAt: z.string(),
|
|
201
|
+
});
|
|
202
|
+
export type GithubAppInstallation = z.infer<typeof githubAppInstallation>;
|
|
203
|
+
|
|
204
|
+
// ── Service ↔ repository link ──────────────────────────────────────────────
|
|
205
|
+
|
|
206
|
+
const linkWatchPaths = webhookCreate.shape.watchPaths.unwrap();
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* PUT /v1/services/:id/github (operator). `sourceId` is a `github_app` source
|
|
210
|
+
* and so names the installation. `repoId` may be omitted; the server then
|
|
211
|
+
* resolves it from the service's repository URL. `tokenScope` widens clone
|
|
212
|
+
* tokens to the whole installation (sibling-repo submodules), so it lives on
|
|
213
|
+
* this operator route and not on the admin feedback PATCH.
|
|
214
|
+
*/
|
|
215
|
+
export const serviceGithubLinkPut = z.object({
|
|
216
|
+
sourceId: id,
|
|
217
|
+
repoId: githubNumericId.optional(),
|
|
218
|
+
tokenScope: githubTokenScope.optional(),
|
|
219
|
+
/** Newline/comma-separated globs (same rules as a webhook); null clears. */
|
|
220
|
+
watchPaths: linkWatchPaths.nullable().optional(),
|
|
221
|
+
enabled: z.boolean().optional(),
|
|
222
|
+
});
|
|
223
|
+
export type ServiceGithubLinkPut = z.infer<typeof serviceGithubLinkPut>;
|
|
224
|
+
|
|
225
|
+
/** PATCH /v1/services/:id/github/feedback (admin). Both default to off. */
|
|
226
|
+
export const serviceGithubFeedbackPatch = z
|
|
227
|
+
.object({
|
|
228
|
+
/** Post commit statuses (`ninedeploy/<slug>`) for deploys. */
|
|
229
|
+
reportStatus: z.boolean().optional(),
|
|
230
|
+
/** Upsert one comment per PR with the preview URL and outcome. */
|
|
231
|
+
prComment: z.boolean().optional(),
|
|
232
|
+
})
|
|
233
|
+
.refine(atLeastOneField, { message: 'nothing to update' });
|
|
234
|
+
export type ServiceGithubFeedbackPatch = z.infer<typeof serviceGithubFeedbackPatch>;
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* POST /v1/services/:id/github/migrate (operator). Links the service to the
|
|
238
|
+
* App installation behind `sourceId`, remembering the current source as
|
|
239
|
+
* `previousSourceId`; the service's own source and webhook are left as is.
|
|
240
|
+
*/
|
|
241
|
+
export const serviceGithubMigrate = z.object({ sourceId: id });
|
|
242
|
+
export type ServiceGithubMigrate = z.infer<typeof serviceGithubMigrate>;
|
|
243
|
+
|
|
244
|
+
/** GET /v1/services/:id/github → `{ link }`, `link` null when unlinked. */
|
|
245
|
+
export const serviceGithubLink = z.object({
|
|
246
|
+
id: z.number().int(),
|
|
247
|
+
serviceId: z.number().int(),
|
|
248
|
+
installationRowId: z.number().int(),
|
|
249
|
+
githubAppId: z.number().int(),
|
|
250
|
+
/** The installation's `github_app` source, when it still exists. */
|
|
251
|
+
sourceId: z.number().int().nullable(),
|
|
252
|
+
repoId: z.number().int(),
|
|
253
|
+
repoFullName: z.string(),
|
|
254
|
+
enabled: z.boolean(),
|
|
255
|
+
tokenScope: githubTokenScope,
|
|
256
|
+
watchPaths: z.string().nullable(),
|
|
257
|
+
reportStatus: z.boolean(),
|
|
258
|
+
prComment: z.boolean(),
|
|
259
|
+
previousSourceId: z.number().int().nullable(),
|
|
260
|
+
/** Enabled AND the installation is neither suspended nor removed: the App drives deploys. */
|
|
261
|
+
active: z.boolean(),
|
|
262
|
+
createdAt: z.string(),
|
|
263
|
+
updatedAt: z.string(),
|
|
264
|
+
});
|
|
265
|
+
export type ServiceGithubLink = z.infer<typeof serviceGithubLink>;
|
|
266
|
+
|
|
267
|
+
export const serviceGithubStatus = z.object({ link: serviceGithubLink.nullable() });
|
|
268
|
+
export type ServiceGithubStatus = z.infer<typeof serviceGithubStatus>;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export * from './common.js';
|
|
2
|
+
export * from './auth.js';
|
|
3
|
+
export * from './management.js';
|
|
4
|
+
export * from './ninedeployManifest.js';
|
|
5
|
+
export type { AlertWhen } from './ninedeployManifest.js';
|
|
6
|
+
export type { RestartPolicy } from './ninedeployManifest.js';
|
|
7
|
+
export * from './project.js';
|
|
8
|
+
export * from './service.js';
|
|
9
|
+
export * from './insights.js';
|
|
10
|
+
export * from './kernel.js';
|
|
11
|
+
export * from './workspaces.js';
|
|
12
|
+
export * from './labels.js';
|
|
13
|
+
export * from './environments.js';
|
|
14
|
+
export * from './ai.js';
|
|
15
|
+
export * from './secretScan.js';
|
|
16
|
+
export * from './runtimeVersions.js';
|
|
17
|
+
export * from './doctor.js';
|
|
18
|
+
export * from './agentCommand.js';
|
|
19
|
+
|
|
20
|
+
export * from './backupPolicy.js';
|
|
21
|
+
export * from './panelBackup.js';
|
|
22
|
+
export * from './githubApp.js';
|
|
23
|
+
export * from './publicAccess.js';
|
|
24
|
+
export * from './proxyConfig.js';
|
|
25
|
+
export * from './databaseImport.js';
|
|
26
|
+
export * from './secretProviders.js';
|
|
27
|
+
export * from './terminals.js';
|
|
28
|
+
export * from './traffic.js';
|
|
29
|
+
export * from './accessGrants.js';
|
|
30
|
+
export * from './multiNode.js';
|
package/src/insights.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { gitBranch, gitRepoUrl, repoBaseDir } from './common.js';
|
|
3
|
+
|
|
4
|
+
// ── Repository / framework insights ─────────────────────────────────────────
|
|
5
|
+
// The analysis engine (server lib/frameworks.ts) inspects a checked-out repo
|
|
6
|
+
// and reduces it to this shape; the wizard and the service-detail Framework
|
|
7
|
+
// tab render it to explain "what is in this repo and how should it deploy".
|
|
8
|
+
|
|
9
|
+
export const frameworkCategory = z.enum(['ssr', 'spa', 'static', 'backend', 'container', 'unknown']);
|
|
10
|
+
export type FrameworkCategory = z.infer<typeof frameworkCategory>;
|
|
11
|
+
|
|
12
|
+
export const packageManagerId = z.enum(['npm', 'pnpm', 'yarn', 'bun']);
|
|
13
|
+
export type PackageManagerId = z.infer<typeof packageManagerId>;
|
|
14
|
+
|
|
15
|
+
/** One recommended environment variable (pre-filled, user-editable). */
|
|
16
|
+
export const envSuggestion = z.object({
|
|
17
|
+
key: z.string(),
|
|
18
|
+
value: z.string(),
|
|
19
|
+
description: z.string().optional(),
|
|
20
|
+
});
|
|
21
|
+
export type EnvSuggestion = z.infer<typeof envSuggestion>;
|
|
22
|
+
|
|
23
|
+
/** A detected framework plus its deploy preset (commands already adapted to
|
|
24
|
+
* the detected package manager). Null commands mean "no safe suggestion". */
|
|
25
|
+
export const frameworkPreset = z.object({
|
|
26
|
+
id: z.string(),
|
|
27
|
+
name: z.string(),
|
|
28
|
+
emoji: z.string(),
|
|
29
|
+
category: frameworkCategory,
|
|
30
|
+
port: z.number().int().min(1).max(65535),
|
|
31
|
+
installCmd: z.string().nullable(),
|
|
32
|
+
buildCmd: z.string().nullable(),
|
|
33
|
+
startCmd: z.string().nullable(),
|
|
34
|
+
env: z.array(envSuggestion),
|
|
35
|
+
notes: z.array(z.string()),
|
|
36
|
+
});
|
|
37
|
+
export type FrameworkPreset = z.infer<typeof frameworkPreset>;
|
|
38
|
+
|
|
39
|
+
export const repoInsights = z.object({
|
|
40
|
+
framework: frameworkPreset,
|
|
41
|
+
language: z.string(),
|
|
42
|
+
packageManager: packageManagerId.nullable(),
|
|
43
|
+
nodeVersion: z.string().nullable(),
|
|
44
|
+
frameworkVersion: z.string().nullable(),
|
|
45
|
+
scripts: z.record(z.string(), z.string()),
|
|
46
|
+
dependencyCount: z.number().int(),
|
|
47
|
+
devDependencyCount: z.number().int(),
|
|
48
|
+
hasDockerfile: z.boolean(),
|
|
49
|
+
hasComposeFile: z.boolean(),
|
|
50
|
+
monorepo: z.boolean(),
|
|
51
|
+
/** Marker files the detection was derived from (e.g. package.json, go.mod). */
|
|
52
|
+
detectedFiles: z.array(z.string()),
|
|
53
|
+
/** Workspace member packages of a monorepo root (empty for single-app repos
|
|
54
|
+
* and for analyses scoped to a base directory). Powers the wizard's
|
|
55
|
+
* "deploy each sub-app as its own service" picker. */
|
|
56
|
+
workspacePackages: z.array(
|
|
57
|
+
z.object({
|
|
58
|
+
/** Repo-relative directory, e.g. "apps/web". */
|
|
59
|
+
dir: z.string(),
|
|
60
|
+
name: z.string().nullable(),
|
|
61
|
+
framework: z.string().nullable(),
|
|
62
|
+
frameworkVersion: z.string().nullable(),
|
|
63
|
+
}),
|
|
64
|
+
),
|
|
65
|
+
baseDir: z.string(),
|
|
66
|
+
commitSha: z.string().nullable().optional(),
|
|
67
|
+
analyzedAt: z.string().datetime(),
|
|
68
|
+
});
|
|
69
|
+
export type RepoInsights = z.infer<typeof repoInsights>;
|
|
70
|
+
|
|
71
|
+
/** Pre-deploy inspection request (DeployWizard, before a service exists). */
|
|
72
|
+
export const analyzeRepoInput = z.object({
|
|
73
|
+
repoUrl: gitRepoUrl,
|
|
74
|
+
branch: gitBranch.default('main'),
|
|
75
|
+
sourceId: z.number().int().positive().optional(),
|
|
76
|
+
baseDir: repoBaseDir.optional(),
|
|
77
|
+
});
|
|
78
|
+
export type AnalyzeRepoInput = z.input<typeof analyzeRepoInput>;
|
package/src/kernel.ts
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
// ─── Configuration Center Schemas ───────────────────────────────────────────
|
|
4
|
+
export const configItemSchema = z.object({
|
|
5
|
+
key: z.string(),
|
|
6
|
+
pluginId: z.string().nullable().optional(),
|
|
7
|
+
type: z.enum(['string', 'number', 'boolean', 'enum', 'json']).default('string'),
|
|
8
|
+
isSecret: z.boolean(),
|
|
9
|
+
label: z.string(),
|
|
10
|
+
category: z.string(),
|
|
11
|
+
description: z.string().optional(),
|
|
12
|
+
tags: z.array(z.string()),
|
|
13
|
+
options: z.array(z.string()).optional(),
|
|
14
|
+
value: z.unknown(),
|
|
15
|
+
isConfigured: z.boolean(),
|
|
16
|
+
updatedAt: z.string().optional(),
|
|
17
|
+
});
|
|
18
|
+
export type ConfigItem = z.infer<typeof configItemSchema>;
|
|
19
|
+
|
|
20
|
+
export const configListSchema = z.object({
|
|
21
|
+
entries: z.array(configItemSchema),
|
|
22
|
+
});
|
|
23
|
+
export type ConfigListResponse = z.infer<typeof configListSchema>;
|
|
24
|
+
|
|
25
|
+
export const setConfigSchema = z.object({
|
|
26
|
+
// Optional: an omitted value keeps the current one (metadata-only update).
|
|
27
|
+
value: z.unknown().optional(),
|
|
28
|
+
isSecret: z.boolean().optional(),
|
|
29
|
+
description: z.string().max(500).optional(),
|
|
30
|
+
tags: z.array(z.string().max(50)).optional(),
|
|
31
|
+
});
|
|
32
|
+
export type SetConfigInput = z.infer<typeof setConfigSchema>;
|
|
33
|
+
|
|
34
|
+
// ─── Plugin Ecosystem Schemas ───────────────────────────────────────────────
|
|
35
|
+
export const pluginItemSchema = z.object({
|
|
36
|
+
id: z.string(),
|
|
37
|
+
name: z.string(),
|
|
38
|
+
version: z.string(),
|
|
39
|
+
description: z.string().optional(),
|
|
40
|
+
author: z.string().optional(),
|
|
41
|
+
icon: z.string().optional(),
|
|
42
|
+
isOfficial: z.boolean(),
|
|
43
|
+
enabled: z.boolean(),
|
|
44
|
+
status: z.enum(['active', 'disabled', 'errored', 'installing']),
|
|
45
|
+
configSchema: z.array(z.record(z.string(), z.unknown())).optional(),
|
|
46
|
+
menuItems: z.array(z.record(z.string(), z.unknown())).optional(),
|
|
47
|
+
dependencies: z.array(z.string()).optional(),
|
|
48
|
+
error: z.string().optional(),
|
|
49
|
+
/**
|
|
50
|
+
* r600: sandbox plugins only — false when this Node's permission model has
|
|
51
|
+
* no network scope (Node < 25) and the plugin can open sockets / fetch.
|
|
52
|
+
*/
|
|
53
|
+
networkRestricted: z.boolean().optional(),
|
|
54
|
+
installedAt: z.string().optional(),
|
|
55
|
+
});
|
|
56
|
+
export type PluginItem = z.infer<typeof pluginItemSchema>;
|
|
57
|
+
|
|
58
|
+
export const pluginListSchema = z.object({
|
|
59
|
+
plugins: z.array(pluginItemSchema),
|
|
60
|
+
});
|
|
61
|
+
export type PluginListResponse = z.infer<typeof pluginListSchema>;
|
|
62
|
+
|
|
63
|
+
export const installPluginSchema = z.object({
|
|
64
|
+
source: z.enum(['marketplace', 'npm', 'git', 'local', 'sandbox']).default('marketplace'),
|
|
65
|
+
target: z.string().min(1),
|
|
66
|
+
name: z.string().optional(),
|
|
67
|
+
version: z.string().optional(),
|
|
68
|
+
description: z.string().optional(),
|
|
69
|
+
author: z.string().optional(),
|
|
70
|
+
icon: z.string().optional(),
|
|
71
|
+
dependencies: z.array(z.string()).optional(),
|
|
72
|
+
configSchema: z.array(z.record(z.string(), z.unknown())).optional(),
|
|
73
|
+
menuItems: z.array(z.record(z.string(), z.unknown())).optional(),
|
|
74
|
+
/**
|
|
75
|
+
* Sandbox-source only: the worker script the plugin runs. This field (and
|
|
76
|
+
* `manifest`) MUST travel with the install — a sandbox plugin installed
|
|
77
|
+
* without its code used to register as "active" while silently executing
|
|
78
|
+
* nothing, and after a restart the restored row had no way to reload the
|
|
79
|
+
* code at all. Bounded so one install cannot balloon the SQLite row.
|
|
80
|
+
*/
|
|
81
|
+
code: z.string().min(1).max(256 * 1024).optional(),
|
|
82
|
+
manifest: z.record(z.string(), z.unknown()).optional(),
|
|
83
|
+
});
|
|
84
|
+
export type InstallPluginInput = z.infer<typeof installPluginSchema>;
|
|
85
|
+
|
|
86
|
+
export const marketplacePluginItemSchema = z.object({
|
|
87
|
+
id: z.string(),
|
|
88
|
+
name: z.string(),
|
|
89
|
+
version: z.string(),
|
|
90
|
+
description: z.string(),
|
|
91
|
+
author: z.string(),
|
|
92
|
+
icon: z.string().optional(),
|
|
93
|
+
category: z.string(),
|
|
94
|
+
isOfficial: z.boolean(),
|
|
95
|
+
isInstalled: z.boolean().default(false),
|
|
96
|
+
/**
|
|
97
|
+
* Whether installing this entry actually does anything.
|
|
98
|
+
*
|
|
99
|
+
* NineDeploy does not load third-party plugin code, so every catalog entry is
|
|
100
|
+
* a roadmap item until the behaviour is compiled into the server. Marking it
|
|
101
|
+
* matters because several entries shadow features that already exist by
|
|
102
|
+
* another name — an operator who "installed" the S3 sync plugin, filled in a
|
|
103
|
+
* bucket and secret key, and saw it reported as active would reasonably
|
|
104
|
+
* believe their backups were being copied off-site. They were not.
|
|
105
|
+
*/
|
|
106
|
+
implemented: z.boolean().default(false),
|
|
107
|
+
/** Where the real feature lives, for entries that shadow a shipped one. */
|
|
108
|
+
builtIn: z
|
|
109
|
+
.object({
|
|
110
|
+
label: z.string(),
|
|
111
|
+
/** Panel route, e.g. `/settings?tab=storage`. */
|
|
112
|
+
path: z.string(),
|
|
113
|
+
})
|
|
114
|
+
.optional(),
|
|
115
|
+
dependencies: z.array(z.string()).optional(),
|
|
116
|
+
configSchema: z.array(z.record(z.string(), z.unknown())).optional(),
|
|
117
|
+
menuItems: z.array(z.record(z.string(), z.unknown())).optional(),
|
|
118
|
+
});
|
|
119
|
+
export type MarketplacePluginItem = z.infer<typeof marketplacePluginItemSchema>;
|
|
120
|
+
|
|
121
|
+
export const marketplaceCatalogSchema = z.object({
|
|
122
|
+
catalog: z.array(marketplacePluginItemSchema),
|
|
123
|
+
/** True when the live signed index was reached and
|
|
124
|
+
* verified; false when the upstream was unreachable
|
|
125
|
+
* and the static catalog was served. */
|
|
126
|
+
live: z.boolean().default(false),
|
|
127
|
+
/** The verifying key id, when the live index was used. */
|
|
128
|
+
keyId: z.string().nullable().optional(),
|
|
129
|
+
/** When the cache was last refreshed (ms since epoch). */
|
|
130
|
+
fetchedAt: z.number().optional(),
|
|
131
|
+
});
|
|
132
|
+
export type MarketplaceCatalogResponse = z.infer<typeof marketplaceCatalogSchema>;
|
|
133
|
+
|
|
134
|
+
// ─── Menu & Navigation Schemas ──────────────────────────────────────────────
|
|
135
|
+
export const menuItemSchema = z.object({
|
|
136
|
+
id: z.string(),
|
|
137
|
+
pluginId: z.string().optional(),
|
|
138
|
+
slot: z.string(),
|
|
139
|
+
label: z.string(),
|
|
140
|
+
route: z.string(),
|
|
141
|
+
icon: z.string().optional(),
|
|
142
|
+
order: z.number().optional(),
|
|
143
|
+
permission: z.enum(['admin', 'member']).optional(),
|
|
144
|
+
});
|
|
145
|
+
export type MenuItem = z.infer<typeof menuItemSchema>;
|
|
146
|
+
|
|
147
|
+
export const menuListSchema = z.object({
|
|
148
|
+
slots: z.record(z.string(), z.array(menuItemSchema)),
|
|
149
|
+
items: z.array(menuItemSchema),
|
|
150
|
+
});
|
|
151
|
+
export type MenuListResponse = z.infer<typeof menuListSchema>;
|
|
152
|
+
|
|
153
|
+
export const pluginInspectSchema = z.object({
|
|
154
|
+
id: z.string(),
|
|
155
|
+
name: z.string(),
|
|
156
|
+
version: z.string(),
|
|
157
|
+
description: z.string().optional(),
|
|
158
|
+
author: z.string().optional(),
|
|
159
|
+
isOfficial: z.boolean(),
|
|
160
|
+
enabled: z.boolean(),
|
|
161
|
+
status: z.enum(['active', 'disabled', 'errored', 'installing']),
|
|
162
|
+
dependencies: z.array(z.string()),
|
|
163
|
+
hooks: z.array(z.string()),
|
|
164
|
+
services: z.array(z.string()),
|
|
165
|
+
menus: z.array(menuItemSchema),
|
|
166
|
+
configSchema: z.array(z.record(z.string(), z.unknown())),
|
|
167
|
+
error: z.string().nullable().optional(),
|
|
168
|
+
/** r600: see pluginItemSchema.networkRestricted. */
|
|
169
|
+
networkRestricted: z.boolean().optional(),
|
|
170
|
+
installedAt: z.string().optional(),
|
|
171
|
+
runtimeStats: z.object({
|
|
172
|
+
/** null = not tracked per plugin by the kernel (r286: was a hard-coded 42). */
|
|
173
|
+
eventsHandled: z.number().nullable(),
|
|
174
|
+
/** null = not tracked per plugin by the kernel (r286: was a hard-coded 3600). */
|
|
175
|
+
uptimeSeconds: z.number().nullable(),
|
|
176
|
+
loadedAt: z.string().optional(),
|
|
177
|
+
}),
|
|
178
|
+
});
|
|
179
|
+
export type PluginInspectResponse = z.infer<typeof pluginInspectSchema>;
|
|
180
|
+
|
package/src/labels.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Workspace-scoped free-form labels used to tag services without tying them
|
|
5
|
+
* to the project hierarchy (e.g. "production", "staging", "team-x"). Labels
|
|
6
|
+
* can be created on the fly; the color is a tailwind-style palette token
|
|
7
|
+
* consumed by the UI.
|
|
8
|
+
*/
|
|
9
|
+
export const labelColor = z.enum([
|
|
10
|
+
'indigo',
|
|
11
|
+
'emerald',
|
|
12
|
+
'amber',
|
|
13
|
+
'rose',
|
|
14
|
+
'sky',
|
|
15
|
+
'slate',
|
|
16
|
+
'violet',
|
|
17
|
+
'lime',
|
|
18
|
+
]);
|
|
19
|
+
export type LabelColor = z.infer<typeof labelColor>;
|
|
20
|
+
|
|
21
|
+
export const createLabel = z.object({
|
|
22
|
+
workspaceId: z.number().int().positive().nullable().optional(),
|
|
23
|
+
name: z.string().trim().min(1).max(40),
|
|
24
|
+
color: labelColor.default('indigo'),
|
|
25
|
+
});
|
|
26
|
+
export type CreateLabel = z.infer<typeof createLabel>;
|
|
27
|
+
export type CreateLabelInput = CreateLabel;
|
|
28
|
+
|
|
29
|
+
export const labelPatch = z
|
|
30
|
+
.object({
|
|
31
|
+
name: z.string().trim().min(1).max(40).optional(),
|
|
32
|
+
color: labelColor.optional(),
|
|
33
|
+
})
|
|
34
|
+
.refine((v) => Object.keys(v).length > 0, { message: 'nothing to update' });
|
|
35
|
+
export type LabelPatch = z.infer<typeof labelPatch>;
|
|
36
|
+
export type LabelPatchInput = LabelPatch;
|
|
37
|
+
|
|
38
|
+
export const label = z.object({
|
|
39
|
+
id: z.number().int(),
|
|
40
|
+
workspaceId: z.number().int().nullable(),
|
|
41
|
+
name: z.string(),
|
|
42
|
+
color: z.string(),
|
|
43
|
+
serviceCount: z.number().int().nonnegative(),
|
|
44
|
+
createdAt: z.string().datetime(),
|
|
45
|
+
updatedAt: z.string().datetime(),
|
|
46
|
+
});
|
|
47
|
+
export type Label = z.infer<typeof label>;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* PUT /v1/services/:id/tags body — replaces the service's project / workspace
|
|
51
|
+
* / label memberships wholesale. The caller must be a member of every
|
|
52
|
+
* workspace in `workspaceIds` (or an operator), and every project/label id
|
|
53
|
+
* must be valid. Empty arrays clear the corresponding dimension.
|
|
54
|
+
*/
|
|
55
|
+
export const setServiceTags = z.object({
|
|
56
|
+
projectIds: z.array(z.number().int().positive()).default([]),
|
|
57
|
+
workspaceIds: z.array(z.number().int().positive()).default([]),
|
|
58
|
+
labelIds: z.array(z.number().int().positive()).default([]),
|
|
59
|
+
});
|
|
60
|
+
export type SetServiceTags = z.infer<typeof setServiceTags>;
|
|
61
|
+
export type SetServiceTagsInput = SetServiceTags;
|
|
62
|
+
|
|
63
|
+
export const serviceTags = z.object({
|
|
64
|
+
serviceId: z.number().int(),
|
|
65
|
+
projects: z.array(z.object({ id: z.number().int(), name: z.string(), slug: z.string() })),
|
|
66
|
+
workspaces: z.array(z.object({ id: z.number().int(), name: z.string(), slug: z.string() })),
|
|
67
|
+
labels: z.array(z.object({ id: z.number().int(), name: z.string(), color: z.string() })),
|
|
68
|
+
});
|
|
69
|
+
export type ServiceTags = z.infer<typeof serviceTags>;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* /v1/services list filter — every present array is OR'd within its dimension
|
|
73
|
+
* and AND'd across dimensions. An empty array means "no filter on this
|
|
74
|
+
* dimension" (so omitting all three is equivalent to the unfiltered list).
|
|
75
|
+
*
|
|
76
|
+
* ?tagWorkspaceIds=1,2 → service is in ws 1 OR ws 2
|
|
77
|
+
* ?tagProjectIds=10 → service has project 10
|
|
78
|
+
* ?tagLabelIds=3,4 → service has label 3 OR 4
|
|
79
|
+
* combined: AND across dimensions
|
|
80
|
+
*/
|
|
81
|
+
export const serviceListFilter = z.object({
|
|
82
|
+
tagWorkspaceIds: z.array(z.number().int().positive()).default([]),
|
|
83
|
+
tagProjectIds: z.array(z.number().int().positive()).default([]),
|
|
84
|
+
tagLabelIds: z.array(z.number().int().positive()).default([]),
|
|
85
|
+
});
|
|
86
|
+
export type ServiceListFilter = z.infer<typeof serviceListFilter>;
|