@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,141 @@
1
+ import { z } from 'zod';
2
+ import { userRoleSchema } from './role';
3
+
4
+ /**
5
+ * État d'un compte au niveau du site. `suspended` conserve toutes les données
6
+ * mais refuse la connexion — c'est une révocation réversible, pas une
7
+ * suppression.
8
+ */
9
+ export const userStatusSchema = z.enum(['active', 'suspended']);
10
+ export type UserStatus = z.infer<typeof userStatusSchema>;
11
+
12
+ /**
13
+ * Couleur d'identité du compte : son curseur chez les autres, sa bulle dans la
14
+ * barre du haut, la bordure du nœud où il se trouve. Une identité visuelle, donc
15
+ * portée par le compte et non par l'adhésion à un espace.
16
+ *
17
+ * Huit teintes, le cyan exclu — c'est celui de `--accent`, un curseur de cette
18
+ * couleur se lirait comme un élément de l'interface. La valeur est toujours
19
+ * rendue via le jeton `--user-<nom>` (Styles/theme.css), jamais en hexadécimal.
20
+ */
21
+ export const userColorSchema = z.enum([
22
+ 'red',
23
+ 'orange',
24
+ 'yellow',
25
+ 'green',
26
+ 'blue',
27
+ 'indigo',
28
+ 'purple',
29
+ 'pink'
30
+ ]);
31
+ export type UserColor = z.infer<typeof userColorSchema>;
32
+
33
+ /** L'ordre fait foi : `defaultUserColor` et la migration 059 l'indexent tous deux. */
34
+ export const USER_COLORS = userColorSchema.options;
35
+
36
+ /**
37
+ * Teinte attribuée d'office à un compte, depuis son identifiant. Deux comptes
38
+ * créés à la suite n'ont pas la même, et aucun compte n'existe sans couleur —
39
+ * la migration 059 colorie l'existant, `usersRepo.create` fait de même à
40
+ * l'inscription.
41
+ */
42
+ export function defaultUserColor(userId: number): UserColor {
43
+ return USER_COLORS[Math.abs(userId) % USER_COLORS.length];
44
+ }
45
+
46
+ export const minimalUserSchema = z.object({
47
+ id: z.number().int().nonnegative(),
48
+ email: z.string().email(),
49
+ username: z.string().min(1),
50
+ avatar: z.string(),
51
+ color: userColorSchema,
52
+ /** Époque Unix de la dernière connexion ; `0` pour un compte jamais venu. */
53
+ lastLogin: z.number().int().nonnegative(),
54
+ created: z.number().int().nonnegative()
55
+ });
56
+
57
+ export type MinimalUser = z.infer<typeof minimalUserSchema>;
58
+
59
+ /**
60
+ * Account security posture, surfaced in the profile as "Sécurité → x / 3".
61
+ * Each flag is one of the three main protections.
62
+ */
63
+ export const userSecuritySchema = z.object({
64
+ /** TOTP two-factor authentication is enabled. */
65
+ twoFactor: z.boolean(),
66
+ /** Password-based encryption (DEK wrapped by the user's password) is on. */
67
+ passwordEncryption: z.boolean(),
68
+ /**
69
+ * Password re-validation window is configured to a strict value: enabled and
70
+ * short enough (≤ 5 min) to count as a protection. Disabled (0) or a long
71
+ * window does not count.
72
+ */
73
+ reAuthValidation: z.boolean()
74
+ });
75
+
76
+ export type UserSecurity = z.infer<typeof userSecuritySchema>;
77
+
78
+ /**
79
+ * Drapeaux de compte, rangés dans `users.settings` — un simple sac de chaînes.
80
+ *
81
+ * L'**absence** d'un drapeau est sa valeur par défaut : un compte créé avant
82
+ * qu'un drapeau existe se comporte donc comme le reste, sans migration ni
83
+ * rattrapage. C'est ce qui fait de cette colonne le bon endroit pour un réglage
84
+ * booléen privé, là où une colonne dédiée (le modèle de `color`) se justifie
85
+ * quand la valeur n'est pas binaire ou qu'elle intéresse d'autres comptes.
86
+ *
87
+ * - `hideLiveCursors` : ne pas afficher les curseurs des autres membres. La
88
+ * coupure est **réciproque** — le client cesse aussi d'émettre le sien (voir
89
+ * `live/LiveProvider.tsx`), si bien qu'on ne peut pas regarder sans être vu.
90
+ */
91
+ export const userSettingFlagSchema = z.enum(['hideLiveCursors']);
92
+ export type UserSettingFlag = z.infer<typeof userSettingFlagSchema>;
93
+
94
+ export const userSchema = z.object({
95
+ id: z.number().int().nonnegative(),
96
+ email: z.string().email(),
97
+ username: z.string().min(1),
98
+ avatar: z.string(),
99
+ color: userColorSchema,
100
+ role: userRoleSchema,
101
+ /** Les drapeaux posés — voir `userSettingFlagSchema`. Un drapeau inconnu du
102
+ * client (compte revenu d'une version plus récente) est simplement ignoré,
103
+ * d'où le tableau de chaînes plutôt qu'un tableau d'énumérés. */
104
+ settings: z.array(z.string()),
105
+ security: userSecuritySchema,
106
+ /** Espace personnel du compte : créé avec lui, toujours présent. */
107
+ personalWorkspaceId: z.number().int().positive(),
108
+ /**
109
+ * Espace « favori », chargé en premier à la connexion et au rechargement.
110
+ * `null` → l'espace personnel. Pointe sur un espace dont le compte est
111
+ * membre ; il repasse à `null` si cet espace disparaît ou si l'accès est
112
+ * révoqué.
113
+ */
114
+ defaultWorkspaceId: z.number().int().positive().nullable(),
115
+ lastLogin: z.number().int().nonnegative(),
116
+ created: z.number().int().nonnegative()
117
+ });
118
+
119
+ export type User = z.infer<typeof userSchema>;
120
+
121
+ /**
122
+ * Database row shape (server-only). Mirrors columns exactly.
123
+ */
124
+ export interface UserRow {
125
+ id: number;
126
+ email: string;
127
+ username: string;
128
+ password_hash: string;
129
+ avatar: string;
130
+ color: UserColor;
131
+ role: string;
132
+ status: UserStatus;
133
+ settings: string;
134
+ /** Espace personnel du compte (FK `workspaces.id`), unique par utilisateur. */
135
+ personal_workspace_id: number;
136
+ /** Espace favori chargé en premier ; `null` → l'espace personnel. */
137
+ default_workspace_id: number | null;
138
+ re_auth_interval: number | null;
139
+ last_login: number;
140
+ created: number;
141
+ }
@@ -0,0 +1,56 @@
1
+ import { z } from 'zod';
2
+ import { minimalUserSchema } from './user';
3
+
4
+ /**
5
+ * Nature d'un espace.
6
+ *
7
+ * - `personal` : créé avec le compte, un seul membre (son propriétaire), ni
8
+ * quittable ni supprimable, jamais partageable. C'est le repli implicite quand
9
+ * une commande ne vise aucun espace en particulier.
10
+ * - `shared` : créé à la demande, plusieurs membres, rôles et invitations.
11
+ *
12
+ * Les deux sont de vraies lignes de `workspaces` : il n'existe plus d'espace
13
+ * virtuel d'id 0.
14
+ */
15
+ export const workspaceKindSchema = z.enum(['personal', 'shared']);
16
+ export type WorkspaceKind = z.infer<typeof workspaceKindSchema>;
17
+
18
+ export const workspaceSchema = z.object({
19
+ id: z.number().int().positive(),
20
+ kind: workspaceKindSchema,
21
+ name: z.string().min(1),
22
+ logo: z.string(),
23
+ /** Propriétaire : tous les droits, ne peut être ni exclu ni rétrogradé. */
24
+ ownerUserId: z.number().int().positive(),
25
+ users: z.array(minimalUserSchema),
26
+ features: z.array(z.string().min(1)),
27
+ created: z.number().int().nonnegative()
28
+ });
29
+
30
+ export type Workspace = z.infer<typeof workspaceSchema>;
31
+
32
+ /**
33
+ * Ligne SQL (serveur uniquement). `theme` et `home_layout` sont portés par
34
+ * l'espace, pas par l'utilisateur : chaque espace a sa propre apparence et sa
35
+ * propre disposition d'accueil.
36
+ */
37
+ export interface WorkspaceRow {
38
+ id: number;
39
+ kind: WorkspaceKind;
40
+ name: string;
41
+ logo: string;
42
+ owner_user_id: number;
43
+ features: string;
44
+ /** ThemeStateDTO sérialisé, ou null tant qu'aucun thème n'a été enregistré. */
45
+ theme: string | null;
46
+ /** HomeLayout sérialisé, ou null tant qu'aucune disposition n'a été enregistrée. */
47
+ home_layout: string | null;
48
+ created: number;
49
+ }
50
+
51
+ export interface WorkspaceMemberRow {
52
+ id: number;
53
+ user_id: number;
54
+ workspace_id: number;
55
+ date: number;
56
+ }
@@ -0,0 +1,251 @@
1
+ import { z } from 'zod';
2
+
3
+ /**
4
+ * Rôles d'un espace de travail : deux dimensions volontairement distinctes.
5
+ *
6
+ * Les **capacités** portent sur le gouvernement de l'espace lui-même. L'enum est
7
+ * court et fermé : il ne grandit que si la notion d'espace grandit.
8
+ *
9
+ * Les **droits par feature** forment une carte uniforme `feature → lecture ou
10
+ * écriture`. Une nouvelle feature coûte une entrée dans un tableau et hérite du
11
+ * cloisonnement lecture/écriture sans qu'aucun enum ni aucun handler ne change.
12
+ */
13
+
14
+ export const workspaceCapabilitySchema = z.enum([
15
+ /** Renommer l'espace, changer son logo, le supprimer. */
16
+ 'workspace.manage',
17
+ /** Ajouter un membre par son adresse, l'exclure, lui attribuer un rôle. */
18
+ 'workspace.members',
19
+ /** Créer, modifier, supprimer et ordonner les rôles. */
20
+ 'workspace.roles',
21
+ /** Modifier le thème de l'espace. */
22
+ 'workspace.appearance',
23
+ /** Modifier la disposition de l'accueil de l'espace. */
24
+ 'workspace.layout'
25
+ // « Gérer les canaux d'alerte » a vécu ici (`workspace.notifications`)
26
+ // puis est passée PAR FONCTIONNALITÉ (migration 093) : depuis que chaque
27
+ // émetteur possède ses canaux (091), une capacité d'espace accordait d'un
28
+ // bloc l'astreinte d'Uptime et le salon des sauvegardes. Voir le champ
29
+ // `channels` du grant de feature.
30
+ ]);
31
+
32
+ export type WorkspaceCapability = z.infer<typeof workspaceCapabilitySchema>;
33
+
34
+ export const WORKSPACE_CAPABILITIES = workspaceCapabilitySchema.options;
35
+
36
+ /**
37
+ * Features qu'un rôle peut ouvrir. Surensemble de `HomeFeatureId` : `devices`
38
+ * s'y ajoute, parce que voir la flotte d'un espace est un droit comme un autre
39
+ * (lecture = voir les appareils et leur supervision, écriture = les appairer,
40
+ * approuver, renommer, supprimer).
41
+ *
42
+ * `monitoring` n'y figure pas : la carte d'agrégat du même nom est réservée à
43
+ * l'administrateur global dans son espace personnel, donc aucun rôle d'espace
44
+ * ne peut l'accorder. Les vues d'appareil, elles, relèvent de `devices`.
45
+ */
46
+ export const workspaceFeatureIdSchema = z.enum([
47
+ 'devices',
48
+ /**
49
+ * Sentinelle. Distincte de `devices` exprès : voir la supervision d'une
50
+ * machine et voir ce qu'un détecteur soupçonne d'elle ne se confondent pas.
51
+ * `read` = consulter constats et posture, `write` = acquitter, régler les
52
+ * cadences, relancer un relevé.
53
+ */
54
+ 'sentinel',
55
+ 'weather',
56
+ 'password',
57
+ 'notes',
58
+ 'cloudsync',
59
+ 'uptime',
60
+ 'mail',
61
+ 'projects',
62
+ 'git',
63
+ /**
64
+ * Déploiement. `read` = voir les cibles de l'espace et leur historique,
65
+ * `write` = déclarer une cible, poser la clé d'API de l'instance, et
66
+ * **déclencher une mise en production**.
67
+ *
68
+ * ⚠️ Le droit le plus lourd de conséquences hors de DevEye : c'est le seul
69
+ * qui pousse quelque chose chez un tiers. Distinct de `projects` exprès —
70
+ * piloter le travail et livrer ne se confondent pas, et tout le monde n'a
71
+ * pas à pouvoir faire les deux.
72
+ */
73
+ 'deploy',
74
+ 'database',
75
+ /**
76
+ * Sauvegardes. Les destinations de l'espace (dossier serveur, dossier d'une
77
+ * machine enrôlée, bucket S3) et les travaux qui y écrivent.
78
+ *
79
+ * `read` = voir les destinations, les travaux et leur historique, `write` =
80
+ * déclarer une destination, poser sa clé secrète, créer un travail et le
81
+ * déclencher.
82
+ *
83
+ * ⚠️ Le droit le plus lourd en lecture après `finance`, et pour une raison
84
+ * différente: la liste des destinations dit **où sont les copies de tout**.
85
+ * Qui la lit sait quel bucket viser pour obtenir la base entière sans jamais
86
+ * toucher à DevEye. Distinct de `database` exprès — superviser une base et
87
+ * savoir où en dorment les vidages ne se confondent pas.
88
+ */
89
+ 'backup',
90
+ /**
91
+ * Finances. Le grand livre de l'espace: comptes, opérations, budgets,
92
+ * échéances.
93
+ *
94
+ * `read` = consulter soldes, journal et tableau de bord, `write` = saisir et
95
+ * corriger des opérations, tenir comptes, catégories, budgets et échéances.
96
+ *
97
+ * ⚠️ Le droit dont la lecture seule est déjà lourde: un livre de comptes dit
98
+ * ce qu'une structure gagne, ce qu'elle doit et à qui elle paie quoi. Le
99
+ * distinguer de `projects` n'est donc pas une commodité de rangement, c'est
100
+ * la raison d'être de la séparation.
101
+ */
102
+ 'finance',
103
+ /**
104
+ * Audience. Le suivi d'usage des sites livrés — dernier maillon de la même
105
+ * famille que `projects`, `git` et `database` : un objet de l'**espace**
106
+ * qu'un projet ne fait que pointer.
107
+ *
108
+ * `read` = consulter les statistiques, `write` = déclarer un site, changer
109
+ * ses origines autorisées, sa rétention, le supprimer.
110
+ *
111
+ * ⚠️ Distinct de `projects` exprès, comme `git` l'est déjà : voir les
112
+ * chiffres d'un site livré et piloter le travail qui le produit ne se
113
+ * confondent pas, et tout le monde n'a pas à voir les deux.
114
+ */
115
+ 'audience',
116
+ /**
117
+ * OSINT. `read` = chercher et consulter l'historique de l'espace, `write` =
118
+ * effacer l'historique et poser les clés d'API des fournisseurs.
119
+ */
120
+ 'osint'
121
+ ]);
122
+
123
+ export type WorkspaceFeatureId = z.infer<typeof workspaceFeatureIdSchema>;
124
+
125
+ export const WORKSPACE_FEATURE_IDS = workspaceFeatureIdSchema.options;
126
+
127
+ /**
128
+ * Identifiant d'une feature **externe** (module tiers compilé dans l'app).
129
+ *
130
+ * Le préfixe `x-` porte trois garanties d'un coup : aucune collision possible
131
+ * avec les seize ids natifs ni avec les sujets réservés (`workspace`, `home`,
132
+ * `account`, `notify`, `projectsChat`), aucune confusion avec un UUID
133
+ * d'appareil dans une disposition d'accueil (un UUID commence par un chiffre
134
+ * hexadécimal, jamais par `x`), et un tri visuel immédiat dans un grant ou un
135
+ * journal. Pas de tiret intérieur : l'id sert tel quel de préfixe de commande
136
+ * (`x-crypto.list`) et de valeur de segment live.
137
+ */
138
+ export const EXTERNAL_FEATURE_ID_PATTERN = /^x-[a-z][a-z0-9]{1,24}$/;
139
+
140
+ export type ExternalFeatureId = `x-${string}`;
141
+
142
+ export const externalFeatureIdSchema = z
143
+ .string()
144
+ .regex(EXTERNAL_FEATURE_ID_PATTERN) as unknown as z.ZodType<ExternalFeatureId>;
145
+
146
+ /**
147
+ * Toute feature adressable par un droit : native (enum fermé) ou externe.
148
+ *
149
+ * Le surensemble est **pur** : chaque valeur déjà persistée (grants JSON,
150
+ * dispositions) parse inchangée. Une valeur externe inconnue de l'installation
151
+ * courante parse aussi : un rôle peut garder le grant d'un module retiré, il
152
+ * reste simplement inerte tant qu'aucun module ne porte cet id.
153
+ */
154
+ export const featureIdSchema = z.union([workspaceFeatureIdSchema, externalFeatureIdSchema]);
155
+
156
+ export type FeatureId = WorkspaceFeatureId | ExternalFeatureId;
157
+
158
+ /** Cette valeur est-elle l'id d'une feature externe ? */
159
+ export function isExternalFeatureId(id: string): id is ExternalFeatureId {
160
+ return EXTERNAL_FEATURE_ID_PATTERN.test(id);
161
+ }
162
+
163
+ /** `write` implique `read` : il n'existe pas d'écriture aveugle. */
164
+ export const featureAccessSchema = z.enum(['read', 'write']);
165
+ export type FeatureAccess = z.infer<typeof featureAccessSchema>;
166
+
167
+ /**
168
+ * Un droit accordé. L'**absence** d'entrée vaut « aucun accès », et la feature
169
+ * disparaît alors de l'interface du membre — une seule règle à retenir, et pas
170
+ * d'état ternaire à normaliser partout.
171
+ */
172
+ export const workspaceFeatureGrantSchema = z.object({
173
+ feature: featureIdSchema,
174
+ access: featureAccessSchema,
175
+ /**
176
+ * Gérer les **canaux d'alerte** de cette fonctionnalité : en déclarer,
177
+ * corriger une adresse ou une URL, en supprimer, lire leurs destinations.
178
+ *
179
+ * Par fonctionnalité et non par espace (migration 093) : depuis que chaque
180
+ * émetteur possède ses canaux (091), l'adresse de l'astreinte d'Uptime et
181
+ * le salon des sauvegardes ne se confient pas d'un bloc. Distinct de
182
+ * `access` exprès : régler où Uptime écrit relève de `access: write`, et
183
+ * se donne sans livrer les destinations elles-mêmes. Sans effet sur une
184
+ * fonctionnalité qui n'émet pas de notifications.
185
+ */
186
+ channels: z.boolean(),
187
+ /**
188
+ * Permissions **déclarées par la feature elle-même** (module externe, ou
189
+ * native modernisée) au-delà de lecture/écriture : la clé vient de son
190
+ * manifest (`extraPermissions`), la valeur est un booléen (`toggle`) ou la
191
+ * valeur d'un choix (`choice`).
192
+ *
193
+ * Fermeture par défaut, une seule règle : une clé **absente** vaut « refusé »
194
+ * pour un toggle et « valeur par défaut du manifest » (la moins privilégiée)
195
+ * pour un choix. Le propriétaire, qui a tout, reçoit `true` / la valeur
196
+ * `ownerValue`. Une clé inconnue du manifest courant est rejetée à
197
+ * l'écriture du rôle et ignorée à la lecture ; un grant survivant à la
198
+ * dépose d'un module reste donc inerte, jamais dangereux.
199
+ */
200
+ extras: z.record(z.string().max(24), z.union([z.boolean(), z.string().max(32)])).default({})
201
+ });
202
+
203
+ export type WorkspaceFeatureGrant = z.infer<typeof workspaceFeatureGrantSchema>;
204
+
205
+ export const WORKSPACE_ROLE_NAME_MAX = 64;
206
+
207
+ export const workspaceRoleSchema = z.object({
208
+ id: z.number().int().positive(),
209
+ name: z.string().min(1).max(WORKSPACE_ROLE_NAME_MAX),
210
+ /** Pastille de couleur dans la liste des membres. */
211
+ color: z.string().regex(/^#[0-9a-fA-F]{6}$/),
212
+ /** Ordre d'affichage seul : aucune hiérarchie implicite entre rôles. */
213
+ position: z.number().int().nonnegative(),
214
+ capabilities: z.array(workspaceCapabilitySchema),
215
+ features: z.array(workspaceFeatureGrantSchema),
216
+ /** Attribué d'office à qui rejoint l'espace par invitation. */
217
+ isDefault: z.boolean(),
218
+ memberCount: z.number().int().nonnegative()
219
+ });
220
+
221
+ export type WorkspaceRole = z.infer<typeof workspaceRoleSchema>;
222
+
223
+ /** Ligne SQL (serveur uniquement). */
224
+ export interface WorkspaceRoleRow {
225
+ id: number;
226
+ workspace_id: number;
227
+ name: string;
228
+ color: string;
229
+ position: number;
230
+ /** Tableau JSON de `WorkspaceCapability`. */
231
+ capabilities: string;
232
+ /** Tableau JSON de `WorkspaceFeatureGrant`. */
233
+ features: string;
234
+ is_default: number;
235
+ created: number;
236
+ }
237
+
238
+ /**
239
+ * Droits effectifs de l'appelant dans l'espace actif, tels que le client les
240
+ * reçoit pour n'afficher que ce qui est réellement accessible.
241
+ *
242
+ * L'interface s'en sert pour masquer, jamais pour autoriser : chaque commande
243
+ * est de toute façon vérifiée côté serveur.
244
+ */
245
+ export const workspacePermissionsSchema = z.object({
246
+ isOwner: z.boolean(),
247
+ capabilities: z.array(workspaceCapabilitySchema),
248
+ features: z.array(workspaceFeatureGrantSchema)
249
+ });
250
+
251
+ export type WorkspacePermissions = z.infer<typeof workspacePermissionsSchema>;
@@ -0,0 +1,112 @@
1
+ import { z } from 'zod';
2
+ import { userRoleSchema } from '../domain/role';
3
+ import { userStatusSchema } from '../domain/user';
4
+
5
+ /**
6
+ * Administration des comptes, à l'échelle du site et non d'un espace.
7
+ *
8
+ * Toutes ces commandes déclarent `admin: true` côté serveur : elles sont
9
+ * réservées au rôle global, seul niveau au-dessus des espaces.
10
+ */
11
+
12
+ /** Un compte, tel que la page Utilisateurs l'affiche. */
13
+ export const adminUserSchema = z.object({
14
+ id: z.number().int().positive(),
15
+ username: z.string(),
16
+ email: z.string(),
17
+ avatar: z.string(),
18
+ role: userRoleSchema,
19
+ status: userStatusSchema,
20
+ /** Nombre d'espaces auxquels le compte a accès, personnel compris. */
21
+ workspaceCount: z.number().int().nonnegative(),
22
+ lastLogin: z.number().int().nonnegative(),
23
+ created: z.number().int().nonnegative()
24
+ });
25
+
26
+ export type AdminUser = z.infer<typeof adminUserSchema>;
27
+
28
+ /** Invitation à créer un compte, en attente d'être consommée. */
29
+ export const adminInviteSchema = z.object({
30
+ token: z.string(),
31
+ url: z.string(),
32
+ /** Adresse sur laquelle l'invitation est verrouillée, ou `null`. */
33
+ email: z.string().nullable(),
34
+ /** Espace rejoint dès la création du compte, ou `null`. */
35
+ workspaceName: z.string().nullable(),
36
+ expiresAt: z.number().int().nullable(),
37
+ maxUses: z.number().int().positive().nullable(),
38
+ uses: z.number().int().nonnegative(),
39
+ createdBy: z.string(),
40
+ created: z.number().int().nonnegative()
41
+ });
42
+
43
+ export type AdminInvite = z.infer<typeof adminInviteSchema>;
44
+
45
+ export const adminUserList = {
46
+ command: 'admin.userList' as const,
47
+ input: z.object({}),
48
+ output: z.object({ users: z.array(adminUserSchema) })
49
+ };
50
+
51
+ export const adminSetUserRole = {
52
+ command: 'admin.setUserRole' as const,
53
+ input: z.object({ userId: z.number().int().positive(), role: userRoleSchema }),
54
+ output: z.object({ userId: z.number().int().positive(), role: userRoleSchema })
55
+ };
56
+
57
+ /** Suspendre refuse la connexion sans rien détruire : c'est réversible. */
58
+ export const adminSetUserStatus = {
59
+ command: 'admin.setUserStatus' as const,
60
+ input: z.object({ userId: z.number().int().positive(), status: userStatusSchema }),
61
+ output: z.object({ userId: z.number().int().positive(), status: userStatusSchema })
62
+ };
63
+
64
+ /**
65
+ * Supprime un compte **et tout ce qu'il possède** : son espace personnel, les
66
+ * espaces partagés dont il est propriétaire, et leur contenu, par cascade.
67
+ */
68
+ export const adminDeleteUser = {
69
+ command: 'admin.deleteUser' as const,
70
+ input: z.object({ userId: z.number().int().positive() }),
71
+ output: z.object({ userId: z.number().int().positive() })
72
+ };
73
+
74
+ export const adminInviteList = {
75
+ command: 'admin.inviteList' as const,
76
+ input: z.object({}),
77
+ output: z.object({ invites: z.array(adminInviteSchema) })
78
+ };
79
+
80
+ export const adminInviteCreate = {
81
+ command: 'admin.inviteCreate' as const,
82
+ input: z.object({
83
+ /** Verrouille l'invitation sur une adresse ; vide = ouverte. */
84
+ email: z.string().max(320),
85
+ /** Espace rejoint à la création du compte ; `null` = aucun. */
86
+ workspaceId: z.number().int().positive().nullable(),
87
+ ttlSeconds: z
88
+ .number()
89
+ .int()
90
+ .positive()
91
+ .max(30 * 24 * 3600)
92
+ .nullable(),
93
+ maxUses: z.number().int().positive().max(100).nullable()
94
+ }),
95
+ output: z.object({ invite: adminInviteSchema })
96
+ };
97
+
98
+ export const adminInviteRevoke = {
99
+ command: 'admin.inviteRevoke' as const,
100
+ input: z.object({ token: z.string().min(1) }),
101
+ output: z.object({ token: z.string().min(1) })
102
+ };
103
+
104
+ export const adminCommands = [
105
+ adminUserList,
106
+ adminSetUserRole,
107
+ adminSetUserStatus,
108
+ adminDeleteUser,
109
+ adminInviteList,
110
+ adminInviteCreate,
111
+ adminInviteRevoke
112
+ ] as const;