@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.
- package/LICENSE +21 -0
- package/README.md +23 -0
- package/package.json +68 -0
- package/src/domain/audience.ts +549 -0
- package/src/domain/backup.ts +355 -0
- package/src/domain/credential.ts +55 -0
- package/src/domain/database.ts +467 -0
- package/src/domain/deploy.ts +231 -0
- package/src/domain/device.ts +172 -0
- package/src/domain/deviceFiles.ts +84 -0
- package/src/domain/deviceLogs.ts +82 -0
- package/src/domain/featureRegistry.ts +392 -0
- package/src/domain/finance.ts +477 -0
- package/src/domain/git.ts +419 -0
- package/src/domain/home.ts +314 -0
- package/src/domain/live.ts +272 -0
- package/src/domain/logs.ts +117 -0
- package/src/domain/mail.ts +394 -0
- package/src/domain/metrics.ts +127 -0
- package/src/domain/note.ts +202 -0
- package/src/domain/notifications.ts +268 -0
- package/src/domain/packages.ts +35 -0
- package/src/domain/password.ts +36 -0
- package/src/domain/presence.ts +21 -0
- package/src/domain/project.ts +168 -0
- package/src/domain/projectBoard.ts +130 -0
- package/src/domain/projectChat.ts +46 -0
- package/src/domain/projectHistory.ts +82 -0
- package/src/domain/projectLink.ts +87 -0
- package/src/domain/projectPlan.ts +68 -0
- package/src/domain/report.ts +492 -0
- package/src/domain/role.ts +8 -0
- package/src/domain/secrecy.ts +66 -0
- package/src/domain/sentinel.ts +623 -0
- package/src/domain/sharing.ts +186 -0
- package/src/domain/syncProtocol.ts +116 -0
- package/src/domain/twoFactor.ts +40 -0
- package/src/domain/uptime.ts +216 -0
- package/src/domain/user.ts +141 -0
- package/src/domain/workspace.ts +56 -0
- package/src/domain/workspaceRole.ts +251 -0
- package/src/features/admin.ts +112 -0
- package/src/features/audience.ts +275 -0
- package/src/features/backup.ts +230 -0
- package/src/features/database.ts +461 -0
- package/src/features/deploy.ts +245 -0
- package/src/features/device.ts +292 -0
- package/src/features/deviceFiles.ts +83 -0
- package/src/features/deviceLogs.ts +36 -0
- package/src/features/deviceTerminal.ts +57 -0
- package/src/features/finance.ts +360 -0
- package/src/features/git.ts +368 -0
- package/src/features/home.ts +32 -0
- package/src/features/live.ts +113 -0
- package/src/features/logs.ts +86 -0
- package/src/features/mail.ts +374 -0
- package/src/features/metrics.ts +185 -0
- package/src/features/note.ts +189 -0
- package/src/features/notify.ts +164 -0
- package/src/features/password.ts +67 -0
- package/src/features/project.ts +709 -0
- package/src/features/registry.ts +103 -0
- package/src/features/secrecy.ts +120 -0
- package/src/features/sentinel.ts +233 -0
- package/src/features/sharing.ts +79 -0
- package/src/features/twoFactor.ts +47 -0
- package/src/features/uptime.ts +186 -0
- package/src/features/user.ts +91 -0
- package/src/features/workspace.ts +200 -0
- package/src/http/auth.ts +94 -0
- package/src/http/device.ts +222 -0
- package/src/http/status.ts +45 -0
- package/src/index.ts +1700 -0
- package/src/protocol/agent.ts +1171 -0
- package/src/protocol/envelope.ts +46 -0
- package/src/protocol/error.ts +27 -0
- package/src/protocol/result.ts +17 -0
- package/src/protocol/version.ts +6 -0
- package/src/sdk/client-ambient.d.ts +238 -0
- package/src/sdk/client.ts +66 -0
- package/src/sdk/ids.ts +25 -0
- package/src/sdk/index.ts +11 -0
- package/src/sdk/manifest.ts +326 -0
- package/src/sdk/providers.ts +48 -0
- package/src/sdk/server.ts +378 -0
- package/src/sdk/testing.ts +179 -0
- 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
|
+
}
|