@deveye/types 0.15.2 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/package.json +6 -6
  2. package/src/domain/device.ts +4 -29
  3. package/src/domain/featureRegistry.ts +40 -108
  4. package/src/domain/home.ts +40 -101
  5. package/src/domain/live.ts +36 -87
  6. package/src/domain/metrics.ts +10 -14
  7. package/src/domain/notifications.ts +39 -110
  8. package/src/domain/project.ts +3 -162
  9. package/src/domain/report.ts +46 -95
  10. package/src/domain/secrecy.ts +3 -5
  11. package/src/domain/sharing.ts +33 -70
  12. package/src/domain/syncProtocol.ts +5 -12
  13. package/src/domain/user.ts +8 -14
  14. package/src/domain/workspace.ts +0 -3
  15. package/src/domain/workspaceRole.ts +34 -82
  16. package/src/features/agent.ts +306 -0
  17. package/src/features/live.ts +17 -37
  18. package/src/features/notify.ts +19 -57
  19. package/src/features/registry.ts +7 -44
  20. package/src/features/secrecy.ts +4 -9
  21. package/src/features/sharing.ts +12 -26
  22. package/src/features/user.ts +6 -11
  23. package/src/features/workspace.ts +6 -11
  24. package/src/http/auth.ts +6 -13
  25. package/src/http/device.ts +13 -17
  26. package/src/http/status.ts +6 -10
  27. package/src/index.ts +29 -932
  28. package/src/protocol/agent.ts +40 -70
  29. package/src/protocol/envelope.ts +3 -10
  30. package/src/sdk/client-ambient.d.ts +228 -21
  31. package/src/sdk/client.ts +277 -17
  32. package/src/sdk/devb.ts +120 -0
  33. package/src/sdk/manifest.test.ts +0 -1
  34. package/src/sdk/manifest.ts +94 -25
  35. package/src/sdk/providers.ts +310 -5
  36. package/src/sdk/server.ts +483 -19
  37. package/src/sdk/testing.test.ts +2 -2
  38. package/src/sdk/testing.ts +302 -26
  39. package/src/utils/version.ts +5 -8
  40. package/src/domain/audience.ts +0 -549
  41. package/src/domain/backup.ts +0 -355
  42. package/src/domain/credential.ts +0 -55
  43. package/src/domain/database.ts +0 -467
  44. package/src/domain/deploy.ts +0 -231
  45. package/src/domain/finance.ts +0 -477
  46. package/src/domain/git.ts +0 -419
  47. package/src/domain/mail.ts +0 -394
  48. package/src/domain/note.ts +0 -202
  49. package/src/domain/password.ts +0 -36
  50. package/src/domain/projectBoard.ts +0 -130
  51. package/src/domain/projectChat.ts +0 -46
  52. package/src/domain/projectHistory.ts +0 -82
  53. package/src/domain/projectLink.ts +0 -87
  54. package/src/domain/projectPlan.ts +0 -68
  55. package/src/domain/sentinel.ts +0 -623
  56. package/src/domain/uptime.ts +0 -216
  57. package/src/features/audience.ts +0 -275
  58. package/src/features/backup.ts +0 -230
  59. package/src/features/database.ts +0 -461
  60. package/src/features/deploy.ts +0 -245
  61. package/src/features/device.ts +0 -292
  62. package/src/features/deviceFiles.ts +0 -83
  63. package/src/features/deviceLogs.ts +0 -36
  64. package/src/features/deviceTerminal.ts +0 -57
  65. package/src/features/finance.ts +0 -360
  66. package/src/features/git.ts +0 -368
  67. package/src/features/mail.ts +0 -374
  68. package/src/features/metrics.ts +0 -185
  69. package/src/features/note.ts +0 -189
  70. package/src/features/password.ts +0 -67
  71. package/src/features/project.ts +0 -709
  72. package/src/features/sentinel.ts +0 -233
  73. package/src/features/uptime.ts +0 -186
@@ -22,11 +22,6 @@ export const workspaceCapabilitySchema = z.enum([
22
22
  'workspace.appearance',
23
23
  /** Modifier la disposition de l'accueil de l'espace. */
24
24
  'workspace.layout'
25
- // « Gérer les canaux d'alerte » a vécu ici (`workspace.notifications`)
26
- // puis est passée PAR FONCTIONNALITÉ (migration 093) : depuis que chaque
27
- // émetteur possède ses canaux (091), une capacité d'espace accordait d'un
28
- // bloc l'astreinte d'Uptime et le salon des sauvegardes. Voir le champ
29
- // `channels` du grant de feature.
30
25
  ]);
31
26
 
32
27
  export type WorkspaceCapability = z.infer<typeof workspaceCapabilitySchema>;
@@ -34,14 +29,9 @@ export type WorkspaceCapability = z.infer<typeof workspaceCapabilitySchema>;
34
29
  export const WORKSPACE_CAPABILITIES = workspaceCapabilitySchema.options;
35
30
 
