@deveye/types 0.15.1 → 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 +9 -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 +244 -22
  31. package/src/sdk/client.ts +277 -17
  32. package/src/sdk/devb.ts +120 -0
  33. package/src/sdk/manifest.test.ts +96 -0
  34. package/src/sdk/manifest.ts +160 -25
  35. package/src/sdk/providers.ts +310 -5
  36. package/src/sdk/server.ts +483 -19
  37. package/src/sdk/testing.test.ts +62 -0
  38. package/src/sdk/testing.ts +416 -40
  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
@@ -53,17 +53,10 @@ export function segmentValue(segment: string): string {
53
53
  }
54
54
 
55
55
  /**
56
- * L'**état** du curseur, tel que le navigateur le dessine à celui qui le tient.
57
- *
58
- * Transmis avec la position parce qu'il porte l'intention : une flèche qui
59
- * devient main dit « il s'apprête à cliquer », un curseur de texte dit « il
60
- * lit ou il sélectionne », une main fermée dit « il déplace quelque chose ».
61
- * Sans lui, tous les pairs seraient perpétuellement en flèche neutre, et le
62
- * geste d'en face resterait illisible.
63
- *
64
- * Volontairement **court** : l'ensemble des curseurs CSS compte une trentaine de
65
- * valeurs, dont la plupart ne se distinguent pas à seize pixels. Sept familles
66
- * suffisent, et c'est autant de dessins à tenir.
56
+ * L'état du curseur, tel que le navigateur le dessine à celui qui le tient.
57
+ * Transmis avec la position parce qu'il porte l'intention (cliquer, lire,
58
+ * déplacer). Sept familles seulement : la plupart des curseurs CSS ne se
59
+ * distinguent pas à seize pixels.
67
60
  */
