@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,374 @@
1
+ import { z } from 'zod';
2
+ import {
3
+ MAIL_DISPLAY_NAME_MAX_LENGTH,
4
+ MAIL_SEARCH_QUERY_MAX_LENGTH,
5
+ MAIL_SUBJECT_MAX_LENGTH,
6
+ MAIL_SYNC_INTERVAL_MAX_MINUTES,
7
+ MAIL_SYNC_INTERVAL_MIN_MINUTES,
8
+ mailAccountDraftSchema,
9
+ mailAccountEditSchema,
10
+ mailAccountSchema,
11
+ mailAddressSchema,
12
+ mailFolderSchema,
13
+ mailMessageSchema,
14
+ mailMessageCursorSchema,
15
+ mailMessageSummarySchema,
16
+ mailOAuthProviderSchema,
17
+ mailProxySchema,
18
+ mailSecurityTierSchema,
19
+ mailSettingsSchema
20
+ } from '../domain/mail';
21
+
22
+ const accountId = z.number().int().positive();
23
+ const folderId = z.number().int().positive();
24
+ const messageId = z.number().int().positive();
25
+
26
+ export const mailAccountList = {
27
+ command: 'mail.accountList' as const,
28
+ input: z.object({}),
29
+ output: z.object({ accounts: z.array(mailAccountSchema) })
30
+ };
31
+
32
+ /**
33
+ * Clear metadata only (row count across the user's accounts), so — like
34
+ * `password.count`/`uptime.count` — it never requires any account's session to
35
+ * be unlocked and a summary widget always renders a number.
36
+ */
37
+ export const mailAccountCount = {
38
+ command: 'mail.accountCount' as const,
39
+ input: z.object({}),
40
+ output: z.object({ count: z.number().int().nonnegative() })
41
+ };
42
+
43
+ /** Password-auth only — OAuth accounts are created via `mail.oauthStart`. */
44
+ export const mailAccountAdd = {
45
+ command: 'mail.accountAdd' as const,
46
+ input: z.object({ draft: mailAccountDraftSchema }),
47
+ output: z.object({ account: mailAccountSchema })
48
+ };
49
+
50
+ /**
51
+ * Replaces a password-auth account's server configuration. Blank credentials
52
+ * and an omitted proxy keep whatever is stored — see `mailAccountEditSchema`.
53
+ */
54
+ export const mailAccountUpdate = {
55
+ command: 'mail.accountUpdate' as const,
56
+ input: z.object({ id: accountId, draft: mailAccountEditSchema }),
57
+ output: z.object({ account: mailAccountSchema })
58
+ };
59
+
60
+ /**
61
+ * The part of an account that exists whatever its auth method: label, storage
62
+ * tier, optional proxy. Separate from `mailAccountUpdate` because that one is
63
+ * inseparable from a full set of server credentials — which an OAuth mailbox
64
+ * simply doesn't have to give, its servers and secrets being the provider's.
65
+ * This is therefore the only edit path for an OAuth account, and a perfectly
66
+ * good one for a password account whose servers aren't changing.
67
+ *
68
+ * `proxy` omitted means "leave it as it is" — the account DTO deliberately
69
+ * never echoes proxy credentials back, so the client has nothing to resubmit
70
+ * and an absent field must not be read as "remove it". `null` removes it.
71
+ */
72
+ export const mailAccountSetProfile = {
73
+ command: 'mail.accountSetProfile' as const,
74
+ input: z.object({
75
+ id: accountId,
76
+ displayName: z.string().min(1).max(MAIL_DISPLAY_NAME_MAX_LENGTH),
77
+ securityTier: mailSecurityTierSchema,
78
+ syncIntervalMinutes: z
79
+ .number()
80
+ .int()
81
+ .min(MAIL_SYNC_INTERVAL_MIN_MINUTES)
82
+ .max(MAIL_SYNC_INTERVAL_MAX_MINUTES),
83
+ proxy: mailProxySchema.nullable().optional()
84
+ }),
85
+ output: z.object({ account: mailAccountSchema })
86
+ };
87
+
88
+ export const mailAccountDelete = {
89
+ command: 'mail.accountDelete' as const,
90
+ input: z.object({ id: accountId }),
91
+ output: z.object({ id: accountId })
92
+ };
93
+
94
+ /** `ids` is the complete, final order — identical convention to `uptime.reorder`/`note.reorder`. */
95
+ export const mailAccountReorder = {
96
+ command: 'mail.accountReorder' as const,
97
+ input: z.object({ ids: z.array(accountId).min(1) }),
98
+ output: z.object({ ids: z.array(accountId) })
99
+ };
100
+
101
+ /** Pause/resume background sync without deleting the account. */
102
+ export const mailAccountSetEnabled = {
103
+ command: 'mail.accountSetEnabled' as const,
104
+ input: z.object({ id: accountId, enabled: z.boolean() }),
105
+ output: z.object({ account: mailAccountSchema })
106
+ };
107
+
108
+ /**
109
+ * Test IMAP/SMTP reachability either for an already-saved account (`id`) or a
110
+ * not-yet-submitted draft (`draft`) — exactly one must be given, so the "Test
111
+ * connection" button in the add form works before the account exists.
112
+ */
113
+ export const mailAccountTestConnection = {
114
+ command: 'mail.accountTestConnection' as const,
115
+ input: z
116
+ .object({ id: accountId.optional(), draft: mailAccountDraftSchema.optional() })
117
+ .refine((v) => Boolean(v.id) !== Boolean(v.draft), {
118
+ message: 'Fournir soit id, soit draft, jamais les deux'
119
+ }),
120
+ output: z.object({ imapOk: z.boolean(), smtpOk: z.boolean(), error: z.string().nullable() })
121
+ };
122
+
123
+ /**
124
+ * Authorization URL to send the browser to for a Google/Microsoft OAuth
125
+ * mailbox. The account row is created server-side once the provider redirects
126
+ * back to the callback HTTP route (never over this WS command) — the callback
127
+ * page closes itself via `window.opener.postMessage`, which the client uses to
128
+ * detect completion and refresh `mail.accountList`. No WS push event needed.
129
+ */
130
+ export const mailOAuthStart = {
131
+ command: 'mail.oauthStart' as const,
132
+ input: z.object({ provider: mailOAuthProviderSchema, securityTier: mailSecurityTierSchema }),
133
+ output: z.object({ authUrl: z.string().url() })
134
+ };
135
+
136
+ export const mailFolderList = {
137
+ command: 'mail.folderList' as const,
138
+ input: z.object({ accountId }),
139
+ output: z.object({ folders: z.array(mailFolderSchema) })
140
+ };
141
+
142
+ export const mailFolderReorder = {
143
+ command: 'mail.folderReorder' as const,
144
+ input: z.object({ accountId, ids: z.array(folderId).min(1) }),
145
+ output: z.object({ ids: z.array(folderId) })
146
+ };
147
+
148
+ /**
149
+ * Force a refresh now instead of waiting for the next background tick (or for
150
+ * guarded accounts, which are never background-synced). Incrémentale : elle
151
+ * rapatrie les arrivées et réconcilie la fenêtre récente, sans rien jeter —
152
+ * contrairement à `mail.folderReset`, qui reconstruit tout.
153
+ */
154
+ export const mailFolderSync = {
155
+ command: 'mail.folderSync' as const,
156
+ input: z.object({ folderId }),
157
+ output: z.object({
158
+ ok: z.boolean(),
159
+ newCount: z.number().int().nonnegative(),
160
+ /** Drapeaux corrigés par la passe de réconciliation. */
161
+ changedCount: z.number().int().nonnegative(),
162
+ /** Lignes retirées du cache parce que le serveur ne les a plus. */
163
+ removedCount: z.number().int().nonnegative()
164
+ })
165
+ };
166
+
167
+ /**
168
+ * Fetches one older batch (`limit`, newest-first among the older ones) for a
169
+ * folder — the "force refetch" path, since the regular sync only ever moves
170
+ * forward from `last_seen_uid` and can never backfill history that fell
171
+ * outside a folder's initial sync window. Call repeatedly (client-driven, not
172
+ * a server-side loop) until `reachedStart`.
173
+ */
174
+ export const mailFolderBackfill = {
175
+ command: 'mail.folderBackfill' as const,
176
+ input: z.object({ folderId, limit: z.number().int().min(1).max(500) }),
177
+ output: z.object({ addedCount: z.number().int().nonnegative(), reachedStart: z.boolean() })
178
+ };
179
+
180
+ /**
181
+ * Drops a folder's whole cache and re-syncs it from scratch — the "really
182
+ * refresh" path, next to `mail.folderSync` (which only pulls what arrived
183
+ * since) and `mail.folderBackfill` (which only extends downwards). Destructive
184
+ * only in appearance: `mail_messages` is a metadata cache, rebuilt from IMAP,
185
+ * and nothing on the mail server is touched. Use it when the cache and the
186
+ * mailbox have drifted apart — a partial first sync, a folder rebuilt server
187
+ * side, anything where reconciling is less trustworthy than starting over.
188
+ */
189
+ export const mailFolderReset = {
190
+ command: 'mail.folderReset' as const,
191
+ input: z.object({ folderId }),
192
+ output: z.object({ count: z.number().int().nonnegative() })
193
+ };
194
+
195
+ export const mailMessageList = {
196
+ command: 'mail.messageList' as const,
197
+ input: z.object({
198
+ folderId,
199
+ /** Opaque page cursor from a previous call; `null` for the first page. */
200
+ cursor: mailMessageCursorSchema.nullable(),
201
+ limit: z.number().int().min(1).max(200)
202
+ }),
203
+ output: z.object({
204
+ messages: z.array(mailMessageSummarySchema),
205
+ nextCursor: mailMessageCursorSchema.nullable()
206
+ })
207
+ };
208
+
209
+ /**
210
+ * Search one folder, on two legs that complement each other:
211
+ *
212
+ * 1. **Local** — decrypts the folder's cached envelopes and filters them on
213
+ * subject, sender and recipients. Necessarily server-side even though the
214
+ * data is already ours: `envelope_enc` is encrypted at rest, so no SQL
215
+ * predicate can see inside it. Capped, with `scanned` reporting how far it
216
+ * reached.
217
+ * 2. **Remote** — an IMAP `UID SEARCH` against the real mailbox, covering
218
+ * message **bodies** and every message that was never synced. Envelopes for
219
+ * hits missing from the cache are fetched and cached, so a result is a
220
+ * normal row: openable, flaggable, deletable like any other.
221
+ *
222
+ * The remote leg is best-effort. If the mailbox is unreachable, the account is
223
+ * paused, or the server rejects the search, the local results still come back
224
+ * with `remote: false` and `remoteError` set — a search never fails outright
225
+ * just because IMAP was.
226
+ *
227
+ * Whitespace splits the query into terms that must *all* match, in any order.
228
+ * The local leg ignores case and accents (`reunion` finds « Réunion »); the
229
+ * remote leg is at the mercy of the server's own matching, which by RFC 3501
230
+ * is case-insensitive but says nothing about accents.
231
+ */
232
+ export const mailMessageSearch = {
233
+ command: 'mail.messageSearch' as const,
234
+ input: z.object({
235
+ folderId,
236
+ query: z.string().min(1).max(MAIL_SEARCH_QUERY_MAX_LENGTH),
237
+ limit: z.number().int().min(1).max(200)
238
+ }),
239
+ output: z.object({
240
+ messages: z.array(mailMessageSummarySchema),
241
+ /** More rows matched than `limit` allowed through — the UI says so rather than implying completeness. */
242
+ truncated: z.boolean(),
243
+ /** Cached envelopes actually examined, so the UI can be honest about how far the search reached. */
244
+ scanned: z.number().int().nonnegative(),
245
+ /** The IMAP leg ran: results cover the whole mailbox and message bodies, not just the cache. */
246
+ remote: z.boolean(),
247
+ /** Why the IMAP leg didn't run or didn't finish. Non-null implies `remote: false`. */
248
+ remoteError: z.string().nullable()
249
+ })
250
+ };
251
+
252
+ /**
253
+ * Fetches the body live from IMAP — never served from cache — and runs the
254
+ * sanitize/remote-image-block/suspicious-link pipeline server-side.
255
+ * `allowRemoteImages` unblocks images for this single response only; nothing
256
+ * is persisted about the choice.
257
+ */
258
+ export const mailMessageGet = {
259
+ command: 'mail.messageGet' as const,
260
+ input: z.object({ messageId, allowRemoteImages: z.boolean() }),
261
+ output: z.object({ message: mailMessageSchema })
262
+ };
263
+
264
+ export const mailMessageSetFlags = {
265
+ command: 'mail.messageSetFlags' as const,
266
+ input: z.object({
267
+ messageId,
268
+ flags: z.object({
269
+ seen: z.boolean().optional(),
270
+ flagged: z.boolean().optional(),
271
+ answered: z.boolean().optional()
272
+ })
273
+ }),
274
+ output: z.object({ message: mailMessageSummarySchema })
275
+ };
276
+
277
+ export const mailMessageMove = {
278
+ command: 'mail.messageMove' as const,
279
+ input: z.object({ messageId, toFolderId: folderId }),
280
+ output: z.object({ id: messageId })
281
+ };
282
+
283
+ export const mailMessageDelete = {
284
+ command: 'mail.messageDelete' as const,
285
+ input: z.object({ messageId }),
286
+ output: z.object({ id: messageId })
287
+ };
288
+
289
+ /** Returns a short-lived, cookie-authed download URL — mirrors the agent file-serve pattern rather than chunking over WS. */
290
+ export const mailAttachmentDownload = {
291
+ command: 'mail.attachmentDownload' as const,
292
+ input: z.object({ messageId, attachmentId: z.string() }),
293
+ output: z.object({ downloadUrl: z.string() })
294
+ };
295
+
296
+ /**
297
+ * Opt-in external scan of one attachment. Rejected with `forbidden` unless the
298
+ * account/message has scanning explicitly enabled — never triggered implicitly.
299
+ */
300
+ export const mailAttachmentScan = {
301
+ command: 'mail.attachmentScan' as const,
302
+ input: z.object({ messageId, attachmentId: z.string() }),
303
+ output: z.object({
304
+ status: z.enum(['clean', 'suspicious', 'malicious', 'unknown']),
305
+ provider: z.string().nullable()
306
+ })
307
+ };
308
+
309
+ export const mailSend = {
310
+ command: 'mail.send' as const,
311
+ input: z.object({
312
+ accountId,
313
+ to: z.array(mailAddressSchema).min(1),
314
+ cc: z.array(mailAddressSchema).optional(),
315
+ bcc: z.array(mailAddressSchema).optional(),
316
+ subject: z.string().max(MAIL_SUBJECT_MAX_LENGTH),
317
+ bodyText: z.string(),
318
+ bodyHtml: z.string().nullable().optional(),
319
+ attachments: z
320
+ .array(
321
+ z.object({ filename: z.string(), mimeType: z.string(), contentBase64: z.string() })
322
+ )
323
+ .optional(),
324
+ inReplyTo: z.string().nullable().optional()
325
+ }),
326
+ output: z.object({ ok: z.boolean(), messageId: z.string().nullable() })
327
+ };
328
+
329
+ export const mailGetSettings = {
330
+ command: 'mail.getSettings' as const,
331
+ input: z.object({}),
332
+ output: z.object({ settings: mailSettingsSchema })
333
+ };
334
+
335
+ /**
336
+ * Input is `mailSettingsSchema` itself (not a hand-duplicated field list) —
337
+ * a settings save always resends the whole object (see `withSettingsDefaults`
338
+ * client-side), and duplicating the shape here is exactly what let this drift
339
+ * out of sync with the domain schema once already.
340
+ */
341
+ export const mailSetSettings = {
342
+ command: 'mail.setSettings' as const,
343
+ input: mailSettingsSchema,
344
+ output: z.object({ settings: mailSettingsSchema })
345
+ };
346
+
347
+ export const mailCommands = [
348
+ mailAccountList,
349
+ mailAccountCount,
350
+ mailAccountAdd,
351
+ mailAccountUpdate,
352
+ mailAccountSetProfile,
353
+ mailAccountDelete,
354
+ mailAccountReorder,
355
+ mailAccountSetEnabled,
356
+ mailAccountTestConnection,
357
+ mailOAuthStart,
358
+ mailFolderList,
359
+ mailFolderReorder,
360
+ mailFolderSync,
361
+ mailFolderBackfill,
362
+ mailFolderReset,
363
+ mailMessageList,
364
+ mailMessageSearch,
365
+ mailMessageGet,
366
+ mailMessageSetFlags,
367
+ mailMessageMove,
368
+ mailMessageDelete,
369
+ mailAttachmentDownload,
370
+ mailAttachmentScan,
371
+ mailSend,
372
+ mailGetSettings,
373
+ mailSetSettings
374
+ ] as const;
@@ -0,0 +1,185 @@
1
+ import { z } from 'zod';
2
+ import { metricSeriesPointSchema, metricsResolutionSchema } from '../domain/metrics';
3
+ import { presenceEventSchema } from '../domain/presence';
4
+ import { processSampleSchema } from '../domain/report';
5
+
6
+ const deviceId = z.uuid();
7
+
8
+ /** Fetch a time-series window for graphs, optionally downsampled. */
9
+ export const metricsQuery = {
10
+ command: 'metrics.query' as const,
11
+ input: z.object({
12
+ deviceId,
13
+ from: z.number().int().nonnegative(),
14
+ to: z.number().int().positive(),
15
+ resolution: metricsResolutionSchema.default('raw')
16
+ }),
17
+ output: z.object({
18
+ deviceId,
19
+ points: z.array(metricSeriesPointSchema)
20
+ })
21
+ };
22
+
23
+ /** Subscribe to live metric pushes for one or more devices. */
24
+ export const metricsSubscribe = {
25
+ command: 'metrics.subscribe' as const,
26
+ input: z.object({ deviceIds: z.array(deviceId).min(1).max(50) }),
27
+ output: z.object({ deviceIds: z.array(deviceId) })
28
+ };
29
+
30
+ export const metricsUnsubscribe = {
31
+ command: 'metrics.unsubscribe' as const,
32
+ input: z.object({ deviceIds: z.array(deviceId).min(1).max(50) }),
33
+ output: z.object({ deviceIds: z.array(deviceId) })
34
+ };
35
+
36
+ /** Ask an online device to push a fresh sample + report right now. */
37
+ export const metricsRefresh = {
38
+ command: 'metrics.refresh' as const,
39
+ input: z.object({ deviceId }),
40
+ /** `requested` is false when the device isn't currently connected. */
41
+ output: z.object({ deviceId, requested: z.boolean() })
42
+ };
43
+
44
+ /** Agent connectivity over a window, to draw the uptime timeline. */
45
+ export const metricsPresence = {
46
+ command: 'metrics.presence' as const,
47
+ input: z.object({
48
+ deviceId,
49
+ from: z.number().int().nonnegative(),
50
+ to: z.number().int().positive()
51
+ }),
52
+ output: z.object({
53
+ deviceId,
54
+ /** Online state at `from` (carried over from the last prior event). */
55
+ onlineAtStart: z.boolean(),
56
+ events: z.array(presenceEventSchema)
57
+ })
58
+ };
59
+
60
+ /** Processes captured nearest to a given instant (null if none in range). */
61
+ export const metricsProcessesAt = {
62
+ command: 'metrics.processesAt' as const,
63
+ input: z.object({ deviceId, at: z.number().int().positive() }),
64
+ output: z.object({ deviceId, sample: processSampleSchema.nullable() })
65
+ };
66
+
67
+ /** Distinct local days (YYYY-MM-DD) that have metric data, for the calendar. */
68
+ export const metricsAvailability = {
69
+ command: 'metrics.availability' as const,
70
+ input: z.object({
71
+ deviceId,
72
+ /** Client UTC offset (`Date.getTimezoneOffset()`), to bucket by local day. */
73
+ tzOffsetMinutes: z.number().int().default(0)
74
+ }),
75
+ output: z.object({ deviceId, days: z.array(z.string()) })
76
+ };
77
+
78
+ /**
79
+ * Timestamps of the stored instants in a window, to mark them on the timeline.
80
+ * With a single collection cadence there is one per metric point, so this is
81
+ * dense — the timeline draws continuous bands rather than individual marks above
82
+ * a threshold.
83
+ *
84
+ * `timestamps` are the *metric* instants, not the process samples: process
85
+ * capture is optional (`processCapture: 'off'`), and keying the marks on the
86
+ * process blob made the whole instant navigation — marks, ‹ › stepping, keyboard
87
+ * arrows — silently vanish whenever a device chose not to record processes.
88
+ * `withProcesses` is the subset that additionally carries a process list.
89
+ */
90
+ export const metricsSnapshots = {
91
+ command: 'metrics.snapshots' as const,
92
+ input: z.object({
93
+ deviceId,
94
+ from: z.number().int().nonnegative(),
95
+ to: z.number().int().positive()
96
+ }),
97
+ output: z.object({
98
+ deviceId,
99
+ timestamps: z.array(z.number().int().positive()),
100
+ /** Subset of `timestamps` that are pinned (kept past retention). */
101
+ pinned: z.array(z.number().int().positive()),
102
+ /** Subset of `timestamps` whose process list was recorded. */
103
+ withProcesses: z.array(z.number().int().positive()),
104
+ /**
105
+ * Le relevé a buté sur son plafond : la fenêtre contenait plus d'instants
106
+ * que ce qui peut être rendu, et seuls les plus **récents** sont là.
107
+ * L'interface le dit plutôt que de laisser croire à un trou de données.
108
+ */
109
+ truncated: z.boolean().default(false)
110
+ })
111
+ };
112
+
113
+ /**
114
+ * Pin (or unpin) the snapshots within `[from, to]` (inclusive). Pinned snapshots
115
+ * keep their process list *and* their metric point past the device's retention.
116
+ * Unpinning lets them expire again: rows already past their retention deadline are
117
+ * deleted immediately, the rest at the next retention sweep.
118
+ */
119
+ export const metricsSetSnapshotsPinned = {
120
+ command: 'metrics.setSnapshotsPinned' as const,
121
+ input: z
122
+ .object({
123
+ deviceId,
124
+ from: z.number().int().nonnegative(),
125
+ to: z.number().int().positive(),
126
+ pinned: z.boolean()
127
+ })
128
+ .refine((v) => v.to >= v.from, { message: 'to must be >= from' }),
129
+ output: z.object({
130
+ deviceId,
131
+ /** Snapshot instants whose pin state changed. */
132
+ affected: z.number().int().nonnegative(),
133
+ /** Snapshot instants deleted right away on unpin (already past retention). */
134
+ deletedSnapshots: z.number().int().nonnegative()
135
+ })
136
+ };
137
+
138
+ /** Storage footprint of a device's stored process snapshots (count + bytes). */
139
+ export const metricsStorage = {
140
+ command: 'metrics.storage' as const,
141
+ input: z.object({ deviceId }),
142
+ output: z.object({
143
+ deviceId,
144
+ /** Snapshot instants kept for the device (one stored row each). */
145
+ snapshots: z.number().int().nonnegative(),
146
+ /** Total process entries recorded across those instants. */
147
+ processes: z.number().int().nonnegative(),
148
+ /** Bytes the compressed process blobs occupy — measured, not estimated. */
149
+ bytes: z.number().int().nonnegative()
150
+ })
151
+ };
152
+
153
+ /**
154
+ * Delete the process snapshots within `[from, to]` (inclusive). A single snapshot
155
+ * is removed by passing `from === to === ts`; a dragged zone passes its bounds.
156
+ */
157
+ export const metricsDeleteSnapshots = {
158
+ command: 'metrics.deleteSnapshots' as const,
159
+ input: z
160
+ .object({
161
+ deviceId,
162
+ from: z.number().int().nonnegative(),
163
+ to: z.number().int().positive()
164
+ })
165
+ .refine((v) => v.to >= v.from, { message: 'to must be >= from' }),
166
+ output: z.object({
167
+ deviceId,
168
+ /** Snapshot instants removed. */
169
+ deletedSnapshots: z.number().int().nonnegative()
170
+ })
171
+ };
172
+
173
+ export const metricsCommands = [
174
+ metricsQuery,
175
+ metricsSubscribe,
176
+ metricsUnsubscribe,
177
+ metricsRefresh,
178
+ metricsPresence,
179
+ metricsProcessesAt,
180
+ metricsAvailability,
181
+ metricsSnapshots,
182
+ metricsStorage,
183
+ metricsDeleteSnapshots,
184
+ metricsSetSnapshotsPinned
185
+ ] as const;