36
31
  /**
37
- * Features qu'un rôle peut ouvrir. Surensemble de `HomeFeatureId` : `devices`
38
- * s'y ajoute, parce que voir la flotte d'un espace est un droit comme un autre
39
- * (lecture = voir les appareils et leur supervision, écriture = les appairer,
40
- * approuver, renommer, supprimer).
41
- *
42
- * `monitoring` n'y figure pas : la carte d'agrégat du même nom est réservée à
43
- * l'administrateur global dans son espace personnel, donc aucun rôle d'espace
44
- * ne peut l'accorder. Les vues d'appareil, elles, relèvent de `devices`.
32
+ * Features qu'un rôle peut ouvrir. `devices` en fait partie : voir la flotte
33
+ * d'un espace est un droit comme un autre (lecture = voir les appareils et leur
34
+ * supervision, écriture = les appairer, approuver, renommer, supprimer).
45
35
  */
46
36
  export const workspaceFeatureIdSchema = z.enum([
47
37
  'devices',
@@ -61,56 +51,32 @@ export const workspaceFeatureIdSchema = z.enum([
61
51
  'projects',
62
52
  'git',
63
53
  /**
64
- * Déploiement. `read` = voir les cibles de l'espace et leur historique,
65
- * `write` = déclarer une cible, poser la clé d'API de l'instance, et
66
- * **déclencher une mise en production**.
67
- *
68
- * ⚠️ Le droit le plus lourd de conséquences hors de DevEye : c'est le seul
69
- * qui pousse quelque chose chez un tiers. Distinct de `projects` exprès —
70
- * piloter le travail et livrer ne se confondent pas, et tout le monde n'a
71
- * pas à pouvoir faire les deux.
54
+ * Déploiement. `read` = voir les cibles et leur historique, `write` =
55
+ * déclarer une cible, poser la clé d'API et déclencher une mise en
56
+ * production : le seul droit qui pousse quelque chose chez un tiers, d'où
57
+ * sa séparation de `projects`.
72
58
  */
73
59
  'deploy',
74
60
  'database',
75
61
  /**
76
- * Sauvegardes. Les destinations de l'espace (dossier serveur, dossier d'une
77
- * machine enrôlée, bucket S3) et les travaux qui y écrivent.
78
- *
79
- * `read` = voir les destinations, les travaux et leur historique, `write` =
80
- * déclarer une destination, poser sa clé secrète, créer un travail et le
81
- * déclencher.
82
- *
83
- * ⚠️ Le droit le plus lourd en lecture après `finance`, et pour une raison
84
- * différente: la liste des destinations dit **où sont les copies de tout**.
85
- * Qui la lit sait quel bucket viser pour obtenir la base entière sans jamais
86
- * toucher à DevEye. Distinct de `database` exprès — superviser une base et
87
- * savoir où en dorment les vidages ne se confondent pas.
62
+ * Sauvegardes. `read` = voir destinations, travaux et historique, `write` =
63
+ * déclarer une destination, poser sa clé, créer et déclencher un travail.
64
+ * La lecture est lourde : la liste des destinations dit où sont les copies
65
+ * de tout, d'où sa séparation de `database`.
88
66
  */
89
67
  'backup',
90
68
  /**
91
- * Finances. Le grand livre de l'espace: comptes, opérations, budgets,
92
- * échéances.
93
- *
94
- * `read` = consulter soldes, journal et tableau de bord, `write` = saisir et
95
- * corriger des opérations, tenir comptes, catégories, budgets et échéances.
96
- *
97
- * ⚠️ Le droit dont la lecture seule est déjà lourde: un livre de comptes dit
98
- * ce qu'une structure gagne, ce qu'elle doit et à qui elle paie quoi. Le
99
- * distinguer de `projects` n'est donc pas une commodité de rangement, c'est
100
- * la raison d'être de la séparation.
69
+ * Finances. `read` = consulter soldes, journal et tableau de bord, `write` =
70
+ * saisir et corriger des opérations, tenir comptes, catégories, budgets et
71
+ * échéances. La lecture seule est déjà lourde, d'où sa séparation de
72
+ * `projects`.
101
73
  */
102
74
  'finance',
103
75
  /**
104
- * Audience. Le suivi d'usage des sites livrés dernier maillon de la même
105
- * famille que `projects`, `git` et `database` : un objet de l'**espace**
106
- * qu'un projet ne fait que pointer.
107
- *
108
- * `read` = consulter les statistiques, `write` = déclarer un site, changer
109
- * ses origines autorisées, sa rétention, le supprimer.
110
- *
111
- * ⚠️ Distinct de `projects` exprès, comme `git` l'est déjà : voir les
112
- * chiffres d'un site livré et piloter le travail qui le produit ne se
113
- * confondent pas, et tout le monde n'a pas à voir les deux.
76
+ * Audience. `read` = consulter les statistiques, `write` = déclarer un site,
77
+ * changer ses origines autorisées, sa rétention, le supprimer. Distinct de
78
+ * `projects`, comme `git` : un objet de l'espace qu'un projet ne fait que
79
+ * pointer.
114
80
  */
115
81
  'audience',
116
82
  /**
@@ -125,15 +91,11 @@ export type WorkspaceFeatureId = z.infer<typeof workspaceFeatureIdSchema>;
125
91
  export const WORKSPACE_FEATURE_IDS = workspaceFeatureIdSchema.options;
126
92
 
127
93
  /**
128
- * Identifiant d'une feature **externe** (module tiers compilé dans l'app).
129
- *
130
- * Le préfixe `x-` porte trois garanties d'un coup : aucune collision possible
131
- * avec les seize ids natifs ni avec les sujets réservés (`workspace`, `home`,
132
- * `account`, `notify`, `projectsChat`), aucune confusion avec un UUID
133
- * d'appareil dans une disposition d'accueil (un UUID commence par un chiffre
134
- * hexadécimal, jamais par `x`), et un tri visuel immédiat dans un grant ou un
135
- * journal. Pas de tiret intérieur : l'id sert tel quel de préfixe de commande
136
- * (`x-crypto.list`) et de valeur de segment live.
94
+ * Identifiant d'une feature externe (module tiers compilé dans l'app). Le
95
+ * préfixe `x-` exclut toute collision avec les ids natifs, les sujets réservés
96
+ * (`workspace`, `home`, `account`, `notify`) et les UUID d'appareil d'une
97
+ * disposition (un UUID commence par un chiffre hexadécimal). Pas de tiret
98
+ * intérieur : l'id sert tel quel de préfixe de commande (`x-crypto.list`).
137
99
  */
138
100
  export const EXTERNAL_FEATURE_ID_PATTERN = /^x-[a-z][a-z0-9]{1,24}$/;
139
101
 
@@ -173,29 +135,19 @@ export const workspaceFeatureGrantSchema = z.object({
173
135
  feature: featureIdSchema,
174
136
  access: featureAccessSchema,
175
137
  /**
176
- * Gérer les **canaux d'alerte** de cette fonctionnalité : en déclarer,
177
- * corriger une adresse ou une URL, en supprimer, lire leurs destinations.
178
- *
179
- * Par fonctionnalité et non par espace (migration 093) : depuis que chaque
180
- * émetteur possède ses canaux (091), l'adresse de l'astreinte d'Uptime et
181
- * le salon des sauvegardes ne se confient pas d'un bloc. Distinct de
182
- * `access` exprès : régler où Uptime écrit relève de `access: write`, et
183
- * se donne sans livrer les destinations elles-mêmes. Sans effet sur une
184
- * fonctionnalité qui n'émet pas de notifications.
138
+ * Gérer les canaux d'alerte de cette fonctionnalité : en déclarer, corriger
139
+ * une adresse ou une URL, en supprimer, lire leurs destinations. Distinct de
140
+ * `access` : régler où Uptime écrit relève de `access: write` et se donne
141
+ * sans livrer les destinations. Sans effet sur une fonctionnalité qui
142
+ * n'émet pas de notifications.
185
143
  */
186
144
  channels: z.boolean(),
187
145
  /**
188
- * Permissions **déclarées par la feature elle-même** (module externe, ou
189
- * native modernisée) au-delà de lecture/écriture : la clé vient de son
190
- * manifest (`extraPermissions`), la valeur est un booléen (`toggle`) ou la
191
- * valeur d'un choix (`choice`).
192
- *
193
- * Fermeture par défaut, une seule règle : une clé **absente** vaut « refusé »
194
- * pour un toggle et « valeur par défaut du manifest » (la moins privilégiée)
195
- * pour un choix. Le propriétaire, qui a tout, reçoit `true` / la valeur
196
- * `ownerValue`. Une clé inconnue du manifest courant est rejetée à
197
- * l'écriture du rôle et ignorée à la lecture ; un grant survivant à la
198
- * dépose d'un module reste donc inerte, jamais dangereux.
146
+ * Permissions déclarées par la feature elle-même (`extraPermissions` du
147
+ * manifest) : booléen pour un `toggle`, valeur d'un `choice`. Une clé
148
+ * absente vaut « refusé » pour un toggle et « valeur par défaut du manifest »
149
+ * pour un choix ; le propriétaire reçoit `true` / `ownerValue`. Une clé
150
+ * inconnue du manifest est rejetée à l'écriture et ignorée à la lecture.
199
151
  */
200
152
  extras: z.record(z.string().max(24), z.union([z.boolean(), z.string().max(32)])).default({})
201
153
  });
@@ -0,0 +1,306 @@
1
+ import { z } from 'zod';
2
+ import { deviceSchema } from '../domain/device';
3
+ import { fileMutateOpSchema, fileSearchFilterSchema } from '../domain/deviceFiles';
4
+ import { deviceLogFilterSchema, DEVICE_LOG_PAGE_MAX } from '../domain/deviceLogs';
5
+ import { packageManagerIdSchema } from '../domain/packages';
6
+ import { agentLifecycleActionSchema, agentPowerActionSchema } from '../protocol/agent';
7
+
8
+ /**
9
+ * Le transport des agents : les commandes WS qui relaient un ordre du
10
+ * `MonitorHub` à l'agent d'un appareil (abonnement aux métriques, fichiers,
11
+ * terminal, journaux, paquets, alimentation, service, mise à jour).
12
+ * Infrastructure native ; un module s'en sert par la capacité `agents`.
13
+ *
14
+ * Deux espaces de noms `agent.*` coexistent : les TRAMES du protocole agent
15
+ * (`protocol/agent.ts`, entre le serveur et le binaire) et les COMMANDES
16
+ * ci-dessous, envoyées par le client sur la socket de session. Quatre noms
17
+ * existent des deux côtés (`agent.collect`, `agent.update`, `agent.lifecycle`,
18
+ * `agent.power`) : la commande est ce que demande l'utilisateur, la trame ce
19
+ * que reçoit l'agent.
20
+ */
21
+
22
+ const deviceId = z.uuid();
23
+
24
+ /** Subscribe to live metric pushes for one or more devices. */
25
+ export const agentSubscribe = {
26
+ command: 'agent.subscribe' as const,
27
+ input: z.object({ deviceIds: z.array(deviceId).min(1).max(50) }),
28
+ output: z.object({ deviceIds: z.array(deviceId) })
29
+ };
30
+
31
+ export const agentUnsubscribe = {
32
+ command: 'agent.unsubscribe' as const,
33
+ input: z.object({ deviceIds: z.array(deviceId).min(1).max(50) }),
34
+ output: z.object({ deviceIds: z.array(deviceId) })
35
+ };
36
+
37
+ /** Ask an online device to push a fresh sample + report right now. */
38
+ export const agentCollect = {
39
+ command: 'agent.collect' as const,
40
+ input: z.object({ deviceId }),
41
+ /** `requested` is false when the device isn't currently connected. */
42
+ output: z.object({ deviceId, requested: z.boolean() })
43
+ };
44
+
45
+ /**
46
+ * Run a system power action on the device (shutdown / reboot / suspend / hibernate
47
+ * / lock). Owner-or-admin; the agent must be online. The command only acknowledges
48
+ * the request — the agent applies it best-effort and the outcome streams back as a
49
+ * `device.powerResult` push event (the caller must be subscribed to the device).
50
+ */
51
+ export const agentPower = {
52
+ command: 'agent.power' as const,
53
+ input: z.object({ deviceId, action: agentPowerActionSchema }),
54
+ output: z.object({ ok: z.boolean() })
55
+ };
56
+
57
+ /**
58
+ * Stop or cleanly restart the agent process on the device (not the machine).
59
+ * `stop`: a supervised agent is relaunched by its manager, a standalone one
60
+ * stays offline until relaunched on the machine. `restart`: exit-and-relaunch.
61
+ * Owner-or-admin; the agent must be online. Fire-and-forget: the outcome is
62
+ * observed through presence.
63
+ */
64
+ export const agentLifecycle = {
65
+ command: 'agent.lifecycle' as const,
66
+ input: z.object({ deviceId, action: agentLifecycleActionSchema }),
67
+ output: z.object({ ok: z.boolean() })
68
+ };
69
+
70
+ /**
71
+ * Enable/disable the agent's per-user autostart (survives reboot, no privilege).
72
+ * Pushes `agent.service` to the connected agent (`install-user`/`uninstall-user`).
73
+ */
74
+ export const agentSetAutostart = {
75
+ command: 'agent.setAutostart' as const,
76
+ input: z.object({ deviceId, enabled: z.boolean() }),
77
+ output: z.object({ device: deviceSchema })
78
+ };
79
+
80
+ /**
81
+ * Ask the agent to become a root/system service. Hybrid: the agent pops an OS auth
82
+ * prompt if it has an interactive session, else replies `needsManualCommand` and the
83
+ * UI shows `manualCommand` (always returned, deterministic per platform) to run on
84
+ * the device. The new privilege/scope is observed on the agent's next report.
85
+ */
86
+ export const agentElevate = {
87
+ command: 'agent.elevate' as const,
88
+ input: z.object({ deviceId }),
89
+ output: z.object({ device: deviceSchema, manualCommand: z.string() })
90
+ };
91
+
92
+ /** Ask a root/system agent to drop back to a per-user service. */
93
+ export const agentDropPrivileges = {
94
+ command: 'agent.dropPrivileges' as const,
95
+ input: z.object({ deviceId }),
96
+ output: z.object({ device: deviceSchema, manualCommand: z.string() })
97
+ };
98
+
99
+ /**
100
+ * Push a self-update to a connected device's agent: the server resolves the newer
101
+ * signed binary for the device's build target and sends the `agent.update` frame.
102
+ * Admin-only (Appareils page). Fails if the agent is offline, has no known target,
103
+ * the binary is missing/unsigned, or it's already up to date.
104
+ */
105
+ export const agentUpdate = {
106
+ command: 'agent.update' as const,
107
+ input: z.object({ deviceId }),
108
+ output: z.object({ device: deviceSchema })
109
+ };
110
+
111
+ /**
112
+ * Ask the agent to enumerate its package managers + pending updates. The result
113
+ * arrives asynchronously as a `package.list` push event (the caller must be
114
+ * subscribed to the device). The command itself only acknowledges the request.
115
+ */
116
+ export const agentListPackages = {
117
+ command: 'agent.listPackages' as const,
118
+ input: z.object({ deviceId }),
119
+ output: z.object({ ok: z.boolean() })
120
+ };
121
+
122
+ /**
123
+ * Apply all pending updates of one manager. Progress streams as `package.progress`
124
+ * events, ending with `package.done`. Fails if the agent is offline or the manager
125
+ * needs root and the agent isn't privileged (elevate it first, see `agent.elevate`).
126
+ */
127
+ export const agentUpgradePackages = {
128
+ command: 'agent.upgradePackages' as const,
129
+ input: z.object({ deviceId, manager: packageManagerIdSchema }),
130
+ output: z.object({ ok: z.boolean() })
131
+ };
132
+
133
+ /**
134
+ * Ask the agent to enumerate the log sources present on the device (system journal,
135
+ * Docker containers, log files…). Owner-or-admin + agent online. The command only
136
+ * acknowledges; the list arrives as a `device.logSources` push event (the caller
137
+ * must be subscribed to the device).
138
+ */
139
+ export const agentLogSources = {
140
+ command: 'agent.logSources' as const,
141
+ input: z.object({ deviceId }),
142
+ output: z.object({ ok: z.boolean() })
143
+ };
144
+
145
+ /**
146
+ * Query one source with an advanced filter. `queryId` correlates the streamed
147
+ * result back to this request (results have no requestId). Lines arrive as one or
148
+ * more `device.logLines` push events, the last carrying `done: true`.
149
+ */
150
+ export const agentLogQuery = {
151
+ command: 'agent.logQuery' as const,
152
+ input: z.object({
153
+ deviceId,
154
+ sourceId: z.string().min(1).max(512),
155
+ /** Client-generated id echoed back on every `device.logLines` for this query. */
156
+ queryId: z.string().min(1).max(64),
157
+ filter: deviceLogFilterSchema.optional(),
158
+ limit: z.number().int().positive().max(DEVICE_LOG_PAGE_MAX).optional()
159
+ }),
160
+ output: z.object({ ok: z.boolean() })
161
+ };
162
+
163
+ const sessionId = z.string().min(1).max(64);
164
+ const cols = z.number().int().min(1).max(2000);
165
+ const rows = z.number().int().min(1).max(2000);
166
+ /** Base64-encoded terminal bytes, capped per frame (~1.5 MB). */
167
+ const termData = z.string().max(2_000_000);
168
+ /**
169
+ * Optional OS account to open the session under. Restricted to safe username
170
+ * characters (no shell metacharacters), since it reaches a `su` on the device.
171
+ */
172
+ export const terminalUser = z
173
+ .string()
174
+ .regex(/^[A-Za-z0-9._-]+$/, 'Nom d’utilisateur invalide')
175
+ .max(32);
176
+
177
+ /**
178
+ * Open an interactive terminal (PTY) on the device. Owner-or-admin + agent online.
179
+ * `sessionId` is client-generated and ties every later input/resize/close and the
180
+ * streamed `device.termOutput` / `device.termExit` push events together (the caller
181
+ * must be subscribed). `user` runs the shell under that account (`su -l`); omitted
182
+ * → the account the agent itself runs as.
183
+ */
184
+ export const agentTermOpen = {
185
+ command: 'agent.termOpen' as const,
186
+ input: z.object({ deviceId, sessionId, cols, rows, user: terminalUser.optional() }),
187
+ output: z.object({ ok: z.boolean() })
188
+ };
189
+
190
+ /** Send input (keystrokes / paste) to a terminal session. `data` is base64 bytes. */
191
+ export const agentTermInput = {
192
+ command: 'agent.termInput' as const,
193
+ input: z.object({ deviceId, sessionId, data: termData }),
194
+ output: z.object({ ok: z.boolean() })
195
+ };
196
+
197
+ /** Resize a terminal session's PTY to match the client viewport. */
198
+ export const agentTermResize = {
199
+ command: 'agent.termResize' as const,
200
+ input: z.object({ deviceId, sessionId, cols, rows }),
201
+ output: z.object({ ok: z.boolean() })
202
+ };
203
+
204
+ /** Close a terminal session (kills the shell and frees the PTY). */
205
+ export const agentTermClose = {
206
+ command: 'agent.termClose' as const,
207
+ input: z.object({ deviceId, sessionId }),
208
+ output: z.object({ ok: z.boolean() })
209
+ };
210
+
211
+ /** Correlates a request to its streamed result (results carry no requestId). */
212
+ const opId = z.string().min(1).max(64);
213
+ const path = z.string().min(1).max(4096);
214
+
215
+ /**
216
+ * List a directory. Owner-or-admin + agent online. The listing arrives as a
217
+ * `device.filesListing` push event keyed by `opId` (the caller must be subscribed).
218
+ */
219
+ export const agentFilesList = {
220
+ command: 'agent.filesList' as const,
221
+ input: z.object({ deviceId, opId, path }),
222
+ output: z.object({ ok: z.boolean() })
223
+ };
224
+
225
+ /**
226
+ * Analyse a directory's recursive disk usage (ncdu-style): the total size of each
227
+ * immediate child. Result arrives as a `device.filesUsage` push event.
228
+ */
229
+ export const agentFilesAnalyze = {
230
+ command: 'agent.filesAnalyze' as const,
231
+ input: z.object({ deviceId, opId, path }),
232
+ output: z.object({ ok: z.boolean() })
233
+ };
234
+
235
+ /**
236
+ * Recursively search a directory with an advanced filter (name/extension/content,
237
+ * date & size windows). Matches arrive as a `device.filesMatches` push event.
238
+ */
239
+ export const agentFilesSearch = {
240
+ command: 'agent.filesSearch' as const,
241
+ input: z.object({ deviceId, opId, path, filter: fileSearchFilterSchema }),
242
+ output: z.object({ ok: z.boolean() })
243
+ };
244
+
245
+ /**
246
+ * Mutate the filesystem: `delete` (file or directory, recursive), `mkdir`, or
247
+ * `rename` (needs `dest`). The outcome arrives as a `device.filesOp` push event.
248
+ */
249
+ export const agentFilesMutate = {
250
+ command: 'agent.filesMutate' as const,
251
+ input: z.object({ deviceId, opId, op: fileMutateOpSchema, path, dest: path.optional() }),
252
+ output: z.object({ ok: z.boolean() })
253
+ };
254
+
255
+ /**
256
+ * Download a file. Bytes stream back as `device.filesChunk` push events keyed by
257
+ * `opId` (base64, the last with `done: true`); the client reassembles them.
258
+ */
259
+ export const agentFilesDownload = {
260
+ command: 'agent.filesDownload' as const,
261
+ input: z.object({ deviceId, opId, path }),
262
+ output: z.object({ ok: z.boolean() })
263
+ };
264
+
265
+ /**
266
+ * Upload one chunk of a file at `offset` (0 truncates/creates it). The completion
267
+ * (on `done`) arrives as a `device.filesOp` push event (op `upload`).
268
+ */
269
+ export const agentFilesUpload = {
270
+ command: 'agent.filesUpload' as const,
271
+ input: z.object({
272
+ deviceId,
273
+ opId,
274
+ path,
275
+ offset: z.number().int().nonnegative(),
276
+ data: z.string().max(1_400_000),
277
+ done: z.boolean()
278
+ }),
279
+ output: z.object({ ok: z.boolean() })
280
+ };
281
+
282
+ export const agentCommands = [
283
+ agentSubscribe,
284
+ agentUnsubscribe,
285
+ agentCollect,
286
+ agentPower,
287
+ agentLifecycle,
288
+ agentSetAutostart,
289
+ agentElevate,
290
+ agentDropPrivileges,
291
+ agentUpdate,
292
+ agentListPackages,
293
+ agentUpgradePackages,
294
+ agentLogSources,
295
+ agentLogQuery,
296
+ agentTermOpen,
297
+ agentTermInput,
298
+ agentTermResize,
299
+ agentTermClose,
300
+ agentFilesList,
301
+ agentFilesAnalyze,
302
+ agentFilesSearch,
303
+ agentFilesMutate,
304
+ agentFilesDownload,
305
+ agentFilesUpload
306
+ ] as const;
@@ -2,16 +2,11 @@ import { z } from 'zod';
2
2
  import { liveCursorSchema, livePathSchema, livePeerSchema, liveTopicSchema } from '../domain/live';
3
3
 
4
4
  /**
5
- * Déclare où je suis, et récupère l'état de la salle.
6
- *
7
- * L'espace **n'est pas dans l'entrée** : il voyage sur l'enveloppe comme toute
8
- * commande, donc il est résolu et son appartenance vérifiée par le dispatcheur
9
- * avant que le handler ne s'exécute. L'entrée en salle est ainsi autorisée
10
- * gratuitement, par le même chemin que tout le reste.
11
- *
12
- * La réponse porte l'instantané de la salle — même motif que
13
- * `cloudSync.subscribe` : aucun trou entre l'inscription et la première
14
- * diffusion, et une reconnexion se resynchronise par ce seul appel.
5
+ * Déclare où je suis, et récupère l'état de la salle. L'espace n'est pas dans
6
+ * l'entrée : il voyage sur l'enveloppe, résolu et vérifié par le dispatcheur.
7
+ * La réponse porte l'instantané de la salle : aucun trou entre l'inscription
8
+ * et la première diffusion, et une reconnexion se resynchronise par ce seul
9
+ * appel.
15
10
  */
16
11
  export const liveHere = {
17
12
  command: 'live.here' as const,
@@ -22,42 +17,27 @@ export const liveHere = {
22
17
  export const liveCommands = [liveHere] as const;
23
18
 
24
19
  /**
25
- * Les positions de curseur, **hors du registre des commandes**.
26
- *
27
- * Délibérément absente de `featureCommandRegistry` : `ws.send` y trouverait un
28
- * descripteur, ouvrirait une promesse en attente et armerait un délai de 15 s —
29
- * pour une trame émise vingt fois par seconde dont on n'attend aucune réponse.
30
- * Le client la poste par `ws.post`, le serveur la traite sur une voie rapide
31
- * avant la recherche de commande.
20
+ * Les positions de curseur, hors du registre des commandes : `ws.send` y
21
+ * ouvrirait une promesse et armerait un délai pour une trame émise vingt fois
22
+ * par seconde sans réponse. Le client la poste par `ws.post`, le serveur la
23
+ * traite sur une voie rapide.
32
24
  */
33
25
  export const LIVE_CURSOR_COMMAND = 'live.cursor' as const;
34
26
 
35
27
  /**
36
- * Ce que porte une trame de curseur : des coordonnées, et rien d'autre.
37
- *
38
- * Ni chemin ni espace : la voie rapide court-circuite la résolution
39
- * d'autorisation, elle ne peut donc rien accepter du client qui déciderait de
40
- * *qui verra* la trame. Le lieu vient du dernier `live.here`, lui passé par le
41
- * dispatcheur. `cursor: null` = le pointeur a quitté la surface.
28
+ * Ce que porte une trame de curseur : des coordonnées, rien d'autre. Ni chemin
29
+ * ni espace : la voie rapide court-circuite l'autorisation, le lieu vient du
30
+ * dernier `live.here`. `cursor: null` = le pointeur a quitté la surface.
42
31
  */
43
32
  export const liveCursorFrameSchema = z.object({ cursor: liveCursorSchema.nullable() });
44
33
  export type LiveCursorFrame = z.infer<typeof liveCursorFrameSchema>;
45
34
 
46
35
  /**
47
- * « Untel est en train d'écrire… », sur la même voie rapide que les curseurs.
48
- *
49
- * Hors du registre des commandes, pour exactement la même raison : c'est une
50
- * trame sans réponse, émise par `ws.post`, qu'il serait absurde de faire passer
51
- * par une promesse en attente, un journal d'audit et une validation d'accès.
52
- *
53
- * Volontairement **générique** : la trame ne dit pas *quoi* est en train d'être
54
- * écrit. Le lieu vient du dernier `live.here`, comme pour les curseurs, donc
55
- * n'importe quelle feature peut s'en servir sans toucher au moteur — un fil de
56
- * discussion de projet aujourd'hui, une note à plusieurs demain.
57
- *
58
- * Le serveur applique une péremption : sans rafraîchissement, un pair cesse
59
- * d'être « en train d'écrire » tout seul. C'est ce qui empêche un onglet fermé
60
- * brutalement de laisser un fantôme à l'écran.
36
+ * « Untel est en train d'écrire… », sur la même voie rapide que les curseurs et
37
+ * hors du registre pour la même raison. Générique : la trame ne dit pas quoi,
38
+ * le lieu vient du dernier `live.here`. Le serveur applique une péremption :
39
+ * sans rafraîchissement, un pair cesse d'écrire tout seul (pas de fantôme
40
+ * après un onglet fermé brutalement).
61
41
  */
62
42
  export const LIVE_TYPING_COMMAND = 'live.typing' as const;
63
43
  export const liveTypingFrameSchema = z.object({ typing: z.boolean() });
@@ -12,31 +12,15 @@ import {
12
12
  } from '../domain/notifications';
13
13
 
14
14
  /**
15
- * Les canaux d'alerte de l'espace, et les routes qui pointent vers eux.
15
+ * Les canaux d'alerte de l'espace, et les routes qui pointent vers eux. Un
16
+ * module à part : la fonctionnalité est un argument, pas un préfixe de
17
+ * commande, et un émetteur de plus ne coûte qu'une valeur dans
18
+ * `notificationFeatureSchema`.
16
19
  *
17
- * ## Pourquoi un module à part, et pas trois commandes par émetteur
18
- *
19
- * Il y en avait quinze `getSettings`, `setSettings`, `testNotification`, pour
20
- * chacun des cinq émetteurs — strictement identiques à leur préfixe près. Le
21
- * dialogue client les reconstituait déjà par concaténation
22
- * (`` `${feature}.getSettings` ``), ce qui disait tout : la fonctionnalité
23
- * n'était pas dans la commande, elle était dans un **argument**. Elle l'est
24
- * désormais pour de bon.
25
- *
26
- * Conséquence directe : brancher un sixième émetteur ne coûte plus trois
27
- * commandes, trois entrées de registre et trois handlers, mais une valeur de
28
- * plus dans `notificationFeatureSchema`.
29
- *
30
- * ## Deux étages d'autorisation, et ils ne sont pas les mêmes
31
- *
32
- * Gérer les **canaux** d'une fonctionnalité relève du champ `channels` de son
33
- * grant de rôle (migration 093) : un canal appartient à une fonctionnalité
34
- * (091), et son adresse ne se livre qu'à qui gère les canaux de celle-ci. Les
35
- * **routes**, elles, relèvent de la fonctionnalité visée (`{ feature, level:
36
- * 'write' }`) : décider où Uptime écrit fait partie du réglage d'Uptime, et n'a
37
- * pas à ouvrir la gestion des destinations. C'est la séparation qui permet de
38
- * confier le routage d'une fonctionnalité sans confier l'adresse de
39
- * l'astreinte.
20
+ * Deux étages d'autorisation : gérer les canaux d'une fonctionnalité relève du
21
+ * champ `channels` de son grant ; les routes relèvent de la fonctionnalité
22
+ * visée (`{ feature, level: 'write' }`). On peut confier le routage sans
23
+ * confier l'adresse de l'astreinte.
40
24
  */
41
25
 
42
26
  const channelId = z.number().int().positive();
@@ -62,11 +46,8 @@ export const notifyChannelUpdate = {
62
46
  };
63
47
 
64
48
  /**
65
- * Ce qu'une suppression emporterait, **sans rien supprimer**.
66
- *
67
- * Lue par la confirmation pour nommer les routes qui vont cesser de prévenir.
68
- * Séparée de la suppression elle-même parce qu'un écran qui demande « êtes-vous
69
- * sûr ? » sans dire de quoi ne fait pas confirmer, il fait cliquer.
49
+ * Ce qu'une suppression emporterait, sans rien supprimer : lue par la
50
+ * confirmation pour nommer les routes qui vont cesser de prévenir.
70
51
  */
71
52
  export const notifyChannelUsage = {
72
53
  command: 'notify.channelUsage' as const,
@@ -86,14 +67,7 @@ export const notifyChannelReorder = {
86
67
  output: z.object({ ok: z.literal(true) })
87
68
  };
88
69
 
89
- /**
90
- * Un envoi d'essai **sur un seul canal**, tel qu'il est enregistré.
91
- *
92
- * L'ancien dialogue devait enregistrer avant de tester, faute de quoi l'essai
93
- * partait sur les réglages précédents. Un canal étant une entité à part entière,
94
- * l'essai vise directement son identifiant : plus d'enregistrement forcé, et
95
- * plus de doute sur ce qui vient d'être éprouvé.
96
- */
70
+ /** Un envoi d'essai sur un seul canal, tel qu'il est enregistré. */
97
71
  export const notifyChannelTest = {
98
72
  command: 'notify.channelTest' as const,
99
73
  input: z.object({ id: channelId }),
@@ -107,31 +81,19 @@ export const notifyRouteGet = {
107
81
  output: z.object({
108
82
  route: notificationRouteSchema,
109
83
  /**
110
- * Les canaux de la route qui **n'appartiennent pas à cet espace**.
111
- *
112
- * Le cas d'un élément projeté depuis ailleurs : ses destinations vivent
113
- * dans son espace d'origine. Sans cette liste, l'écran afficherait
114
- * « aucun canal » sur un élément qui prévient bel et bien — le mensonge
115
- * exact que la projection devait éviter.
116
- *
117
- * Rendus **masqués** : leur genre (« Salon Discord d'un autre espace »),
118
- * jamais leur identité ni leur adresse.
84
+ * Les canaux de la route qui n'appartiennent pas à cet espace (élément
85
+ * projeté depuis ailleurs). Rendus masqués : leur genre, jamais leur
86
+ * identité ni leur adresse.
119
87
  */
120
88
  foreign: z.array(notificationChannelSchema),
121
89
  /**
122
- * Cette route se règle-t-elle **d'ici** ?
123
- *
124
- * Faux sur un élément projeté depuis un autre espace : ses canaux
125
- * appartiennent à cet espace-là, et l'ordonnanceur qui le sonde y
126
- * tourne. Laisser l'écran proposer le réglage produirait un geste que
127
- * le serveur refuse — un écran qui ment, pas une garde.
90
+ * Cette route se règle-t-elle d'ici ? Faux sur un élément projeté
91
+ * depuis un autre espace : ses canaux appartiennent à cet espace-là.
128
92
  */
129
93
  managedHere: z.boolean(),
130
94
  /**
131
- * L'espace **où cette route se règle** : le domicile de l'élément.
132
- * Égal à l'espace de l'enveloppe quand `managedHere` est vrai. C'est ce
133
- * qui permet à l'écran, sur un élément projeté, de proposer d'aller
134
- * régler chez lui plutôt que d'expliquer un refus.
95
+ * L'espace où cette route se règle : le domicile de l'élément (égal à
96
+ * l'espace de l'enveloppe quand `managedHere` est vrai).
135
97
  */
136
98
  homeWorkspaceId: z.number().int().positive()
137
99
  })
@@ -143,7 +105,7 @@ export const notifyRouteSet = {
143
105
  output: z.object({ route: notificationRouteSchema })
144
106
  };
145
107
 
146
- /** Un essai sur la route entière, héritage compris — ce que verrait une vraie alerte. */
108
+ /** Un essai sur la route entière : ce que verrait une vraie alerte. */
147
109
  export const notifyRouteTest = {
148
110
  command: 'notify.routeTest' as const,
149
111
  input: notificationRouteTargetSchema,