@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/terminals.ts
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { id } from './common.js';
|
|
3
|
+
|
|
4
|
+
// ── Terminals (0.15) ────────────────────────────────────────────────────────
|
|
5
|
+
// Request and response shapes for `/v1/terminals`: interactive shells into a
|
|
6
|
+
// service replica, a managed database, any managed container, or (off by
|
|
7
|
+
// default, owner decision O1) the host of a server. A session is created over
|
|
8
|
+
// HTTP and attached over a WebSocket with a single-use ticket. Metadata only:
|
|
9
|
+
// no transcript is ever recorded (O2). These are SHAPE checks; the server
|
|
10
|
+
// resolves and authorises every target. Design: DESIGN.md §1.
|
|
11
|
+
|
|
12
|
+
export const TERMINAL_COLS_MIN = 10;
|
|
13
|
+
export const TERMINAL_COLS_MAX = 500;
|
|
14
|
+
export const TERMINAL_ROWS_MIN = 5;
|
|
15
|
+
export const TERMINAL_ROWS_MAX = 200;
|
|
16
|
+
/** Per-frame cap of the attach socket (protocol v1); a larger frame closes 1009. */
|
|
17
|
+
export const TERMINAL_FRAME_MAX_BYTES = 64 * 1024;
|
|
18
|
+
export const TERMINAL_HISTORY_LIMIT_MAX = 200;
|
|
19
|
+
|
|
20
|
+
/** Mirrors `terminalTargetKind` in `@ninedeploy/db`. */
|
|
21
|
+
export const terminalTargetKind = z.enum(['service', 'database', 'container', 'host']);
|
|
22
|
+
export type TerminalTargetKind = z.infer<typeof terminalTargetKind>;
|
|
23
|
+
|
|
24
|
+
/** Mirrors `terminalSessionStatus` in `@ninedeploy/db`. */
|
|
25
|
+
export const terminalSessionStatus = z.enum(['pending', 'active', 'ended', 'failed', 'expired']);
|
|
26
|
+
export type TerminalSessionStatus = z.infer<typeof terminalSessionStatus>;
|
|
27
|
+
|
|
28
|
+
/** A managed container name (`isManagedContainer` is checked server side). */
|
|
29
|
+
export const terminalContainerName = z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9_.-]{1,127}$/, 'must be a container name');
|
|
30
|
+
|
|
31
|
+
export const terminalTarget = z.discriminatedUnion('kind', [
|
|
32
|
+
z
|
|
33
|
+
.object({
|
|
34
|
+
kind: z.literal('service'),
|
|
35
|
+
serviceId: id,
|
|
36
|
+
/** 1-based replica index; omitted = the primary container. */
|
|
37
|
+
replica: z.number().int().min(1).max(50).optional(),
|
|
38
|
+
/** A node the service runs on (primary or fan-out target); omitted = its primary. */
|
|
39
|
+
serverId: id.optional(),
|
|
40
|
+
})
|
|
41
|
+
.strict(),
|
|
42
|
+
z
|
|
43
|
+
.object({
|
|
44
|
+
kind: z.literal('database'),
|
|
45
|
+
databaseId: id,
|
|
46
|
+
/** `client` runs the engine CLI with the stored credentials. */
|
|
47
|
+
mode: z.enum(['shell', 'client']).default('shell'),
|
|
48
|
+
})
|
|
49
|
+
.strict(),
|
|
50
|
+
z.object({ kind: z.literal('container'), name: terminalContainerName }).strict(),
|
|
51
|
+
z
|
|
52
|
+
.object({
|
|
53
|
+
kind: z.literal('host'),
|
|
54
|
+
/** null = the panel host. */
|
|
55
|
+
serverId: id.nullable(),
|
|
56
|
+
})
|
|
57
|
+
.strict(),
|
|
58
|
+
]);
|
|
59
|
+
export type TerminalTarget = z.infer<typeof terminalTarget>;
|
|
60
|
+
export type TerminalTargetInput = z.input<typeof terminalTarget>;
|
|
61
|
+
|
|
62
|
+
export const terminalCols = z.number().int().min(TERMINAL_COLS_MIN).max(TERMINAL_COLS_MAX);
|
|
63
|
+
export const terminalRows = z.number().int().min(TERMINAL_ROWS_MIN).max(TERMINAL_ROWS_MAX);
|
|
64
|
+
|
|
65
|
+
/** POST /v1/terminals (operator). `password` is the step-up for host shells. */
|
|
66
|
+
export const createTerminalSession = z
|
|
67
|
+
.object({
|
|
68
|
+
target: terminalTarget,
|
|
69
|
+
cols: terminalCols.default(120),
|
|
70
|
+
rows: terminalRows.default(32),
|
|
71
|
+
password: z.string().min(1).max(1024).optional(),
|
|
72
|
+
})
|
|
73
|
+
.strict();
|
|
74
|
+
export type CreateTerminalSession = z.infer<typeof createTerminalSession>;
|
|
75
|
+
export type CreateTerminalSessionInput = z.input<typeof createTerminalSession>;
|
|
76
|
+
|
|
77
|
+
/** One session row as the API returns it (history, detail, create). */
|
|
78
|
+
export const terminalSession = z.object({
|
|
79
|
+
id,
|
|
80
|
+
status: terminalSessionStatus,
|
|
81
|
+
targetKind: terminalTargetKind,
|
|
82
|
+
targetLabel: z.string(),
|
|
83
|
+
/** null = the panel host. */
|
|
84
|
+
serverId: id.nullable(),
|
|
85
|
+
userId: id.nullable(),
|
|
86
|
+
userEmail: z.string().nullable(),
|
|
87
|
+
createdAt: z.string(),
|
|
88
|
+
startedAt: z.string().nullable(),
|
|
89
|
+
endedAt: z.string().nullable(),
|
|
90
|
+
durationMs: z.number().int().nonnegative().nullable(),
|
|
91
|
+
bytesIn: z.number().int().nonnegative(),
|
|
92
|
+
bytesOut: z.number().int().nonnegative(),
|
|
93
|
+
endReason: z.string().nullable(),
|
|
94
|
+
exitCode: z.number().int().nullable(),
|
|
95
|
+
clientIp: z.string().nullable(),
|
|
96
|
+
});
|
|
97
|
+
export type TerminalSession = z.infer<typeof terminalSession>;
|
|
98
|
+
|
|
99
|
+
/** 201 answer of POST /v1/terminals. The ticket is single use and valid for 30s. */
|
|
100
|
+
export const terminalSessionCreated = z.object({
|
|
101
|
+
session: terminalSession,
|
|
102
|
+
ticket: z.string().min(1),
|
|
103
|
+
ticketExpiresAt: z.string(),
|
|
104
|
+
/** `/v1/terminals/:id/attach`. */
|
|
105
|
+
attachPath: z.string().regex(/^\/v1\/terminals\/\d+\/attach$/),
|
|
106
|
+
});
|
|
107
|
+
export type TerminalSessionCreated = z.infer<typeof terminalSessionCreated>;
|
|
108
|
+
|
|
109
|
+
/** GET /v1/terminals answer: newest first; `nextBefore` pages further back (null = no more). */
|
|
110
|
+
export const terminalSessionList = z.object({
|
|
111
|
+
items: z.array(terminalSession),
|
|
112
|
+
nextBefore: id.nullable(),
|
|
113
|
+
});
|
|
114
|
+
export type TerminalSessionList = z.infer<typeof terminalSessionList>;
|
|
115
|
+
|
|
116
|
+
/** DELETE /v1/terminals/:id answer. `wasLive`: a running shell was closed (otherwise a pending ticket was revoked). */
|
|
117
|
+
export const terminalTerminateResult = z.object({
|
|
118
|
+
ok: z.literal(true),
|
|
119
|
+
wasLive: z.boolean(),
|
|
120
|
+
});
|
|
121
|
+
export type TerminalTerminateResult = z.infer<typeof terminalTerminateResult>;
|
|
122
|
+
|
|
123
|
+
/** GET /v1/terminals query (history). Query strings arrive as text. */
|
|
124
|
+
export const terminalSessionListQuery = z
|
|
125
|
+
.object({
|
|
126
|
+
status: terminalSessionStatus.optional(),
|
|
127
|
+
userId: z.coerce.number().int().positive().optional(),
|
|
128
|
+
targetKind: terminalTargetKind.optional(),
|
|
129
|
+
limit: z.coerce.number().int().min(1).max(TERMINAL_HISTORY_LIMIT_MAX).default(50),
|
|
130
|
+
/** Page backwards from this session id. */
|
|
131
|
+
before: z.coerce.number().int().positive().optional(),
|
|
132
|
+
})
|
|
133
|
+
.strict();
|
|
134
|
+
export type TerminalSessionListQuery = z.infer<typeof terminalSessionListQuery>;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* PUT /v1/terminals/settings (operator). Partial: only the named keys change.
|
|
138
|
+
* Turning `hostTerminalEnabled` on needs step-up (`password`, or a sign-in
|
|
139
|
+
* less than 10 minutes old) and an interactive session.
|
|
140
|
+
*/
|
|
141
|
+
export const terminalSettings = z
|
|
142
|
+
.object({
|
|
143
|
+
hostTerminalEnabled: z.boolean(),
|
|
144
|
+
idleTimeoutMinutes: z.number().int().min(1).max(240),
|
|
145
|
+
maxSessionMinutes: z.number().int().min(5).max(1440),
|
|
146
|
+
maxConcurrent: z.number().int().min(1).max(50),
|
|
147
|
+
retentionDays: z.number().int().min(30).max(3650),
|
|
148
|
+
password: z.string().max(1024).optional(),
|
|
149
|
+
})
|
|
150
|
+
.partial()
|
|
151
|
+
.strict();
|
|
152
|
+
export type TerminalSettingsInput = z.infer<typeof terminalSettings>;
|
|
153
|
+
|
|
154
|
+
/** GET /v1/terminals/settings: the stored values (defaults when unset). */
|
|
155
|
+
export const terminalSettingsView = z.object({
|
|
156
|
+
hostTerminalEnabled: z.boolean(),
|
|
157
|
+
/** `NINEDEPLOY_HOST_TERMINAL=off` forbids host shells whatever the setting says. */
|
|
158
|
+
hostTerminalForbiddenByEnv: z.boolean(),
|
|
159
|
+
idleTimeoutMinutes: z.number().int(),
|
|
160
|
+
maxSessionMinutes: z.number().int(),
|
|
161
|
+
maxConcurrent: z.number().int(),
|
|
162
|
+
retentionDays: z.number().int(),
|
|
163
|
+
});
|
|
164
|
+
export type TerminalSettingsView = z.infer<typeof terminalSettingsView>;
|
|
165
|
+
|
|
166
|
+
/** Defaults applied when a settings key is absent (fresh and upgraded installs alike). */
|
|
167
|
+
export const TERMINAL_SETTINGS_DEFAULTS = {
|
|
168
|
+
hostTerminalEnabled: false,
|
|
169
|
+
idleTimeoutMinutes: 15,
|
|
170
|
+
maxSessionMinutes: 240,
|
|
171
|
+
maxConcurrent: 10,
|
|
172
|
+
retentionDays: 180,
|
|
173
|
+
} as const;
|
|
174
|
+
|
|
175
|
+
// ── attach protocol v1 (`ninedeploy.terminal.v1`) ──────────────────────────
|
|
176
|
+
// Binary frames carry stdin / output. Text frames carry these JSON messages.
|
|
177
|
+
|
|
178
|
+
/** Client → server text frames. */
|
|
179
|
+
export const terminalClientMessage = z.discriminatedUnion('t', [
|
|
180
|
+
z.object({ t: z.literal('resize'), cols: terminalCols, rows: terminalRows }).strict(),
|
|
181
|
+
z.object({ t: z.literal('ping') }).strict(),
|
|
182
|
+
]);
|
|
183
|
+
export type TerminalClientMessage = z.infer<typeof terminalClientMessage>;
|
|
184
|
+
|
|
185
|
+
/** Server → client text frames. */
|
|
186
|
+
export const terminalServerMessage = z.discriminatedUnion('t', [
|
|
187
|
+
z.object({
|
|
188
|
+
t: z.literal('ready'),
|
|
189
|
+
sessionId: id,
|
|
190
|
+
target: z.object({ kind: terminalTargetKind, label: z.string(), serverId: id.nullable() }),
|
|
191
|
+
}),
|
|
192
|
+
z.object({ t: z.literal('notice'), message: z.string() }),
|
|
193
|
+
z.object({ t: z.literal('exit'), code: z.number().int().nullable(), reason: z.string() }),
|
|
194
|
+
]);
|
|
195
|
+
export type TerminalServerMessage = z.infer<typeof terminalServerMessage>;
|
|
196
|
+
|
|
197
|
+
/** WebSocket close codes of the attach socket. */
|
|
198
|
+
export const TERMINAL_CLOSE = {
|
|
199
|
+
shellExited: 1000,
|
|
200
|
+
frameTooLarge: 1009,
|
|
201
|
+
badTicket: 4401,
|
|
202
|
+
forbidden: 4403,
|
|
203
|
+
idle: 4408,
|
|
204
|
+
maxDuration: 4409,
|
|
205
|
+
terminated: 4410,
|
|
206
|
+
tooManySessions: 4429,
|
|
207
|
+
targetUnreachable: 4502,
|
|
208
|
+
} as const;
|
|
209
|
+
|
|
210
|
+
/** Subprotocols of the attach socket: the protocol name, then the ticket. */
|
|
211
|
+
export const TERMINAL_PROTOCOL = 'ninedeploy.terminal.v1';
|
|
212
|
+
export const TERMINAL_TICKET_PROTOCOL_PREFIX = 'ninedeploy.ticket.';
|
package/src/traffic.ts
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
// ── Traffic analytics (0.15) ────────────────────────────────────────────────
|
|
4
|
+
// Request and response shapes for `/v1/traffic` and
|
|
5
|
+
// `/v1/services/:id/traffic`. Analytics is opt-in everywhere (owner decision
|
|
6
|
+
// O3): with it off, Traefik's static config is byte-identical to 0.14. Only
|
|
7
|
+
// aggregate counters are kept — no client IP, path, query string or header.
|
|
8
|
+
// Design: DESIGN.md §2.
|
|
9
|
+
|
|
10
|
+
export const TRAFFIC_RETENTION_DAYS_MIN = 1;
|
|
11
|
+
export const TRAFFIC_RETENTION_DAYS_MAX = 400;
|
|
12
|
+
export const TRAFFIC_RETENTION_DAYS_DEFAULT = 30;
|
|
13
|
+
export const TRAFFIC_TOP_MAX = 50;
|
|
14
|
+
|
|
15
|
+
/** Bucket widths in seconds: minute rows and hour rows. */
|
|
16
|
+
export const TRAFFIC_GRANULARITIES = [60, 3600] as const;
|
|
17
|
+
export const trafficGranularity = z.union([z.literal(60), z.literal(3600)]);
|
|
18
|
+
export type TrafficGranularity = z.infer<typeof trafficGranularity>;
|
|
19
|
+
|
|
20
|
+
/** Upper edges (ms) of the latency histogram; one overflow bucket follows. */
|
|
21
|
+
export const TRAFFIC_LATENCY_EDGES_MS = [5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000] as const;
|
|
22
|
+
/** Buckets in `latencyHist`: one per edge plus the overflow. */
|
|
23
|
+
export const TRAFFIC_LATENCY_BUCKETS = TRAFFIC_LATENCY_EDGES_MS.length + 1;
|
|
24
|
+
|
|
25
|
+
export const trafficRange = z.enum(['1h', '24h', '7d', '30d']);
|
|
26
|
+
export type TrafficRange = z.infer<typeof trafficRange>;
|
|
27
|
+
|
|
28
|
+
/** Minute buckets for 1h/24h, hour buckets for 7d/30d. */
|
|
29
|
+
export function trafficRangeGranularity(range: TrafficRange): TrafficGranularity {
|
|
30
|
+
return range === '1h' || range === '24h' ? 60 : 3600;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const trafficStatus = z.enum(['off', 'starting', 'running', 'error']);
|
|
34
|
+
export type TrafficStatus = z.infer<typeof trafficStatus>;
|
|
35
|
+
|
|
36
|
+
/** PUT /v1/traffic/settings (operator). Enabling or disabling recreates Traefik once. */
|
|
37
|
+
export const trafficSettingsUpdate = z
|
|
38
|
+
.object({
|
|
39
|
+
enabled: z.boolean().optional(),
|
|
40
|
+
retentionDays: z.number().int().min(TRAFFIC_RETENTION_DAYS_MIN).max(TRAFFIC_RETENTION_DAYS_MAX).optional(),
|
|
41
|
+
})
|
|
42
|
+
.strict();
|
|
43
|
+
export type TrafficSettingsUpdate = z.infer<typeof trafficSettingsUpdate>;
|
|
44
|
+
|
|
45
|
+
/** GET /v1/traffic/settings, and the PUT answer. */
|
|
46
|
+
export const trafficSettingsView = z.object({
|
|
47
|
+
enabled: z.boolean(),
|
|
48
|
+
retentionDays: z.number().int(),
|
|
49
|
+
status: trafficStatus,
|
|
50
|
+
lastError: z.string().nullable(),
|
|
51
|
+
lastIngestAt: z.string().nullable(),
|
|
52
|
+
logBytes: z.number().int().nonnegative(),
|
|
53
|
+
malformedLines: z.number().int().nonnegative(),
|
|
54
|
+
/** `docker info` LoggingDriver; null when Docker did not answer. */
|
|
55
|
+
dockerLogDriver: z.string().nullable(),
|
|
56
|
+
});
|
|
57
|
+
export type TrafficSettingsView = z.infer<typeof trafficSettingsView>;
|
|
58
|
+
|
|
59
|
+
/** GET /v1/traffic/summary and /v1/services/:id/traffic query. */
|
|
60
|
+
export const trafficSummaryQuery = z
|
|
61
|
+
.object({
|
|
62
|
+
range: trafficRange.default('24h'),
|
|
63
|
+
top: z.coerce.number().int().min(1).max(TRAFFIC_TOP_MAX).default(10),
|
|
64
|
+
})
|
|
65
|
+
.strict();
|
|
66
|
+
export type TrafficSummaryQuery = z.infer<typeof trafficSummaryQuery>;
|
|
67
|
+
|
|
68
|
+
/** Counters shared by a bucket, a domain row and the totals. */
|
|
69
|
+
export const trafficCounters = z.object({
|
|
70
|
+
requests: z.number().int().nonnegative(),
|
|
71
|
+
status1xx: z.number().int().nonnegative(),
|
|
72
|
+
status2xx: z.number().int().nonnegative(),
|
|
73
|
+
status3xx: z.number().int().nonnegative(),
|
|
74
|
+
status4xx: z.number().int().nonnegative(),
|
|
75
|
+
status5xx: z.number().int().nonnegative(),
|
|
76
|
+
statusOther: z.number().int().nonnegative(),
|
|
77
|
+
bytesOut: z.number().int().nonnegative(),
|
|
78
|
+
durationSumMs: z.number().int().nonnegative(),
|
|
79
|
+
durationMaxMs: z.number().int().nonnegative(),
|
|
80
|
+
});
|
|
81
|
+
export type TrafficCounters = z.infer<typeof trafficCounters>;
|
|
82
|
+
|
|
83
|
+
/** Percentiles estimated from the histogram; null when there were no requests. */
|
|
84
|
+
export const trafficPercentiles = z.object({
|
|
85
|
+
p50Ms: z.number().nonnegative().nullable(),
|
|
86
|
+
p95Ms: z.number().nonnegative().nullable(),
|
|
87
|
+
p99Ms: z.number().nonnegative().nullable(),
|
|
88
|
+
});
|
|
89
|
+
export type TrafficPercentiles = z.infer<typeof trafficPercentiles>;
|
|
90
|
+
|
|
91
|
+
export const trafficBucket = trafficCounters.extend({
|
|
92
|
+
/** Bucket start, ISO 8601. */
|
|
93
|
+
t: z.string(),
|
|
94
|
+
});
|
|
95
|
+
export type TrafficBucket = z.infer<typeof trafficBucket>;
|
|
96
|
+
|
|
97
|
+
/** One domain (or the panel / custom / other bucket) over the range. */
|
|
98
|
+
export const trafficScopeTotal = trafficCounters.extend(trafficPercentiles.shape).extend({
|
|
99
|
+
/** `d:<domainId>` | `panel` | `custom` | `other`. */
|
|
100
|
+
scopeKey: z.string(),
|
|
101
|
+
domainId: z.number().int().positive().nullable(),
|
|
102
|
+
serviceId: z.number().int().positive().nullable(),
|
|
103
|
+
/** The host as recorded, kept after the domain is deleted. */
|
|
104
|
+
host: z.string().nullable(),
|
|
105
|
+
});
|
|
106
|
+
export type TrafficScopeTotal = z.infer<typeof trafficScopeTotal>;
|
|
107
|
+
|
|
108
|
+
/** GET /v1/traffic/summary (operator): the whole instance. */
|
|
109
|
+
export const trafficSummary = z.object({
|
|
110
|
+
enabled: z.boolean(),
|
|
111
|
+
range: trafficRange,
|
|
112
|
+
granularity: trafficGranularity,
|
|
113
|
+
totals: trafficCounters.extend(trafficPercentiles.shape),
|
|
114
|
+
series: z.array(trafficBucket),
|
|
115
|
+
topDomains: z.array(trafficScopeTotal),
|
|
116
|
+
panel: trafficScopeTotal.nullable(),
|
|
117
|
+
custom: trafficScopeTotal.nullable(),
|
|
118
|
+
});
|
|
119
|
+
export type TrafficSummary = z.infer<typeof trafficSummary>;
|
|
120
|
+
|
|
121
|
+
/** GET /v1/services/:id/traffic (any seat on the service). Empty series, never 404, without data. */
|
|
122
|
+
export const serviceTrafficSummary = z.object({
|
|
123
|
+
enabled: z.boolean(),
|
|
124
|
+
range: trafficRange,
|
|
125
|
+
granularity: trafficGranularity,
|
|
126
|
+
totals: trafficCounters.extend(trafficPercentiles.shape),
|
|
127
|
+
series: z.array(trafficBucket),
|
|
128
|
+
domains: z.array(trafficScopeTotal),
|
|
129
|
+
});
|
|
130
|
+
export type ServiceTrafficSummary = z.infer<typeof serviceTrafficSummary>;
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { slug } from './common.js';
|
|
3
|
+
|
|
4
|
+
export const workspaceRoleEnum = z.enum(['owner', 'admin', 'member', 'viewer']);
|
|
5
|
+
export type WorkspaceRole = z.infer<typeof workspaceRoleEnum>;
|
|
6
|
+
|
|
7
|
+
/** Roles that may be GRANTED directly when adding a member or creating an
|
|
8
|
+
* invitation. 'owner' is deliberately excluded: the PATCH member route is
|
|
9
|
+
* the only place that performs the full ownership transfer (demote the
|
|
10
|
+
* current owner, re-key `workspaces.ownerId`) — an owner-rank row inserted
|
|
11
|
+
* through add/invite would mint owner authority without that bookkeeping. */
|
|
12
|
+
export const assignableWorkspaceRoleEnum = z.enum(['admin', 'member', 'viewer']);
|
|
13
|
+
export type AssignableWorkspaceRole = z.infer<typeof assignableWorkspaceRoleEnum>;
|
|
14
|
+
|
|
15
|
+
export const workspaceCreate = z.object({
|
|
16
|
+
name: z.string().trim().min(2).max(63),
|
|
17
|
+
slug: slug.optional(),
|
|
18
|
+
description: z.string().trim().max(500).optional(),
|
|
19
|
+
});
|
|
20
|
+
export type WorkspaceCreate = z.infer<typeof workspaceCreate>;
|
|
21
|
+
export type WorkspaceCreateInput = WorkspaceCreate;
|
|
22
|
+
|
|
23
|
+
export const workspaceUpdate = z
|
|
24
|
+
.object({
|
|
25
|
+
name: z.string().trim().min(2).max(63).optional(),
|
|
26
|
+
description: z.string().trim().max(500).nullable().optional(),
|
|
27
|
+
})
|
|
28
|
+
.refine((v) => Object.keys(v).length > 0, { message: 'nothing to update' });
|
|
29
|
+
export type WorkspaceUpdate = z.infer<typeof workspaceUpdate>;
|
|
30
|
+
export type WorkspaceUpdateInput = WorkspaceUpdate;
|
|
31
|
+
|
|
32
|
+
export const workspaceMemberAdd = z.object({
|
|
33
|
+
email: z.string().email(),
|
|
34
|
+
role: assignableWorkspaceRoleEnum.default('member'),
|
|
35
|
+
});
|
|
36
|
+
export type WorkspaceMemberAdd = z.infer<typeof workspaceMemberAdd>;
|
|
37
|
+
export type WorkspaceMemberAddInput = WorkspaceMemberAdd;
|
|
38
|
+
|
|
39
|
+
export const workspaceMemberRoleUpdate = z.object({
|
|
40
|
+
role: workspaceRoleEnum,
|
|
41
|
+
});
|
|
42
|
+
export type WorkspaceMemberRoleUpdate = z.infer<typeof workspaceMemberRoleUpdate>;
|
|
43
|
+
export type WorkspaceMemberRoleUpdateInput = WorkspaceMemberRoleUpdate;
|
|
44
|
+
|
|
45
|
+
export interface WorkspaceMemberEntry {
|
|
46
|
+
id: number;
|
|
47
|
+
workspaceId: number;
|
|
48
|
+
userId: number;
|
|
49
|
+
email: string;
|
|
50
|
+
name: string | null;
|
|
51
|
+
role: WorkspaceRole;
|
|
52
|
+
createdAt: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Response from POST /v1/workspaces/:id/members when the address is NOT a
|
|
57
|
+
* registered user yet. The server creates a pending invitation and returns
|
|
58
|
+
* the accept URL (also emailed when an SMTP channel is configured).
|
|
59
|
+
*/
|
|
60
|
+
export interface WorkspaceMemberInviteEntry {
|
|
61
|
+
kind: 'invitation';
|
|
62
|
+
id: number;
|
|
63
|
+
workspaceId: number;
|
|
64
|
+
email: string;
|
|
65
|
+
role: WorkspaceRole;
|
|
66
|
+
acceptUrl: string;
|
|
67
|
+
expiresAt: string;
|
|
68
|
+
createdAt: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface WorkspaceEntry {
|
|
72
|
+
id: number;
|
|
73
|
+
name: string;
|
|
74
|
+
slug: string;
|
|
75
|
+
description: string | null;
|
|
76
|
+
ownerId: number;
|
|
77
|
+
myRole: WorkspaceRole;
|
|
78
|
+
memberCount: number;
|
|
79
|
+
projectCount: number;
|
|
80
|
+
createdAt: string;
|
|
81
|
+
updatedAt: string;
|
|
82
|
+
}
|
|
83
|
+
export type Workspace = WorkspaceEntry;
|
|
84
|
+
|
|
85
|
+
export interface WorkspaceDetail extends WorkspaceEntry {
|
|
86
|
+
members: WorkspaceMemberEntry[];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ─── Workspace Invitations ──────────────────────────────────────────────────
|
|
90
|
+
// A pending invite lets an owner/admin onboard an address that may or may not
|
|
91
|
+
// have a `users` row yet. While the row is outstanding, the only way the
|
|
92
|
+
// address becomes a workspace member is by visiting the accept URL (or by
|
|
93
|
+
// being promoted by an auto-accept hook on first login / OIDC).
|
|
94
|
+
export const workspaceInvitationCreate = z.object({
|
|
95
|
+
email: z.string().email().max(254),
|
|
96
|
+
role: assignableWorkspaceRoleEnum.default('member'),
|
|
97
|
+
});
|
|
98
|
+
export type WorkspaceInvitationCreate = z.infer<typeof workspaceInvitationCreate>;
|
|
99
|
+
export type WorkspaceInvitationCreateInput = WorkspaceInvitationCreate;
|
|
100
|
+
|
|
101
|
+
export interface WorkspaceInvitationEntry {
|
|
102
|
+
id: number;
|
|
103
|
+
workspaceId: number;
|
|
104
|
+
email: string;
|
|
105
|
+
role: WorkspaceRole;
|
|
106
|
+
invitedByUserId: number;
|
|
107
|
+
invitedByName: string | null;
|
|
108
|
+
expiresAt: string;
|
|
109
|
+
acceptedAt: string | null;
|
|
110
|
+
acceptedByUserId: number | null;
|
|
111
|
+
revokedAt: string | null;
|
|
112
|
+
createdAt: string;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Public view of an invitation (token is only returned once at create time). */
|
|
116
|
+
export interface WorkspaceInvitationPublic {
|
|
117
|
+
workspaceId: number;
|
|
118
|
+
workspaceName: string;
|
|
119
|
+
workspaceSlug: string;
|
|
120
|
+
email: string;
|
|
121
|
+
role: WorkspaceRole;
|
|
122
|
+
invitedByName: string | null;
|
|
123
|
+
expiresAt: string;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ─── OIDC & OAuth2 SSO Schemas ───────────────────────────────────────────────
|
|
127
|
+
/**
|
|
128
|
+
* r507: one allowed email domain for an SSO provider (`corp.com`; a leading
|
|
129
|
+
* `@` is tolerated and dropped). Matching is exact — list subdomains you mean.
|
|
130
|
+
*/
|
|
131
|
+
export const oidcAllowedDomain = z
|
|
132
|
+
.string()
|
|
133
|
+
.trim()
|
|
134
|
+
.toLowerCase()
|
|
135
|
+
.transform((v) => v.replace(/^@/, ''))
|
|
136
|
+
.pipe(
|
|
137
|
+
z
|
|
138
|
+
.string()
|
|
139
|
+
.min(3)
|
|
140
|
+
.max(253)
|
|
141
|
+
.regex(/^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z][a-z0-9-]{0,61}[a-z0-9]$/, 'must be a domain like corp.com'),
|
|
142
|
+
);
|
|
143
|
+
const oidcAllowedDomains = z.array(oidcAllowedDomain).max(50);
|
|
144
|
+
|
|
145
|
+
export const oidcProviderCreate = z.object({
|
|
146
|
+
name: z.string().trim().min(2).max(63),
|
|
147
|
+
slug: slug,
|
|
148
|
+
issuerUrl: z.string().url().nullable().optional(),
|
|
149
|
+
clientId: z.string().trim().min(1).max(255),
|
|
150
|
+
clientSecret: z.string().trim().min(1).max(500),
|
|
151
|
+
scopes: z.string().trim().default('openid profile email'),
|
|
152
|
+
enabled: z.boolean().default(true),
|
|
153
|
+
autoEnroll: z.boolean().default(true),
|
|
154
|
+
/**
|
|
155
|
+
* @deprecated F1014: IGNORED for OIDC sign-ins. Since D1/F146 a user an OIDC
|
|
156
|
+
* provider enrolls always owns their own personal workspace, and an OIDC
|
|
157
|
+
* provider maps users into no team workspace, so nothing reads this role.
|
|
158
|
+
* Team seats come from invitations (which carry their own role) or SCIM.
|
|
159
|
+
* Still accepted and stored so existing clients and rows keep working.
|
|
160
|
+
*/
|
|
161
|
+
defaultRole: z.enum(['admin', 'member']).default('member'),
|
|
162
|
+
/** r507: empty (the default) = any verified email the IdP vouches for. */
|
|
163
|
+
allowedDomains: oidcAllowedDomains.optional(),
|
|
164
|
+
});
|
|
165
|
+
export type OidcProviderCreate = z.infer<typeof oidcProviderCreate>;
|
|
166
|
+
/**
|
|
167
|
+
* What a client sends: fields with a server-side default are optional (F1014:
|
|
168
|
+
* so a caller never has to send the deprecated `defaultRole`). Same shape as
|
|
169
|
+
* the other `*Input` aliases (`z.input`).
|
|
170
|
+
*/
|
|
171
|
+
export type OidcProviderCreateInput = z.input<typeof oidcProviderCreate>;
|
|
172
|
+
|
|
173
|
+
export const oidcProviderUpdate = z
|
|
174
|
+
.object({
|
|
175
|
+
name: z.string().trim().min(2).max(63).optional(),
|
|
176
|
+
issuerUrl: z.string().url().nullable().optional(),
|
|
177
|
+
clientId: z.string().trim().min(1).max(255).optional(),
|
|
178
|
+
clientSecret: z.string().trim().min(1).max(500).optional(),
|
|
179
|
+
scopes: z.string().trim().optional(),
|
|
180
|
+
enabled: z.boolean().optional(),
|
|
181
|
+
autoEnroll: z.boolean().optional(),
|
|
182
|
+
/** @deprecated F1014: ignored for OIDC sign-ins; see oidcProviderCreate.defaultRole. */
|
|
183
|
+
defaultRole: z.enum(['admin', 'member']).optional(),
|
|
184
|
+
allowedDomains: oidcAllowedDomains.optional(),
|
|
185
|
+
})
|
|
186
|
+
.refine((v) => Object.keys(v).length > 0, { message: 'nothing to update' });
|
|
187
|
+
export type OidcProviderUpdate = z.infer<typeof oidcProviderUpdate>;
|
|
188
|
+
export type OidcProviderUpdateInput = OidcProviderUpdate;
|
|
189
|
+
|
|
190
|
+
export interface OidcProviderEntry {
|
|
191
|
+
id: number;
|
|
192
|
+
name: string;
|
|
193
|
+
slug: string;
|
|
194
|
+
issuerUrl: string | null;
|
|
195
|
+
clientId: string;
|
|
196
|
+
scopes: string;
|
|
197
|
+
enabled: boolean;
|
|
198
|
+
autoEnroll: boolean;
|
|
199
|
+
/**
|
|
200
|
+
* @deprecated F1014: the stored value, echoed for compatibility. It has no
|
|
201
|
+
* effect on OIDC sign-ins; see oidcProviderCreate.defaultRole.
|
|
202
|
+
*/
|
|
203
|
+
defaultRole: 'admin' | 'member';
|
|
204
|
+
/** r507: email domains allowed to sign in / enroll; empty = unrestricted. */
|
|
205
|
+
allowedDomains: string[];
|
|
206
|
+
createdAt: string;
|
|
207
|
+
updatedAt: string;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export interface OidcPublicProvider {
|
|
211
|
+
id: number;
|
|
212
|
+
name: string;
|
|
213
|
+
slug: string;
|
|
214
|
+
authUrl: string;
|
|
215
|
+
}
|