@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,368 @@
1
+ import { z } from 'zod';
2
+ import {
3
+ CREDENTIAL_LABEL_MAX_LENGTH,
4
+ CREDENTIAL_SECRET_MAX_LENGTH,
5
+ credentialSchema
6
+ } from '../domain/credential';
7
+ import {
8
+ GIT_REPO_NAME_MAX_LENGTH,
9
+ GIT_REPO_OWNER_MAX_LENGTH,
10
+ gitBranchSchema,
11
+ gitCommitAuthorSchema,
12
+ gitCommitDetailSchema,
13
+ gitCommitCursorSchema,
14
+ gitCommitPointsSchema,
15
+ gitCommitSchema,
16
+ gitRepoCandidateSchema,
17
+ gitProviderSchema,
18
+ gitPullRequestSchema,
19
+ gitReleaseSchema,
20
+ gitRepoSchema,
21
+ gitRepoSyncStateSchema,
22
+ gitRepoUsageSchema,
23
+ gitSyncStatusSchema
24
+ } from '../domain/git';
25
+
26
+ /**
27
+ * Commandes des dépôts git de l'espace.
28
+ *
29
+ * Préfixe unique `git.`, comme `project.` — d'où le camelCase derrière le point.
30
+ *
31
+ * ⚠️ Conséquence à connaître : le filet de démarrage (`MUTATION_VERB` dans
32
+ * `src/features/_topics.ts`) cherche un verbe **juste après le point**. Il ne
33
+ * verra donc **aucune** de ces commandes, et un `mutates` oublié ne produira
34
+ * aucun avertissement. Il se relit à la main.
35
+ *
36
+ * L'espace visé n'apparaît dans aucune entrée : il voyage sur l'enveloppe WS et
37
+ * le dispatcheur le résout, appartenance vérifiée, avant le handler.
38
+ */
39
+
40
+ const repoId = z.number().int().positive();
41
+ const credentialId = z.number().int().positive();
42
+
43
+ // ------------------------------------------------------------- identifiants
44
+
45
+ /**
46
+ * Les jetons **GitHub** de l'espace.
47
+ *
48
+ * Ils vivent ici et non dans les projets : un même jeton ouvre en général
49
+ * plusieurs dépôts. Les secrets n'en sortent jamais — la sortie ne porte qu'un
50
+ * `hasSecret`.
51
+ *
52
+ * Les clés **Dokploy** ne sont plus ici : elles ont leur propre feature
53
+ * (`deploy.credential*`). Elles n'avaient atterri dans cet écran que faute d'un
54
+ * module de déploiement pour les accueillir, et poser la clé qui met en
55
+ * production ne relève pas du droit de lire des dépôts. La table est restée
56
+ * commune ; c'est la porte, et le droit, qui se sont séparés.
57
+ */
58
+ export const gitCredentialList = {
59
+ command: 'git.credentialList' as const,
60
+ input: z.object({}),
61
+ output: z.object({ credentials: z.array(credentialSchema) })
62
+ };
63
+
64
+ /**
65
+ * Aucun `provider` ni `baseUrl` en entrée : GitHub, et son API publique. Le
66
+ * fournisseur se choisissait autrefois dans un sélecteur, parce que le même
67
+ * écran servait les deux — il n'y a plus rien à choisir ici.
68
+ */
69
+ export const gitCredentialAdd = {
70
+ command: 'git.credentialAdd' as const,
71
+ input: z.object({
72
+ label: z.string().min(1).max(CREDENTIAL_LABEL_MAX_LENGTH),
73
+ secret: z.string().min(1).max(CREDENTIAL_SECRET_MAX_LENGTH)
74
+ }),
75
+ output: z.object({ credential: credentialSchema })
76
+ };
77
+
78
+ /**
79
+ * Modifie un jeton. `secret` absent = on garde celui en place ; une chaîne non
80
+ * vide le remplace. Il n'y a pas de « vider » : un accès sans secret ne sert à
81
+ * rien, on retire le jeton entier.
82
+ */
83
+ export const gitCredentialUpdate = {
84
+ command: 'git.credentialUpdate' as const,
85
+ input: z.object({
86
+ credentialId,
87
+ label: z.string().min(1).max(CREDENTIAL_LABEL_MAX_LENGTH),
88
+ secret: z.string().min(1).max(CREDENTIAL_SECRET_MAX_LENGTH).optional()
89
+ }),
90
+ output: z.object({ credential: credentialSchema })
91
+ };
92
+
93
+ /**
94
+ * Retire un jeton.
95
+ *
96
+ * Les dépôts qui s'en servaient gardent leur lien mais perdent leur accès
97
+ * (`ON DELETE SET NULL`) : la synchronisation s'arrête proprement et le dit, au
98
+ * lieu de disparaître avec le jeton.
99
+ */
100
+ export const gitCredentialRemove = {
101
+ command: 'git.credentialRemove' as const,
102
+ input: z.object({ credentialId }),
103
+ output: z.object({ credentialId })
104
+ };
105
+
106
+ // -------------------------------------------------------------------- dépôts
107
+
108
+ /** Le nombre de dépôts de l'espace, pour la tuile de l'accueil. */
109
+ export const gitCount = {
110
+ command: 'git.count' as const,
111
+ input: z.object({}),
112
+ output: z.object({ count: z.number().int().nonnegative() })
113
+ };
114
+
115
+ export const gitRepoList = {
116
+ command: 'git.repoList' as const,
117
+ input: z.object({}),
118
+ output: z.object({ repos: z.array(gitRepoSchema) })
119
+ };
120
+
121
+ /** Un dépôt, avec les projets qui s'en servent — tout doit être cliquable. */
122
+ export const gitRepoGet = {
123
+ command: 'git.repoGet' as const,
124
+ input: z.object({ repoId }),
125
+ output: z.object({ repo: gitRepoSchema, usage: z.array(gitRepoUsageSchema) })
126
+ };
127
+
128
+ /**
129
+ * Ajoute un dépôt à l'espace.
130
+ *
131
+ * **Idempotente** : le même `owner/repo` déjà présent rend la ligne existante
132
+ * (avec son jeton mis à jour) au lieu d'un doublon. C'est ce qui permet à un
133
+ * projet de « créer » un dépôt sans savoir s'il existe déjà ailleurs, et
134
+ * garantit qu'un dépôt n'est jamais synchronisé deux fois.
135
+ */
136
+ export const gitRepoAdd = {
137
+ command: 'git.repoAdd' as const,
138
+ input: z.object({
139
+ provider: gitProviderSchema,
140
+ owner: z.string().min(1).max(GIT_REPO_OWNER_MAX_LENGTH),
141
+ repo: z.string().min(1).max(GIT_REPO_NAME_MAX_LENGTH),
142
+ credentialId: credentialId.nullable()
143
+ }),
144
+ output: z.object({ repo: gitRepoSchema })
145
+ };
146
+
147
+ /**
148
+ * Les dépôts d'un propriétaire ou d'une organisation, chez le fournisseur.
149
+ *
150
+ * Interroge GitHub **au moment de la demande** — c'est, avec le diff d'un
151
+ * commit, la seule commande du module dans ce cas. La liste dépend du jeton
152
+ * choisi (un jeton donne accès aux dépôts privés, l'absence de jeton n'ouvre que
153
+ * le public), d'où le `credentialId` en entrée : changer de jeton change le
154
+ * résultat, et l'interface doit pouvoir le re-demander.
155
+ */
156
+ export const gitRepoCandidates = {
157
+ command: 'git.repoCandidates' as const,
158
+ input: z.object({
159
+ owner: z.string().min(1).max(GIT_REPO_OWNER_MAX_LENGTH),
160
+ credentialId: credentialId.nullable()
161
+ }),
162
+ output: z.object({ repos: z.array(gitRepoCandidateSchema) })
163
+ };
164
+
165
+ /**
166
+ * Range les dépôts de l'espace : `ids` est la liste **complète** dans son ordre
167
+ * final (rang le plus faible en tête).
168
+ *
169
+ * Rien d'autre ne positionne un dépôt — un nouveau prend le rang suivant, donc
170
+ * la fin de la liste — de sorte que l'ordre appartient entièrement à
171
+ * l'utilisateur, comme celui des services surveillés et des notes. Ne touche ni
172
+ * au cache ni à l'état de synchronisation : ranger n'est pas configurer.
173
+ */
174
+ export const gitRepoReorder = {
175
+ command: 'git.repoReorder' as const,
176
+ input: z.object({ ids: z.array(repoId).min(1) }),
177
+ output: z.object({ ids: z.array(repoId) })
178
+ };
179
+
180
+ /** Change le jeton d'un dépôt, ou suspend sa synchronisation. */
181
+ export const gitRepoUpdate = {
182
+ command: 'git.repoUpdate' as const,
183
+ input: z.object({ repoId, credentialId: credentialId.nullable(), enabled: z.boolean() }),
184
+ output: z.object({ repo: gitRepoSchema })
185
+ };
186
+
187
+ /**
188
+ * Supprime un dépôt de l'espace, avec son cache et toutes ses liaisons.
189
+ *
190
+ * Les projets liés ne sont **pas** touchés : ils perdent leur dépôt, rien
191
+ * d'autre. Le dépôt chez le fournisseur, lui, n'est évidemment jamais atteint.
192
+ */
193
+ export const gitRepoRemove = {
194
+ command: 'git.repoRemove' as const,
195
+ input: z.object({ repoId }),
196
+ output: z.object({ repoId })
197
+ };
198
+
199
+ /**
200
+ * Force une synchronisation immédiate, sans attendre le tour de
201
+ * l'ordonnanceur. N'écrit rien elle-même : elle réveille le service de fond.
202
+ */
203
+ export const gitRepoSyncNow = {
204
+ command: 'git.repoSyncNow' as const,
205
+ input: z.object({ repoId }),
206
+ output: z.object({ repo: gitRepoSchema })
207
+ };
208
+
209
+ /**
210
+ * Jette le cache local du dépôt et repart de zéro.
211
+ *
212
+ * À distinguer de `gitRepoSyncNow`, qui reprend là où le service s'était
213
+ * arrêté : ici on efface commits, branches, releases et pull requests, ainsi que
214
+ * les ETags et le drapeau de backfill, de sorte que le tour suivant relise
215
+ * **tout** l'historique depuis le fournisseur.
216
+ *
217
+ * Deux choses survivent, et ce n'est pas un oubli :
218
+ *
219
+ * - le **rattachement des auteurs** à des membres de l'espace, qui est du
220
+ * travail fait à la main et que rien ne permettrait de reconstituer ;
221
+ * - les **liaisons aux projets**, qui ne relèvent pas du cache.
222
+ *
223
+ * N'écrit rien elle-même côté fournisseur : elle vide, puis réveille
224
+ * l'ordonnanceur.
225
+ */
226
+ export const gitRepoResync = {
227
+ command: 'git.repoResync' as const,
228
+ input: z.object({ repoId }),
229
+ output: z.object({ repo: gitRepoSchema })
230
+ };
231
+
232
+ /**
233
+ * L'avancement de la synchronisation d'un dépôt.
234
+ *
235
+ * Lecture pure et **très bon marché** : elle n'interroge qu'une table en
236
+ * mémoire du service de fond. C'est ce qui la rend sondable pendant qu'une
237
+ * synchronisation tourne, sans passer par une diffusion `live` qui ferait
238
+ * re-solliciter tout l'écran à chaque étape, chez tous les membres.
239
+ */
240
+ export const gitRepoSyncStatus = {
241
+ command: 'git.repoSyncStatus' as const,
242
+ input: z.object({ repoId }),
243
+ output: z.object({ status: gitSyncStatusSchema })
244
+ };
245
+
246
+ /**
247
+ * Les synchronisations en cours dans l'espace, toutes d'un coup.
248
+ *
249
+ * **Aucune requête, aucun déchiffrement** : le service tient l'avancement en
250
+ * mémoire le temps d'un tour. C'est ce qui rend la liste des dépôts sondable à
251
+ * la seconde pour y animer une bande de progression, sans que cela coûte quoi
252
+ * que ce soit — même parti pris que la barre d'une synchro mail.
253
+ *
254
+ * Un dépôt absent de la réponse ne synchronise pas : il n'y a pas d'entrée
255
+ * « au repos », seulement celles qui tournent.
256
+ */
257
+ export const gitSyncStatuses = {
258
+ command: 'git.syncStatuses' as const,
259
+ input: z.object({}),
260
+ output: z.object({ statuses: z.array(gitRepoSyncStateSchema) })
261
+ };
262
+
263
+ // ------------------------------------------------------------------- lecture
264
+
265
+ export const gitBranchList = {
266
+ command: 'git.branchList' as const,
267
+ input: z.object({ repoId }),
268
+ output: z.object({ branches: z.array(gitBranchSchema) })
269
+ };
270
+
271
+ /**
272
+ * Les derniers commits, en clair, pour la liste (pas pour le graphe).
273
+ *
274
+ * Pagination par **curseur** `(committedAt, id)` et non par identifiant seul :
275
+ * voir `gitCommitCursorSchema`. La page suivante se demande avec le couple porté
276
+ * par le dernier commit reçu.
277
+ */
278
+ export const gitCommitList = {
279
+ command: 'git.commitList' as const,
280
+ input: z.object({
281
+ repoId,
282
+ before: gitCommitCursorSchema.optional(),
283
+ limit: z.number().int().positive().max(200).optional()
284
+ }),
285
+ output: z.object({ commits: z.array(gitCommitSchema), hasMore: z.boolean() })
286
+ };
287
+
288
+ /**
289
+ * Le graphe : un point par commit, du premier au dernier, avec la liste des
290
+ * auteurs et leur couleur.
291
+ *
292
+ * Les points ne portent **pas** les messages — un graphe en affiche des
293
+ * milliers, et les déchiffrer tous pour dessiner des ronds serait absurde.
294
+ */
295
+ export const gitCommitGraph = {
296
+ command: 'git.commitGraph' as const,
297
+ input: z.object({ repoId }),
298
+ output: z.object({
299
+ points: gitCommitPointsSchema,
300
+ authors: z.array(gitCommitAuthorSchema),
301
+ /** Bornes réelles de l'historique, même si les points sont écrêtés. */
302
+ firstCommitAt: z.number().int().nullable(),
303
+ lastCommitAt: z.number().int().nullable(),
304
+ total: z.number().int().nonnegative()
305
+ })
306
+ };
307
+
308
+ /** Rattache un auteur git à un membre de l'espace (ou l'en détache). */
309
+ export const gitAuthorMap = {
310
+ command: 'git.authorMap' as const,
311
+ input: z.object({
312
+ repoId,
313
+ authorRef: z.string().min(1).max(32),
314
+ userId: z.number().int().positive().nullable()
315
+ }),
316
+ output: z.object({ authorRef: z.string(), userId: z.number().int().positive().nullable() })
317
+ };
318
+
319
+ export const gitReleaseList = {
320
+ command: 'git.releaseList' as const,
321
+ input: z.object({ repoId }),
322
+ output: z.object({ releases: z.array(gitReleaseSchema) })
323
+ };
324
+
325
+ export const gitPullRequestList = {
326
+ command: 'git.pullRequestList' as const,
327
+ input: z.object({ repoId }),
328
+ output: z.object({ pullRequests: z.array(gitPullRequestSchema) })
329
+ };
330
+
331
+ /**
332
+ * Le diff d'un commit, lu **chez le fournisseur au moment de la demande**.
333
+ *
334
+ * La seule commande du module qui sorte du cache local : voir
335
+ * `gitCommitDetailSchema` pour la raison. Elle est donc aussi la seule dont la
336
+ * latence dépend d'une API tierce — l'interface doit l'annoncer.
337
+ */
338
+ export const gitCommitDetail = {
339
+ command: 'git.commitDetail' as const,
340
+ input: z.object({ repoId, sha: z.string().min(7).max(40) }),
341
+ output: z.object({ detail: gitCommitDetailSchema })
342
+ };
343
+
344
+ export const gitCommands = [
345
+ gitCredentialList,
346
+ gitCredentialAdd,
347
+ gitCredentialUpdate,
348
+ gitCredentialRemove,
349
+ gitCount,
350
+ gitRepoList,
351
+ gitRepoGet,
352
+ gitRepoAdd,
353
+ gitRepoCandidates,
354
+ gitRepoReorder,
355
+ gitRepoUpdate,
356
+ gitRepoRemove,
357
+ gitRepoResync,
358
+ gitRepoSyncNow,
359
+ gitRepoSyncStatus,
360
+ gitSyncStatuses,
361
+ gitBranchList,
362
+ gitCommitList,
363
+ gitCommitGraph,
364
+ gitAuthorMap,
365
+ gitReleaseList,
366
+ gitPullRequestList,
367
+ gitCommitDetail
368
+ ] as const;
@@ -0,0 +1,32 @@
1
+ import { z } from 'zod';
2
+ import {
3
+ homeLayoutSchema,
4
+ shortcutPreviewSchema,
5
+ shortcutTemplateSchema,
6
+ SHORTCUT_URL_MAX_LENGTH
7
+ } from '../domain/home';
8
+
9
+ /** Persist the user's home grid layout (tiles + order). */
10
+ export const homeSetLayout = {
11
+ command: 'home.setLayout' as const,
12
+ input: homeLayoutSchema,
13
+ output: z.object({ ok: z.literal(true) })
14
+ };
15
+
16
+ /**
17
+ * Fetch a live preview for a rich shortcut template (e.g. a GitHub profile).
18
+ * Server-side fetch + cache so the client avoids CORS / rate-limit handling and
19
+ * the whole app stays "one typed WS command per action".
20
+ */
21
+ export const homeShortcutPreview = {
22
+ command: 'home.shortcutPreview' as const,
23
+ input: z.object({
24
+ template: shortcutTemplateSchema,
25
+ url: z.string().url().max(SHORTCUT_URL_MAX_LENGTH),
26
+ /** Bypass the server cache and re-fetch (Ctrl/Cmd-click on the tile). */
27
+ refresh: z.boolean().optional()
28
+ }),
29
+ output: shortcutPreviewSchema
30
+ };
31
+
32
+ export const homeCommands = [homeSetLayout, homeShortcutPreview] as const;
@@ -0,0 +1,113 @@
1
+ import { z } from 'zod';
2
+ import { liveCursorSchema, livePathSchema, livePeerSchema, liveTopicSchema } from '../domain/live';
3
+
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.
15
+ */
16
+ export const liveHere = {
17
+ command: 'live.here' as const,
18
+ input: z.object({ path: livePathSchema }),
19
+ output: z.object({ peers: z.array(livePeerSchema) })
20
+ };
21
+
22
+ export const liveCommands = [liveHere] as const;
23
+
24
+ /**
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.
32
+ */
33
+ export const LIVE_CURSOR_COMMAND = 'live.cursor' as const;
34
+
35
+ /**
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.
42
+ */
43
+ export const liveCursorFrameSchema = z.object({ cursor: liveCursorSchema.nullable() });
44
+ export type LiveCursorFrame = z.infer<typeof liveCursorFrameSchema>;
45
+
46
+ /**
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.
61
+ */
62
+ export const LIVE_TYPING_COMMAND = 'live.typing' as const;
63
+ export const liveTypingFrameSchema = z.object({ typing: z.boolean() });
64
+ export type LiveTypingFrame = z.infer<typeof liveTypingFrameSchema>;
65
+
66
+ /** Qui écrit, parmi les pairs situés au **même chemin exactement**. */
67
+ export const LIVE_TYPERS_EVENT = 'live.typers' as const;
68
+ export const liveTypersPushSchema = z.object({
69
+ workspaceId: z.number().int().positive(),
70
+ typers: z.array(
71
+ z.object({
72
+ connId: z.string().min(1),
73
+ userId: z.number().int().positive()
74
+ })
75
+ )
76
+ });
77
+ export type LiveTypersPush = z.infer<typeof liveTypersPushSchema>;
78
+
79
+ /** Roster de la salle. Projeté par destinataire : les chemins y sont tronqués. */
80
+ export const LIVE_PEERS_EVENT = 'live.peers' as const;
81
+ export const livePeersPushSchema = z.object({
82
+ workspaceId: z.number().int().positive(),
83
+ peers: z.array(livePeerSchema)
84
+ });
85
+ export type LivePeersPush = z.infer<typeof livePeersPushSchema>;
86
+
87
+ /** Curseurs des pairs situés au **même chemin exactement** que le destinataire. */
88
+ export const LIVE_CURSORS_EVENT = 'live.cursors' as const;
89
+ export const liveCursorsPushSchema = z.object({
90
+ workspaceId: z.number().int().positive(),
91
+ cursors: z.array(
92
+ z.object({
93
+ connId: z.string().min(1),
94
+ userId: z.number().int().positive(),
95
+ cursor: liveCursorSchema
96
+ })
97
+ )
98
+ });
99
+ export type LiveCursorsPush = z.infer<typeof liveCursorsPushSchema>;
100
+
101
+ /**
102
+ * Quelque chose a changé dans l'espace : les vues qui en dépendent
103
+ * re-sollicitent. Émis par le dispatcheur après toute commande déclarant
104
+ * `mutates`, et par les tâches de fond qui écrivent sans commande.
105
+ */
106
+ export const LIVE_CHANGED_EVENT = 'live.changed' as const;
107
+ export const liveChangedPushSchema = z.object({
108
+ workspaceId: z.number().int().positive(),
109
+ topics: z.array(liveTopicSchema).min(1),
110
+ /** L'auteur, ou `null` pour une tâche de fond. Jamais renvoyé à lui-même. */
111
+ by: z.number().int().positive().nullable()
112
+ });
113
+ export type LiveChangedPush = z.infer<typeof liveChangedPushSchema>;
@@ -0,0 +1,86 @@
1
+ import { z } from 'zod';
2
+ import { logEntrySchema, logSourceSchema } from '../domain/logs';
3
+
4
+ /** Upper bound on a single page of results (the server clamps to this). */
5
+ export const LOGS_PAGE_MAX = 200;
6
+ export const LOGS_PAGE_DEFAULT = 50;
7
+
8
+ /**
9
+ * The shared filter surface for the logs feature. Every field is optional and
10
+ * combines with the others as AND, so the UI can drill down precisely:
11
+ * by user, origin channel, feature, exact action, importance floor, free text,
12
+ * client IP and a date window. All omitted → the unfiltered, newest-first feed.
13
+ */
14
+ export const logFilterSchema = z.object({
15
+ /** Restrict to one acting user (0 = system events). */
16
+ uid: z.number().int().nonnegative().optional(),
17
+ /** Restrict to one origin channel (web / api / agent / system). */
18
+ source: logSourceSchema.optional(),
19
+ /** Restrict to one emitting feature/subsystem. */
20
+ category: z.string().min(1).max(64).optional(),
21
+ /** Restrict to one exact event key (e.g. `login.failed`). */
22
+ action: z.string().min(1).max(64).optional(),
23
+ /** Importance floor (inclusive): only entries with `level >= levelMin`. */
24
+ levelMin: z.number().int().optional(),
25
+ /** Free-text match against description / action / ip (case-insensitive). */
26
+ search: z.string().max(200).optional(),
27
+ /** Exact client IP. */
28
+ ip: z.string().max(64).optional(),
29
+ /** Window start, unix epoch seconds, inclusive. */
30
+ dateFrom: z.number().int().nonnegative().optional(),
31
+ /** Window end, unix epoch seconds, inclusive. */
32
+ dateTo: z.number().int().nonnegative().optional()
33
+ });
34
+
35
+ export type LogFilter = z.infer<typeof logFilterSchema>;
36
+
37
+ /**
38
+ * Page through log entries newest-first, applying the filter. Admin-only.
39
+ * `total` is the count matching the filter (ignoring paging) so the UI can show
40
+ * "x / total"; `hasMore` says whether another page exists past this one.
41
+ */
42
+ export const logsList = {
43
+ command: 'logs.list' as const,
44
+ input: logFilterSchema.extend({
45
+ limit: z.number().int().positive().max(LOGS_PAGE_MAX).optional(),
46
+ offset: z.number().int().nonnegative().optional()
47
+ }),
48
+ output: z.object({
49
+ logs: z.array(logEntrySchema),
50
+ total: z.number().int().nonnegative(),
51
+ hasMore: z.boolean()
52
+ })
53
+ };
54
+
55
+ /** One distinct value present in the logs, with how many entries carry it. */
56
+ export const logsFacetSchema = z.object({
57
+ value: z.string(),
58
+ count: z.number().int().nonnegative()
59
+ });
60
+
61
+ /** A user that appears in the logs, with a display name and entry count. */
62
+ export const logsFacetUserSchema = z.object({
63
+ uid: z.number().int().nonnegative(),
64
+ username: z.string().nullable(),
65
+ count: z.number().int().nonnegative()
66
+ });
67
+
68
+ /**
69
+ * Distinct filter values actually present in the logs (users, categories,
70
+ * sources, actions) with counts, so the client can offer data-driven filter
71
+ * controls — naming users instead of bare ids — rather than a hard-coded list.
72
+ * Admin-only.
73
+ */
74
+ export const logsFacets = {
75
+ command: 'logs.facets' as const,
76
+ input: z.object({}),
77
+ output: z.object({
78
+ users: z.array(logsFacetUserSchema),
79
+ categories: z.array(logsFacetSchema),
80
+ sources: z.array(logsFacetSchema),
81
+ actions: z.array(logsFacetSchema),
82
+ total: z.number().int().nonnegative()
83
+ })
84
+ };
85
+
86
+ export const logsCommands = [logsList, logsFacets] as const;