@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.
- package/LICENSE +21 -0
- package/README.md +23 -0
- package/package.json +68 -0
- package/src/domain/audience.ts +549 -0
- package/src/domain/backup.ts +355 -0
- package/src/domain/credential.ts +55 -0
- package/src/domain/database.ts +467 -0
- package/src/domain/deploy.ts +231 -0
- package/src/domain/device.ts +172 -0
- package/src/domain/deviceFiles.ts +84 -0
- package/src/domain/deviceLogs.ts +82 -0
- package/src/domain/featureRegistry.ts +392 -0
- package/src/domain/finance.ts +477 -0
- package/src/domain/git.ts +419 -0
- package/src/domain/home.ts +314 -0
- package/src/domain/live.ts +272 -0
- package/src/domain/logs.ts +117 -0
- package/src/domain/mail.ts +394 -0
- package/src/domain/metrics.ts +127 -0
- package/src/domain/note.ts +202 -0
- package/src/domain/notifications.ts +268 -0
- package/src/domain/packages.ts +35 -0
- package/src/domain/password.ts +36 -0
- package/src/domain/presence.ts +21 -0
- package/src/domain/project.ts +168 -0
- package/src/domain/projectBoard.ts +130 -0
- package/src/domain/projectChat.ts +46 -0
- package/src/domain/projectHistory.ts +82 -0
- package/src/domain/projectLink.ts +87 -0
- package/src/domain/projectPlan.ts +68 -0
- package/src/domain/report.ts +492 -0
- package/src/domain/role.ts +8 -0
- package/src/domain/secrecy.ts +66 -0
- package/src/domain/sentinel.ts +623 -0
- package/src/domain/sharing.ts +186 -0
- package/src/domain/syncProtocol.ts +116 -0
- package/src/domain/twoFactor.ts +40 -0
- package/src/domain/uptime.ts +216 -0
- package/src/domain/user.ts +141 -0
- package/src/domain/workspace.ts +56 -0
- package/src/domain/workspaceRole.ts +251 -0
- package/src/features/admin.ts +112 -0
- package/src/features/audience.ts +275 -0
- package/src/features/backup.ts +230 -0
- package/src/features/database.ts +461 -0
- package/src/features/deploy.ts +245 -0
- package/src/features/device.ts +292 -0
- package/src/features/deviceFiles.ts +83 -0
- package/src/features/deviceLogs.ts +36 -0
- package/src/features/deviceTerminal.ts +57 -0
- package/src/features/finance.ts +360 -0
- package/src/features/git.ts +368 -0
- package/src/features/home.ts +32 -0
- package/src/features/live.ts +113 -0
- package/src/features/logs.ts +86 -0
- package/src/features/mail.ts +374 -0
- package/src/features/metrics.ts +185 -0
- package/src/features/note.ts +189 -0
- package/src/features/notify.ts +164 -0
- package/src/features/password.ts +67 -0
- package/src/features/project.ts +709 -0
- package/src/features/registry.ts +103 -0
- package/src/features/secrecy.ts +120 -0
- package/src/features/sentinel.ts +233 -0
- package/src/features/sharing.ts +79 -0
- package/src/features/twoFactor.ts +47 -0
- package/src/features/uptime.ts +186 -0
- package/src/features/user.ts +91 -0
- package/src/features/workspace.ts +200 -0
- package/src/http/auth.ts +94 -0
- package/src/http/device.ts +222 -0
- package/src/http/status.ts +45 -0
- package/src/index.ts +1700 -0
- package/src/protocol/agent.ts +1171 -0
- package/src/protocol/envelope.ts +46 -0
- package/src/protocol/error.ts +27 -0
- package/src/protocol/result.ts +17 -0
- package/src/protocol/version.ts +6 -0
- package/src/sdk/client-ambient.d.ts +238 -0
- package/src/sdk/client.ts +66 -0
- package/src/sdk/ids.ts +25 -0
- package/src/sdk/index.ts +11 -0
- package/src/sdk/manifest.ts +326 -0
- package/src/sdk/providers.ts +48 -0
- package/src/sdk/server.ts +378 -0
- package/src/sdk/testing.ts +179 -0
- 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
|
+
}
|