@deveye/types 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +23 -0
  3. package/package.json +68 -0
  4. package/src/domain/audience.ts +549 -0
  5. package/src/domain/backup.ts +355 -0
  6. package/src/domain/credential.ts +55 -0
  7. package/src/domain/database.ts +467 -0
  8. package/src/domain/deploy.ts +231 -0
  9. package/src/domain/device.ts +172 -0
  10. package/src/domain/deviceFiles.ts +84 -0
  11. package/src/domain/deviceLogs.ts +82 -0
  12. package/src/domain/featureRegistry.ts +392 -0
  13. package/src/domain/finance.ts +477 -0
  14. package/src/domain/git.ts +419 -0
  15. package/src/domain/home.ts +314 -0
  16. package/src/domain/live.ts +272 -0
  17. package/src/domain/logs.ts +117 -0
  18. package/src/domain/mail.ts +394 -0
  19. package/src/domain/metrics.ts +127 -0
  20. package/src/domain/note.ts +202 -0
  21. package/src/domain/notifications.ts +268 -0
  22. package/src/domain/packages.ts +35 -0
  23. package/src/domain/password.ts +36 -0
  24. package/src/domain/presence.ts +21 -0
  25. package/src/domain/project.ts +168 -0
  26. package/src/domain/projectBoard.ts +130 -0
  27. package/src/domain/projectChat.ts +46 -0
  28. package/src/domain/projectHistory.ts +82 -0
  29. package/src/domain/projectLink.ts +87 -0
  30. package/src/domain/projectPlan.ts +68 -0
  31. package/src/domain/report.ts +492 -0
  32. package/src/domain/role.ts +8 -0
  33. package/src/domain/secrecy.ts +66 -0
  34. package/src/domain/sentinel.ts +623 -0
  35. package/src/domain/sharing.ts +186 -0
  36. package/src/domain/syncProtocol.ts +116 -0
  37. package/src/domain/twoFactor.ts +40 -0
  38. package/src/domain/uptime.ts +216 -0
  39. package/src/domain/user.ts +141 -0
  40. package/src/domain/workspace.ts +56 -0
  41. package/src/domain/workspaceRole.ts +251 -0
  42. package/src/features/admin.ts +112 -0
  43. package/src/features/audience.ts +275 -0
  44. package/src/features/backup.ts +230 -0
  45. package/src/features/database.ts +461 -0
  46. package/src/features/deploy.ts +245 -0
  47. package/src/features/device.ts +292 -0
  48. package/src/features/deviceFiles.ts +83 -0
  49. package/src/features/deviceLogs.ts +36 -0
  50. package/src/features/deviceTerminal.ts +57 -0
  51. package/src/features/finance.ts +360 -0
  52. package/src/features/git.ts +368 -0
  53. package/src/features/home.ts +32 -0
  54. package/src/features/live.ts +113 -0
  55. package/src/features/logs.ts +86 -0
  56. package/src/features/mail.ts +374 -0
  57. package/src/features/metrics.ts +185 -0
  58. package/src/features/note.ts +189 -0
  59. package/src/features/notify.ts +164 -0
  60. package/src/features/password.ts +67 -0
  61. package/src/features/project.ts +709 -0
  62. package/src/features/registry.ts +103 -0
  63. package/src/features/secrecy.ts +120 -0
  64. package/src/features/sentinel.ts +233 -0
  65. package/src/features/sharing.ts +79 -0
  66. package/src/features/twoFactor.ts +47 -0
  67. package/src/features/uptime.ts +186 -0
  68. package/src/features/user.ts +91 -0
  69. package/src/features/workspace.ts +200 -0
  70. package/src/http/auth.ts +94 -0
  71. package/src/http/device.ts +222 -0
  72. package/src/http/status.ts +45 -0
  73. package/src/index.ts +1700 -0
  74. package/src/protocol/agent.ts +1171 -0
  75. package/src/protocol/envelope.ts +46 -0
  76. package/src/protocol/error.ts +27 -0
  77. package/src/protocol/result.ts +17 -0
  78. package/src/protocol/version.ts +6 -0
  79. package/src/sdk/client-ambient.d.ts +238 -0
  80. package/src/sdk/client.ts +66 -0
  81. package/src/sdk/ids.ts +25 -0
  82. package/src/sdk/index.ts +11 -0
  83. package/src/sdk/manifest.ts +326 -0
  84. package/src/sdk/providers.ts +48 -0
  85. package/src/sdk/server.ts +378 -0
  86. package/src/sdk/testing.ts +179 -0
  87. package/src/utils/version.ts +28 -0
