@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,314 @@
1
+ import { z } from 'zod';
2
+ import {
3
+ externalFeatureIdSchema,
4
+ featureIdSchema,
5
+ isExternalFeatureId,
6
+ type ExternalFeatureId,
7
+ type FeatureId
8
+ } from './workspaceRole';
9
+
10
+ /**
11
+ * Disposition de l'accueil (par espace). La grille est composée de **sections**
12
+ * ordonnées, chacune tenant des **tuiles** ordonnées de n'importe quels genres —
13
+ * appareil, fonctionnalité, raccourci, dossier. Les sections sont entièrement
14
+ * modulaires : aucune par défaut, ajoutées / retirées / réordonnées librement.
15
+ * Leur intitulé est facultatif — sans lui, la section se rend comme un simple
16
+ * groupe légèrement espacé, sans titre.
17
+ *
18
+ * Stockée en clair : métadonnée de personnalisation non sensible (comme le
19
+ * thème), jamais de charge zero-knowledge.
20
+ */
21
+
22
+ /** Les seize tuiles de fonctionnalités natives. */
23
+ export const nativeHomeFeatureIdSchema = z.enum([
24
+ 'monitoring',
25
+ 'sentinel',
26
+ 'weather',
27
+ 'password',
28
+ 'notes',
29
+ 'cloudsync',
30
+ 'uptime',
31
+ 'mail',
32
+ 'projects',
33
+ 'git',
34
+ 'deploy',
35
+ 'database',
36
+ 'backup',
37
+ 'finance',
38
+ 'audience',
39
+ 'osint'
40
+ ]);
41
+ export type NativeHomeFeatureId = z.infer<typeof nativeHomeFeatureIdSchema>;
42
+
43
+ /**
44
+ * Une tuile de fonctionnalité posable sur la grille : native, ou module externe
45
+ * (préfixe `x-`, voir `workspaceRole.ts`). Surensemble pur : les dispositions
46
+ * persistées parsent inchangées, et une tuile `x-` dont le module a disparu
47
+ * parse aussi : la grille l'ignore au rendu tant que rien ne porte cet id.
48
+ */
49
+ export const homeFeatureIdSchema = z.union([nativeHomeFeatureIdSchema, externalFeatureIdSchema]);
50
+ export type HomeFeatureId = NativeHomeFeatureId | ExternalFeatureId;
51
+
52
+ /**
53
+ * Compact widgets that can be pinned to the top-right of the navbar. Like the
54
+ * grid features they are individually add/remove/reorderable; the default set is
55
+ * empty (the navbar shows none until the user adds some).
56
+ * - `weather` → current temperature of the primary city.
57
+ * - `devices` → online / total device count.
58
+ * - `secrecy` → password-encryption lock state + re-validation countdown.
59
+ * - `uptime` → services up / total monitored.
60
+ * - `live` → qui d'autre est dans l'espace, et où (bulles cliquables).
61
+ */
62
+ export const nativeHomeTopbarWidgetIdSchema = z.enum([
63
+ 'weather',
64
+ 'devices',
65
+ 'secrecy',
66
+ 'uptime',
67
+ 'live'
68
+ ]);
69
+ export type NativeHomeTopbarWidgetId = z.infer<typeof nativeHomeTopbarWidgetIdSchema>;
70
+
71
+ /**
72
+ * Un module peut épingler SON widget de topbar : son id de feature sert d'id
73
+ * de widget. Surensemble pur, comme les tuiles : les dispositions persistées
74
+ * parsent inchangées, un id sans widget est ignoré au rendu.
75
+ */
76
+ export const homeTopbarWidgetIdSchema = z.union([nativeHomeTopbarWidgetIdSchema, featureIdSchema]);
77
+ export type HomeTopbarWidgetId = NativeHomeTopbarWidgetId | FeatureId;
78
+
79
+ /**
80
+ * Shortcut preview type, auto-detected from the URL's domain (the user never
81
+ * picks it manually). Each value has a server-side adapter under
82
+ * `src/Services/shortcutTemplates/` — dedicated logic where a real source exists
83
+ * (GitHub API, YouTube/Spotify/SoundCloud/TikTok oEmbed, Wikipedia REST, npm
84
+ * registry), and the generic Open Graph + favicon adapter for the rest. `link`
85
+ * is that generic adapter; unknown domains fall back to it.
86
+ */
87
+ export const shortcutTemplateSchema = z.enum([
88
+ 'link',
89
+ 'github',
90
+ 'youtube',
91
+ 'twitch',
92
+ 'twitter',
93
+ 'instagram',
94
+ 'tiktok',
95
+ 'reddit',
96
+ 'linkedin',
97
+ 'spotify',
98
+ 'soundcloud',
99
+ 'discord',
100
+ 'wikipedia',
101
+ 'medium',
102
+ 'npm',
103
+ 'dribbble',
104
+ 'pinterest',
105
+ 'facebook'
106
+ ]);
107
+ export type ShortcutTemplate = z.infer<typeof shortcutTemplateSchema>;
108
+
109
+ /** Max length of a shortcut URL kept in the layout. */
110
+ export const SHORTCUT_URL_MAX_LENGTH = 2048;
111
+
112
+ /** Un lien épinglé par l'utilisateur : une tuile qui porte son propre objet. */
113
+ export const shortcutItemSchema = z.object({
114
+ /** Stable client-generated id, used as the React / drag key. */
115
+ id: z.string().min(1).max(64),
116
+ /** Unknown/legacy templates degrade to a generic link rather than dropping the tile. */
117
+ template: shortcutTemplateSchema.catch('link'),
118
+ url: z.string().url().max(SHORTCUT_URL_MAX_LENGTH),
119
+ /** Optional: empty → the tile falls back to the fetched name (account, og:title…). */
120
+ title: z.string().max(80),
121
+ description: z.string().max(200).optional(),
122
+ /** Optional icon class name (e.g. `other`); falls back per template. */
123
+ icon: z.string().max(40).optional()
124
+ });
125
+ export type ShortcutItem = z.infer<typeof shortcutItemSchema>;
126
+
127
+ /**
128
+ * Combien de tuiles tient une section, et combien de fonctionnalités tient un
129
+ * dossier.
130
+ *
131
+ * Exporté, et pas seulement écrit dans le schéma : le client doit refuser
132
+ * **avant** d'écrire. Une disposition qui dépasse le plafond ne passe plus la
133
+ * validation, donc le serveur la rejette et le client la relit vide au
134
+ * démarrage suivant, ce qui revient à un accueil effacé sans un mot. Le
135
+ * plafond des dossiers n'est atteignable par aucun geste (il y a moins de
136
+ * fonctionnalités que ça, et aucune ne peut être rangée deux fois), celui des
137
+ * tuiles l'est en créant des dossiers à la chaîne.
138
+ */
139
+ export const HOME_SECTION_MAX_TILES = 60;
140
+ export const HOME_FOLDER_MAX_ITEMS = 20;
141
+
142
+ /**
143
+ * Un dossier de la grille : plusieurs fonctionnalités derrière une seule tuile.
144
+ *
145
+ * Il vit dans une section au milieu des tuiles ordinaires, parce que c'en est
146
+ * une : même carte, même place dans la grille, même glisser-déposer. Ce qui
147
+ * change est ce qui se passe au clic (côté client, les cartes qu'il tient se
148
+ * déploient par-dessus l'accueil).
149
+ *
150
+ * Il ne range que des **fonctionnalités**, là où une section range tout : une
151
+ * carte d'appareil et un raccourci sont déjà des tuiles courtes, les empiler
152
+ * derrière une tuile de pleine hauteur coûterait plus de place qu'il n'en
153
+ * gagnerait. C'est la seule asymétrie qui reste après l'unification, et elle
154
+ * est de mise en page, pas de modèle.
155
+ *
156
+ * Les fonctionnalités qu'il tient comptent comme **posées sur l'accueil** : le
157
+ * sélecteur d'ajout les exclut, exactement comme celles qui ont leur propre
158
+ * tuile. Une fonctionnalité n'est donc jamais à deux endroits à la fois, et la
159
+ * règle « pas deux fois la même » reste une seule règle.
160
+ */
161
+ export const homeFolderSchema = z.object({
162
+ /** Discriminant : c'est lui qui distingue un dossier d'un raccourci. */
163
+ kind: z.literal('folder'),
164
+ /** Id stable généré par le client : clé React, id de glissé, cible des mutations. */
165
+ id: z.string().min(1).max(64),
166
+ /** Intitulé porté par la carte. Vide, l'affichage retombe sur « Dossier ». */
167
+ title: z.string().max(40),
168
+ items: z.array(homeFeatureIdSchema).max(HOME_FOLDER_MAX_ITEMS)
169
+ });
170
+ export type HomeFolder = z.infer<typeof homeFolderSchema>;
171
+
172
+ /**
173
+ * Une tuile de l'accueil — appareil, fonctionnalité, raccourci ou dossier.
174
+ *
175
+ * ## Un seul genre de section, donc un seul genre de tuile
176
+ *
177
+ * Les sections étaient auparavant typées (« appareils », « fonctionnalités »,
178
+ * « raccourcis ») et ne tenaient qu'une sorte de tuile. Ça obligeait à choisir
179
+ * le genre **avant** d'avoir quelque chose à poser, à ouvrir une popup pour
180
+ * ajouter une section, et à trois sélecteurs d'ajout différents. Une section
181
+ * n'est plus qu'une rangée de tuiles ; c'est la tuile qui sait ce qu'elle est.
182
+ *
183
+ * ## Chaque tuile garde l'écriture qu'elle avait
184
+ *
185
+ * Un appareil et une fonctionnalité **sont** leur identifiant (l'entité vit
186
+ * ailleurs) ; un raccourci et un dossier portent l'objet lui-même, parce que
187
+ * rien d'autre ne les décrit. Les deux familles d'identifiants ne peuvent pas
188
+ * se confondre — les fonctionnalités forment un enum fermé, les appareils sont
189
+ * des UUID — et c'est {@link homeTileKind} qui tranche, en un seul endroit.
190
+ *
191
+ * Conséquence utile : les dispositions écrites avant l'unification restent
192
+ * valides telles quelles. Leurs sections portent encore un champ `kind`, qui
193
+ * tombe à la lecture comme n'importe quelle clé inconnue, et la première
194
+ * écriture le fait disparaître. Rien à migrer, rien à rattraper au chargement.
195
+ */
196
+ export const homeTileSchema = z.union([
197
+ homeFolderSchema,
198
+ shortcutItemSchema,
199
+ homeFeatureIdSchema,
200
+ z.uuid()
201
+ ]);
202
+ export type HomeTile = z.infer<typeof homeTileSchema>;
203
+
204
+ /** Ce que porte une tuile. Une seule lecture de la forme, partagée par tous. */
205
+ export type HomeTileKind = 'device' | 'feature' | 'shortcut' | 'folder';
206
+
207
+ /**
208
+ * Les fonctionnalités **natives**, pour distinguer leur identifiant d'un id
209
+ * d'appareil. Les externes se reconnaissent à leur préfixe (`isExternalFeatureId`),
210
+ * pas à une liste : la liste dépend de l'installation, le préfixe non.
211
+ */
212
+ export const HOME_FEATURE_IDS = nativeHomeFeatureIdSchema.options;
213
+
214
+ /**
215
+ * Le genre d'une tuile.
216
+ *
217
+ * **Le seul endroit qui connaisse la forme de l'union** : tout le reste passe
218
+ * par lui ou par les gardes ci-dessous, donc changer la représentation ne se
219
+ * paye qu'ici.
220
+ */
221
+ export function homeTileKind(tile: HomeTile): HomeTileKind {
222
+ if (typeof tile !== 'string') return 'kind' in tile ? 'folder' : 'shortcut';
223
+ if (isExternalFeatureId(tile)) return 'feature';
224
+ return (HOME_FEATURE_IDS as readonly string[]).includes(tile) ? 'feature' : 'device';
225
+ }
226
+
227
+ /** Cette tuile est-elle un dossier ? */
228
+ export function isHomeFolder(tile: HomeTile): tile is HomeFolder {
229
+ return homeTileKind(tile) === 'folder';
230
+ }
231
+
232
+ /** Cette tuile est-elle un raccourci ? */
233
+ export function isShortcutTile(tile: HomeTile): tile is ShortcutItem {
234
+ return homeTileKind(tile) === 'shortcut';
235
+ }
236
+
237
+ /** Cette tuile est-elle une fonctionnalité ? */
238
+ export function isFeatureTile(tile: HomeTile): tile is HomeFeatureId {
239
+ return homeTileKind(tile) === 'feature';
240
+ }
241
+
242
+ /**
243
+ * L'identité d'une tuile : sa clé React, son id de glissé, la cible des
244
+ * mutations. Un raccourci et un dossier portent leur `id`, un appareil et une
245
+ * fonctionnalité **sont** le leur.
246
+ */
247
+ export function homeTileId(tile: HomeTile): string {
248
+ return typeof tile === 'string' ? tile : tile.id;
249
+ }
250
+
251
+ /** Fields every section carries. */
252
+ const sectionBase = {
253
+ /** Stable client-generated id: React key, drag id, and mutation target. */
254
+ id: z.string().min(1).max(64),
255
+ /** User-chosen heading; absent → the section renders untitled on the home. */
256
+ title: z.string().max(40).optional(),
257
+ /**
258
+ * The section can be folded away from the home.
259
+ *
260
+ * Absent (the default) → it always shows, and there is nothing to click:
261
+ * a chevron on a section nobody wants to fold is one more thing to ignore.
262
+ */
263
+ collapsible: z.boolean().optional(),
264
+ /**
265
+ * It starts folded.
266
+ *
267
+ * Only meaningful alongside `collapsible` — a section that cannot be
268
+ * unfolded but starts folded would simply be invisible. The home enforces
269
+ * that pairing rather than trusting the flag on its own.
270
+ */
271
+ collapsed: z.boolean().optional()
272
+ };
273
+
274
+ /**
275
+ * Une section : une rangée ordonnée de tuiles, de n'importe quels genres.
276
+ *
277
+ * Elle ne se distingue plus par ce qu'elle tient — un appareil, une
278
+ * fonctionnalité et un raccourci cohabitent dans la même — mais par son seul
279
+ * `id`. Une ligne peut donc mêler une carte de pleine hauteur et des cartes
280
+ * courtes : c'est assumé, la grille aligne les hauts et laisse les cartes
281
+ * courtes à leur taille.
282
+ */
283
+ export const homeSectionSchema = z.object({
284
+ ...sectionBase,
285
+ items: z.array(homeTileSchema).max(HOME_SECTION_MAX_TILES)
286
+ });
287
+ export type HomeSection = z.infer<typeof homeSectionSchema>;
288
+
289
+ export const homeLayoutSchema = z.object({
290
+ /** Navbar mini-widgets — not a grid section, edited in the navbar itself. */
291
+ topbar: z.array(homeTopbarWidgetIdSchema).max(10),
292
+ /** Ordered grid sections. Empty by default: a fresh home shows none. */
293
+ sections: z.array(homeSectionSchema).max(12)
294
+ });
295
+ export type HomeLayout = z.infer<typeof homeLayoutSchema>;
296
+
297
+ /**
298
+ * Normalized preview returned for a rich shortcut template, so the client has a
299
+ * single renderer regardless of the source. `ok: false` means the source could
300
+ * not be fetched/parsed — the tile still works as a plain link.
301
+ */
302
+ export const shortcutPreviewSchema = z.object({
303
+ ok: z.boolean(),
304
+ title: z.string().nullable(),
305
+ subtitle: z.string().nullable(),
306
+ imageUrl: z.string().url().nullable(),
307
+ stats: z.array(z.object({ label: z.string(), value: z.string() })).max(4),
308
+ /**
309
+ * Optional live/online status, rendered as a small green/red dot in the tile
310
+ * corner (e.g. Twitch live vs offline). Omitted when not applicable.
311
+ */
312
+ status: z.enum(['online', 'offline']).optional()
313
+ });
314
+ export type ShortcutPreview = z.infer<typeof shortcutPreviewSchema>;
@@ -0,0 +1,272 @@
1
+ import { z } from 'zod';
2
+ import { userColorSchema } from './user';
3
+ import {
4
+ externalFeatureIdSchema,
5
+ isExternalFeatureId,
6
+ workspaceFeatureIdSchema,
7
+ type FeatureId,
8
+ type WorkspaceFeatureId
9
+ } from './workspaceRole';
10
+
11
+ /**
12
+ * La présence en direct : qui est dans l'espace, où, et ce qui vient d'y changer.
13
+ *
14
+ * Le mot « presence » est déjà pris dans ce dépôt par la présence des *agents*
15
+ * (`domain/presence.ts`, `DEVICE_PRESENCE_EVENT`) : ici le vocabulaire de code
16
+ * est **live**, et « Présence » n'est que le mot de l'interface.
17
+ */
18
+
19
+ /**
20
+ * Un segment de localisation, sous la forme `kind:value`.
21
+ *
22
+ * Le `kind` identifie le **niveau** (`view`, `account`, `folder`…), la valeur
23
+ * identifie le nœud à ce niveau. La valeur peut elle-même contenir des
24
+ * deux-points — une vue d'appareil est `view:device:<uuid>` — donc **on découpe
25
+ * au premier deux-points, jamais avec un `split` complet.**
26
+ */
27
+ export const livePathSegmentSchema = z
28
+ .string()
29
+ .min(3)
30
+ .max(96)
31
+ .regex(/^[a-z][a-z0-9]*:[A-Za-z0-9_:.-]{1,80}$/, 'Segment attendu sous la forme kind:value');
32
+
33
+ /**
34
+ * Le chemin complet, de la racine vers la feuille. `[]` = l'accueil.
35
+ *
36
+ * Plafonné à six niveaux : c'est deux de plus que la feature la plus profonde
37
+ * (Mail : vue → compte → dossier → message), et ça borne le coût de la
38
+ * projection par destinataire quoi qu'envoie un client.
39
+ */
40
+ export const livePathSchema = z.array(livePathSegmentSchema).max(6);
41
+ export type LivePath = z.infer<typeof livePathSchema>;
42
+
43
+ /** Le `kind` d'un segment, sans sa valeur. */
44
+ export function segmentKind(segment: string): string {
45
+ const i = segment.indexOf(':');
46
+ return i === -1 ? segment : segment.slice(0, i);
47
+ }
48
+
49
+ /** La valeur d'un segment, deux-points internes compris. */
50
+ export function segmentValue(segment: string): string {
51
+ const i = segment.indexOf(':');
52
+ return i === -1 ? '' : segment.slice(i + 1);
53
+ }
54
+
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.
67
+ */
68
+ export const liveCursorKindSchema = z.enum([
69
+ 'default',
70
+ /** `pointer` — quelque chose de cliquable est sous le curseur. */
71
+ 'pointer',
72
+ /** `text` — du texte lisible ou sélectionnable. */
73
+ 'text',
74
+ /** `grab` — saisissable, mais pas encore saisi. */
75
+ 'grab',
76
+ /** `grabbing` / `move` — quelque chose est en train d'être déplacé. */
77
+ 'grabbing',
78
+ /** Les `*-resize` — une poignée de redimensionnement. */
79
+ 'resize',
80
+ /** `not-allowed` / `no-drop` — l'action est refusée ici. */
81
+ 'blocked'
82
+ ]);
83
+ export type LiveCursorKind = z.infer<typeof liveCursorKindSchema>;
84
+
85
+ /**
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.
106
+ */
107
+ export const liveCursorSchema = z.object({
108
+ x: z.number().min(-10).max(10),
109
+ y: z.number().min(-100_000).max(100_000),
110
+ kind: liveCursorKindSchema
111
+ });
112
+ export type LiveCursor = z.infer<typeof liveCursorSchema>;
113
+
114
+ /**
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.
122
+ */
123
+ export const livePeerSchema = z.object({
124
+ /** Identité de la *connexion*, pas du compte : deux onglets = deux pairs. */
125
+ connId: z.string().min(1),
126
+ userId: z.number().int().positive(),
127
+ color: userColorSchema,
128
+ workspaceId: z.number().int().positive(),
129
+ /** Tronqué à `[]` si le destinataire n'a pas le droit de voir ce lieu. */
130
+ path: livePathSchema,
131
+ cursor: liveCursorSchema.nullable()
132
+ });
133
+ export type LivePeer = z.infer<typeof livePeerSchema>;
134
+
135
+ /**
136
+ * Ce qui vient de changer dans un espace, à la maille de la feature.
137
+ *
138
+ * Volontairement grossier : le client ne tient aucun cache normalisé, il
139
+ * re-sollicite. Un sujet plus fin ne ferait qu'ajouter de la synchronisation
140
+ * sans rien économiser.
141
+ */
142
+ export const nativeLiveTopicSchema = z.enum([
143
+ ...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
+ /** Membres, rôles, nom, logo de l'espace. */
155
+ 'workspace',
156
+ /**
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.
164
+ */
165
+ 'home',
166
+ /** Réglages de compte (avatar, couleur, thème, chiffrement). */
167
+ 'account',
168
+ /**
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.
178
+ */
179
+ 'notify'
180
+ ]);
181
+ export type NativeLiveTopic = z.infer<typeof nativeLiveTopicSchema>;
182
+
183
+ /**
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é.
188
+ */
189
+ export const liveTopicSchema = z.union([nativeLiveTopicSchema, externalFeatureIdSchema]);
190
+ export type LiveTopic = z.infer<typeof liveTopicSchema>;
191
+
192
+ /**
193
+ * La feature dont relève un sujet, natif ou externe. Seule porte d'entrée à
194
+ * garder : la table `TOPIC_FEATURE` ne connaît que les sujets natifs, et un
195
+ * sujet externe **est** sa feature.
196
+ */
197
+ export function topicFeatureOf(topic: LiveTopic): FeatureId | null {
198
+ if (isExternalFeatureId(topic)) return topic;
199
+ return TOPIC_FEATURE[topic as NativeLiveTopic];
200
+ }
201
+
202
+ /**
203
+ * La feature dont un sujet relève, ou `null` quand il n'en relève d'aucune.
204
+ *
205
+ * `null` **n'est pas** « visible par personne » mais « aucun droit de feature à
206
+ * vérifier » : l'appartenance à l'espace suffit. Les trois qui y tombent le
207
+ * méritent — la liste des membres est visible de tous les membres, la
208
+ * disposition de l'accueil est commune, et `account` n'est jamais diffusé que
209
+ * dans un espace personnel, c'est-à-dire à ses propres autres onglets.
210
+ */
211
+ export const TOPIC_FEATURE: Record<NativeLiveTopic, WorkspaceFeatureId | null> = {
212
+ devices: 'devices',
213
+ sentinel: 'sentinel',
214
+ weather: 'weather',
215
+ password: 'password',
216
+ notes: 'notes',
217
+ cloudsync: 'cloudsync',
218
+ uptime: 'uptime',
219
+ mail: 'mail',
220
+ projects: 'projects',
221
+ projectsChat: 'projects',
222
+ git: 'git',
223
+ deploy: 'deploy',
224
+ database: 'database',
225
+ backup: 'backup',
226
+ finance: 'finance',
227
+ audience: 'audience',
228
+ osint: 'osint',
229
+ workspace: null,
230
+ home: null,
231
+ 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`.
236
+ notify: null
237
+ };
238
+
239
+ /**
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.
254
+ */
255
+ export type LivePathGate = FeatureId | 'public' | 'private';
256
+
257
+ const DEVICE_VIEW_PREFIX = 'device:';
258
+
259
+ export function livePathGate(rootSegment: string | undefined): LivePathGate {
260
+ if (!rootSegment) return 'public';
261
+ if (segmentKind(rootSegment) !== 'view') return 'private';
262
+ const viewId = segmentValue(rootSegment);
263
+ const asFeature = workspaceFeatureIdSchema.safeParse(viewId);
264
+ if (asFeature.success) return asFeature.data;
265
+ // Une vue de module externe est gardée par le droit du module, comme une
266
+ // feature native : même règle, reconnue au préfixe plutôt qu'à l'enum.
267
+ if (isExternalFeatureId(viewId)) return viewId;
268
+ // La page Appareils et chaque vue d'appareil relèvent du même droit — miroir
269
+ // exact de `featureBehind` côté client.
270
+ if (viewId === 'clients' || viewId.startsWith(DEVICE_VIEW_PREFIX)) return 'devices';
271
+ return 'private';
272
+ }