68
61
  export const liveCursorKindSchema = z.enum([
69
62
  'default',
@@ -83,26 +76,14 @@ export const liveCursorKindSchema = z.enum([
83
76
  export type LiveCursorKind = z.infer<typeof liveCursorKindSchema>;
84
77
 
85
78
  /**
86
- * Position du curseur dans la **surface** de la vue (le corps de la popup, ou la
87
- * grille de l'accueil quand rien n'est ouvert).
88
- *
89
- * Unités volontairement mixtes, parce que les deux axes n'ont pas le même sens :
90
- *
91
- * - `x` est **relatif** (0..1) à la largeur de la surface. La popup est bornée
92
- * à 1240 px : au-delà les deux fenêtres ont la même boîte, en dessous elles
93
- * divergent, et seule une fraction reste juste.
94
- * - `y` est en **pixels absolus du contenu**, défilement compris. Le contenu
95
- * est le même des deux côtés (même liste, mêmes lignes) : « le pair est sur
96
- * le 14ᵉ message » est le sens qu'on veut, alors qu'une fraction de la
97
- * hauteur totale se décalerait dès qu'une liste est chargée plus loin d'un
98
- * côté que de l'autre.
99
- *
100
- * Les bornes de `x` dépassent [0, 1] très largement, et à dessein : le pointeur
101
- * vit aussi **à côté** de la boîte de contenu — ses marges, les bords de l'écran
102
- * — et l'y écrêter ferait disparaître le curseur d'un pair alors qu'on est
103
- * toujours sur la même page. Sur un écran très large, ces marges représentent
104
- * plusieurs fois la largeur du contenu ; les bornes ne sont donc qu'un garde-fou
105
- * contre l'absurde, et seul le cadre de la fenêtre décide de ce qui s'affiche.
79
+ * Position du curseur dans la surface de la vue. Unités mixtes à dessein :
80
+ * - `x` est relatif (0..1) à la largeur de la surface, bornée à 1240 px : seule
81
+ * une fraction reste juste quand les deux fenêtres divergent ;
82
+ * - `y` est en pixels absolus du contenu, défilement compris : le contenu est
83
+ * le même des deux côtés, alors qu'une fraction de la hauteur se décalerait
84
+ * dès qu'une liste est chargée plus loin d'un côté.
85
+ * Les bornes de `x` dépassent [0, 1] : le pointeur vit aussi dans les marges,
86
+ * et l'écrêter ferait disparaître le curseur d'un pair sur la même page.
106
87
  */
107
88
  export const liveCursorSchema = z.object({
108
89
  x: z.number().min(-10).max(10),
@@ -112,13 +93,11 @@ export const liveCursorSchema = z.object({
112
93
  export type LiveCursor = z.infer<typeof liveCursorSchema>;
113
94
 
114
95
  /**
115
- * Un pair tel qu'il est diffusé.
116
- *
117
- * Ni pseudo ni avatar : `users.avatar` est une URL de données pouvant atteindre
118
- * 1,5 Mo (`AVATAR_MAX_LENGTH`), et le roster repart à chaque changement de
119
- * chemin. Le client résout les deux par `userId` contre les membres de l'espace,
120
- * que la session lui a déjà donnés. Seule la **couleur** voyage, parce qu'elle
121
- * doit changer à l'instant où son propriétaire la change.
96
+ * Un pair tel qu'il est diffusé. Ni pseudo ni avatar : `users.avatar` est une
97
+ * URL de données pouvant atteindre 1,5 Mo, et le roster repart à chaque
98
+ * changement de chemin ; le client résout les deux par `userId` contre les
99
+ * membres de l'espace. Seule la couleur voyage, parce qu'elle doit changer à
100
+ * l'instant son propriétaire la change.
122
101
  */
123
102
  export const livePeerSchema = z.object({
124
103
  /** Identité de la *connexion*, pas du compte : deux onglets = deux pairs. */
@@ -141,50 +120,32 @@ export type LivePeer = z.infer<typeof livePeerSchema>;
141
120
  */
142
121
  export const nativeLiveTopicSchema = z.enum([
143
122
  ...workspaceFeatureIdSchema.options,
144
- /**
145
- * Les messages des projets, séparés de `projects` exprès.
146
- *
147
- * Une feature vaut normalement un sujet, mais un fil de discussion bat à une
148
- * toute autre cadence que la structure qui le porte : sans cette coupure,
149
- * chaque message ferait re-solliciter le tableau, la frise et le portefeuille
150
- * entiers. `TOPIC_FEATURE` le rattache au même droit — c'est bien la même
151
- * feature, vue à deux vitesses.
152
- */
153
- 'projectsChat',
154
123
  /** Membres, rôles, nom, logo de l'espace. */
155
124
  'workspace',
156
125
  /**
157
- * L'accueil de l'espace : sa **disposition** et son **apparence**.
158
- *
159
- * Les deux voyagent ensemble parce qu'ils se relisent ensemble une seule
160
- * commande (`workspace.activate`) les rend tous les deux, donc les séparer
161
- * en deux sujets ne ferait que doubler les allers-retours pour un même
162
- * rafraîchissement. Ce sont aussi des réglages **de l'espace** : `account`
163
- * ne conviendrait pas au thème, il ne sort jamais de l'espace personnel.
126
+ * L'accueil de l'espace : disposition et apparence. Ensemble parce qu'ils
127
+ * se relisent ensemble (`workspace.activate` rend les deux), et sous
128
+ * l'espace et non `account` : le thème est un réglage de l'espace.
164
129
  */
165
130
  'home',
166
131
  /** Réglages de compte (avatar, couleur, thème, chiffrement). */
167
132
  'account',
168
133
  /**
169
- * Les canaux d'alerte de l'espace, et les routes qui pointent dessus.
170
- *
171
- * Un sujet à lui, et non `workspace` : les canaux se relisent depuis
172
- * l'écran de réglages de n'importe quelle fonctionnalité, et les rattacher
173
- * au sujet de l'espace ferait re-solliciter la liste des membres, les rôles
174
- * et le nom à chaque fois qu'on coche une case. Ni `uptime` ni ses voisins
175
- * ne conviennent non plus : une route change pour **une** fonctionnalité,
176
- * mais un canal change pour toutes à la fois, et `mutates` est déclaré par
177
- * commande, pas par argument.
134
+ * Les canaux d'alerte de l'espace et les routes qui pointent dessus. Un
135
+ * sujet à part : les canaux se relisent depuis les réglages de n'importe
136
+ * quelle fonctionnalité, et un canal change pour toutes à la fois alors que
137
+ * `mutates` est déclaré par commande.
178
138
  */
179
139
  'notify'
180
140
  ]);
181
141
  export type NativeLiveTopic = z.infer<typeof nativeLiveTopicSchema>;
182
142
 
183
143
  /**
184
- * Un module externe vaut **un** sujet, qui est son id : la coupure fine de
185
- * `projectsChat` reste un privilège natif, un module re-sollicite tout ce qu'il
186
- * expose. Le préfixe `x-` garantit qu'un sujet externe ne percute ni une
187
- * feature native ni un sujet réservé.
144
+ * Un module vaut **un** sujet, qui est son id, plus les sujets secondaires que
145
+ * son manifest déclare (`topics`, validés au boot : `projectsChat` bat les
146
+ * messages sans faire re-solliciter le tableau). Ces sujets-là ne figurent pas
147
+ * ici : l'app les lit dans les manifests installés. Le préfixe `x-` garantit
148
+ * qu'un sujet externe ne percute ni une feature native ni un sujet réservé.
188
149
  */
189
150
  export const liveTopicSchema = z.union([nativeLiveTopicSchema, externalFeatureIdSchema]);
190
151
  export type LiveTopic = z.infer<typeof liveTopicSchema>;
@@ -218,7 +179,6 @@ export const TOPIC_FEATURE: Record<NativeLiveTopic, WorkspaceFeatureId | null> =
218
179
  uptime: 'uptime',
219
180
  mail: 'mail',
220
181
  projects: 'projects',
221
- projectsChat: 'projects',
222
182
  git: 'git',
223
183
  deploy: 'deploy',
224
184
  database: 'database',
@@ -229,28 +189,17 @@ export const TOPIC_FEATURE: Record<NativeLiveTopic, WorkspaceFeatureId | null> =
229
189
  workspace: null,
230
190
  home: null,
231
191
  account: null,
232
- // Aucun droit de feature à vérifier : la diffusion ne dit que « quelque
233
- // chose a changé », et la relecture qu'elle déclenche est gardée côté
234
- // commande (droits de la fonctionnalité et gestion de ses canaux). Même
235
- // nature que `workspace`.
192
+ // Aucun droit de feature à vérifier : la relecture déclenchée est gardée
193
+ // côté commande. Même nature que `workspace`.
236
194
  notify: null
237
195
  };
238
196
 
239
197
  /**
240
- * Le droit qu'exige la **racine** d'un chemin, pour décider si un pair est
241
- * montré là où il est ou renvoyé à « ailleurs ».
242
- *
243
- * Trois issues :
244
- * - une feature montré au destinataire qui a `read` dessus ;
245
- * - `'public'` → montré à tout membre (aujourd'hui : personne, gardé pour un
246
- * éventuel lieu commun) ;
247
- * - `'private'` → **jamais montré**, à personne.
248
- *
249
- * Les vues de compte et d'administration (Profil, Sécurité, Journaux,
250
- * Utilisateurs, Gestion de l'espace) tombent dans `'private'`. `featureBehind`
251
- * côté client leur rend `null` parce qu'elles ont leurs propres gardes ; ici
252
- * `null` voudrait dire « visible par tous », ce qui ferait fuiter « untel est
253
- * dans Sécurité ». D'où le troisième cas, plutôt qu'une réutilisation directe.
198
+ * Le droit qu'exige la racine d'un chemin, pour décider si un pair est montré
199
+ * là où il est ou renvoyé à « ailleurs » : une feature (montré à qui a `read`
200
+ * dessus), `'public'` (tout membre), `'private'` (jamais montré). Les vues de
201
+ * compte et d'administration tombent dans `'private'` : `null` voudrait dire
202
+ * « visible par tous » et ferait fuiter « untel est dans Sécurité ».
254
203
  */
255
204
  export type LivePathGate = FeatureId | 'public' | 'private';
256
205
 
@@ -49,11 +49,9 @@ export const metricSnapshotSchema = z.object({
49
49
  batteryCharging: z.boolean().nullable().default(null),
50
50
  /**
51
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`.
52
+ * `null` = not carried by this row (capture mode `off`, or trimmed from a
53
+ * long offline queue). Persisted separately (`device_process_samples`)
54
+ * under this row's `timestamp`, so `metrics.query` does not echo it back.
57
55
  */
58
56
  processes: z.array(reportProcessSchema).max(2000).nullable().default(null),
59
57
  /**
@@ -67,10 +65,10 @@ export const metricSnapshotSchema = z.object({
67
65
  export type MetricSnapshot = z.infer<typeof metricSnapshotSchema>;
68
66
 
69
67
  /**
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.
68
+ * Batch of snapshots pushed by an agent over the agent WebSocket. Bounded so an
69
+ * offline queue drains in chunks; the agent additionally caps a batch by
70
+ * serialized size, since 100 snapshots with process lists would make a
71
+ * multi-megabyte frame.
74
72
  */
75
73
  export const metricsBatchSchema = z.object({
76
74
  deviceId: z.uuid(),
@@ -88,11 +86,9 @@ export const metricsResolutionSchema = z.enum(['raw', 'minute', 'hour']);
88
86
  export type MetricsResolution = z.infer<typeof metricsResolutionSchema>;
89
87
 
90
88
  /**
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.
89
+ * A point read back from `device_metrics`: a snapshot minus its process list,
90
+ * which lives in its own table (`metrics.processesAt`). A graph window holds
91
+ * hundreds of points; carrying every process list along would cost megabytes.
96
92
  */
97
93
  export const metricSeriesPointSchema = metricSnapshotSchema.omit({
98
94
  processes: true,
@@ -4,64 +4,27 @@ import { NOTIFYING_FEATURES } from './featureRegistry';
4
4
  import { externalFeatureIdSchema } from './workspaceRole';
5
5
 
6
6
  /**
7
- * Les canaux d'alerte d'un espace **une liste, et des liaisons vers elle**.
7
+ * Les canaux d'alerte d'un espace : une liste, et des liaisons vers elle.
8
8
  *
9
- * ## Ce que le modèle précédent ne savait pas faire
9
+ * Un canal est une destination nommée (type, libellé, cible) qui appartient à
10
+ * une fonctionnalité : c'est une source de cette fonctionnalité, gérée dans
11
+ * ses réglages. Deux features qui préviennent le même salon le déclarent deux
12
+ * fois, et chacune se corrige à un seul endroit.
10
13
  *
11
- * Il portait deux canaux binaires un mail, un webhook par couple
12
- * `(espace, feature)`. Trois conséquences, toutes rencontrées :
13
- *
14
- * 1. **Le même salon Discord était redéclaré cinq fois.** Le corriger demandait
15
- * d'ouvrir cinq écrans, et en oublier un ne se voyait qu'à la première alerte
16
- * qui n'arrivait plus.
17
- * 2. **Deux destinataires étaient impossibles.** Une équipe pour la production,
18
- * une autre pour la recette : il fallait choisir.
19
- * 3. **Aucun routage par élément.** Toutes les bases d'un espace prévenaient les
20
- * mêmes gens, quel que soit le projet derrière.
21
- *
22
- * ## La forme retenue
23
- *
24
- * Un **canal** est une destination nommée : un type, un libellé, une cible. Il
25
- * appartient à **une fonctionnalité** (091) : c'est une source de cette
26
- * fonctionnalité, au même titre qu'un jeton Dokploy pour le Déploiement, et il
27
- * se gère dans ses réglages à elle. La 087 l'avait fait vivre à l'échelle de
28
- * l'espace, partagé par les cinq émetteurs ; on retrouvait alors une liste
29
- * commune gérée depuis cinq endroits, l'inverse du patron des sources. Le prix
30
- * assumé du retour : deux features qui préviennent le même salon le déclarent
31
- * deux fois. On en déclare autant qu'on veut, et on les corrige **à un seul
32
- * endroit** : les réglages de leur fonctionnalité.
33
- *
34
- * Une **route** dit qui écrit vers quels canaux, et la sélection vit **sur
35
- * l'élément** (092) : chaque cible coche un ou plusieurs canaux de sa feature
36
- * dans ses propres réglages, et sans sélection rien ne part. L'héritage
37
- * d'une « route de la fonctionnalité » a été essayé (087) puis retiré : cocher
38
- * un canal à l'échelle de la feature ne visait aucun élément nommable, et les
39
- * cases des éléments, grisées tant qu'ils « suivaient » la feature, semblaient
40
- * ne jamais pouvoir se cocher. Une route de fonctionnalité (`itemId` absent)
41
- * ne subsiste que pour les émetteurs **sans éléments** (Sentinelle), dont les
42
- * alertes ne visent rien de plus fin.
43
- *
44
- * Ce qui n'a pas changé, et qui compte : **tout est éteint par défaut.** Sans
45
- * canal ni route, rien ne part. Une fonctionnalité qui se met à écrire à des
46
- * gens sans qu'ils l'aient demandé reste le travers que ce modèle refuse.
14
+ * Une route dit qui écrit vers quels canaux ; la sélection vit sur l'élément.
15
+ * Une route de fonctionnalité (`itemId` absent) ne subsiste que pour les
16
+ * émetteurs sans éléments (Sentinelle). Tout est éteint par défaut : sans
17
+ * canal ni route, rien ne part.
47
18
  */
48
19
 
49
20
  /**
50
- * Le type d'un canal, et ce qu'il change à l'envoi.
51
- *
52
- * `webhook` et `discord` **séparent ce que l'URL devinait**. Le module d'envoi
53
- * reconnaissait Discord en analysant l'adresse, ce qui marchait mais décidait à
54
- * la place de l'utilisateur : un point d'entrée maison hébergé derrière un
55
- * domaine Discord aurait reçu des embeds au lieu de son texte, et rien ne
56
- * permettait de demander l'inverse. C'est désormais une déclaration.
57
- *
58
- * - `email` — un compte Mail « open » de l'espace expédie vers une adresse.
59
- * - `webhook` — un POST JSON générique : le message lisible est répété dans
60
- * `content` (Discord) et `text` (Slack), les champs structurés suivent pour
61
- * un point d'entrée maison. Aucune des trois têtes ne gêne les autres.
62
- * - `discord` — la mise en page riche de Discord (embeds, couleurs, champs),
63
- * et pour le déploiement le **suivi vivant** : un seul message qui se met à
64
- * jour du début à la fin.
21
+ * Le type d'un canal, et ce qu'il change à l'envoi. `webhook` et `discord`
22
+ * sont une déclaration, jamais devinés d'après l'URL.
23
+ * - `email` : un compte Mail « open » de l'espace expédie vers une adresse.
24
+ * - `webhook` : un POST JSON générique, le message lisible répété dans
25
+ * `content` (Discord) et `text` (Slack), les champs structurés à côté.
26
+ * - `discord` : embeds, couleurs, champs, et le suivi vivant d'un déploiement
27
+ * (un seul message mis à jour du début à la fin).
65
28
  */
66
29
  export const notificationChannelKindSchema = z.enum(['email', 'webhook', 'discord']);
67
30
  export type NotificationChannelKind = z.infer<typeof notificationChannelKindSchema>;
@@ -69,12 +32,9 @@ export type NotificationChannelKind = z.infer<typeof notificationChannelKindSche
69
32
  export const NOTIFICATION_CHANNEL_KINDS = notificationChannelKindSchema.options;
70
33
 
71
34
  /**
72
- * Les fonctionnalités **natives** qui savent prévenir.
73
- *
74
- * Enum fermé plutôt que chaîne libre : c'est lui qui garde une route d'être
75
- * posée sur une fonctionnalité qui n'écrira jamais. Il double le drapeau
76
- * `notifies` du registre, et le contrôle en bas de fichier interdit qu'ils
77
- * divergent.
35
+ * Les fonctionnalités natives qui savent prévenir. Enum fermé : c'est lui qui
36
+ * garde une route d'être posée sur une fonctionnalité qui n'écrira jamais. Il
37
+ * double `notifies` du registre ; le contrôle en bas de fichier les tient égaux.
78
38
  */
79
39
  export const nativeNotificationFeatureSchema = z.enum([
80
40
  'uptime',
@@ -102,11 +62,9 @@ export const NOTIFICATION_TARGET_MAX = 2048;
102
62
  export const NOTIFICATION_EMAIL_MAX = 320;
103
63
 
104
64
  /**
105
- * Un canal tel que le client le reçoit.
106
- *
107
- * `target` sort **en clair** : c'est une adresse que son auteur a saisie et doit
108
- * pouvoir relire pour la corriger. Elle est chiffrée au repos (étage ouvert),
109
- * comme l'étaient déjà les réglages qu'elle remplace.
65
+ * Un canal tel que le client le reçoit. `target` sort en clair : c'est une
66
+ * adresse que son auteur a saisie et doit pouvoir relire. Chiffrée au repos
67
+ * (étage ouvert).
110
68
  */
111
69
  export const notificationChannelSchema = z.object({
112
70
  id: z.number().int().positive(),
@@ -115,35 +73,25 @@ export const notificationChannelSchema = z.object({
115
73
  label: z.string().min(1).max(NOTIFICATION_LABEL_MAX),
116
74
  /**
117
75
  * Adresse destinataire (`email`) ou URL appelée en POST (`webhook`,
118
- * `discord`) : **vide pour qui n'a pas la gestion des canaux de la
119
- * fonctionnalité** (le champ `channels` de son grant, migration 093).
120
- *
121
- * La liste est lisible avec la fonctionnalité, parce qu'il faut voir les
122
- * destinations pour router vers l'une d'elles. Leur *contenu* ne l'est
123
- * pas : confier le réglage d'Uptime ne confie pas l'adresse de l'astreinte
124
- * ni l'URL du salon de production. On voit donc « Astreinte · e-mail », on
125
- * peut y router, et on ne peut ni la lire ni la modifier.
76
+ * `discord`). Vide pour qui n'a pas la gestion des canaux de la
77
+ * fonctionnalité (`channels` du grant) : la liste est lisible avec la
78
+ * fonctionnalité, pour router, mais confier le réglage d'Uptime ne confie
79
+ * pas l'adresse de l'astreinte.
126
80
  */
127
81
  target: z.string().max(NOTIFICATION_TARGET_MAX),
128
82
  /** Le compte Mail expéditeur ; `null` hors des canaux `email`. */
129
83
  mailAccountId: z.number().int().positive().nullable(),
130
84
  /**
131
- * Ce canal partirait-il **maintenant** ?
132
- *
133
- * Faux quand le compte expéditeur manque, a disparu, est désactivé ou n'est
134
- * pas au palier « open ». L'interface le dit au lieu de laisser croire à un
135
- * canal actif — l'avertissement n'existait à l'origine que dans Uptime, et
136
- * son absence ailleurs faisait passer un canal muet pour un canal réglé.
85
+ * Ce canal partirait-il maintenant ? Faux quand le compte expéditeur
86
+ * manque, a disparu, est désactivé ou n'est pas au palier « open ».
137
87
  */
138
88
  ready: z.boolean(),
139
89
  /** Éteint sans être supprimé : ses routes restent, rien ne part. */
140
90
  enabled: z.boolean(),
141
91
  position: z.number().int().nonnegative(),
142
92
  /**
143
- * Combien de routes le désignent ce que l'écran affiche en « utilisé par
144
- * N ». Compté côté serveur : le client n'a pas les routes des éléments sous
145
- * la main, et les demander toutes pour afficher un nombre serait une
146
- * requête par ligne.
93
+ * Combien de routes le désignent utilisé par N »). Compté côté serveur :
94
+ * le client n'a pas les routes des éléments sous la main.
147
95
  */
148
96
  usageCount: z.number().int().nonnegative()
149
97
  });
@@ -172,13 +120,7 @@ export const notificationRouteTargetSchema = z.object({
172
120
  });
173
121
  export type NotificationRouteTarget = z.infer<typeof notificationRouteTargetSchema>;
174
122
 
175
- /**
176
- * Où écrit une cible : sa sélection de canaux, rien de plus.
177
- *
178
- * Vide, elle ne prévient personne — il n'y a plus d'héritage à distinguer
179
- * (092), donc plus de drapeau `inherits` : une sélection vide et une sélection
180
- * jamais faite disent la même chose, le silence.
181
- */
123
+ /** Où écrit une cible : sa sélection de canaux. Vide, elle ne prévient personne. */
182
124
  export const notificationRouteSchema = z.object({
183
125
  channelIds: z.array(z.number().int().positive())
184
126
  });
@@ -191,21 +133,15 @@ export const notificationRouteInputSchema = notificationRouteTargetSchema.extend
191
133
  export type NotificationRouteInput = z.infer<typeof notificationRouteInputSchema>;
192
134
 
193
135
  /**
194
- * Ce que rend un envoi d'essai : parti, ou pourquoi non.
195
- *
196
- * `sent: false` avec un `error` n'est pas une exception — « aucun canal activé »
197
- * est une réponse, pas une panne, et la remonter comme telle laisserait l'écran
198
- * afficher « échec » là où il n'y a rien à échouer.
136
+ * Ce que rend un envoi d'essai : parti, ou pourquoi non. `sent: false` avec un
137
+ * `error` n'est pas une exception : « aucun canal activé » est une réponse.
199
138
  */
200
139
  export const notificationTestSchema = z.object({ sent: z.boolean(), error: z.string().nullable() });
201
140
  export type NotificationTest = z.infer<typeof notificationTestSchema>;
202
141
 
203
142
  /**
204
- * Ce qu'une suppression de canal emporte avec elle.
205
- *
206
- * Rendu **avant** la suppression pour que la confirmation nomme ce qui va
207
- * cesser de prévenir, plutôt que de demander « êtes-vous sûr ? » sans dire de
208
- * quoi. Une liste vide veut dire qu'aucune route ne le désigne.
143
+ * Ce qu'une suppression de canal emporte, rendu avant la suppression pour que
144
+ * la confirmation nomme ce qui va cesser de prévenir. Liste vide = aucune route.
209
145
  */
210
146
  export const notificationChannelUsageSchema = z.object({
211
147
  channelId: z.number().int().positive(),
@@ -224,7 +160,7 @@ export type NotificationChannelUsage = z.infer<typeof notificationChannelUsageSc
224
160
  export interface NotificationChannelRow {
225
161
  id: number;
226
162
  workspace_id: number;
227
- /** La fonctionnalité propriétaire : un canal est une source de SA feature (091). */
163
+ /** La fonctionnalité propriétaire : un canal est une source de sa feature. */
228
164
  feature: NotificationFeature;
229
165
  kind: NotificationChannelKind;
230
166
  label_enc: string;
@@ -245,16 +181,9 @@ export interface NotificationRouteRow {
245
181
  }
246
182
 
247
183
  /**
248
- * Contrôle de cohérence, au chargement du module.
249
- *
250
- * `notifies` dans le registre et cet enum répondent à la même question ; les
251
- * tenir séparés est un choix (l'un décrit, l'autre valide), les laisser diverger
252
- * n'en est pas un. Une fonctionnalité marquée `notifies` mais absente de l'enum
253
- * afficherait un onglet Notifications dont toutes les commandes seraient
254
- * refusées — un écran qui ment, découvert à la première alerte attendue.
255
- *
256
- * Même esprit que le contrôle des sujets `mutates` côté serveur : attraper
257
- * l'oubli au démarrage plutôt qu'en production.
184
+ * Contrôle de cohérence au chargement : une fonctionnalité marquée `notifies`
185
+ * mais absente de l'enum afficherait un onglet Notifications dont toutes les
186
+ * commandes seraient refusées.
258
187
  */
259
188
  {
260
189
  const registry = [...NOTIFYING_FEATURES].sort().join(', ');
@@ -1,168 +1,9 @@
1
1
  import { z } from 'zod';
2
2
 
3
3
  /**
4
- * Projets : le suivi d'un travail, de ses premières phases à son déploiement.
5
- *
6
- * Découpage du stockage (voir `Docs/SECURITY_MODEL.md`). En **clair** tout ce
7
- * dont le serveur a besoin pour lister, trier, compter et router sans rien
8
- * déchiffrer — `workspace_id`, `status`, `security_tier`, `sort_order`, les
9
- * dates, `archived_at`. **Chiffré** tout ce qui identifie : titre, description,
10
- * étiquettes, version.
11
- *
12
- * Comme le mail, et contrairement à Uptime ou au coffre, l'étage de chiffrement
13
- * n'est pas fixé par la feature mais **choisi par projet** (`securityTier`) :
14
- * un projet `open` peut être synchronisé en tâche de fond (dépôt git,
15
- * déploiement) ; un projet `guarded` ne se déchiffre que pendant une session
16
- * vivante et déverrouillée, et perd donc ses intégrations automatiques.
17
- */
18
-
19
- export const PROJECT_TITLE_MAX_LENGTH = 120;
20
- export const PROJECT_DESCRIPTION_MAX_LENGTH = 4000;
21
- export const PROJECT_VERSION_MAX_LENGTH = 40;
22
- export const PROJECT_TAG_LABEL_MAX_LENGTH = 32;
23
- export const PROJECT_MAX_TAGS = 24;
24
-
25
- /**
26
- * Borne de l'icône d'un projet, en caractères de son URL de données.
27
- *
28
- * ~400 ko : de quoi loger confortablement une vignette carrée redimensionnée
29
- * par le client, sans laisser une charge utile WS grossir au gré de ce qu'on
30
- * dépose. L'icône vit **dans le payload chiffré** comme le titre : elle
31
- * identifie le projet autant qu'un nom, et un projet confidentiel ne doit pas
32
- * la laisser lire.
33
- */
34
- export const PROJECT_ICON_MAX_LENGTH = 400_000;
35
-
36
- /** Vide = icône par défaut. Sinon, une URL de données d'image. */
37
- export const projectIconSchema = z
38
- .string()
39
- .max(PROJECT_ICON_MAX_LENGTH)
40
- .refine((v) => v === '' || /^data:image\/(png|jpeg|webp);base64,[A-Za-z0-9+/]+=*$/.test(v), {
41
- message: 'L’icône doit être une image encodée en base64.'
42
- });
43
-
44
- /** Quel coffre chiffre l'arbre du projet. Voir l'en-tête de ce fichier. */
45
- export const projectSecurityTierSchema = z.enum(['open', 'guarded']);
46
- export type ProjectSecurityTier = z.infer<typeof projectSecurityTierSchema>;
47
-
48
- /**
49
- * Où en est le projet. Volontairement court : c'est un état de pilotage, pas un
50
- * workflow — le détail de l'avancement vit dans les colonnes du kanban.
4
+ * en est un projet : un état de pilotage, pas un workflow (l'avancement vit
5
+ * dans les colonnes du kanban). Seul vocabulaire de Projets gardé ici, parce que
6
+ * d'autres features le parlent (`ProjectUsage.status`, `sdk/providers.ts`).
51
7
  */
52
8
  export const projectStatusSchema = z.enum(['draft', 'active', 'paused', 'done']);
53
9
  export type ProjectStatus = z.infer<typeof projectStatusSchema>;
54
-
55
- /**
56
- * D'où vient le numéro de version affiché.
57
- * - `manual` — saisi à la main dans le profil du projet ;
58
- * - `github_release` — la dernière release publiée du dépôt lié. Le champ
59
- * devient alors en lecture seule dans l'interface, et un projet `guarded` ne
60
- * peut pas le choisir (sa synchronisation de fond est impossible).
61
- */
62
- export const projectVersionSourceSchema = z.enum(['manual', 'github_release']);
63
- export type ProjectVersionSource = z.infer<typeof projectVersionSourceSchema>;
64
-
65
- /**
66
- * Les deux familles d'étiquettes, cumulables sur un même projet :
67
- * - `type` — ce que le projet *est* (app mobile, site web, service…) ;
68
- * - `tech` — ce avec quoi il est fait (react, react native, express, vite…).
69
- *
70
- * Le libellé est libre plutôt qu'énuméré : une pile technique se renouvelle plus
71
- * vite qu'un schéma, et une valeur inconnue ne doit jamais faire disparaître un
72
- * projet de la liste. Les étiquettes voyagent **dans le payload chiffré** — le
73
- * filtrage se fait donc côté client, ce qui suffit largement à cette échelle.
74
- */
75
- export const projectTagKindSchema = z.enum(['type', 'tech']);
76
- export type ProjectTagKind = z.infer<typeof projectTagKindSchema>;
77
-
78
- export const projectTagSchema = z.object({
79
- kind: projectTagKindSchema,
80
- label: z.string().min(1).max(PROJECT_TAG_LABEL_MAX_LENGTH)
81
- });
82
- export type ProjectTag = z.infer<typeof projectTagSchema>;
83
-
84
- export const projectSchema = z.object({
85
- id: z.number().int().positive(),
86
- title: z.string().max(PROJECT_TITLE_MAX_LENGTH),
87
- /** Vignette du projet ; vide = l'icône par défaut de la feature. */
88
- icon: projectIconSchema,
89
- description: z.string().max(PROJECT_DESCRIPTION_MAX_LENGTH),
90
- tags: z.array(projectTagSchema).max(PROJECT_MAX_TAGS),
91
- version: z.string().max(PROJECT_VERSION_MAX_LENGTH),
92
- versionSource: projectVersionSourceSchema,
93
- status: projectStatusSchema,
94
- securityTier: projectSecurityTierSchema,
95
- /** Bornes de la fenêtre du projet, en secondes unix. Facultatives. */
96
- startDate: z.number().int().nullable(),
97
- dueDate: z.number().int().nullable(),
98
- sortOrder: z.number().int().nonnegative(),
99
- /**
100
- * Qui l'a créé : attribution, jamais une frontière d'accès. `null` quand le
101
- * compte a été supprimé depuis — un départ n'emporte pas le travail d'un
102
- * espace partagé (cf. `ON DELETE SET NULL` dans la migration).
103
- */
104
- authorUserId: z.number().int().positive().nullable(),
105
- archived: z.boolean(),
106
- created: z.number().int(),
107
- updated: z.number().int()
108
- });
109
- export type Project = z.infer<typeof projectSchema>;
110
-
111
- /**
112
- * La ligne du portefeuille : le projet, plus ce que le serveur sait compter
113
- * **sans déchiffrer** (colonnes en clair uniquement).
114
- *
115
- * `masked: true` désigne un projet `guarded` dont le corps n'a pas pu être
116
- * déchiffré parce que la session est verrouillée. La ligne reste listée, avec
117
- * ses compteurs — on doit pouvoir voir qu'un projet existe, et le déverrouiller
118
- * en connaissance de cause, sans que la liste entière disparaisse. Même parti
119
- * pris que les notes privées.
120
- */
121
- export const projectSummarySchema = z.object({
122
- project: projectSchema,
123
- masked: z.boolean(),
124
- /** Cartes actives (non archivées) et celles assises dans une colonne de fin. */
125
- cardTotal: z.number().int().nonnegative(),
126
- cardDone: z.number().int().nonnegative(),
127
- /** Cartes actives dont l'échéance est dépassée. */
128
- cardOverdue: z.number().int().nonnegative(),
129
- /** Prochaine échéance à venir, toutes cartes confondues. */
130
- nextDueDate: z.number().int().nullable(),
131
- /** Messages non lus par l'appelant sur tout le projet. */
132
- unread: z.number().int().nonnegative()
133
- });
134
- export type ProjectSummary = z.infer<typeof projectSummarySchema>;
135
-
136
- /**
137
- * Ce que le client peut poser sur un projet. La version n'y est pas quand elle
138
- * est pilotée par les releases : `project.setVersion` la traite à part, pour que
139
- * l'édition du profil ne puisse pas écraser en silence une valeur synchronisée.
140
- */
141
- export const projectDraftSchema = z.object({
142
- title: z.string().min(1).max(PROJECT_TITLE_MAX_LENGTH),
143
- icon: projectIconSchema,
144
- description: z.string().max(PROJECT_DESCRIPTION_MAX_LENGTH),
145
- tags: z.array(projectTagSchema).max(PROJECT_MAX_TAGS),
146
- status: projectStatusSchema,
147
- startDate: z.number().int().nullable(),
148
- dueDate: z.number().int().nullable()
149
- });
150
- export type ProjectDraft = z.infer<typeof projectDraftSchema>;
151
-
152
- /** Ligne SQL (serveur uniquement). `content` porte le payload chiffré. */
153
- export interface ProjectRow {
154
- id: number;
155
- workspace_id: number;
156
- /** Auteur. En espace partagé il dit qui a créé la ligne, rien de plus. */
157
- user_id: number | null;
158
- status: ProjectStatus;
159
- security_tier: ProjectSecurityTier;
160
- version_source: ProjectVersionSource;
161
- sort_order: number;
162
- start_date: number | null;
163
- due_date: number | null;
164
- archived_at: number | null;
165
- content: string;
166
- created: number;
167
- updated: number;
168
- }