@@ -0,0 +1,186 @@
1
+ import { z } from 'zod';
2
+
3
+ import { featureAccessSchema, workspaceFeatureIdSchema } from './workspaceRole';
4
+
5
+ /**
6
+ * Rendre un élément visible depuis un autre espace, **sans le déplacer**.
7
+ *
8
+ * ## L'invariant, avant tout le reste
9
+ *
10
+ * Un élément partagé **ne change jamais de clé**. Il reste chiffré sous celle de
11
+ * son espace d'origine ; servi ailleurs, il est déchiffré avec le codec ouvert
12
+ * de cet espace-là. C'est le prolongement direct du levier L3 de
13
+ * `WORKSPACES.md` — « un espace résout les clés de son propriétaire » — et la
14
+ * raison pour laquelle ce chantier ne re-chiffre rien.
15
+ *
16
+ * `WORKSPACES.md` §10 range **déplacer** un élément hors périmètre, précisément
17
+ * parce que ce serait la seule opération à exiger un déchiffrement clé A puis un
18
+ * re-chiffrement clé B sous session vivante. Partager ne l'exige pas : c'est une
19
+ * **projection**, pas un transfert. L'élément a un seul domicile, et des
20
+ * fenêtres ailleurs.
21
+ *
22
+ * ## Ce qui en découle, et qu'il faut assumer
23
+ *
24
+ * Seule la clé de l'étage ouvert est résoluble par le serveur seul. Un élément
25
+ * de l'étage gardé ne peut donc pas être partagé — pas par prudence, par
26
+ * impossibilité mécanique. Voir `shareTier` dans le registre.
27
+ *
28
+ * ## On ne partage qu'avec soi-même
29
+ *
30
+ * La liste proposée est celle des espaces **dont l'appelant est membre**. Ce
31
+ * n'est pas une restriction d'interface mais la règle : partager vers un espace
32
+ * où l'on n'entre pas reviendrait à y déposer une donnée sans pouvoir en
33
+ * répondre, et à contourner l'appartenance — qui est la frontière absolue du
34
+ * modèle (`WORKSPACES.md` §3).
35
+ */
36
+
37
+ export const itemShareSchema = z.object({
38
+ /** L'espace vers lequel l'élément est projeté. */
39
+ workspaceId: z.number().int().positive(),
40
+ workspaceName: z.string(),
41
+ /** Faux quand c'est l'espace d'origine : il n'est pas décochable. */
42
+ isHome: z.boolean(),
43
+ shared: z.boolean(),
44
+ /**
45
+ * L'appelant peut régler, **depuis ici**, ce que chaque rôle de cet espace
46
+ * voit de l'élément (`share.grantList` / `share.grantSet` avec ce
47
+ * `workspaceId`).
48
+ *
49
+ * Vrai quand l'élément y est visible, que l'espace est partagé (un espace
50
+ * personnel n'a pas de rôles) et que l'appelant y tient `workspace.roles`.
51
+ * C'est ce qui permet de gérer les permissions de toutes les fenêtres
52
+ * depuis l'onglet Partage, sans changer d'espace.
53
+ */
54
+ grantsManageable: z.boolean()
55
+ });
56
+ export type ItemShare = z.infer<typeof itemShareSchema>;
57
+
58
+ /**
59
+ * Pourquoi un élément ne peut pas être partagé, quand c'est le cas.
60
+ *
61
+ * Une phrase plutôt qu'un booléen : « impossible » sans raison donne à chercher
62
+ * un réglage qui n'existe pas. Ici la cause est toujours structurelle, et la
63
+ * dire évite qu'on la prenne pour une panne.
64
+ */
65
+ export const shareBlockerSchema = z.enum([
66
+ /** La fonctionnalité entière vit à l'étage gardé, ou n'a pas de sens ici. */
67
+ 'feature',
68
+ /** Cet élément précis est gardé — note privée, compte mail « guarded ». */
69
+ 'item',
70
+ /** L'appelant n'a pas le droit de partager cette fonctionnalité. */
71
+ 'forbidden',
72
+ /**
73
+ * L'élément vient d'un autre espace : on ne re-projette pas ce qu'on ne
74
+ * fait que voir. Son partage se règle depuis chez lui.
75
+ */
76
+ 'foreign'
77
+ ]);
78
+ export type ShareBlocker = z.infer<typeof shareBlockerSchema>;
79
+
80
+ export const itemShareStateSchema = z.object({
81
+ /** Les espaces de l'appelant, l'origine comprise et marquée. */
82
+ workspaces: z.array(itemShareSchema),
83
+ /** Renseigné quand le partage est impossible ; les cases sont alors inertes. */
84
+ blocker: shareBlockerSchema.nullable()
85
+ });
86
+ export type ItemShareState = z.infer<typeof itemShareStateSchema>;
87
+
88
+ /**
89
+ * Ce qu'un rôle peut faire sur **un** élément.
90
+ *
91
+ * Volontairement **restrictif seulement** : `none` ou `read` abaissent ce que le
92
+ * rôle a sur la fonctionnalité, jamais l'inverse. Le droit de feature reste le
93
+ * plafond, ici comme pour les droits fins.
94
+ *
95
+ * L'alternative — permettre d'élever — a été écartée : l'accès effectif à une
96
+ * fonctionnalité deviendrait « le maximum entre le rôle et le meilleur droit
97
+ * d'élément », donc une requête de plus dans la résolution d'accès, et surtout
98
+ * un écran des rôles qui ne dirait plus à lui seul qui voit quoi.
99
+ */
100
+ export const itemAccessSchema = z.enum(['none', 'read']);
101
+ export type ItemAccess = z.infer<typeof itemAccessSchema>;
102
+
103
+ export const itemRoleGrantSchema = z.object({
104
+ roleId: z.number().int().positive(),
105
+ /** Absent de la liste = le rôle garde ce que la fonctionnalité lui donne. */
106
+ access: itemAccessSchema
107
+ });
108
+ export type ItemRoleGrant = z.infer<typeof itemRoleGrantSchema>;
109
+
110
+ /**
111
+ * Un rôle d'un espace, vu depuis l'écran des restrictions d'un élément.
112
+ *
113
+ * Porte tout ce que l'écran affiche, pour qu'il n'ait **aucun** recoupement à
114
+ * faire : l'identité du rôle, ce que la fonctionnalité lui donne (le droit
115
+ * *hérité*, affiché même quand aucune exception n'est posée — une vue
116
+ * d'ensemble qui ne montre que les exceptions oblige à deviner le reste), et
117
+ * l'exception posée s'il y en a une.
118
+ */
119
+ export const itemRoleGrantViewSchema = z.object({
120
+ roleId: z.number().int().positive(),
121
+ name: z.string(),
122
+ color: z.string(),
123
+ /**
124
+ * Ce que le rôle a sur la **fonctionnalité** — le plafond, et la valeur
125
+ * effective quand `access` est `null`.
126
+ */
127
+ featureAccess: z.enum(['none', 'read', 'write']),
128
+ /** L'exception posée sur cet élément, ou `null` : « comme la fonctionnalité ». */
129
+ access: itemAccessSchema.nullable()
130
+ });
131
+ export type ItemRoleGrantView = z.infer<typeof itemRoleGrantViewSchema>;
132
+
133
+ /** L'état des restrictions d'un élément dans **un** espace, prêt à afficher. */
134
+ export const itemGrantStateSchema = z.object({
135
+ workspaceId: z.number().int().positive(),
136
+ workspaceName: z.string(),
137
+ roles: z.array(itemRoleGrantViewSchema)
138
+ });
139
+ export type ItemGrantState = z.infer<typeof itemGrantStateSchema>;
140
+
141
+ /** La cible d'un partage ou d'une restriction. */
142
+ export const itemRefSchema = z.object({
143
+ feature: workspaceFeatureIdSchema,
144
+ itemId: z.number().int().positive()
145
+ });
146
+ export type ItemRef = z.infer<typeof itemRefSchema>;
147
+
148
+ /**
149
+ * Une référence qu'on voit sans pouvoir la lire.
150
+ *
151
+ * Le cas : un élément partagé vers B pointe une donnée de A — un compte mail, un
152
+ * appareil, un canal d'alerte. Un membre de B qui n'est pas membre de A doit
153
+ * **savoir que le lien existe** sans en connaître le contenu. Le masquer
154
+ * entièrement ferait croire à un élément mal réglé ; le montrer ferait fuiter
155
+ * l'espace d'origine.
156
+ *
157
+ * Il peut la **retirer** si ses droits le permettent — retirer un lien ne
158
+ * demande pas de le lire. Il ne peut ni le voir ni le modifier.
159
+ */
160
+ export const foreignRefSchema = z.object({
161
+ kind: z.literal('inaccessible'),
162
+ /** « Compte mail d'un autre espace » — le genre, jamais l'identité. */
163
+ label: z.string()
164
+ });
165
+ export type ForeignRef = z.infer<typeof foreignRefSchema>;
166
+
167
+ /** Ligne de `item_shares` (serveur uniquement). */
168
+ export interface ItemShareRow {
169
+ workspace_id: number;
170
+ feature: string;
171
+ item_id: number;
172
+ home_workspace_id: number;
173
+ shared_by_user_id: number;
174
+ created: number;
175
+ }
176
+
177
+ /** Ligne de `item_role_grants` (serveur uniquement). */
178
+ export interface ItemRoleGrantRow {
179
+ workspace_id: number;
180
+ feature: string;
181
+ item_id: number;
182
+ role_id: number;
183
+ access: ItemAccess;
184
+ }
185
+
186
+ export { featureAccessSchema };
@@ -0,0 +1,116 @@
1
+ import { z } from 'zod';
2
+
3
+ /**
4
+ * La part PUBLIQUE du vocabulaire CloudSync : ce que le protocole agent
5
+ * transporte (scan, index, progression, état de partage) et rien d'autre.
6
+ *
7
+ * Le reste du domaine CloudSync (partages, versions, instantanés, lignes SQL)
8
+ * vit dans le module privé deveye-feature-cloudsync : seul le fil, parlé par
9
+ * l'agent public et poussé aux navigateurs, appartient au contrat publié.
10
+ */
11
+ /** Bornes partagées serveur/agent/client. */
12
+ export const SYNC_REL_PATH_MAX = 1024;
13
+ export const SYNC_PATTERN_MAX = 512;
14
+ export const SYNC_STORAGE_PATH_MAX = 1024;
15
+ /** Taille max d'un chunk base64 sur le fil (≈ 1 Mo de binaire). */
16
+ export const SYNC_CHUNK_MAX = 1_400_000;
17
+ /** Nombre max d'entrées d'index par frame `sync.index`. */
18
+ export const SYNC_INDEX_BATCH_MAX = 500;
19
+ /**
20
+ * Séparateur des champs d'une ligne d'empreinte d'index (voir
21
+ * `src/cloudSync/fingerprint.ts` côté serveur et `sync::fingerprint` côté
22
+ * agent). U+0001 est sûr : les caractères de contrôle sont déjà refusés dans un
23
+ * chemin par `relPathProblem` / `rel_path_problem`, des deux côtés.
24
+ */
25
+ export const SYNC_FINGERPRINT_SEP = '\u0001';
26
+ /**
27
+ * Mode de scan demandé à l'agent.
28
+ * - `auto` → l'agent a le droit de répondre « rien n'a bougé » sans parcourir
29
+ * le disque, en se contentant d'annoncer l'empreinte de ce qu'il détient ;
30
+ * - `full` → parcours complet obligatoire (filet de sécurité horaire).
31
+ */
32
+ export const syncScanModeSchema = z.enum(['auto', 'full']);
33
+ export type SyncScanMode = z.infer<typeof syncScanModeSchema>;
34
+ /**
35
+ * Empreinte d'un index : `{nombre}.{octets}.{sha256hex}`.
36
+ *
37
+ * Le pli est un XOR des hachages par ligne, donc INDÉPENDANT DE L'ORDRE, et
38
+ * c'est délibéré : un tri obligerait Rust (ordre octet UTF-8) et TypeScript
39
+ * (ordre unité UTF-16) à s'accorder sur les caractères hors BMP, ce qu'ils ne
40
+ * font pas. Un seul emoji dans un nom de fichier aurait alors désactivé le
41
+ * chemin rapide pour toujours, sans que rien ne le signale.
42
+ */
43
+ export const syncIndexFingerprintSchema = z.string().regex(/^\d+\.\d+\.[0-9a-f]{64}$/);
44
+ /** SHA-256 hexadécimal (du clair d'un fichier, ou d'un chemin normalisé). */
45
+ export const sha256HexSchema = z.string().regex(/^[a-f0-9]{64}$/);
46
+ export const syncShareStatusSchema = z.enum(['active', 'paused']);
47
+ export type SyncShareStatus = z.infer<typeof syncShareStatusSchema>;
48
+ /**
49
+ * Type d'exclusion :
50
+ * - `path` → chemin relatif exact (fichier ou préfixe de dossier),
51
+ * - `name` → nom exact d'un composant de chemin (ex. `node_modules`),
52
+ * - `regex` → expression régulière sur le chemin relatif (moteur linéaire).
53
+ */
54
+ export const syncExclusionKindSchema = z.enum(['path', 'name', 'regex']);
55
+ export type SyncExclusionKind = z.infer<typeof syncExclusionKindSchema>;
56
+ export const syncSessionStateSchema = z.enum([
57
+ 'scanning',
58
+ 'planning',
59
+ 'transferring',
60
+ 'done',
61
+ 'error',
62
+ 'cancelled'
63
+ ]);
64
+ export type SyncSessionState = z.infer<typeof syncSessionStateSchema>;
65
+ /**
66
+ * Nature d'une entrée d'index. Seuls les dossiers VIDES sont indexés comme
67
+ * `dir` : un dossier peuplé est implicite (ses fichiers le recréent partout).
68
+ * Sans ça, un dossier vide créé sur une machine n'existerait sur aucune autre,
69
+ * et un dossier vidé resterait en coquille chez les pairs.
70
+ */
71
+ export const syncEntryKindSchema = z.enum(['file', 'dir']);
72
+ export type SyncEntryKind = z.infer<typeof syncEntryKindSchema>;
73
+ /** Volumétrie d'un partage (index + versions ; tailles logiques en clair). */
74
+ export const cloudSyncShareStatsSchema = z.object({
75
+ fileCount: z.number().int().nonnegative(),
76
+ liveBytes: z.number().int().nonnegative(),
77
+ versionCount: z.number().int().nonnegative(),
78
+ versionBytes: z.number().int().nonnegative()
79
+ });
80
+ export type CloudSyncShareStats = z.infer<typeof cloudSyncShareStatsSchema>;
81
+ /** Sens du transfert en cours (du point de vue de l'appareil). */
82
+ export const syncDirectionSchema = z.enum(['up', 'down', 'delete']);
83
+ export type SyncDirection = z.infer<typeof syncDirectionSchema>;
84
+ /** Progression d'une session, poussée aux clients web abonnés (throttlée). */
85
+ export const cloudSyncProgressSchema = z.object({
86
+ shareId: z.number().int().positive(),
87
+ deviceId: z.uuid(),
88
+ sessionId: z.string().max(64),
89
+ state: syncSessionStateSchema,
90
+ filesTotal: z.number().int().nonnegative(),
91
+ bytesTotal: z.number().int().nonnegative(),
92
+ filesDone: z.number().int().nonnegative(),
93
+ bytesDone: z.number().int().nonnegative(),
94
+ /** Fichier en cours de transfert, pour la ligne discrète de l'UI. */
95
+ currentPath: z.string().max(SYNC_REL_PATH_MAX).nullable(),
96
+ /**
97
+ * Avancement DANS le fichier en cours. Sans ça, un fichier de plusieurs Go
98
+ * laissait la barre parfaitement figée du début à la fin de son transfert :
99
+ * les octets n'étaient comptés qu'une fois le fichier terminé.
100
+ */
101
+ currentBytes: z.number().int().nonnegative(),
102
+ currentTotal: z.number().int().nonnegative(),
103
+ direction: syncDirectionSchema.nullable(),
104
+ error: z.string().max(500).nullable()
105
+ });
106
+ export type CloudSyncProgress = z.infer<typeof cloudSyncProgressSchema>;
107
+ /** État agrégé d'un partage — pilote le héros « Synchronisé » de l'UI. */
108
+ export const cloudSyncShareStateSchema = z.object({
109
+ shareId: z.number().int().positive(),
110
+ state: z.enum(['synced', 'syncing', 'paused', 'offline', 'error']),
111
+ /** Précision facultative (ex. nom de l'appareil hors ligne ou en erreur). */
112
+ detail: z.string().max(200).nullable(),
113
+ /** Volumétrie fraîche — l'en-tête de l'UI se met à jour sans re-fetch. */
114
+ stats: cloudSyncShareStatsSchema
115
+ });
116
+ export type CloudSyncShareState = z.infer<typeof cloudSyncShareStateSchema>;
@@ -0,0 +1,40 @@
1
+ import { z } from 'zod';
2
+
3
+ /** Current 2FA state for the authenticated account. */
4
+ export const twoFactorStatusSchema = z.object({
5
+ enabled: z.boolean(),
6
+ /** Remaining unused recovery codes (only meaningful when enabled). */
7
+ backupCodesRemaining: z.number().int().nonnegative()
8
+ });
9
+
10
+ export type TwoFactorStatus = z.infer<typeof twoFactorStatusSchema>;
11
+
12
+ /**
13
+ * One-time provisioning payload returned when starting 2FA setup. The secret
14
+ * and otpauth URL are shown once; the client renders the QR code locally.
15
+ */
16
+ export const twoFactorSetupSchema = z.object({
17
+ secret: z.string().min(1),
18
+ otpauthUrl: z.string().min(1),
19
+ backupCodes: z.array(z.string().min(1))
20
+ });
21
+
22
+ export type TwoFactorSetup = z.infer<typeof twoFactorSetupSchema>;
23
+
24
+ export interface TwoFactorRow {
25
+ user_id: number;
26
+ /** Encrypted TOTP secret (zero-knowledge: never stored in clear). */
27
+ secret_enc: string;
28
+ enabled: number;
29
+ created: number;
30
+ confirmed_at: number | null;
31
+ }
32
+
33
+ export interface BackupCodeRow {
34
+ id: number;
35
+ user_id: number;
36
+ /** SHA-256 hash of the recovery code; raw codes are shown once to the user. */
37
+ code_hash: string;
38
+ used_at: number | null;
39
+ created: number;
40
+ }
@@ -0,0 +1,216 @@
1
+ import { z } from 'zod';
2
+
3
+ /**
4
+ * Uptime monitoring: user-defined HTTP services the server pings on a schedule,
5
+ * with long-term history, incidents and notifications.
6
+ *
7
+ * Storage split (see `Docs/SECURITY_MODEL.md`): everything the scheduler needs
8
+ * to *plan* a check (cadence, timeout, enabled…) and everything a chart
9
+ * aggregates (status, latency, timestamps) lives in clear columns; what
10
+ * identifies the target — name, URL, expected keyword — and the error strings
11
+ * are encrypted with the **open** tier, since the checker runs in the
12
+ * background, with no session and no password.
13
+ */
14
+
15
+ export const UPTIME_NAME_MAX_LENGTH = 80;
16
+ export const UPTIME_URL_MAX_LENGTH = 2048;
17
+ export const UPTIME_KEYWORD_MAX_LENGTH = 200;
18
+ /** Bounds of a service's cadence, in seconds (30 s → 24 h). */
19
+ export const UPTIME_INTERVAL_MIN = 30;
20
+ export const UPTIME_INTERVAL_MAX = 86400;
21
+ /** Bounds of a single request's timeout, in seconds. */
22
+ export const UPTIME_TIMEOUT_MIN = 1;
23
+ export const UPTIME_TIMEOUT_MAX = 120;
24
+ /** Consecutive failures required before a service is declared down. */
25
+ export const UPTIME_THRESHOLD_MAX = 10;
26
+
27
+ /** HTTP verb used for the probe. `POST` sends no body — it only pokes the route. */
28
+ export const uptimeMethodSchema = z.enum(['GET', 'HEAD', 'POST']);
29
+ export type UptimeMethod = z.infer<typeof uptimeMethodSchema>;
30
+
31
+ /** `unknown` = never probed yet (just created, or paused before its first check). */
32
+ export const uptimeStatusSchema = z.enum(['up', 'down', 'unknown']);
33
+ export type UptimeStatus = z.infer<typeof uptimeStatusSchema>;
34
+
35
+ /**
36
+ * How long a service keeps its **raw** per-ping rows. The daily rollup is never
37
+ * pruned, so uptime ratios stay readable years back whatever this is set to.
38
+ * `null` = keep every ping forever (the default).
39
+ */
40
+ export const uptimeRetentionSchema = z.number().int().positive().max(3650).nullable();
41
+
42
+ /** Window a chart or a ratio is computed over. */
43
+ export const uptimeRangeSchema = z.enum(['24h', '7d', '30d', '90d', '1y', 'all']);
44
+ export type UptimeRange = z.infer<typeof uptimeRangeSchema>;
45
+
46
+ /**
47
+ * Bucket size the server picked for a history query: one point per ping (`raw`),
48
+ * per hour, or per day. Chosen from the range so a year-long chart never carries
49
+ * hundreds of thousands of points.
50
+ */
51
+ export const uptimeResolutionSchema = z.enum(['raw', 'hour', 'day']);
52
+ export type UptimeResolution = z.infer<typeof uptimeResolutionSchema>;
53
+
54
+ /** One monitored service: its configuration, its live state and its ratios. */
55
+ export const uptimeServiceSchema = z.object({
56
+ id: z.number().int().positive(),
57
+ name: z.string(),
58
+ url: z.string(),
59
+ method: uptimeMethodSchema,
60
+ /** Exact status code required, or `null` to accept any 2xx/3xx. */
61
+ expectedStatus: z.number().int().min(100).max(599).nullable(),
62
+ /** Substring the response body must contain, or `null` to skip the check. */
63
+ keyword: z.string().nullable(),
64
+ intervalSeconds: z.number().int().positive(),
65
+ timeoutSeconds: z.number().int().positive(),
66
+ failureThreshold: z.number().int().positive(),
67
+ retentionDays: uptimeRetentionSchema,
68
+ /** Paused services keep their history but are never probed. */
69
+ enabled: z.boolean(),
70
+ /** Rank in the list; only the user's drag & drop changes it. */
71
+ sortOrder: z.number().int().nonnegative(),
72
+ /**
73
+ * Ce service vient d'un **autre espace**, qui le projette ici.
74
+ *
75
+ * Il se lit et se modifie normalement — c'est tout l'objet de la projection
76
+ * — mais l'écran le signale : sans ça, le supprimer depuis l'espace où on le
77
+ * voit donnerait l'impression de retirer une ligne locale, alors qu'on
78
+ * toucherait la donnée d'ailleurs.
79
+ */
80
+ foreign: z.boolean(),
81
+
82
+ status: uptimeStatusSchema,
83
+ lastCheckedAt: z.number().int().nonnegative().nullable(),
84
+ lastResponseMs: z.number().int().nonnegative().nullable(),
85
+ lastHttpStatus: z.number().int().nullable(),
86
+ /** Why the last probe failed, or `null` when it succeeded. */
87
+ lastError: z.string().nullable(),
88
+ /** Start of the ongoing outage, or `null` while the service is healthy. */
89
+ downSince: z.number().int().nonnegative().nullable(),
90
+
91
+ /** Share of successful pings over the window (0 → 1), `null` without data. */
92
+ ratio24h: z.number().min(0).max(1).nullable(),
93
+ ratio7d: z.number().min(0).max(1).nullable(),
94
+ ratio30d: z.number().min(0).max(1).nullable(),
95
+ /** Mean response time over the last 24 h, in ms. */
96
+ avgMs24h: z.number().int().nonnegative().nullable(),
97
+
98
+ created: z.number().int().nonnegative()
99
+ });
100
+ export type UptimeService = z.infer<typeof uptimeServiceSchema>;
101
+
102
+ /** A single recorded probe — the "journal des pings". */
103
+ export const uptimeCheckSchema = z.object({
104
+ at: z.number().int().nonnegative(),
105
+ up: z.boolean(),
106
+ httpStatus: z.number().int().nullable(),
107
+ responseMs: z.number().int().nonnegative().nullable(),
108
+ error: z.string().nullable()
109
+ });
110
+ export type UptimeCheck = z.infer<typeof uptimeCheckSchema>;
111
+
112
+ /**
113
+ * Aggregates over a filtered slice of a service's raw pings — what the measures
114
+ * browser shows above its list, computed over the **whole** selection rather
115
+ * than the loaded page.
116
+ */
117
+ export const uptimeCheckStatsSchema = z.object({
118
+ count: z.number().int().nonnegative(),
119
+ failures: z.number().int().nonnegative(),
120
+ avgMs: z.number().int().nonnegative().nullable(),
121
+ minMs: z.number().int().nonnegative().nullable(),
122
+ maxMs: z.number().int().nonnegative().nullable(),
123
+ /** Bounds of the selection; `null` when it holds nothing. */
124
+ firstAt: z.number().int().nonnegative().nullable(),
125
+ lastAt: z.number().int().nonnegative().nullable()
126
+ });
127
+ export type UptimeCheckStats = z.infer<typeof uptimeCheckStatsSchema>;
128
+
129
+ /**
130
+ * One chart point. A `raw` point is a single ping (`checks === 1`); an `hour` or
131
+ * `day` point aggregates every ping of its bucket, which is what keeps a
132
+ * multi-year chart cheap.
133
+ */
134
+ export const uptimePointSchema = z.object({
135
+ /** Bucket start, epoch seconds. */
136
+ at: z.number().int().nonnegative(),
137
+ checks: z.number().int().positive(),
138
+ upChecks: z.number().int().nonnegative(),
139
+ avgMs: z.number().int().nonnegative().nullable(),
140
+ minMs: z.number().int().nonnegative().nullable(),
141
+ maxMs: z.number().int().nonnegative().nullable()
142
+ });
143
+ export type UptimePoint = z.infer<typeof uptimePointSchema>;
144
+
145
+ /**
146
+ * A continuous outage. Opened when a service crosses its failure threshold,
147
+ * closed on the first successful probe — so the list reads as a plain incident
148
+ * history, and it survives raw-history pruning.
149
+ */
150
+ export const uptimeIncidentSchema = z.object({
151
+ id: z.number().int().positive(),
152
+ startedAt: z.number().int().nonnegative(),
153
+ /** `null` while the outage is still ongoing. */
154
+ endedAt: z.number().int().nonnegative().nullable(),
155
+ httpStatus: z.number().int().nullable(),
156
+ error: z.string().nullable()
157
+ });
158
+ export type UptimeIncident = z.infer<typeof uptimeIncidentSchema>;
159
+
160
+ /** Database row shapes (server-only). Mirror the columns exactly. */
161
+ export interface UptimeServiceRow {
162
+ id: number;
163
+ user_id: number;
164
+ workspace_id: number;
165
+ /** Encrypted `{ name, url, keyword }` (open tier). */
166
+ content: string;
167
+ method: UptimeMethod;
168
+ expected_status: number | null;
169
+ interval_seconds: number;
170
+ timeout_seconds: number;
171
+ failure_threshold: number;
172
+ retention_days: number | null;
173
+ enabled: number;
174
+ sort_order: number;
175
+ status: UptimeStatus;
176
+ consecutive_failures: number;
177
+ last_checked_at: number | null;
178
+ last_response_ms: number | null;
179
+ last_http_status: number | null;
180
+ /** Encrypted error string (open tier), or null after a success. */
181
+ last_error: string | null;
182
+ created: number;
183
+ }
184
+
185
+ export interface UptimeCheckRow {
186
+ id: number;
187
+ service_id: number;
188
+ checked_at: number;
189
+ up: number;
190
+ http_status: number | null;
191
+ response_ms: number | null;
192
+ /** Encrypted error string (open tier). */
193
+ error: string | null;
194
+ }
195
+
196
+ export interface UptimeDayRow {
197
+ service_id: number;
198
+ day: number;
199
+ checks: number;
200
+ up_checks: number;
201
+ total_ms: number;
202
+ ms_samples: number;
203
+ min_ms: number | null;
204
+ max_ms: number | null;
205
+ }
206
+
207
+ export interface UptimeIncidentRow {
208
+ id: number;
209
+ service_id: number;
210
+ started_at: number;
211
+ ended_at: number | null;
212
+ http_status: number | null;
213
+ /** Encrypted error string (open tier). */
214
+ error: string | null;
215
+ notified: number;
216
+ }