@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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Gérémy L.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,23 @@
1
+ # deveye-types
2
+
3
+ Les contrats partagés de [DevEye](https://github.com/Gerem66/DevEye) : types
4
+ TypeScript et schémas zod, consommés par le serveur, le client et les modules
5
+ de features.
6
+
7
+ Le package sert ses sources directement (`src/*.ts`, aucun build) :
8
+
9
+ - **`deveye-types`** — le domaine transverse (espaces, rôles, live, partage…),
10
+ les protocoles (enveloppe WS, agent), le registre d'identité des features.
11
+ - **`deveye-types/sdk`** — le contrat des modules de features : `FeatureManifest`,
12
+ `validateManifest`, ids `x-<slug>`.
13
+ - **`deveye-types/sdk/server`** — le contexte serveur d'un module (handlers,
14
+ store, façade, service).
15
+ - **`deveye-types/sdk/client`** — les contrats de l'entrée client d'un module.
16
+ - **`deveye-types/sdk/testing`** — le harnais de test en mémoire des handlers.
17
+
18
+ Le portrait typé du barrel `deveye-sdk-client` (le runtime que l'app fournit
19
+ aux modules) est publié ici aussi (`src/sdk/client-ambient.d.ts`) ; la CI de
20
+ DevEye vérifie mécaniquement que le vrai barrel l'honore.
21
+
22
+ Pour écrire un module : partir du
23
+ [template](https://github.com/Gerem66/DevEye-Feature-Template) et sa doc.
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "@deveye/types",
3
+ "version": "0.15.0",
4
+ "description": "Shared contracts (types + zod schemas) for DevEye apps",
5
+ "main": "src/index.ts",
6
+ "types": "src/index.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./src/index.ts",
10
+ "default": "./src/index.ts"
11
+ },
12
+ "./sdk": {
13
+ "types": "./src/sdk/index.ts",
14
+ "default": "./src/sdk/index.ts"
15
+ },
16
+ "./sdk/server": {
17
+ "types": "./src/sdk/server.ts",
18
+ "default": "./src/sdk/server.ts"
19
+ },
20
+ "./sdk/client": {
21
+ "types": "./src/sdk/client.ts",
22
+ "default": "./src/sdk/client.ts"
23
+ },
24
+ "./sdk/testing": {
25
+ "types": "./src/sdk/testing.ts",
26
+ "default": "./src/sdk/testing.ts"
27
+ },
28
+ "./package.json": "./package.json"
29
+ },
30
+ "files": [
31
+ "src",
32
+ "LICENSE",
33
+ "README.md"
34
+ ],
35
+ "publishConfig": {
36
+ "registry": "https://registry.npmjs.org/",
37
+ "access": "public"
38
+ },
39
+ "scripts": {
40
+ "prepublishOnly": "npm run ci",
41
+ "lint": "eslint .",
42
+ "lint:fix": "eslint . --fix",
43
+ "typecheck": "tsc --noEmit",
44
+ "ci": "npm run lint && npm run typecheck",
45
+ "prettier": "prettier --check .",
46
+ "prettier:fix": "prettier --write ."
47
+ },
48
+ "dependencies": {
49
+ "zod": "^4.4.3"
50
+ },
51
+ "repository": {
52
+ "type": "git",
53
+ "url": "git+https://github.com/Gerem66/DevEye-Types.git"
54
+ },
55
+ "devDependencies": {
56
+ "@eslint/js": "^8.0.0",
57
+ "@types/react": "^19.0.0",
58
+ "@typescript-eslint/eslint-plugin": "^6.0.0",
59
+ "@typescript-eslint/parser": "^6.0.0",
60
+ "eslint": "^8.0.0",
61
+ "eslint-plugin-prettier": "^5.5.4",
62
+ "typescript": "^5.0.0",
63
+ "typescript-eslint": "^8.39.1"
64
+ },
65
+ "type": "module",
66
+ "license": "MIT",
67
+ "author": "Gérémy L."
68
+ }
@@ -0,0 +1,549 @@
1
+ import { z } from 'zod';
2
+ import { projectStatusSchema } from './project';
3
+
4
+ /**
5
+ * L'audience d'un espace : ce que les gens font des projets une fois livrés.
6
+ *
7
+ * Même renversement que pour les dépôts git et les bases de données : un **site
8
+ * suivi** appartient à l'espace, pas à un projet, et plusieurs projets peuvent
9
+ * pointer le même. Il vit donc à l'étage **ouvert** du chiffrement — un projet
10
+ * confidentiel ne peut pas en lier.
11
+ *
12
+ * ## Pourquoi si peu de choses sont chiffrées ici
13
+ *
14
+ * Une statistique est un `GROUP BY`. Rien de ce sur quoi on agrège ne peut donc
15
+ * être chiffré, le chiffrement étant non déterministe. La sortie est celle que
16
+ * le dépôt emploie déjà — les colonnes `*_ref` — poussée jusqu'à une **table de
17
+ * dimensions** : `audience_labels` porte le libellé chiffré, et tout le reste ne
18
+ * manipule que son identifiant entier. On agrège sans clé, on ne déchiffre que
19
+ * les quelques dizaines de libellés effectivement affichés.
20
+ *
21
+ * Restent **en clair** sur le site lui-même sa clé publique, ses origines
22
+ * autorisées, son état et sa plateforme : ce sont exactement les champs dont
23
+ * l'ingestion a besoin pour router une requête *sans session ni clé*, et la clé
24
+ * comme les origines sont de toute façon lisibles dans la page suivie.
25
+ *
26
+ * ## Aucun cookie, aucun identifiant persistant
27
+ *
28
+ * Un visiteur est un condensé `(clé du site, IP, user-agent, sel du jour)` : ni
29
+ * l'IP ni le user-agent ne sont stockés, et l'identifiant ne traverse pas les
30
+ * jours. Rien à faire accepter par un bandeau de consentement.
31
+ *
32
+ * Le « par qui » nominatif vient d'ailleurs, et seulement si le site le veut :
33
+ * un `identity` que **lui** envoie pour ses propres utilisateurs connectés.
34
+ */
35
+
36
+ export const AUDIENCE_SITE_NAME_MAX_LENGTH = 96;
37
+ export const AUDIENCE_SITE_DESCRIPTION_MAX_LENGTH = 500;
38
+ export const AUDIENCE_ORIGIN_MAX_LENGTH = 255;
39
+ export const AUDIENCE_MAX_ORIGINS = 20;
40
+
41
+ /**
42
+ * Longueur d'un libellé de dimension : un chemin, un référent, un nom
43
+ * d'événement. Généreuse pour les chemins, qui portent parfois une requête
44
+ * entière — c'est le serveur qui tronque, jamais le client.
45
+ */
46
+ export const AUDIENCE_LABEL_MAX_LENGTH = 512;
47
+
48
+ /** `pk_` + 24 caractères. Publique par nature : elle est dans la page suivie. */
49
+ export const AUDIENCE_PUBLIC_KEY_LENGTH = 27;
50
+
51
+ export const AUDIENCE_RETENTION_MIN_DAYS = 7;
52
+ export const AUDIENCE_RETENTION_MAX_DAYS = 730;
53
+ export const AUDIENCE_RETENTION_DEFAULT_DAYS = 180;
54
+
55
+ /** Événements acceptés dans un seul envoi. Borne le coût d'une requête publique. */
56
+ export const AUDIENCE_BATCH_MAX = 20;
57
+
58
+ /** Au-delà, une ligne du classement n'apprend plus rien et pèse un déchiffrement. */
59
+ export const AUDIENCE_BREAKDOWN_MAX = 50;
60
+
61
+ export const AUDIENCE_FUNNEL_NAME_MAX_LENGTH = 96;
62
+
63
+ /**
64
+ * Marches d'un entonnoir, et entonnoirs par site.
65
+ *
66
+ * Dix marches est déjà beaucoup : au-delà, la lecture d'un entonnoir devient un
67
+ * exercice de comptage plutôt qu'un coup d'œil. La borne sert aussi de garde
68
+ * technique — le nombre de marches entre dans la **forme** de la requête de
69
+ * rétention, et une borne connue est ce qui rend cette construction sûre.
70
+ */
71
+ export const AUDIENCE_FUNNEL_MAX_STEPS = 10;
72
+ export const AUDIENCE_MAX_FUNNELS = 20;
73
+
74
+ /**
75
+ * Inactivité au-delà de laquelle une nouvelle visite ouvre une **autre**
76
+ * session. Trente minutes est la convention du domaine ; ce qui compte est
77
+ * surtout qu'elle soit unique et connue, puisque la durée moyenne et le taux de
78
+ * rebond en découlent tous les deux.
79
+ */
80
+ export const AUDIENCE_SESSION_GAP_SECONDS = 30 * 60;
81
+
82
+ /**
83
+ * Ce qu'un site est capable d'envoyer, et donc ce qu'on est en droit d'exiger
84
+ * de lui.
85
+ *
86
+ * `web` — une page dans un navigateur : elle envoie un en-tête `Origin`, qui est
87
+ * confronté à la liste des origines autorisées.
88
+ * `app` — un client natif (React Native, binaire) : il n'en envoie aucun, la
89
+ * confrontation est donc désactivée pour ce site.
90
+ * `both` — les deux voies pour un même produit ; l'`Origin`, quand il est
91
+ * présent, doit être autorisé, mais son absence n'est pas un refus.
92
+ *
93
+ * ⚠️ À dire franchement : la clé d'un site `app` est extractible du binaire, et
94
+ * seuls elle et le plafond de débit le protègent. Il n'existe pas mieux sans
95
+ * imposer un compte utilisateur à chaque visiteur.
96
+ */
97
+ export const audiencePlatformSchema = z.enum(['web', 'app', 'both']);
98
+ export type AudiencePlatform = z.infer<typeof audiencePlatformSchema>;
99
+
100
+ /**
101
+ * Comment un visiteur est reconnu. C'est le seul réglage de la feature qui
102
+ * change ce que la mesure *est*, et non ce qu'elle affiche.
103
+ *
104
+ * `anonymous` (défaut) : un condensé de l'IP, du user-agent et d'un sel qui
105
+ * tourne chaque jour. Rien n'est écrit chez le visiteur, donc rien à faire
106
+ * accepter. Le prix est qu'une même personne revenant le lendemain compte pour
107
+ * une nouvelle, ce qui rend « visiteurs récurrents » impossible par
108
+ * construction.
109
+ *
110
+ * `persistent` : le site range un identifiant tiré au sort dans le stockage du
111
+ * navigateur et le renvoie à chaque mesure. La même personne est alors reconnue
112
+ * d'un jour à l'autre, ce qui ouvre les visiteurs connus et le nombre de
113
+ * visites par personne.
114
+ *
115
+ * ⚠️ **Ce mode relève du consentement.** Écrire un identifiant durable chez le
116
+ * visiteur, que ce soit un cookie ou du `localStorage`, tombe sous la directive
117
+ * ePrivacy exactement de la même façon. L'interface le dit à l'endroit où on
118
+ * l'active ; il appartient au site de recueillir ce consentement avant de poser
119
+ * `data-visitor="persistent"` sur sa balise.
120
+ *
121
+ * Les deux côtés doivent être d'accord : le serveur ignore un identifiant reçu
122
+ * si le site est en `anonymous`, et la balise n'en envoie aucun sans son
123
+ * attribut. Éteindre le réglage suffit donc à revenir en arrière, sans toucher
124
+ * aux pages.
125
+ */
126
+ export const audienceVisitorModeSchema = z.enum(['anonymous', 'persistent']);
127
+ export type AudienceVisitorMode = z.infer<typeof audienceVisitorModeSchema>;
128
+
129
+ /** Longueur maximale de l'identifiant qu'un client persistant peut proposer. */
130
+ export const AUDIENCE_VISITOR_ID_MAX_LENGTH = 64;
131
+
132
+ /**
133
+ * Les axes selon lesquels on peut ventiler.
134
+ *
135
+ * **Un seul vocabulaire pour deux usages** : c'est à la fois le `kind` d'une
136
+ * ligne d'`audience_labels` et l'axe demandé par `audience.breakdown`. Les
137
+ * séparer aurait produit deux listes à garder synchrones à la main, pour
138
+ * exactement le même ensemble de valeurs.
139
+ */
140
+ export const audienceDimensionSchema = z.enum([
141
+ /** Le chemin de la page, ou le nom de l'écran d'un client natif. */
142
+ 'path',
143
+ /** D'où vient le visiteur ; l'hôte seul, jamais l'URL complète. */
144
+ 'referrer',
145
+ 'browser',
146
+ 'os',
147
+ /** `desktop` | `mobile` | `tablet`, déduit du user-agent ou envoyé tel quel. */
148
+ 'device',
149
+ /** Le fuseau déclaré par le client (`Europe/Paris`) — pas un pays. */
150
+ 'timezone',
151
+ 'language',
152
+ /** Le nom d'un événement nommé (`inscription`, `paiement`…). */
153
+ 'event',
154
+ /** Ce que le site suivi appelle son utilisateur, quand il le déclare. */
155
+ 'identity'
156
+ ]);
157
+ export type AudienceDimension = z.infer<typeof audienceDimensionSchema>;
158
+
159
+ export const AUDIENCE_DIMENSIONS = audienceDimensionSchema.options;
160
+
161
+ /** Fenêtres offertes. Fermées exprès : chacune a sa résolution et ses index. */
162
+ export const audienceRangeSchema = z.enum(['24h', '7d', '30d', '90d', '365d']);
163
+ export type AudienceRange = z.infer<typeof audienceRangeSchema>;
164
+
165
+ /** Le pas d'une courbe, déduit de la fenêtre et jamais choisi par l'appelant. */
166
+ export const audienceResolutionSchema = z.enum(['hour', 'day', 'week']);
167
+ export type AudienceResolution = z.infer<typeof audienceResolutionSchema>;
168
+
169
+ // ----------------------------------------------------------------- le site
170
+
171
+ export const audienceSiteSchema = z.object({
172
+ id: z.number().int().positive(),
173
+ /** Le nom que lui donne l'utilisateur ; porte l'unicité dans l'espace. */
174
+ name: z.string().max(AUDIENCE_SITE_NAME_MAX_LENGTH),
175
+ description: z.string().max(AUDIENCE_SITE_DESCRIPTION_MAX_LENGTH),
176
+ /**
177
+ * La clé à coller dans la page. **Publique**, et c'est assumé : elle ne
178
+ * protège rien, ce sont les origines autorisées qui filtrent.
179
+ */
180
+ publicKey: z.string().length(AUDIENCE_PUBLIC_KEY_LENGTH),
181
+ platform: audiencePlatformSchema,
182
+ visitorMode: audienceVisitorModeSchema,
183
+ /**
184
+ * Les hôtes autorisés à écrire (`exemple.fr`, `www.exemple.fr`). Vide = on
185
+ * accepte n'importe quelle origine, ce que l'écran signale comme un état
186
+ * transitoire — le temps de brancher, pas un réglage à laisser en place.
187
+ */
188
+ origins: z.array(z.string().max(AUDIENCE_ORIGIN_MAX_LENGTH)).max(AUDIENCE_MAX_ORIGINS),
189
+ /** Éteint, plus rien n'entre ; l'historique déjà là ne bouge pas. */
190
+ active: z.boolean(),
191
+ /** Conservation des événements bruts. L'agrégat journalier, lui, survit. */
192
+ retentionDays: z
193
+ .number()
194
+ .int()
195
+ .min(AUDIENCE_RETENTION_MIN_DAYS)
196
+ .max(AUDIENCE_RETENTION_MAX_DAYS),
197
+ /**
198
+ * Quand le dernier événement est entré. `null` = jamais rien reçu, ce qui
199
+ * est l'état normal d'un site qu'on vient de déclarer et non une panne :
200
+ * c'est ce que l'écran d'installation attend pour se déclarer satisfait.
201
+ */
202
+ lastEventAt: z.number().int().nullable(),
203
+ /** De quoi ranger la liste sans ouvrir chaque fiche. */
204
+ views24h: z.number().int().nonnegative(),
205
+ visitors24h: z.number().int().nonnegative(),
206
+ /** Combien de projets s'en servent — l'interconnexion, comme pour un dépôt. */
207
+ projectCount: z.number().int().nonnegative(),
208
+ /**
209
+ * Cet élément vient d'un **autre espace**, qui le projette ici.
210
+ *
211
+ * L'écran le signale d'une pastille : sans elle, rien ne distingue une
212
+ * ligne locale d'une fenêtre sur l'espace voisin — et les gestes réservés
213
+ * au domicile (supprimer, re-partager) sembleraient cassés au lieu de
214
+ * s'expliquer.
215
+ */
216
+ foreign: z.boolean(),
217
+ created: z.number().int()
218
+ });
219
+ export type AudienceSite = z.infer<typeof audienceSiteSchema>;
220
+
221
+ /**
222
+ * Un projet qui suit ce site.
223
+ *
224
+ * Ne remonte que des projets à l'étage ouvert — un projet confidentiel ne peut
225
+ * pas être lié, donc le titre est toujours lisible sans session. Même forme que
226
+ * `DatabaseUsage` et `GitRepoUsage`, pour que les trois écrans se ressemblent.
227
+ */
228
+ export const audienceUsageSchema = z.object({
229
+ projectId: z.number().int().positive(),
230
+ title: z.string(),
231
+ status: projectStatusSchema
232
+ });
233
+ export type AudienceUsage = z.infer<typeof audienceUsageSchema>;
234
+
235
+ // ------------------------------------------------------------ ce qu'on lit
236
+
237
+ export const audienceMetricsSchema = z.object({
238
+ views: z.number().int().nonnegative(),
239
+ visitors: z.number().int().nonnegative(),
240
+ sessions: z.number().int().nonnegative(),
241
+ /** Durée moyenne d'une session, en secondes. */
242
+ avgDurationSeconds: z.number().nonnegative(),
243
+ /** Part des sessions d'une seule vue, entre 0 et 1. */
244
+ bounceRate: z.number().min(0).max(1),
245
+ /**
246
+ * Visiteurs déjà venus avant la période.
247
+ *
248
+ * Toujours `0` en mode anonyme : personne n'y est jamais reconnu d'un jour
249
+ * à l'autre, et afficher une part de récurrents serait mentir. Ne remonte
250
+ * donc que sur un site en mode persistant, et l'écran ne montre la tuile
251
+ * que dans ce cas.
252
+ *
253
+ * ⚠️ Borné par la conservation du site : quelqu'un dont la dernière visite
254
+ * a expiré repasse pour un nouveau. C'est une conséquence de la rétention,
255
+ * pas une erreur de comptage.
256
+ */
257
+ returningVisitors: z.number().int().nonnegative()
258
+ });
259
+ export type AudienceMetrics = z.infer<typeof audienceMetricsSchema>;
260
+
261
+ export const audiencePointSchema = z.object({
262
+ /** Début du seau, en secondes epoch. */
263
+ at: z.number().int(),
264
+ views: z.number().int().nonnegative(),
265
+ visitors: z.number().int().nonnegative()
266
+ });
267
+ export type AudiencePoint = z.infer<typeof audiencePointSchema>;
268
+
269
+ /**
270
+ * Le bandeau d'un site, et la courbe dessous.
271
+ *
272
+ * `previous` porte les **mêmes** mesures sur la fenêtre précédente de même
273
+ * longueur. C'est ce qui transforme un nombre en information : « 1 240 vues »
274
+ * ne dit rien, « 1 240 vues, +18 % » dit s'il faut regarder de plus près. Le
275
+ * coût est la même requête sur une fenêtre décalée, ce qui est peu cher payé.
276
+ */
277
+ export const audienceOverviewSchema = z.object({
278
+ metrics: audienceMetricsSchema,
279
+ previous: audienceMetricsSchema,
280
+ resolution: audienceResolutionSchema,
281
+ points: z.array(audiencePointSchema)
282
+ });
283
+ export type AudienceOverview = z.infer<typeof audienceOverviewSchema>;
284
+
285
+ export const audienceBreakdownItemSchema = z.object({
286
+ /** Le libellé déchiffré. Vide quand il l'est resté (clé d'espace convertie). */
287
+ label: z.string(),
288
+ views: z.number().int().nonnegative(),
289
+ visitors: z.number().int().nonnegative()
290
+ });
291
+ export type AudienceBreakdownItem = z.infer<typeof audienceBreakdownItemSchema>;
292
+
293
+ /**
294
+ * Une case de la carte d'activité : un jour de la semaine, une heure.
295
+ *
296
+ * L'heure est **locale au visiteur**, reconstituée depuis le décalage qu'il a
297
+ * déclaré. C'est la seule qui réponde à « quand mes utilisateurs sont-ils là ? » :
298
+ * en heure serveur, une audience répartie sur trois continents ne dessine rien.
299
+ */
300
+ export const audienceActivityCellSchema = z.object({
301
+ /** 0 = lundi. Semaine à l'européenne, comme le reste de l'interface. */
302
+ day: z.number().int().min(0).max(6),
303
+ hour: z.number().int().min(0).max(23),
304
+ views: z.number().int().nonnegative()
305
+ });
306
+ export type AudienceActivityCell = z.infer<typeof audienceActivityCellSchema>;
307
+
308
+ export const audienceActivitySchema = z.object({
309
+ cells: z.array(audienceActivityCellSchema),
310
+ /** Les fuseaux les plus représentés — la « zone de temps » la plus active. */
311
+ timezones: z.array(audienceBreakdownItemSchema)
312
+ });
313
+ export type AudienceActivity = z.infer<typeof audienceActivitySchema>;
314
+
315
+ /** Qui est là en ce moment. Lu souvent, donc volontairement minuscule. */
316
+ export const audienceLiveSchema = z.object({
317
+ visitors: z.number().int().nonnegative(),
318
+ pages: z.array(audienceBreakdownItemSchema)
319
+ });
320
+ export type AudienceLive = z.infer<typeof audienceLiveSchema>;
321
+
322
+ // ------------------------------------------------------------ entonnoirs
323
+
324
+ /**
325
+ * Ce qu'une marche reconnaît.
326
+ *
327
+ * `path` — une page vue. `event` — un événement nommé, posé par le site avec
328
+ * `deveye.event('…')`.
329
+ *
330
+ * Ce sont exactement deux des dimensions déjà collectées, et c'est tout l'objet
331
+ * du découpage : **le site émet des signaux, l'entonnoir se compose ici**. Sans
332
+ * lui, mesurer autre chose demanderait de redéployer le site.
333
+ */
334
+ export const audienceFunnelStepKindSchema = z.enum(['path', 'event']);
335
+ export type AudienceFunnelStepKind = z.infer<typeof audienceFunnelStepKindSchema>;
336
+
337
+ /** Une marche telle qu'on la définit : ce qu'elle reconnaît, et rien d'autre. */
338
+ export const audienceFunnelStepDraftSchema = z.object({
339
+ kind: audienceFunnelStepKindSchema,
340
+ value: z.string().min(1).max(AUDIENCE_LABEL_MAX_LENGTH)
341
+ });
342
+ export type AudienceFunnelStepDraft = z.infer<typeof audienceFunnelStepDraftSchema>;
343
+
344
+ /** Une marche telle qu'on la lit : sa définition, et ce qu'elle a mesuré. */
345
+ export const audienceFunnelStepSchema = audienceFunnelStepDraftSchema.extend({
346
+ /** Visites arrivées jusqu'ici, les marches précédentes franchies dans l'ordre. */
347
+ sessions: z.number().int().nonnegative()
348
+ });
349
+ export type AudienceFunnelStep = z.infer<typeof audienceFunnelStepSchema>;
350
+
351
+ export const audienceFunnelSchema = z.object({
352
+ id: z.number().int().positive(),
353
+ name: z.string().max(AUDIENCE_FUNNEL_NAME_MAX_LENGTH),
354
+ steps: z.array(audienceFunnelStepSchema)
355
+ });
356
+ export type AudienceFunnel = z.infer<typeof audienceFunnelSchema>;
357
+
358
+ // --------------------------------------------------------- l'ingestion
359
+
360
+ /**
361
+ * Un événement tel qu'un client l'envoie.
362
+ *
363
+ * **Rien ici ne suppose un navigateur** : aucun champ propre au web n'est
364
+ * requis, `path` désigne une route *ou* un écran, et les trois champs
365
+ * d'appareil peuvent être renseignés explicitement par un client natif qui les
366
+ * connaît, au lieu d'être devinés d'un user-agent qu'il n'a pas. C'est ce qui
367
+ * permettra à un module React Native de se brancher sans toucher au serveur.
368
+ *
369
+ * Tout est optionnel sauf le type et le chemin : un client qui ne sait pas
370
+ * remplir un champ doit pouvoir l'omettre, jamais mentir.
371
+ */
372
+ export const audienceEventInputSchema = z.object({
373
+ type: z.enum(['view', 'event']),
374
+ path: z.string().min(1).max(AUDIENCE_LABEL_MAX_LENGTH),
375
+ /** Requis pour un `event`, ignoré pour une `view`. */
376
+ name: z.string().max(AUDIENCE_LABEL_MAX_LENGTH).optional(),
377
+ /** URL complète ; le serveur n'en garde que l'hôte. */
378
+ referrer: z.string().max(AUDIENCE_LABEL_MAX_LENGTH).optional(),
379
+ /** `Intl.DateTimeFormat().resolvedOptions().timeZone`. */
380
+ timezone: z.string().max(64).optional(),
381
+ /** Décalage local en minutes, tel que `getTimezoneOffset()` le rend. */
382
+ tzOffset: z.number().int().min(-840).max(840).optional(),
383
+ screenWidth: z.number().int().min(0).max(20000).optional(),
384
+ language: z.string().max(35).optional(),
385
+ /** Ce que le site appelle son utilisateur connecté. Chiffré au repos. */
386
+ identity: z.string().max(AUDIENCE_LABEL_MAX_LENGTH).optional(),
387
+ /**
388
+ * L'identifiant que le client garde d'une visite à l'autre.
389
+ *
390
+ * Ignoré si le site n'est pas en mode persistant. Jamais stocké tel quel :
391
+ * le serveur n'en garde qu'un condensé, propre au site, pour qu'un même
392
+ * identifiant sur deux sites ne permette aucun recoupement.
393
+ */
394
+ visitorId: z.string().max(AUDIENCE_VISITOR_ID_MAX_LENGTH).optional(),
395
+ browser: z.string().max(64).optional(),
396
+ os: z.string().max(64).optional(),
397
+ device: z.string().max(32).optional(),
398
+ /**
399
+ * Horodatage client, en secondes epoch — pour un client natif qui a mis des
400
+ * événements de côté hors ligne. Le serveur le **borne** à sa propre
401
+ * fenêtre : une horloge fausse ne doit pas pouvoir dater une visite de 2038
402
+ * et écraser tous les graphes de l'espace.
403
+ */
404
+ at: z.number().int().optional()
405
+ });
406
+ export type AudienceEventInput = z.infer<typeof audienceEventInputSchema>;
407
+
408
+ export const audienceIngestSchema = z.object({
409
+ key: z.string().length(AUDIENCE_PUBLIC_KEY_LENGTH),
410
+ /**
411
+ * Porté par le lot et non par chaque événement : c'est une propriété du
412
+ * client, pas de la mesure. Le répéter vingt fois dans une trame aurait
413
+ * coûté plus que tout le reste du corps.
414
+ */
415
+ visitorId: z.string().max(AUDIENCE_VISITOR_ID_MAX_LENGTH).optional(),
416
+ events: z.array(audienceEventInputSchema).min(1).max(AUDIENCE_BATCH_MAX)
417
+ });
418
+ export type AudienceIngestBody = z.infer<typeof audienceIngestSchema>;
419
+
420
+ // ------------------------------------------------------------- lignes SQL
421
+
422
+ export interface AudienceSiteRow {
423
+ id: number;
424
+ workspace_id: number;
425
+ /**
426
+ * En clair : c'est la seule chose dont l'ingestion dispose pour retrouver le
427
+ * site, et elle tourne sans session ni clé.
428
+ */
429
+ public_key: string;
430
+ /** Condensé du nom en minuscules : porte l'unicité dans l'espace. */
431
+ name_ref: string;
432
+ platform: string;
433
+ /** 'anonymous' | 'persistent'. En clair : l'ingestion s'en sert sans clé. */
434
+ visitor_mode: string;
435
+ /**
436
+ * Hôtes autorisés, séparés par des sauts de ligne. En clair pour la même
437
+ * raison que la clé — et ils sont publics de toute façon, puisqu'ils
438
+ * nomment les pages où la balise est posée.
439
+ */
440
+ origins: string | null;
441
+ active: number;
442
+ retention_days: number;
443
+ sort_order: number;
444
+ last_event_at: number | null;
445
+ /** { name, description } chiffré, étage ouvert. */
446
+ content: string;
447
+ created: number;
448
+ }
449
+
450
+ /**
451
+ * Un libellé de dimension, chiffré, dédoublonné par son condensé.
452
+ *
453
+ * C'est la table qui rend tout le reste possible : un chemin vu mille fois est
454
+ * stocké **une** fois, et les tables de faits ne portent que son `id`.
455
+ */
456
+ export interface AudienceLabelRow {
457
+ id: number;
458
+ site_id: number;
459
+ /** Une valeur d'`audienceDimensionSchema`. */
460
+ kind: string;
461
+ /** 16 premiers caractères du sha256 de la valeur normalisée. */
462
+ label_ref: string;
463
+ content: string;
464
+ }
465
+
466
+ export interface AudienceSessionRow {
467
+ id: number;
468
+ site_id: number;
469
+ /** sha256(clé du site + IP + user-agent + sel du jour), tronqué. */
470
+ visitor_ref: string;
471
+ started_at: number;
472
+ last_at: number;
473
+ views: number;
474
+ entry_path_id: number | null;
475
+ referrer_id: number | null;
476
+ browser_id: number | null;
477
+ os_id: number | null;
478
+ device_id: number | null;
479
+ timezone_id: number | null;
480
+ language_id: number | null;
481
+ identity_id: number | null;
482
+ /** Décalage local du visiteur, en minutes. Sert la carte d'activité. */
483
+ tz_offset: number | null;
484
+ screen_width: number | null;
485
+ }
486
+
487
+ export interface AudienceEventRow {
488
+ id: number;
489
+ site_id: number;
490
+ session_id: number;
491
+ ts: number;
492
+ /** 0 = vue de page, 1 = événement nommé. */
493
+ kind: number;
494
+ path_id: number | null;
495
+ name_id: number | null;
496
+ }
497
+
498
+ /**
499
+ * L'agrégat journalier. **Jamais purgé** — c'est lui qui fait survivre les
500
+ * courbes longues à l'expiration des événements bruts, exactement comme
501
+ * l'agrégat journalier d'Uptime.
502
+ */
503
+ export interface AudienceDailyRow {
504
+ site_id: number;
505
+ /** Jour UTC, en `YYYYMMDD`. Un entier se compare et s'indexe. */
506
+ day: number;
507
+ views: number;
508
+ sessions: number;
509
+ visitors: number;
510
+ }
511
+
512
+ export interface AudienceFunnelRow {
513
+ id: number;
514
+ site_id: number;
515
+ /** Condensé du nom : porte l'unicité de l'entonnoir dans son site. */
516
+ name_ref: string;
517
+ sort_order: number;
518
+ /** { name } chiffré, étage ouvert. */
519
+ content: string;
520
+ created: number;
521
+ }
522
+
523
+ export interface AudienceFunnelStepRow {
524
+ id: number;
525
+ funnel_id: number;
526
+ site_id: number;
527
+ position: number;
528
+ /** Une valeur d'`audienceFunnelStepKindSchema`. */
529
+ match_kind: string;
530
+ /**
531
+ * Condensé de la valeur reconnue — le **même** que celui d'`audience_labels`,
532
+ * ce qui permet de retrouver le libellé sans jamais déchiffrer pour
533
+ * comparer.
534
+ *
535
+ * Une marche peut parfaitement ne correspondre à aucun libellé : c'est le
536
+ * cas d'un événement qu'on a prévu mais que le site n'a encore jamais posé.
537
+ * Elle compte alors zéro, ce qui est la vérité.
538
+ */
539
+ label_ref: string;
540
+ /** La valeur lisible, chiffrée : la marche se décrit toute seule. */
541
+ content: string;
542
+ }
543
+
544
+ export interface ProjectAudienceLinkRow {
545
+ project_id: number;
546
+ site_id: number;
547
+ workspace_id: number;
548
+ created: number;
549
+ }