@deveye/types 0.15.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 (87) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +23 -0
  3. package/package.json +68 -0
  4. package/src/domain/audience.ts +549 -0
  5. package/src/domain/backup.ts +355 -0
  6. package/src/domain/credential.ts +55 -0
  7. package/src/domain/database.ts +467 -0
  8. package/src/domain/deploy.ts +231 -0
  9. package/src/domain/device.ts +172 -0
  10. package/src/domain/deviceFiles.ts +84 -0
  11. package/src/domain/deviceLogs.ts +82 -0
  12. package/src/domain/featureRegistry.ts +392 -0
  13. package/src/domain/finance.ts +477 -0
  14. package/src/domain/git.ts +419 -0
  15. package/src/domain/home.ts +314 -0
  16. package/src/domain/live.ts +272 -0
  17. package/src/domain/logs.ts +117 -0
  18. package/src/domain/mail.ts +394 -0
  19. package/src/domain/metrics.ts +127 -0
  20. package/src/domain/note.ts +202 -0
  21. package/src/domain/notifications.ts +268 -0
  22. package/src/domain/packages.ts +35 -0
  23. package/src/domain/password.ts +36 -0
  24. package/src/domain/presence.ts +21 -0
  25. package/src/domain/project.ts +168 -0
  26. package/src/domain/projectBoard.ts +130 -0
  27. package/src/domain/projectChat.ts +46 -0
  28. package/src/domain/projectHistory.ts +82 -0
  29. package/src/domain/projectLink.ts +87 -0
  30. package/src/domain/projectPlan.ts +68 -0
  31. package/src/domain/report.ts +492 -0
  32. package/src/domain/role.ts +8 -0
  33. package/src/domain/secrecy.ts +66 -0
  34. package/src/domain/sentinel.ts +623 -0
  35. package/src/domain/sharing.ts +186 -0
  36. package/src/domain/syncProtocol.ts +116 -0
  37. package/src/domain/twoFactor.ts +40 -0
  38. package/src/domain/uptime.ts +216 -0
  39. package/src/domain/user.ts +141 -0
  40. package/src/domain/workspace.ts +56 -0
  41. package/src/domain/workspaceRole.ts +251 -0
  42. package/src/features/admin.ts +112 -0
  43. package/src/features/audience.ts +275 -0
  44. package/src/features/backup.ts +230 -0
  45. package/src/features/database.ts +461 -0
  46. package/src/features/deploy.ts +245 -0
  47. package/src/features/device.ts +292 -0
  48. package/src/features/deviceFiles.ts +83 -0
  49. package/src/features/deviceLogs.ts +36 -0
  50. package/src/features/deviceTerminal.ts +57 -0
  51. package/src/features/finance.ts +360 -0
  52. package/src/features/git.ts +368 -0
  53. package/src/features/home.ts +32 -0
  54. package/src/features/live.ts +113 -0
  55. package/src/features/logs.ts +86 -0
  56. package/src/features/mail.ts +374 -0
  57. package/src/features/metrics.ts +185 -0
  58. package/src/features/note.ts +189 -0
  59. package/src/features/notify.ts +164 -0
  60. package/src/features/password.ts +67 -0
  61. package/src/features/project.ts +709 -0
  62. package/src/features/registry.ts +103 -0
  63. package/src/features/secrecy.ts +120 -0
  64. package/src/features/sentinel.ts +233 -0
  65. package/src/features/sharing.ts +79 -0
  66. package/src/features/twoFactor.ts +47 -0
  67. package/src/features/uptime.ts +186 -0
  68. package/src/features/user.ts +91 -0
  69. package/src/features/workspace.ts +200 -0
  70. package/src/http/auth.ts +94 -0
  71. package/src/http/device.ts +222 -0
  72. package/src/http/status.ts +45 -0
  73. package/src/index.ts +1700 -0
  74. package/src/protocol/agent.ts +1171 -0
  75. package/src/protocol/envelope.ts +46 -0
  76. package/src/protocol/error.ts +27 -0
  77. package/src/protocol/result.ts +17 -0
  78. package/src/protocol/version.ts +6 -0
  79. package/src/sdk/client-ambient.d.ts +238 -0
  80. package/src/sdk/client.ts +66 -0
  81. package/src/sdk/ids.ts +25 -0
  82. package/src/sdk/index.ts +11 -0
  83. package/src/sdk/manifest.ts +326 -0
  84. package/src/sdk/providers.ts +48 -0
  85. package/src/sdk/server.ts +378 -0
  86. package/src/sdk/testing.ts +179 -0
  87. package/src/utils/version.ts +28 -0
