@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
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
|
+
}
|