@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,355 @@
1
+ import { z } from 'zod';
2
+
3
+ /**
4
+ * Les sauvegardes de l'espace: **où** ça atterrit, **quoi** part, et **ce qui
5
+ * s'est passé** au dernier passage.
6
+ *
7
+ * Trois entités, et la séparation est la raison d'être du module:
8
+ *
9
+ * - une **destination** est un endroit qui accepte des octets (un dossier du
10
+ * serveur, un dossier d'une machine enrôlée, un bucket S3). Elle ne sait rien
11
+ * de ce qu'on y range;
12
+ * - un **travail** dit quoi sauvegarder, vers quelle destination, à quelle
13
+ * cadence, et combien de copies garder;
14
+ * - une **exécution** est ce qu'un travail a produit une fois: une archive, sa
15
+ * taille, son empreinte, et son sort.
16
+ *
17
+ * Croiser les deux premiers est tout l'intérêt: la même base part vers le
18
+ * Raspberry **et** vers un S3 distant en déclarant deux travaux, sans rien
19
+ * dupliquer de la configuration d'accès.
20
+ *
21
+ * ## Toujours à l'étage ouvert
22
+ *
23
+ * Comme Uptime, Déploiement et Bases de données: l'ordonnanceur tourne sans
24
+ * session ni mot de passe, donc tout ce qu'il doit lire — y compris la clé
25
+ * secrète S3 — vit sous la clé de l'espace à l'étage ouvert. Un travail qui ne
26
+ * pourrait s'exécuter qu'avec un humain devant l'écran ne serait pas une
27
+ * sauvegarde.
28
+ *
29
+ * ## Ce qui est chiffré dans l'archive, et avec quelle clé
30
+ *
31
+ * Le contenu d'une archive est scellé avec le **même format que les blobs
32
+ * CloudSync** (`DEVB` v2, AES-256-GCM par blocs de 1 Mio), sous une clé
33
+ * **dérivée de la clé serveur** (`CRYPT_KEY_A`/`CRYPT_KEY_B`) et non stockée
34
+ * nulle part. C'est la seule construction qui évite le serpent qui se mord la
35
+ * queue: une clé rangée en base serait à l'intérieur de la sauvegarde de cette
36
+ * base, donc illisible précisément le jour où on en a besoin.
37
+ *
38
+ * ⚠️ Corollaire à ne jamais perdre de vue: **`CRYPT_KEY_A` et `CRYPT_KEY_B` sont
39
+ * la sauvegarde**. Une archive chiffrée sans elles n'est qu'un fichier de bruit.
40
+ */
41
+
42
+ export const BACKUP_DESTINATION_NAME_MAX = 120;
43
+ export const BACKUP_JOB_NAME_MAX = 120;
44
+ export const BACKUP_PATH_MAX = 512;
45
+ export const BACKUP_ENDPOINT_MAX = 255;
46
+ export const BACKUP_BUCKET_MAX = 128;
47
+ export const BACKUP_ACCESS_KEY_MAX = 255;
48
+ export const BACKUP_SECRET_MAX = 512;
49
+
50
+ /**
51
+ * Où une archive atterrit.
52
+ *
53
+ * - `local` — un dossier du serveur DevEye, sous `BACKUP_STORAGE_DIR`. Le plus
54
+ * simple, et le moins protecteur: la copie meurt avec la machine qu'elle
55
+ * sauvegarde. Utile comme premier palier, jamais comme seul palier.
56
+ * - `device` — un dossier d'une machine enrôlée, écrit **par son agent**. C'est
57
+ * ce qui fait d'un Raspberry Pi une cible de sauvegarde sans rien y installer
58
+ * d'autre que l'agent qui y tourne déjà.
59
+ * - `s3` — un service compatible S3: Garage, MinIO, Scaleway, Backblaze,
60
+ * AWS. Le seul des trois qui sorte les octets du réseau local.
61
+ */
62
+ export const backupDestinationKindSchema = z.enum(['local', 'device', 's3']);
63
+ export type BackupDestinationKind = z.infer<typeof backupDestinationKindSchema>;
64
+
65
+ /** Ce que le dernier contrôle d'accessibilité a dit d'une destination. */
66
+ export const backupDestinationStatusSchema = z.enum(['unknown', 'ok', 'error']);
67
+ export type BackupDestinationStatus = z.infer<typeof backupDestinationStatusSchema>;
68
+
69
+ /**
70
+ * Ce qu'un travail sauvegarde.
71
+ *
72
+ * - `database` — une base supervisée de la feature Bases de données, par un
73
+ * vidage logique (`mysqldump` / `pg_dump`) à travers le même accès que la
74
+ * supervision, tunnel SSH compris.
75
+ * - `deveye` — la base MySQL de DevEye elle-même. **Couvre le Monitoring,
76
+ * l'index CloudSync, les notes, les mots de passe, tout**: ces données vivent
77
+ * en base, donc les sauvegarder séparément reviendrait à les copier deux
78
+ * fois. C'est la sauvegarde à avoir si on n'en a qu'une.
79
+ * - `cloudsync` — les **blobs** d'un partage CloudSync, qui sont la seule
80
+ * partie de DevEye à ne pas vivre en base. Rendus en clair dans une archive
81
+ * `tar`, arborescence d'origine reconstituée depuis l'index: une archive doit
82
+ * se restaurer sans DevEye, sinon ce n'est pas une sauvegarde.
83
+ */
84
+ export const backupSourceKindSchema = z.enum(['database', 'deveye', 'cloudsync']);
85
+ export type BackupSourceKind = z.infer<typeof backupSourceKindSchema>;
86
+
87
+ /**
88
+ * Quand un travail part.
89
+ *
90
+ * Volontairement **pas** un cron: cinq champs lisibles couvrent tout ce qu'on
91
+ * demande à une sauvegarde, et une expression cron mal écrite est un travail qui
92
+ * ne part jamais sans que rien ne le dise.
93
+ */
94
+ export const backupScheduleKindSchema = z.enum(['manual', 'hourly', 'daily', 'weekly', 'monthly']);
95
+ export type BackupScheduleKind = z.infer<typeof backupScheduleKindSchema>;
96
+
97
+ /** Le sort d'une exécution. */
98
+ export const backupRunStatusSchema = z.enum(['running', 'success', 'failed']);
99
+ export type BackupRunStatus = z.infer<typeof backupRunStatusSchema>;
100
+
101
+ /** Une destination, telle que l'écran la montre. Le secret n'en sort jamais. */
102
+ export const backupDestinationSchema = z.object({
103
+ id: z.number().int().positive(),
104
+ kind: backupDestinationKindSchema,
105
+ name: z.string().max(BACKUP_DESTINATION_NAME_MAX),
106
+ /** `device` uniquement: la machine qui héberge le dossier. */
107
+ deviceId: z.uuid().nullable(),
108
+ /** Nom de l'appareil, joint pour l'affichage; `null` s'il a été supprimé. */
109
+ deviceName: z.string().nullable(),
110
+ /**
111
+ * `local`/`device`: le dossier qui reçoit les archives.
112
+ * `s3`: le préfixe dans le bucket (`''` = la racine).
113
+ */
114
+ path: z.string().max(BACKUP_PATH_MAX),
115
+ /** `s3`: l'URL du service (`https://s3.exemple.fr`). */
116
+ endpoint: z.string().max(BACKUP_ENDPOINT_MAX).nullable(),
117
+ region: z.string().max(64).nullable(),
118
+ bucket: z.string().max(BACKUP_BUCKET_MAX).nullable(),
119
+ accessKeyId: z.string().max(BACKUP_ACCESS_KEY_MAX).nullable(),
120
+ /** Le secret existe-t-il ? Sa valeur n'est jamais rendue. */
121
+ hasSecret: z.boolean(),
122
+ /**
123
+ * Adressage par chemin (`https://hôte/bucket/clé`) plutôt que par
124
+ * sous-domaine. Vrai pour Garage et MinIO, faux pour AWS — et c'est la
125
+ * première chose qui casse quand on l'oublie.
126
+ */
127
+ pathStyle: z.boolean(),
128
+ // Le chiffrement des archives n'est plus un attribut de la destination :
129
+ // il se règle par TRAVAIL (`backupJobSchema.encryption`, migration 094).
130
+ // Une destination dit où écrire, le travail dit sous quelle forme.
131
+ status: backupDestinationStatusSchema,
132
+ /** Message du dernier contrôle raté; `null` quand tout va bien. */
133
+ lastError: z.string().nullable(),
134
+ checkedAt: z.number().int().nullable(),
135
+ /** Combien de travaux l'utilisent — ce qu'une suppression va couper. */
136
+ jobCount: z.number().int().nonnegative(),
137
+ created: z.number().int()
138
+ });
139
+ export type BackupDestination = z.infer<typeof backupDestinationSchema>;
140
+
141
+ /**
142
+ * Sous quelle forme les archives d'un travail sont écrites.
143
+ *
144
+ * - `none` : en clair. Lisible par qui tient la destination ; à réserver aux
145
+ * destinations déjà sous la même garde que le serveur.
146
+ * - `server` : scellées (AES-256-GCM) sous une clé dérivée de
147
+ * CRYPT_KEY_A / CRYPT_KEY_B — jamais stockée, donc jamais dans l'archive
148
+ * qu'elle protège, et récupérable par `scripts/restore-backup.mjs` avec ces
149
+ * deux seules variables.
150
+ *
151
+ * Il n'y a **pas** de mode « mot de passe » et ce n'est pas un oubli :
152
+ * l'ordonnanceur tourne la nuit sans session, or la clé dérivée du mot de
153
+ * passe ne vit que dans une session déverrouillée, en mémoire, à fenêtre
154
+ * glissante (voir Docs/SECURITY_MODEL.md). Un tel mode ne pourrait ni tourner
155
+ * planifié, ni survivre à un dump de plusieurs heures.
156
+ */
157
+ export const backupEncryptionSchema = z.enum(['none', 'server']);
158
+ export type BackupEncryption = z.infer<typeof backupEncryptionSchema>;
159
+
160
+ /** Un travail: quoi, où, quand, et combien de copies on garde. */
161
+ export const backupJobSchema = z.object({
162
+ id: z.number().int().positive(),
163
+ name: z.string().max(BACKUP_JOB_NAME_MAX),
164
+ enabled: z.boolean(),
165
+ /** La forme des archives à venir ; chaque exécution fige la sienne. */
166
+ encryption: backupEncryptionSchema,
167
+ destinationId: z.number().int().positive(),
168
+ /** Recopié pour que la liste n'ait pas à recouper deux jeux de données. */
169
+ destinationName: z.string(),
170
+ destinationKind: backupDestinationKindSchema,
171
+ source: backupSourceKindSchema,
172
+ /** `database` → l'id de la connexion; `cloudsync` → l'id du partage; sinon `null`. */
173
+ sourceId: z.number().int().positive().nullable(),
174
+ /** Intitulé de la source, joint pour l'affichage; `null` si elle a disparu. */
175
+ sourceName: z.string().nullable(),
176
+ schedule: backupScheduleKindSchema,
177
+ /** Heure locale du serveur (0-23), pour tout sauf `hourly` et `manual`. */
178
+ scheduleHour: z.number().int().min(0).max(23),
179
+ /** Jour de la semaine, 0 = dimanche. `weekly` uniquement. */
180
+ scheduleWeekday: z.number().int().min(0).max(6),
181
+ /** Quantième, borné à 28 pour exister tous les mois. `monthly` uniquement. */
182
+ scheduleDay: z.number().int().min(1).max(28),
183
+ /** Combien d'archives réussies on garde. Au-delà, la plus ancienne part. */
184
+ keepLast: z.number().int().min(1).max(365),
185
+ nextRunAt: z.number().int().nullable(),
186
+ lastRunAt: z.number().int().nullable(),
187
+ lastStatus: backupRunStatusSchema.nullable(),
188
+ lastError: z.string().nullable(),
189
+ /** Somme des tailles des archives encore présentes. */
190
+ totalBytes: z.number().int().nonnegative(),
191
+ runCount: z.number().int().nonnegative(),
192
+ /**
193
+ * Cet élément vient d'un **autre espace**, qui le projette ici.
194
+ *
195
+ * L'écran le signale d'une pastille : sans elle, rien ne distingue une
196
+ * ligne locale d'une fenêtre sur l'espace voisin — et les gestes réservés
197
+ * au domicile (supprimer, re-partager) sembleraient cassés au lieu de
198
+ * s'expliquer.
199
+ */
200
+ foreign: z.boolean(),
201
+ created: z.number().int()
202
+ });
203
+ export type BackupJob = z.infer<typeof backupJobSchema>;
204
+
205
+ /** Une exécution, et ce qu'elle a produit. */
206
+ export const backupRunSchema = z.object({
207
+ id: z.number().int().positive(),
208
+ jobId: z.number().int().positive(),
209
+ status: backupRunStatusSchema,
210
+ startedAt: z.number().int(),
211
+ finishedAt: z.number().int().nullable(),
212
+ /** Taille de l'archive écrite, en octets. `0` tant qu'elle n'est pas finie. */
213
+ sizeBytes: z.number().int().nonnegative(),
214
+ /** SHA-256 du **clair**, pour vérifier une restauration. `null` si échec. */
215
+ checksum: z.string().nullable(),
216
+ /** Chemin ou clé de l'objet écrit, tel qu'on le retrouve sur la destination. */
217
+ artifact: z.string().nullable(),
218
+ /** L'archive a-t-elle été scellée ? Recopié de la destination au moment du run. */
219
+ encrypted: z.boolean(),
220
+ /** `null` = déclenchée par l'ordonnanceur. */
221
+ triggeredByUserId: z.number().int().positive().nullable(),
222
+ error: z.string().nullable(),
223
+ /** L'archive existe-t-elle encore, ou la rétention l'a-t-elle effacée ? */
224
+ pruned: z.boolean()
225
+ });
226
+ export type BackupRun = z.infer<typeof backupRunSchema>;
227
+
228
+ /**
229
+ * Une source proposée au choix, quand on crée un travail.
230
+ *
231
+ * Le client ne peut pas la construire seul: les bases vivent dans la feature
232
+ * Bases de données, les partages dans CloudSync, et `deveye` n'existe nulle part
233
+ * ailleurs qu'ici. Une commande dédiée évite trois appels croisés et trois
234
+ * droits à vérifier côté écran.
235
+ */
236
+ export const backupSourceCandidateSchema = z.object({
237
+ kind: backupSourceKindSchema,
238
+ /** `null` pour `deveye`, qui est unique par nature. */
239
+ id: z.number().int().positive().nullable(),
240
+ name: z.string(),
241
+ /** Précision affichée en second ligne (moteur, hôte, chemin du partage). */
242
+ detail: z.string().nullable(),
243
+ /** Faux avec une raison quand la source existe mais n'est pas sauvegardable. */
244
+ available: z.boolean(),
245
+ reason: z.string().nullable()
246
+ });
247
+ export type BackupSourceCandidate = z.infer<typeof backupSourceCandidateSchema>;
248
+
249
+ /** Ce que rend un contrôle de destination. */
250
+ export const backupDestinationProbeSchema = z.object({
251
+ ok: z.boolean(),
252
+ error: z.string().nullable(),
253
+ /** Espace occupé sous le préfixe/dossier, quand la destination sait le dire. */
254
+ usedBytes: z.number().int().nonnegative().nullable(),
255
+ /** Espace libre, quand la destination sait le dire (`local` et `device`). */
256
+ freeBytes: z.number().int().nonnegative().nullable()
257
+ });
258
+ export type BackupDestinationProbe = z.infer<typeof backupDestinationProbeSchema>;
259
+
260
+ // ------------------------------------------------------------- lignes SQL
261
+
262
+ /** Ligne SQL (serveur uniquement). */
263
+ export interface BackupDestinationRow {
264
+ id: number;
265
+ workspace_id: number;
266
+ /** 'local' | 'device' | 's3'. */
267
+ kind: string;
268
+ device_id: string | null;
269
+ /** Adressage par chemin pour S3. */
270
+ path_style: number;
271
+ /** 'unknown' | 'ok' | 'error'. */
272
+ status: string;
273
+ checked_at: number | null;
274
+ /**
275
+ * { name, path, endpoint, region, bucket, accessKeyId, lastError } chiffré,
276
+ * étage ouvert.
277
+ *
278
+ * Le nom d'un bucket et l'adresse d'un service disent où sont les
279
+ * sauvegardes de quelqu'un: ce n'est pas une métadonnée de tri, ça n'a rien
280
+ * à faire en clair. Ne restent dehors que `kind`, `device_id` et les
281
+ * drapeaux — le strict nécessaire pour que l'ordonnanceur choisisse un
282
+ * chemin de code sans déchiffrer.
283
+ */
284
+ content: string;
285
+ /** Clé secrète S3, chiffrée à l'étage ouvert. Vide pour `local`/`device`. */
286
+ secret_enc: string;
287
+ created: number;
288
+ }
289
+
290
+ /** La même, augmentée de ce qu'une liste montre sans ouvrir la fiche. */
291
+ export interface BackupDestinationWithUsageRow extends BackupDestinationRow {
292
+ job_count: number;
293
+ /** `devices.name`, joint pour l'affichage. En clair en base. */
294
+ device_name: string | null;
295
+ }
296
+
297
+ /** Ligne SQL (serveur uniquement). */
298
+ export interface BackupJobRow {
299
+ id: number;
300
+ workspace_id: number;
301
+ destination_id: number;
302
+ /** 'database' | 'deveye' | 'cloudsync'. */
303
+ source_kind: string;
304
+ source_id: number | null;
305
+ enabled: number;
306
+ /** 'manual' | 'hourly' | 'daily' | 'weekly' | 'monthly'. */
307
+ schedule_kind: string;
308
+ schedule_hour: number;
309
+ schedule_weekday: number;
310
+ schedule_day: number;
311
+ keep_last: number;
312
+ /** 'none' | 'server' : la forme des archives à venir. En clair, comme les
313
+ * colonnes d'ordonnancement : l'exécuteur choisit un chemin de code sans
314
+ * déchiffrer. */
315
+ encryption: string;
316
+ /**
317
+ * Quand l'ordonnanceur doit repasser. `NULL` = jamais (travail manuel ou
318
+ * désactivé), ce qui le sort de l'index des travaux dus **sans** condition
319
+ * supplémentaire dans la requête chaude.
320
+ */
321
+ next_run_at: number | null;
322
+ /** { name } chiffré, étage ouvert. */
323
+ content: string;
324
+ created: number;
325
+ }
326
+
327
+ /** La même, augmentée du résumé que la liste affiche. */
328
+ export interface BackupJobWithStateRow extends BackupJobRow {
329
+ destination_kind: string;
330
+ destination_content: string;
331
+ last_run_at: number | null;
332
+ last_status: string | null;
333
+ /** Le `content` chiffré de la dernière exécution ({ artifact, error }). */
334
+ last_run_content: string | null;
335
+ total_bytes: number | string | null;
336
+ run_count: number;
337
+ }
338
+
339
+ /** Ligne SQL (serveur uniquement). */
340
+ export interface BackupRunRow {
341
+ id: number;
342
+ job_id: number;
343
+ workspace_id: number;
344
+ /** 'running' | 'success' | 'failed'. */
345
+ status: string;
346
+ started_at: number;
347
+ finished_at: number | null;
348
+ size_bytes: number | string;
349
+ checksum: string | null;
350
+ encrypted: number;
351
+ triggered_by_user_id: number | null;
352
+ pruned: number;
353
+ /** { artifact, error } chiffré, étage ouvert. */
354
+ content: string;
355
+ }
@@ -0,0 +1,55 @@
1
+ import { z } from 'zod';
2
+
3
+ /**
4
+ * Les jetons d'accès d'un espace, tous fournisseurs confondus.
5
+ *
6
+ * **Ils appartiennent à l'espace**, pas à ce qui s'en sert : un jeton GitHub
7
+ * ouvre en général plusieurs dépôts, une clé Dokploy plusieurs applications, et
8
+ * les ressaisir par objet serait à la fois pénible et plus risqué.
9
+ *
10
+ * Une seule table, deux propriétaires. La forme d'un jeton ne dépend pas du
11
+ * fournisseur (une étiquette, une adresse d'instance quand le service est
12
+ * auto-hébergé, un secret), mais **le droit d'y toucher, si** : les jetons
13
+ * GitHub relèvent de la feature Git, les clés Dokploy de la feature
14
+ * Déploiement. Chacune n'expose que les siens, et c'est ce qui a fait sortir la
15
+ * clé Dokploy de l'écran des dépôts, où elle n'avait jamais eu de raison d'être
16
+ * — elle y avait atterri faute d'un module de déploiement pour l'accueillir.
17
+ *
18
+ * Le secret ne sort **jamais** : seule sa présence est annoncée (`hasSecret`).
19
+ * Même parti pris que la clé d'API météo — un secret qu'on ne renvoie pas est un
20
+ * secret qui ne peut fuiter ni par une capture d'écran ni par un journal.
21
+ *
22
+ * Toujours chiffré à l'**étage ouvert** : les deux services de fond qui s'en
23
+ * servent tournent sans session.
24
+ */
25
+
26
+ export const CREDENTIAL_LABEL_MAX_LENGTH = 64;
27
+ export const CREDENTIAL_SECRET_MAX_LENGTH = 512;
28
+
29
+ /** Les services extérieurs que DevEye sait joindre au nom de l'espace. */
30
+ export const credentialProviderSchema = z.enum(['github', 'dokploy']);
31
+ export type CredentialProvider = z.infer<typeof credentialProviderSchema>;
32
+
33
+ export const credentialSchema = z.object({
34
+ id: z.number().int().positive(),
35
+ provider: credentialProviderSchema,
36
+ label: z.string().max(CREDENTIAL_LABEL_MAX_LENGTH),
37
+ /** Racine de l'instance auto-hébergée ; `null` pour une API publique. */
38
+ baseUrl: z.string().nullable(),
39
+ hasSecret: z.boolean(),
40
+ created: z.number().int(),
41
+ /** Combien d'objets s'en servent — dépôts pour GitHub, cibles pour Dokploy. */
42
+ useCount: z.number().int().nonnegative()
43
+ });
44
+ export type Credential = z.infer<typeof credentialSchema>;
45
+
46
+ /** Ligne SQL (serveur uniquement). */
47
+ export interface CredentialRow {
48
+ id: number;
49
+ workspace_id: number;
50
+ provider: string;
51
+ label: string;
52
+ base_url: string | null;
53
+ secret_enc: string;
54
+ created: number;
55
+ }