@@ -0,0 +1,117 @@
1
+ import { z } from 'zod';
2
+
3
+ /**
4
+ * Importance scale for a log entry, ordered low → high. Stored on the row as a
5
+ * small integer so the DB can filter "at least warning" with a cheap `level >= n`
6
+ * comparison, while the string label drives the UI (colour, filter dropdown).
7
+ *
8
+ * The numeric values are aligned with pino's level scale (debug 20 … fatal 60)
9
+ * so the server's structured logger and the audit log share a single ladder.
10
+ */
11
+ export const LOG_LEVELS = {
12
+ debug: 20,
13
+ info: 30,
14
+ warning: 40,
15
+ error: 50,
16
+ critical: 60
17
+ } as const;
18
+
19
+ export type LogLevelName = keyof typeof LOG_LEVELS;
20
+
21
+ /** Levels ordered from least to most important (drives ordered UI controls). */
22
+ export const LOG_LEVEL_NAMES = ['debug', 'info', 'warning', 'error', 'critical'] as const;
23
+
24
+ export const logLevelNameSchema = z.enum(LOG_LEVEL_NAMES);
25
+
26
+ /** Numeric importance of a named level. */
27
+ export function logLevelValue(name: LogLevelName): number {
28
+ return LOG_LEVELS[name];
29
+ }
30
+
31
+ /**
32
+ * Resolve a stored numeric level to the closest named level at or below it, so
33
+ * any integer maps to a label even if it doesn't land exactly on a rung
34
+ * (`debug` for anything below the floor).
35
+ */
36
+ export function logLevelName(value: number): LogLevelName {
37
+ let resolved: LogLevelName = 'debug';
38
+ for (const name of LOG_LEVEL_NAMES) {
39
+ if (value >= LOG_LEVELS[name]) resolved = name;
40
+ }
41
+ return resolved;
42
+ }
43
+
44
+ /**
45
+ * Origin channel of an action — answers "where did this come from?".
46
+ * - `web` : the web client over the authenticated WS / HTTP session.
47
+ * - `api` : an external API consumer (mobile app, scripts) — "Via API".
48
+ * - `agent` : a device agent connection (metrics, presence).
49
+ * - `system` : the server itself (startup, migrations, scheduled work).
50
+ */
51
+ export const logSourceSchema = z.enum(['web', 'api', 'agent', 'system']);
52
+ export type LogSource = z.infer<typeof logSourceSchema>;
53
+
54
+ /**
55
+ * Known emitting subsystems. The schema stays a plain string (forward-compatible
56
+ * — a new feature can log before this list is updated), but this constant is the
57
+ * canonical set used to populate filter controls and to keep `category` values
58
+ * consistent across emitters.
59
+ */
60
+ export const LOG_CATEGORIES = [
61
+ 'auth',
62
+ 'user',
63
+ 'workspace',
64
+ 'password',
65
+ 'note',
66
+ 'device',
67
+ 'metrics',
68
+ 'weather',
69
+ 'twofa',
70
+ 'secrecy',
71
+ 'logs',
72
+ 'system'
73
+ ] as const;
74
+
75
+ export type LogCategory = (typeof LOG_CATEGORIES)[number];
76
+
77
+ /**
78
+ * A log entry as returned to the client. `username` is resolved server-side from
79
+ * `uid` (null when the actor is the system or the account no longer exists), and
80
+ * `metadata` is the parsed structured payload an emitter optionally attached.
81
+ */
82
+ export const logEntrySchema = z.object({
83
+ id: z.number().int().nonnegative(),
84
+ /** Event time, unix epoch seconds. */
85
+ date: z.number().int().nonnegative(),
86
+ /** Numeric importance (see LOG_LEVELS). */
87
+ level: z.number().int(),
88
+ source: logSourceSchema,
89
+ /** Emitting subsystem/feature (see LOG_CATEGORIES). */
90
+ category: z.string(),
91
+ /** Specific event key within the category, e.g. `login.success`. */
92
+ action: z.string(),
93
+ /** Acting user id; 0 for system / unauthenticated. */
94
+ uid: z.number().int().nonnegative(),
95
+ /** Resolved display name for `uid`, null when system or unknown. */
96
+ username: z.string().nullable(),
97
+ ip: z.string(),
98
+ description: z.string(),
99
+ /** Optional structured context attached by the emitter. */
100
+ metadata: z.record(z.string(), z.unknown()).nullable()
101
+ });
102
+
103
+ export type LogEntry = z.infer<typeof logEntrySchema>;
104
+
105
+ /** Raw `logs` row as stored. `metadata` is MySQL JSON (parsed by the driver). */
106
+ export interface LogRow {
107
+ id: number;
108
+ uid: number;
109
+ ip: string;
110
+ source: string;
111
+ category: string;
112
+ action: string;
113
+ level: number;
114
+ description: string;
115
+ metadata: unknown;
116
+ date: number;
117
+ }
@@ -0,0 +1,394 @@
1
+ import { z } from 'zod';
2
+
3
+ /**
4
+ * Mail: user-configured IMAP/SMTP mailboxes, read and sent through DevEye.
5
+ *
6
+ * Storage split (see `Docs/SECURITY_MODEL.md`): `securityTier`/`authMethod` and
7
+ * everything the server needs to *plan* a sync or pick a cipher (enabled,
8
+ * timestamps) live in clear columns. Everything identifying — display name,
9
+ * address, credentials, message envelopes — is encrypted, but **which** cipher
10
+ * (`ctx.secure` vs `ctx.secure.open`) is chosen per account by `securityTier`,
11
+ * not fixed per feature like Uptime/Password are. An "open" account can be
12
+ * synced/sent in the background (e.g. wired to Uptime alerts); a "guarded" one
13
+ * only ever decrypts during a live, unlocked session. Message **bodies and
14
+ * attachments are never persisted** — only envelopes (subject/from/date/flags)
15
+ * are cached for fast listing; the body is fetched live from IMAP on open.
16
+ */
17
+
18
+ export const MAIL_DISPLAY_NAME_MAX_LENGTH = 80;
19
+ export const MAIL_EMAIL_MAX_LENGTH = 320;
20
+ export const MAIL_HOST_MAX_LENGTH = 255;
21
+ export const MAIL_SUBJECT_MAX_LENGTH = 998; // RFC 5322 line-length ceiling
22
+ export const MAIL_SNIPPET_MAX_LENGTH = 280;
23
+
24
+ /** Upper bound on a `mail.messageSearch` query — a needle, not a document. */
25
+ export const MAIL_SEARCH_QUERY_MAX_LENGTH = 128;
26
+
27
+ export const MAIL_SYNC_INTERVAL_MIN_MINUTES = 1;
28
+ export const MAIL_SYNC_INTERVAL_MAX_MINUTES = 180;
29
+ export const MAIL_SYNC_INTERVAL_DEFAULT_MINUTES = 10;
30
+
31
+ /**
32
+ * Taille d'une page de messages — et, par voie de conséquence, de la fenêtre que
33
+ * la synchro relit à chaque passage pour y réconcilier drapeaux et disparus.
34
+ *
35
+ * Les deux sont le même nombre à dessein : la relève de fond n'avance pas
36
+ * seulement le haut de la boîte, elle garde honnête exactement ce que l'écran
37
+ * affiche sans défiler. Au-delà, la dérive existe toujours mais ne se voit
38
+ * qu'après un défilement, et se répare au changement de dossier.
39
+ */
40
+ export const MAIL_MESSAGE_PAGE_SIZE = 50;
41
+
42
+ export const mailSecurityTierSchema = z.enum(['open', 'guarded']);
43
+ export type MailSecurityTier = z.infer<typeof mailSecurityTierSchema>;
44
+
45
+ /**
46
+ * État de la dernière opération tentée sur une boîte, quelle qu'en soit
47
+ * l'origine — relève de fond ou commande de l'utilisateur.
48
+ *
49
+ * Trois familles d'échec plutôt qu'un booléen, parce qu'elles n'appellent pas la
50
+ * même chose : `auth` se répare en reconnectant le compte, `unreachable` se
51
+ * répare tout seul quand le réseau revient, `error` demande de lire le message.
52
+ * Le libellé exact reste dans `lastSyncError` — chiffré, lui, car il peut citer
53
+ * un hôte ou une adresse.
54
+ */
55
+ export const mailAccountStatusSchema = z.enum(['ok', 'auth', 'unreachable', 'error']);
56
+ export type MailAccountStatus = z.infer<typeof mailAccountStatusSchema>;
57
+
58
+ export const mailAuthMethodSchema = z.enum(['password', 'oauth_google', 'oauth_microsoft']);
59
+ export type MailAuthMethod = z.infer<typeof mailAuthMethodSchema>;
60
+
61
+ export const mailOAuthProviderSchema = z.enum(['google', 'microsoft']);
62
+ export type MailOAuthProvider = z.infer<typeof mailOAuthProviderSchema>;
63
+
64
+ export const mailFolderSpecialUseSchema = z.enum([
65
+ 'inbox',
66
+ 'sent',
67
+ 'drafts',
68
+ 'trash',
69
+ 'junk',
70
+ 'archive',
71
+ 'other'
72
+ ]);
73
+ export type MailFolderSpecialUse = z.infer<typeof mailFolderSpecialUseSchema>;
74
+
75
+ export const mailProxyKindSchema = z.enum(['socks5', 'http']);
76
+ export type MailProxyKind = z.infer<typeof mailProxyKindSchema>;
77
+
78
+ /** Optional manual proxy for a single account — DevEye never provides one itself. */
79
+ export const mailProxySchema = z.object({
80
+ kind: mailProxyKindSchema,
81
+ host: z.string().min(1).max(MAIL_HOST_MAX_LENGTH),
82
+ port: z.number().int().min(1).max(65535),
83
+ username: z.string().max(255).nullable(),
84
+ password: z.string().max(255).nullable()
85
+ });
86
+ export type MailProxy = z.infer<typeof mailProxySchema>;
87
+
88
+ const serverEndpointSchema = z.object({
89
+ host: z.string().min(1).max(MAIL_HOST_MAX_LENGTH),
90
+ port: z.number().int().min(1).max(65535),
91
+ username: z.string().min(1).max(MAIL_EMAIL_MAX_LENGTH),
92
+ password: z.string().min(1)
93
+ });
94
+
95
+ /**
96
+ * What the user submits to create/edit a **password**-auth account. OAuth
97
+ * accounts are never drafted this way — they're created by the
98
+ * `mail.oauthStart` → provider redirect → callback flow, which never puts a
99
+ * secret through this schema.
100
+ */
101
+ export const mailAccountDraftSchema = z.object({
102
+ displayName: z.string().min(1).max(MAIL_DISPLAY_NAME_MAX_LENGTH),
103
+ emailAddress: z.string().email().max(MAIL_EMAIL_MAX_LENGTH),
104
+ securityTier: mailSecurityTierSchema,
105
+ imap: serverEndpointSchema,
106
+ smtp: serverEndpointSchema,
107
+ proxy: mailProxySchema.nullable()
108
+ });
109
+ export type MailAccountDraft = z.infer<typeof mailAccountDraftSchema>;
110
+
111
+ /**
112
+ * Editing an existing password account, as opposed to creating one. Same shape
113
+ * as a draft with two "leave it alone" affordances the add form has no use for:
114
+ * a blank username/password keeps the stored one, and an omitted `proxy` keeps
115
+ * the stored proxy. Both exist because the account DTO deliberately never
116
+ * echoes a secret back, so the form has nothing to prefill — without them,
117
+ * renaming a mailbox would mean retyping both sets of server credentials, and
118
+ * saving anything at all would silently drop a configured proxy.
119
+ */
120
+ export const mailAccountEditSchema = mailAccountDraftSchema.extend({
121
+ imap: serverEndpointSchema.extend({
122
+ username: z.string().max(MAIL_EMAIL_MAX_LENGTH),
123
+ password: z.string()
124
+ }),
125
+ smtp: serverEndpointSchema.extend({
126
+ username: z.string().max(MAIL_EMAIL_MAX_LENGTH),
127
+ password: z.string()
128
+ }),
129
+ proxy: mailProxySchema.nullable().optional()
130
+ });
131
+ export type MailAccountEdit = z.infer<typeof mailAccountEditSchema>;
132
+
133
+ /**
134
+ * Client-facing account DTO. Never carries a secret — not the IMAP/SMTP
135
+ * password, not the OAuth tokens, not the proxy credentials. Editing a
136
+ * password-auth account re-submits a full new `MailAccountDraft`; there is no
137
+ * "reveal secret" endpoint, matching Password's discipline of never re-serving
138
+ * a stored secret except through the one gated read path it actually needs.
139
+ */
140
+ export const mailAccountSchema = z.object({
141
+ id: z.number().int().positive(),
142
+ sortOrder: z.number().int().nonnegative(),
143
+ displayName: z.string(),
144
+ emailAddress: z.string(),
145
+ securityTier: mailSecurityTierSchema,
146
+ authMethod: mailAuthMethodSchema,
147
+ imapHost: z.string(),
148
+ imapPort: z.number().int(),
149
+ smtpHost: z.string(),
150
+ smtpPort: z.number().int(),
151
+ proxyConfigured: z.boolean(),
152
+ enabled: z.boolean(),
153
+ lastSyncAt: z.number().int().nonnegative().nullable(),
154
+ lastSyncError: z.string().nullable(),
155
+ /**
156
+ * État de la dernière opération, persistant jusqu'à ce qu'une réussite le
157
+ * lève. C'est ce qui permet d'annoncer une boîte en panne dès l'ouverture,
158
+ * sans attendre qu'un geste de l'utilisateur reproduise l'échec.
159
+ */
160
+ status: mailAccountStatusSchema,
161
+ /** Quand l'échec courant a été constaté. `null` tant que tout va bien. */
162
+ lastErrorAt: z.number().int().nonnegative().nullable(),
163
+ /** True when an OAuth refresh failed and the user must reconnect. */
164
+ needsReauth: z.boolean(),
165
+ /**
166
+ * How often the background loop re-checks this mailbox. Per account, not
167
+ * per user: an archive box has no reason to be polled as hard as a work
168
+ * one. Ignored for "guarded" accounts, which are never background-synced.
169
+ * Real precision is floored by the server's own tick
170
+ * (`MAIL_SYNC_TICK_SECONDS`), so a value below that just means "every tick".
171
+ */
172
+ syncIntervalMinutes: z
173
+ .number()
174
+ .int()
175
+ .min(MAIL_SYNC_INTERVAL_MIN_MINUTES)
176
+ .max(MAIL_SYNC_INTERVAL_MAX_MINUTES),
177
+ /** A background sync tick is currently running for this account (in-memory, never persisted). */
178
+ syncing: z.boolean(),
179
+ /**
180
+ * Folder-level fraction (0-1) of the in-progress sync — the finest
181
+ * granularity available without instrumenting the IMAP fetch itself.
182
+ * `null` while `syncing` means "just started, folder count not known yet";
183
+ * always `null` when not syncing.
184
+ */
185
+ syncProgress: z.number().min(0).max(1).nullable(),
186
+ created: z.number().int().nonnegative()
187
+ });
188
+ export type MailAccount = z.infer<typeof mailAccountSchema>;
189
+
190
+ export const mailFolderSchema = z.object({
191
+ id: z.number().int().positive(),
192
+ accountId: z.number().int().positive(),
193
+ name: z.string(),
194
+ specialUse: mailFolderSpecialUseSchema,
195
+ sortOrder: z.number().int().nonnegative(),
196
+ unreadCount: z.number().int().nonnegative(),
197
+ totalCount: z.number().int().nonnegative()
198
+ });
199
+ export type MailFolder = z.infer<typeof mailFolderSchema>;
200
+
201
+ export const mailAddressSchema = z.object({
202
+ name: z.string().nullable(),
203
+ address: z.string()
204
+ });
205
+ export type MailAddress = z.infer<typeof mailAddressSchema>;
206
+
207
+ export const mailFlagsSchema = z.object({
208
+ seen: z.boolean(),
209
+ flagged: z.boolean(),
210
+ answered: z.boolean(),
211
+ draft: z.boolean()
212
+ });
213
+ export type MailFlags = z.infer<typeof mailFlagsSchema>;
214
+
215
+ export const mailAttachmentSchema = z.object({
216
+ id: z.string(),
217
+ filename: z.string(),
218
+ mimeType: z.string(),
219
+ size: z.number().int().nonnegative()
220
+ });
221
+ export type MailAttachment = z.infer<typeof mailAttachmentSchema>;
222
+
223
+ /** Why a link was flagged — shown as a warning chip, never blocks the click. */
224
+ export const mailLinkWarningReasonSchema = z.enum([
225
+ 'text-href-mismatch',
226
+ 'lookalike-domain',
227
+ 'unsafe-scheme'
228
+ ]);
229
+ export type MailLinkWarningReason = z.infer<typeof mailLinkWarningReasonSchema>;
230
+
231
+ export const mailSuspiciousLinkSchema = z.object({
232
+ text: z.string(),
233
+ href: z.string(),
234
+ reason: mailLinkWarningReasonSchema
235
+ });
236
+ export type MailSuspiciousLink = z.infer<typeof mailSuspiciousLinkSchema>;
237
+
238
+ /**
239
+ * One header line as the server sent it, in receipt order. Kept raw and
240
+ * unabridged — a `Received:` chain, an SPF/DKIM/DMARC verdict or a mailing-list
241
+ * id is exactly the sort of thing you need when tracing a message, and any
242
+ * summary would be the wrong one for some question.
243
+ */
244
+ export const mailHeaderSchema = z.object({ name: z.string(), value: z.string() });
245
+ export type MailHeader = z.infer<typeof mailHeaderSchema>;
246
+
247
+ /**
248
+ * Page cursor for `mail.messageList`. Opaque to the client: hand back the one
249
+ * from the previous page.
250
+ *
251
+ * A composite of `(date, id)` rather than a bare row id, because the list is
252
+ * ordered by date and row ids are *insertion* order — which is not the same
253
+ * thing. Backfill and remote search both write older messages into the cache
254
+ * after newer ones, so an id-ordered list put freshly-fetched old mail at the
255
+ * top of the mailbox. `id` is only the tiebreak between two identical dates,
256
+ * so a page boundary can never repeat or skip a row.
257
+ */
258
+ export const mailMessageCursorSchema = z.object({
259
+ date: z.number().int().nonnegative(),
260
+ id: z.number().int().positive()
261
+ });
262
+ export type MailMessageCursor = z.infer<typeof mailMessageCursorSchema>;
263
+
264
+ /** List-row shape: the cached envelope, nothing that requires a live IMAP fetch. */
265
+ export const mailMessageSummarySchema = z.object({
266
+ id: z.number().int().positive(),
267
+ accountId: z.number().int().positive(),
268
+ folderId: z.number().int().positive(),
269
+ uid: z.number().int().positive(),
270
+ subject: z.string(),
271
+ from: mailAddressSchema.nullable(),
272
+ to: z.array(mailAddressSchema),
273
+ date: z.number().int().nonnegative(),
274
+ flags: mailFlagsSchema,
275
+ hasAttachments: z.boolean(),
276
+ snippet: z.string()
277
+ });
278
+ export type MailMessageSummary = z.infer<typeof mailMessageSummarySchema>;
279
+
280
+ /**
281
+ * Full message. Fetched live from IMAP on `mail.messageGet` — never cached —
282
+ * with `bodyHtml` already sanitized server-side (scripts/handlers/tracking CSS
283
+ * stripped, remote images blocked unless `allowRemoteImages` was requested).
284
+ */
285
+ export const mailMessageSchema = mailMessageSummarySchema.extend({
286
+ bodyHtml: z.string().nullable(),
287
+ bodyText: z.string().nullable(),
288
+ attachments: z.array(mailAttachmentSchema),
289
+ remoteImagesBlocked: z.boolean(),
290
+ /** Distinct hostnames of the remote images that were blocked, for a "trust these" picker. */
291
+ blockedImageSources: z.array(z.string()),
292
+ suspiciousLinks: z.array(mailSuspiciousLinkSchema),
293
+ /** Every header line as received, in order. Never persisted — read live with the body. */
294
+ headers: z.array(mailHeaderSchema),
295
+ /** Size of the raw RFC822 source, in bytes. */
296
+ sizeBytes: z.number().int().nonnegative(),
297
+ /** IMAP path of the containing mailbox, which with `uid` identifies the message server-side. */
298
+ folderPath: z.string()
299
+ });
300
+ export type MailMessage = z.infer<typeof mailMessageSchema>;
301
+
302
+ export const mailBodyRenderModeSchema = z.enum(['embedded', 'raw']);
303
+ export type MailBodyRenderMode = z.infer<typeof mailBodyRenderModeSchema>;
304
+
305
+ export const mailSettingsSchema = z.object({
306
+ /** Off by default everywhere — opt-in per scope decision, never pushed on the user. */
307
+ externalScanEnabledDefault: z.boolean(),
308
+ /** Hostnames whose remote images auto-load without a per-message prompt. */
309
+ trustedImageDomains: z.array(z.string()),
310
+ /**
311
+ * `embedded` (default): the sanitized body renders inline, styled by DevEye
312
+ * (no `<style>`/inline styles — see `src/mail/sanitize.ts`). `raw`: rendered
313
+ * in an isolated sandboxed iframe on a white background with the message's
314
+ * own styling preserved, for when the embedded look mangles a message.
315
+ */
316
+ bodyRenderMode: mailBodyRenderModeSchema
317
+ });
318
+ export type MailSettings = z.infer<typeof mailSettingsSchema>;
319
+
320
+ /** Database row shapes (server-only). Mirror the columns exactly. */
321
+ export interface MailAccountRow {
322
+ id: number;
323
+ user_id: number;
324
+ workspace_id: number;
325
+ sort_order: number;
326
+ /** Encrypted (tier-dependent). */
327
+ display_name_enc: string;
328
+ /** Encrypted (tier-dependent). */
329
+ email_address_enc: string;
330
+ security_tier: MailSecurityTier;
331
+ auth_method: MailAuthMethod;
332
+ enabled: number;
333
+ /** Background-sync cadence for this mailbox alone. */
334
+ sync_interval_seconds: number;
335
+ last_sync_at: number | null;
336
+ /** Encrypted (tier-dependent), or null after a clean sync. */
337
+ last_sync_error_enc: string | null;
338
+ /**
339
+ * En clair, à côté de `enabled` et `security_tier` : l'état doit être
340
+ * lisible sans clé — par un ordonnanceur qui trie, par une requête de
341
+ * diagnostic, et par l'interface d'un compte dont le message d'erreur,
342
+ * lui, ne se déchiffre pas.
343
+ */
344
+ last_sync_status: MailAccountStatus;
345
+ last_error_at: number | null;
346
+ /**
347
+ * Encrypted (tier-dependent) JSON blob — the only place secrets live:
348
+ * `{ imap: {host,port,username,password}, smtp: {...}, proxy? }` for
349
+ * `password` auth, or `{ provider, accessToken, refreshToken, expiresAt,
350
+ * scope }` for OAuth. Never decrypted into the client-facing DTO.
351
+ */
352
+ credentials_enc: string;
353
+ created: number;
354
+ }
355
+
356
+ export interface MailFolderRow {
357
+ id: number;
358
+ account_id: number;
359
+ /** Clear: the IMAP mailbox path, required to address the mailbox during sync. */
360
+ imap_path: string;
361
+ /** Encrypted (account's tier) display name. */
362
+ name_enc: string;
363
+ special_use: MailFolderSpecialUse;
364
+ sort_order: number;
365
+ /** IMAP UIDVALIDITY — a change invalidates every cached message in this folder. */
366
+ uid_validity: number | null;
367
+ /** High-water mark: forward (incremental) sync fetches strictly after this. */
368
+ last_seen_uid: number | null;
369
+ /** Low-water mark: backward (backfill) sync fetches strictly before this. NULL until the first sync/backfill sets it. */
370
+ first_seen_uid: number | null;
371
+ unread_count: number;
372
+ total_count: number;
373
+ }
374
+
375
+ export interface MailMessageRow {
376
+ id: number;
377
+ folder_id: number;
378
+ uid: number;
379
+ /** Encrypted (account's tier) `{ subject, from, to, snippet }`. Body is never stored. */
380
+ envelope_enc: string;
381
+ date: number;
382
+ seen: number;
383
+ flagged: number;
384
+ answered: number;
385
+ has_attachments: number;
386
+ }
387
+
388
+ export interface MailSettingsRow {
389
+ workspace_id: number;
390
+ external_scan_enabled_default: number;
391
+ /** JSON-encoded string array, or null when empty. Not encrypted — hostnames, not secrets. */
392
+ trusted_image_domains: string | null;
393
+ body_render_mode: MailBodyRenderMode;
394
+ }
@@ -0,0 +1,127 @@
1
+ import { z } from 'zod';
2
+
3
+ import { processKindSchema, reportProcessSchema } from './report';
4
+
5
+ /**
6
+ * A single point-in-time sample emitted by an agent — **one instant, everything
7
+ * together**: graph signals, the process count and the process list itself. The
8
+ * agent runs one collection cadence, so a graph point can never exist without
9
+ * the processes that explain it.
10
+ *
11
+ * Byte counters are absolute (used/total); the UI derives percentages and rates.
12
+ * `timestamp` is unix ms and is the single key correlating a metric row with its
13
+ * stored process list.
14
+ */
15
+ export const metricSnapshotSchema = z.object({
16
+ timestamp: z.number().int().positive(),
17
+ cpuPercent: z.number().min(0).max(100),
18
+ memUsedBytes: z.number().int().nonnegative(),
19
+ memTotalBytes: z.number().int().positive(),
20
+ diskUsedBytes: z.number().int().nonnegative(),
21
+ diskTotalBytes: z.number().int().positive(),
22
+ netRxBytes: z.number().int().nonnegative(),
23
+ netTxBytes: z.number().int().nonnegative(),
24
+ /** Number of logged-in OS users at sample time. */
25
+ usersCount: z.number().int().nonnegative(),
26
+ /** 1-minute load average; null when unavailable on the platform. */
27
+ loadAvg1: z.number().min(0).nullable().default(null),
28
+ /** CPU package temperature in °C; null when no sensor is readable. */
29
+ cpuTempC: z.number().nullable().default(null),
30
+ /** System uptime in seconds; null when unavailable. */
31
+ uptimeSeconds: z.number().int().nonnegative().nullable().default(null),
32
+ /** Total running processes at sample time; null when unavailable. */
33
+ processCount: z.number().int().nonnegative().nullable().default(null),
34
+ /** Active (established) network connections; null when unavailable. */
35
+ activeConnections: z.number().int().nonnegative().nullable().default(null),
36
+ /** GPU utilization (%); null when no readable GPU sensor is available. */
37
+ gpuPercent: z.number().min(0).max(100).nullable().default(null),
38
+ /**
39
+ * Cumulative disk bytes read since boot, summed over every process. Treated
40
+ * as a counter (rate derived). Null when the platform doesn't expose
41
+ * per-process I/O or the agent lacks the privileges to read it.
42
+ */
43
+ diskReadBytes: z.number().int().nonnegative().nullable().default(null),
44
+ /** Cumulative disk bytes written; null under the same conditions. */
45
+ diskWriteBytes: z.number().int().nonnegative().nullable().default(null),
46
+ /** Battery charge (%); null when the machine has no battery. */
47
+ batteryPercent: z.number().min(0).max(100).nullable().default(null),
48
+ /** Whether the battery is charging / on AC; null when unknown or no battery. */
49
+ batteryCharging: z.boolean().nullable().default(null),
50
+ /**
51
+ * Programs running at this instant, heaviest first, aggregated by name.
52
+ * `null` means "not carried by this row" — either the device's capture mode
53
+ * is `off`, or the snapshot sat long enough in the agent's offline queue for
54
+ * the detail to be trimmed (graphs keep full fidelity, process detail is
55
+ * bounded). Persisted separately (`device_process_samples`) under this row's
56
+ * `timestamp`, so it is *not* echoed back by `metrics.query`.
57
+ */
58
+ processes: z.array(reportProcessSchema).max(2000).nullable().default(null),
59
+ /**
60
+ * Capture mode in effect when `processes` was taken, so history stays
61
+ * labelled correctly even after the device's setting later changes (a queued
62
+ * offline snapshot may predate the change). `null` when `processes` is null.
63
+ */
64
+ processKind: processKindSchema.nullable().default(null)
65
+ });
66
+
67
+ export type MetricSnapshot = z.infer<typeof metricSnapshotSchema>;
68
+
69
+ /**
70
+ * Batch of snapshots pushed by an agent over the agent WebSocket. Bounded to
71
+ * keep payloads small and allow draining an offline queue in chunks. The agent
72
+ * additionally caps a batch by serialized size, since a snapshot now carries its
73
+ * process list and 100 of them would make a multi-megabyte frame.
74
+ */
75
+ export const metricsBatchSchema = z.object({
76
+ deviceId: z.uuid(),
77
+ snapshots: z.array(metricSnapshotSchema).min(1).max(100)
78
+ });
79
+
80
+ export type MetricsBatch = z.infer<typeof metricsBatchSchema>;
81
+
82
+ /**
83
+ * Bucketing resolution for time-series queries used to feed graphs. The client
84
+ * picks it from the window span and tops out at `hour` — a coarser bucket would
85
+ * flatten a day into a handful of points.
86
+ */
87
+ export const metricsResolutionSchema = z.enum(['raw', 'minute', 'hour']);
88
+ export type MetricsResolution = z.infer<typeof metricsResolutionSchema>;
89
+
90
+ /**
91
+ * A point read back from `device_metrics` — a live snapshot minus its process
92
+ * list, which lives in its own table and is read through `metrics.processesAt`.
93
+ * Keeping it out of the series is deliberate: a graph window holds hundreds of
94
+ * points, and carrying every process list along would cost megabytes for data
95
+ * the graphs never read.
96
+ */
97
+ export const metricSeriesPointSchema = metricSnapshotSchema.omit({
98
+ processes: true,
99
+ processKind: true
100
+ });
101
+ export type MetricSeriesPoint = z.infer<typeof metricSeriesPointSchema>;
102
+
103
+ export interface MetricRow {
104
+ id: number;
105
+ device_id: string;
106
+ ts: number;
107
+ cpu_percent: number;
108
+ mem_used_bytes: number;
109
+ mem_total_bytes: number;
110
+ disk_used_bytes: number;
111
+ disk_total_bytes: number;
112
+ net_rx_bytes: number;
113
+ net_tx_bytes: number;
114
+ users_count: number;
115
+ load_avg_1: number | null;
116
+ cpu_temp_c: number | null;
117
+ uptime_seconds: number | null;
118
+ process_count: number | null;
119
+ active_connections: number | null;
120
+ gpu_percent: number | null;
121
+ disk_read_bytes: number | null;
122
+ disk_write_bytes: number | null;
123
+ battery_percent: number | null;
124
+ battery_charging: number | null;
125
+ /** 1 when the instant is pinned — kept past the device's retention. */
126
+ pinned: number;
127
+ }