@odoro-cli/fleet-protocol 0.1.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/dist/index.d.ts +424 -0
- package/dist/index.js +299 -0
- package/package.json +43 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Ce qu'un agent repond quand il n'a pas fait ce qu'on lui demandait.
|
|
5
|
+
*
|
|
6
|
+
* ## Le champ qui compte
|
|
7
|
+
*
|
|
8
|
+
* `resourceMayExist` n'est pas un detail de journalisation : c'est lui qui
|
|
9
|
+
* decide de la suite. Une temporisation ne dit pas que rien n'a ete cree, elle
|
|
10
|
+
* dit que **nous ne savons pas**. Traiter ce cas comme un echec net conduit a
|
|
11
|
+
* recreer une ressource qui tourne deja ; le traiter comme un succes conduit a
|
|
12
|
+
* abandonner une ressource que plus rien ne designe.
|
|
13
|
+
*
|
|
14
|
+
* L'agent est le seul a pouvoir renseigner ce champ honnetement : lui seul sait
|
|
15
|
+
* ou il en etait quand il a echoue.
|
|
16
|
+
*
|
|
17
|
+
* ## Pourquoi le protocole est plus riche que le port
|
|
18
|
+
*
|
|
19
|
+
* Le port du plan de controle connait sept sortes d'echecs, et c'est
|
|
20
|
+
* volontairement peu : y ajouter des cas propres a un fournisseur ferait fuiter
|
|
21
|
+
* l'amont dans la logique metier.
|
|
22
|
+
*
|
|
23
|
+
* Mais l'agent, lui, a des refus qui portent une information qu'on perdrait a
|
|
24
|
+
* la reduire. Une suppression refusee parce que des branches en dependent n'est
|
|
25
|
+
* pas un conflit anonyme : c'est un conflit **avec une liste de noms**, et
|
|
26
|
+
* cette liste est ce qui permet a un humain de decider.
|
|
27
|
+
*
|
|
28
|
+
* Le protocole transporte donc le detail, et l'adaptateur le replie dans le
|
|
29
|
+
* message de l'erreur du port. Rien n'est perdu, et le port ne grossit pas.
|
|
30
|
+
*
|
|
31
|
+
* @module
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Les sortes d'echecs, alignees sur le port du plan de controle.
|
|
36
|
+
*
|
|
37
|
+
* Aucune n'est ajoutee ici : un agent qui inventerait une categorie forcerait
|
|
38
|
+
* l'adaptateur a la traduire en `malformed`, ce qui est pire que de choisir la
|
|
39
|
+
* bonne des sept.
|
|
40
|
+
*/
|
|
41
|
+
declare const failureKind: z.ZodEnum<{
|
|
42
|
+
timeout: "timeout";
|
|
43
|
+
quota: "quota";
|
|
44
|
+
conflict: "conflict";
|
|
45
|
+
unauthorized: "unauthorized";
|
|
46
|
+
"not-found": "not-found";
|
|
47
|
+
malformed: "malformed";
|
|
48
|
+
unavailable: "unavailable";
|
|
49
|
+
}>;
|
|
50
|
+
/** Une sorte d'echec. */
|
|
51
|
+
type FailureKind = z.infer<typeof failureKind>;
|
|
52
|
+
/**
|
|
53
|
+
* Ce qui accompagne un refus, quand le refus a une raison nommable.
|
|
54
|
+
*
|
|
55
|
+
* Chaque variante existe parce qu'un message libre aurait force le lecteur a
|
|
56
|
+
* l'analyser pour agir. Ici, l'adaptateur decide sur la forme.
|
|
57
|
+
*/
|
|
58
|
+
declare const failureDetail: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
59
|
+
reason: z.ZodLiteral<"branches-dependantes">;
|
|
60
|
+
branches: z.ZodArray<z.ZodString>;
|
|
61
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
62
|
+
reason: z.ZodLiteral<"base-endormie">;
|
|
63
|
+
ref: z.ZodString;
|
|
64
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
65
|
+
reason: z.ZodLiteral<"place-insuffisante">;
|
|
66
|
+
requiredBytes: z.ZodNumber;
|
|
67
|
+
availableBytes: z.ZodNumber;
|
|
68
|
+
}, z.core.$strip>], "reason">;
|
|
69
|
+
/** Ce qui accompagne un refus. */
|
|
70
|
+
type FailureDetail = z.infer<typeof failureDetail>;
|
|
71
|
+
/**
|
|
72
|
+
* Le corps d'une reponse en echec.
|
|
73
|
+
*
|
|
74
|
+
* Il ne contient jamais de chaine de connexion ni de mot de passe : une erreur
|
|
75
|
+
* est ce qui finit le plus surement dans un journal, un rapport d'incident ou
|
|
76
|
+
* un ticket.
|
|
77
|
+
*/
|
|
78
|
+
declare const failureBody: z.ZodObject<{
|
|
79
|
+
kind: z.ZodEnum<{
|
|
80
|
+
timeout: "timeout";
|
|
81
|
+
quota: "quota";
|
|
82
|
+
conflict: "conflict";
|
|
83
|
+
unauthorized: "unauthorized";
|
|
84
|
+
"not-found": "not-found";
|
|
85
|
+
malformed: "malformed";
|
|
86
|
+
unavailable: "unavailable";
|
|
87
|
+
}>;
|
|
88
|
+
message: z.ZodString;
|
|
89
|
+
resourceMayExist: z.ZodBoolean;
|
|
90
|
+
detail: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
91
|
+
reason: z.ZodLiteral<"branches-dependantes">;
|
|
92
|
+
branches: z.ZodArray<z.ZodString>;
|
|
93
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
94
|
+
reason: z.ZodLiteral<"base-endormie">;
|
|
95
|
+
ref: z.ZodString;
|
|
96
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
97
|
+
reason: z.ZodLiteral<"place-insuffisante">;
|
|
98
|
+
requiredBytes: z.ZodNumber;
|
|
99
|
+
availableBytes: z.ZodNumber;
|
|
100
|
+
}, z.core.$strip>], "reason">>;
|
|
101
|
+
requestId: z.ZodString;
|
|
102
|
+
}, z.core.$strip>;
|
|
103
|
+
/** Le corps d'une reponse en echec. */
|
|
104
|
+
type FailureBody = z.infer<typeof failureBody>;
|
|
105
|
+
/**
|
|
106
|
+
* Le code HTTP qui accompagne chaque sorte.
|
|
107
|
+
*
|
|
108
|
+
* ## Pourquoi cette table plutot qu'un code par cas
|
|
109
|
+
*
|
|
110
|
+
* L'appelant decide sur `kind`, pas sur le code : c'est le corps qui porte le
|
|
111
|
+
* sens. Le code existe pour les intermediaires — relais, journaux, sondes — qui
|
|
112
|
+
* ne lisent pas le corps et pour qui la difference entre 4xx et 5xx decide de
|
|
113
|
+
* reessayer ou non.
|
|
114
|
+
*
|
|
115
|
+
* `timeout` vaut 504 et non 408 : 408 dit que le **client** a ete trop lent, ce
|
|
116
|
+
* qui designerait le mauvais coupable dans tous les journaux qui le liront.
|
|
117
|
+
*/
|
|
118
|
+
declare const STATUS_BY_KIND: Readonly<Record<FailureKind, number>>;
|
|
119
|
+
/**
|
|
120
|
+
* Une sorte d'echec merite-t-elle d'etre reessayee telle quelle ?
|
|
121
|
+
*
|
|
122
|
+
* Exposee ici plutot que decidee chez l'appelant : deux appelants qui en
|
|
123
|
+
* jugeraient differemment produiraient deux comportements pour une meme panne,
|
|
124
|
+
* et le second serait decouvert un jour d'incident.
|
|
125
|
+
*
|
|
126
|
+
* `conflict` est absent a dessein. Reessayer un conflit produit le meme
|
|
127
|
+
* conflit — sauf quand il vient d'un doublon en vol, et ce cas se resout par la
|
|
128
|
+
* cle d'idempotence, pas par un reessai.
|
|
129
|
+
*/
|
|
130
|
+
declare function isRetryable(kind: FailureKind): boolean;
|
|
131
|
+
/**
|
|
132
|
+
* Rend le refus lisible par un humain, detail compris.
|
|
133
|
+
*
|
|
134
|
+
* Un refus dont la raison reste dans un champ structure est un refus que
|
|
135
|
+
* personne ne lit : c'est le message qui arrive dans le ticket.
|
|
136
|
+
*/
|
|
137
|
+
declare function describeFailure(body: FailureBody): string;
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Les neuf operations d'un agent de flotte.
|
|
141
|
+
*
|
|
142
|
+
* ## Elles ne sont pas inventees ici
|
|
143
|
+
*
|
|
144
|
+
* Elles repondent une pour une au port du plan de controle. C'est une
|
|
145
|
+
* contrainte volontaire : une operation de plus ici serait une operation que le
|
|
146
|
+
* plan de controle ne sait pas appeler, donc du code mort qui aurait l'air
|
|
147
|
+
* vivant.
|
|
148
|
+
*
|
|
149
|
+
* ## La region est une chaine libre, et c'est delibere
|
|
150
|
+
*
|
|
151
|
+
* Le plan de controle a son propre vocabulaire de regions, qui decrit une offre
|
|
152
|
+
* commerciale. Un agent, lui, connait la machine sur laquelle il tourne. Les
|
|
153
|
+
* deux ne coincident pas forcement, et les faire coincider par contrat
|
|
154
|
+
* obligerait a modifier la flotte chaque fois que l'offre change.
|
|
155
|
+
*
|
|
156
|
+
* L'agent rend donc la region telle qu'il la connait, et **l'adaptateur la
|
|
157
|
+
* traduit**. Une region qu'il ne sait pas traduire est un `malformed` — un
|
|
158
|
+
* echec nomme, decouvert au premier appel, et non une valeur fausse qui se
|
|
159
|
+
* propage.
|
|
160
|
+
*
|
|
161
|
+
* @module
|
|
162
|
+
*/
|
|
163
|
+
|
|
164
|
+
/** Une reference de ressource, telle que l'agent la fabrique. */
|
|
165
|
+
declare const resourceRef: z.ZodString;
|
|
166
|
+
/**
|
|
167
|
+
* Le nom demande, qui encode l'identifiant du plan de controle.
|
|
168
|
+
*
|
|
169
|
+
* L'agent ne le decode pas et n'a pas a le comprendre : il le stocke et le
|
|
170
|
+
* rend. C'est la reconciliation, en haut, qui en tire l'identifiant — et elle
|
|
171
|
+
* seule connait la regle.
|
|
172
|
+
*/
|
|
173
|
+
declare const resourceName: z.ZodString;
|
|
174
|
+
/**
|
|
175
|
+
* Une cle d'idempotence, fabriquee par l'appelant avant tout appel.
|
|
176
|
+
*
|
|
177
|
+
* Elle est obligatoire sur ce qui cree. Sans elle, un reessai apres
|
|
178
|
+
* temporisation produit une seconde ressource, et c'est exactement le cas que
|
|
179
|
+
* `resourceMayExist` existe pour signaler.
|
|
180
|
+
*/
|
|
181
|
+
declare const idempotencyKey: z.ZodString;
|
|
182
|
+
/** Ce qu'une creation rend. */
|
|
183
|
+
declare const provisioned: z.ZodObject<{
|
|
184
|
+
ref: z.ZodString;
|
|
185
|
+
connectionString: z.ZodString;
|
|
186
|
+
region: z.ZodString;
|
|
187
|
+
}, z.core.$strip>;
|
|
188
|
+
/** Ce qu'une creation rend. */
|
|
189
|
+
type Provisioned = z.infer<typeof provisioned>;
|
|
190
|
+
/** Ce que l'agent sait d'une ressource qu'il heberge. */
|
|
191
|
+
declare const hostedResource: z.ZodObject<{
|
|
192
|
+
ref: z.ZodString;
|
|
193
|
+
name: z.ZodString;
|
|
194
|
+
available: z.ZodBoolean;
|
|
195
|
+
region: z.ZodString;
|
|
196
|
+
sleeping: z.ZodBoolean;
|
|
197
|
+
parentRef: z.ZodOptional<z.ZodString>;
|
|
198
|
+
}, z.core.$strip>;
|
|
199
|
+
/** Ce que l'agent sait d'une ressource qu'il heberge. */
|
|
200
|
+
type HostedResource = z.infer<typeof hostedResource>;
|
|
201
|
+
/**
|
|
202
|
+
* La consommation, relevee et jamais estimee.
|
|
203
|
+
*
|
|
204
|
+
* Le plan de controle refuse de provisionner quand la derniere mesure est
|
|
205
|
+
* perimee. Cette regle ne protege de rien si la mesure est un calcul : elle
|
|
206
|
+
* rendrait un chiffre frais et faux, ce qui est pire qu'un chiffre absent.
|
|
207
|
+
*/
|
|
208
|
+
declare const measuredUsage: z.ZodObject<{
|
|
209
|
+
ref: z.ZodString;
|
|
210
|
+
storageBytes: z.ZodNumber;
|
|
211
|
+
computeSeconds: z.ZodNumber;
|
|
212
|
+
from: z.ZodISODateTime;
|
|
213
|
+
to: z.ZodISODateTime;
|
|
214
|
+
measuredAt: z.ZodISODateTime;
|
|
215
|
+
}, z.core.$strip>;
|
|
216
|
+
/** La consommation relevee. */
|
|
217
|
+
type MeasuredUsage = z.infer<typeof measuredUsage>;
|
|
218
|
+
/** Ce que l'agent dit de lui-meme. */
|
|
219
|
+
declare const agentHealth: z.ZodObject<{
|
|
220
|
+
nodeId: z.ZodString;
|
|
221
|
+
serving: z.ZodBoolean;
|
|
222
|
+
hosted: z.ZodNumber;
|
|
223
|
+
availableBytes: z.ZodNumber;
|
|
224
|
+
startedAt: z.ZodISODateTime;
|
|
225
|
+
version: z.ZodString;
|
|
226
|
+
}, z.core.$strip>;
|
|
227
|
+
/** Ce que l'agent dit de lui-meme. */
|
|
228
|
+
type AgentHealth = z.infer<typeof agentHealth>;
|
|
229
|
+
/** Le corps attendu pour creer une base. */
|
|
230
|
+
declare const createDatabaseRequest: z.ZodObject<{
|
|
231
|
+
name: z.ZodString;
|
|
232
|
+
region: z.ZodString;
|
|
233
|
+
idempotencyKey: z.ZodString;
|
|
234
|
+
}, z.core.$strip>;
|
|
235
|
+
/** Le corps attendu pour brancher une base existante. */
|
|
236
|
+
declare const createBranchRequest: z.ZodObject<{
|
|
237
|
+
name: z.ZodString;
|
|
238
|
+
parentRef: z.ZodString;
|
|
239
|
+
idempotencyKey: z.ZodString;
|
|
240
|
+
}, z.core.$strip>;
|
|
241
|
+
/** Une operation du contrat. */
|
|
242
|
+
interface FleetOperation {
|
|
243
|
+
/** Le nom qu'emploient l'adaptateur et les journaux. */
|
|
244
|
+
readonly id: string;
|
|
245
|
+
readonly method: 'GET' | 'POST' | 'DELETE';
|
|
246
|
+
/** Le chemin, avec `:ref` la ou une reference est attendue. */
|
|
247
|
+
readonly path: string;
|
|
248
|
+
/** Le corps attendu, ou `undefined` quand il n'y en a pas. */
|
|
249
|
+
readonly request: z.ZodType | undefined;
|
|
250
|
+
/** Le corps rendu en cas de succes. */
|
|
251
|
+
readonly response: z.ZodType;
|
|
252
|
+
/**
|
|
253
|
+
* L'operation modifie-t-elle quelque chose ?
|
|
254
|
+
*
|
|
255
|
+
* Sert au relais et aux journaux : une lecture qui echoue se reessaie sans
|
|
256
|
+
* precaution, une ecriture non.
|
|
257
|
+
*/
|
|
258
|
+
readonly mutating: boolean;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Le contrat, en entier.
|
|
262
|
+
*
|
|
263
|
+
* `as const satisfies` et non une annotation de type : une annotation
|
|
264
|
+
* effacerait les schemas au profit de `ZodType`, et tout ce qu'on en
|
|
265
|
+
* inferrait ensuite deviendrait `unknown` — silencieusement, en compilant.
|
|
266
|
+
*/
|
|
267
|
+
declare const OPERATIONS: readonly [{
|
|
268
|
+
readonly id: "createDatabase";
|
|
269
|
+
readonly method: "POST";
|
|
270
|
+
readonly path: "/v1/databases";
|
|
271
|
+
readonly request: z.ZodObject<{
|
|
272
|
+
name: z.ZodString;
|
|
273
|
+
region: z.ZodString;
|
|
274
|
+
idempotencyKey: z.ZodString;
|
|
275
|
+
}, z.core.$strip>;
|
|
276
|
+
readonly response: z.ZodObject<{
|
|
277
|
+
ref: z.ZodString;
|
|
278
|
+
connectionString: z.ZodString;
|
|
279
|
+
region: z.ZodString;
|
|
280
|
+
}, z.core.$strip>;
|
|
281
|
+
readonly mutating: true;
|
|
282
|
+
}, {
|
|
283
|
+
readonly id: "createBranch";
|
|
284
|
+
readonly method: "POST";
|
|
285
|
+
readonly path: "/v1/databases/:ref/branches";
|
|
286
|
+
readonly request: z.ZodObject<{
|
|
287
|
+
name: z.ZodString;
|
|
288
|
+
parentRef: z.ZodString;
|
|
289
|
+
idempotencyKey: z.ZodString;
|
|
290
|
+
}, z.core.$strip>;
|
|
291
|
+
readonly response: z.ZodObject<{
|
|
292
|
+
ref: z.ZodString;
|
|
293
|
+
connectionString: z.ZodString;
|
|
294
|
+
region: z.ZodString;
|
|
295
|
+
}, z.core.$strip>;
|
|
296
|
+
readonly mutating: true;
|
|
297
|
+
}, {
|
|
298
|
+
readonly id: "deleteDatabase";
|
|
299
|
+
readonly method: "DELETE";
|
|
300
|
+
readonly path: "/v1/databases/:ref";
|
|
301
|
+
readonly request: undefined;
|
|
302
|
+
readonly response: z.ZodObject<{
|
|
303
|
+
deleted: z.ZodLiteral<true>;
|
|
304
|
+
}, z.core.$strip>;
|
|
305
|
+
readonly mutating: true;
|
|
306
|
+
}, {
|
|
307
|
+
readonly id: "suspend";
|
|
308
|
+
readonly method: "POST";
|
|
309
|
+
readonly path: "/v1/databases/:ref/suspend";
|
|
310
|
+
readonly request: undefined;
|
|
311
|
+
readonly response: z.ZodObject<{
|
|
312
|
+
sleeping: z.ZodLiteral<true>;
|
|
313
|
+
}, z.core.$strip>;
|
|
314
|
+
readonly mutating: true;
|
|
315
|
+
}, {
|
|
316
|
+
readonly id: "resume";
|
|
317
|
+
readonly method: "POST";
|
|
318
|
+
readonly path: "/v1/databases/:ref/resume";
|
|
319
|
+
readonly request: undefined;
|
|
320
|
+
readonly response: z.ZodObject<{
|
|
321
|
+
sleeping: z.ZodLiteral<false>;
|
|
322
|
+
}, z.core.$strip>;
|
|
323
|
+
readonly mutating: true;
|
|
324
|
+
}, {
|
|
325
|
+
readonly id: "list";
|
|
326
|
+
readonly method: "GET";
|
|
327
|
+
readonly path: "/v1/databases";
|
|
328
|
+
readonly request: undefined;
|
|
329
|
+
readonly response: z.ZodObject<{
|
|
330
|
+
resources: z.ZodArray<z.ZodObject<{
|
|
331
|
+
ref: z.ZodString;
|
|
332
|
+
name: z.ZodString;
|
|
333
|
+
available: z.ZodBoolean;
|
|
334
|
+
region: z.ZodString;
|
|
335
|
+
sleeping: z.ZodBoolean;
|
|
336
|
+
parentRef: z.ZodOptional<z.ZodString>;
|
|
337
|
+
}, z.core.$strip>>;
|
|
338
|
+
}, z.core.$strip>;
|
|
339
|
+
readonly mutating: false;
|
|
340
|
+
}, {
|
|
341
|
+
readonly id: "rotateCredentials";
|
|
342
|
+
readonly method: "POST";
|
|
343
|
+
readonly path: "/v1/databases/:ref/credentials";
|
|
344
|
+
readonly request: undefined;
|
|
345
|
+
readonly response: z.ZodObject<{
|
|
346
|
+
connectionString: z.ZodString;
|
|
347
|
+
}, z.core.$strip>;
|
|
348
|
+
readonly mutating: true;
|
|
349
|
+
}, {
|
|
350
|
+
readonly id: "usage";
|
|
351
|
+
readonly method: "GET";
|
|
352
|
+
readonly path: "/v1/databases/:ref/usage";
|
|
353
|
+
readonly request: undefined;
|
|
354
|
+
readonly response: z.ZodObject<{
|
|
355
|
+
ref: z.ZodString;
|
|
356
|
+
storageBytes: z.ZodNumber;
|
|
357
|
+
computeSeconds: z.ZodNumber;
|
|
358
|
+
from: z.ZodISODateTime;
|
|
359
|
+
to: z.ZodISODateTime;
|
|
360
|
+
measuredAt: z.ZodISODateTime;
|
|
361
|
+
}, z.core.$strip>;
|
|
362
|
+
readonly mutating: false;
|
|
363
|
+
}, {
|
|
364
|
+
readonly id: "health";
|
|
365
|
+
readonly method: "GET";
|
|
366
|
+
readonly path: "/v1/health";
|
|
367
|
+
readonly request: undefined;
|
|
368
|
+
readonly response: z.ZodObject<{
|
|
369
|
+
nodeId: z.ZodString;
|
|
370
|
+
serving: z.ZodBoolean;
|
|
371
|
+
hosted: z.ZodNumber;
|
|
372
|
+
availableBytes: z.ZodNumber;
|
|
373
|
+
startedAt: z.ZodISODateTime;
|
|
374
|
+
version: z.ZodString;
|
|
375
|
+
}, z.core.$strip>;
|
|
376
|
+
readonly mutating: false;
|
|
377
|
+
}];
|
|
378
|
+
/** L'identifiant d'une operation du contrat. */
|
|
379
|
+
type OperationId = (typeof OPERATIONS)[number]['id'];
|
|
380
|
+
/** Le corps rendu en echec, quelle que soit l'operation. */
|
|
381
|
+
declare const errorResponse: z.ZodObject<{
|
|
382
|
+
kind: z.ZodEnum<{
|
|
383
|
+
timeout: "timeout";
|
|
384
|
+
quota: "quota";
|
|
385
|
+
conflict: "conflict";
|
|
386
|
+
unauthorized: "unauthorized";
|
|
387
|
+
"not-found": "not-found";
|
|
388
|
+
malformed: "malformed";
|
|
389
|
+
unavailable: "unavailable";
|
|
390
|
+
}>;
|
|
391
|
+
message: z.ZodString;
|
|
392
|
+
resourceMayExist: z.ZodBoolean;
|
|
393
|
+
detail: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
394
|
+
reason: z.ZodLiteral<"branches-dependantes">;
|
|
395
|
+
branches: z.ZodArray<z.ZodString>;
|
|
396
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
397
|
+
reason: z.ZodLiteral<"base-endormie">;
|
|
398
|
+
ref: z.ZodString;
|
|
399
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
400
|
+
reason: z.ZodLiteral<"place-insuffisante">;
|
|
401
|
+
requiredBytes: z.ZodNumber;
|
|
402
|
+
availableBytes: z.ZodNumber;
|
|
403
|
+
}, z.core.$strip>], "reason">>;
|
|
404
|
+
requestId: z.ZodString;
|
|
405
|
+
}, z.core.$strip>;
|
|
406
|
+
/** Retrouve une operation par son identifiant. */
|
|
407
|
+
declare function operation(id: OperationId): FleetOperation;
|
|
408
|
+
/**
|
|
409
|
+
* Remplace `:ref` dans un chemin.
|
|
410
|
+
*
|
|
411
|
+
* L'encodage n'est pas une precaution de style : une reference contient ce que
|
|
412
|
+
* l'agent y met, et un caractere non encode changerait le chemin appele.
|
|
413
|
+
*/
|
|
414
|
+
declare function pathFor(op: FleetOperation, ref?: string): string;
|
|
415
|
+
/**
|
|
416
|
+
* Les operations qui exigent une cle d'idempotence.
|
|
417
|
+
*
|
|
418
|
+
* Derivee du contrat plutot qu'ecrite a la main : une liste recopiee finit par
|
|
419
|
+
* diverger de ce qu'elle decrit, et la divergence se decouvre le jour ou un
|
|
420
|
+
* reessai cree une seconde base.
|
|
421
|
+
*/
|
|
422
|
+
declare const IDEMPOTENT_OPERATIONS: readonly OperationId[];
|
|
423
|
+
|
|
424
|
+
export { type AgentHealth, type FailureBody, type FailureDetail, type FailureKind, type FleetOperation, type HostedResource, IDEMPOTENT_OPERATIONS, type MeasuredUsage, OPERATIONS, type OperationId, type Provisioned, STATUS_BY_KIND, agentHealth, createBranchRequest, createDatabaseRequest, describeFailure, errorResponse, failureBody, failureDetail, failureKind, hostedResource, idempotencyKey, isRetryable, measuredUsage, operation, pathFor, provisioned, resourceName, resourceRef };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
var failureKind = z.enum([
|
|
4
|
+
/** L'agent n'a pas repondu a temps. La ressource existe peut-etre. */
|
|
5
|
+
"timeout",
|
|
6
|
+
/** Plus de place sur la machine, ou quota de la flotte atteint. */
|
|
7
|
+
"quota",
|
|
8
|
+
/** Le nom demande est deja pris, ou l'operation heurte un etat existant. */
|
|
9
|
+
"conflict",
|
|
10
|
+
/** Jeton absent, expire ou refuse. */
|
|
11
|
+
"unauthorized",
|
|
12
|
+
/** L'agent ne connait pas cette ressource. */
|
|
13
|
+
"not-found",
|
|
14
|
+
/** La requete est recevable mais son contenu est inexploitable. */
|
|
15
|
+
"malformed",
|
|
16
|
+
/** L'agent est la, mais hors d'etat de servir. */
|
|
17
|
+
"unavailable"
|
|
18
|
+
]);
|
|
19
|
+
var failureDetail = z.discriminatedUnion("reason", [
|
|
20
|
+
/**
|
|
21
|
+
* Une suppression refusee parce que des branches vivent sur cette base.
|
|
22
|
+
*
|
|
23
|
+
* Un clone depend de l'instantane dont il est issu : detruire le parent
|
|
24
|
+
* detruirait ses branches. Le refus nomme donc ce qui bloque, sans quoi la
|
|
25
|
+
* personne qui le lit ne sait ni ce qui s'oppose, ni par ou commencer.
|
|
26
|
+
*/
|
|
27
|
+
z.object({
|
|
28
|
+
reason: z.literal("branches-dependantes"),
|
|
29
|
+
/** Les references des branches qui empechent la suppression. */
|
|
30
|
+
branches: z.array(z.string().min(1)).min(1)
|
|
31
|
+
}),
|
|
32
|
+
/**
|
|
33
|
+
* Une operation demandee sur une base endormie qui exige qu'elle tourne.
|
|
34
|
+
*
|
|
35
|
+
* Distingue d'un `unavailable` : la base va parfaitement bien, elle dort.
|
|
36
|
+
* L'appelant peut la reveiller et reessayer, ce qu'aucune panne ne permet.
|
|
37
|
+
*/
|
|
38
|
+
z.object({
|
|
39
|
+
reason: z.literal("base-endormie"),
|
|
40
|
+
ref: z.string().min(1)
|
|
41
|
+
}),
|
|
42
|
+
/**
|
|
43
|
+
* La machine n'a plus assez de place.
|
|
44
|
+
*
|
|
45
|
+
* Les octets sont donnes parce que « plus de place » ne dit pas s'il manque
|
|
46
|
+
* un gigaoctet ou un teraoctet, et que la reponse change la decision.
|
|
47
|
+
*/
|
|
48
|
+
z.object({
|
|
49
|
+
reason: z.literal("place-insuffisante"),
|
|
50
|
+
requiredBytes: z.number().int().nonnegative(),
|
|
51
|
+
availableBytes: z.number().int().nonnegative()
|
|
52
|
+
})
|
|
53
|
+
]);
|
|
54
|
+
var failureBody = z.object({
|
|
55
|
+
kind: failureKind,
|
|
56
|
+
/** Une phrase pour un humain. Jamais analysee par du code. */
|
|
57
|
+
message: z.string().min(1),
|
|
58
|
+
/**
|
|
59
|
+
* L'agent a-t-il pu laisser quelque chose derriere lui ?
|
|
60
|
+
*
|
|
61
|
+
* Renseigne par l'agent, jamais devine par l'appelant.
|
|
62
|
+
*/
|
|
63
|
+
resourceMayExist: z.boolean(),
|
|
64
|
+
/** Le detail structure, quand le refus en a un. */
|
|
65
|
+
detail: failureDetail.optional(),
|
|
66
|
+
/**
|
|
67
|
+
* L'identifiant de la requete, tel que l'agent l'a journalise.
|
|
68
|
+
*
|
|
69
|
+
* C'est ce qui permet de retrouver la trace cote agent a partir d'un rapport
|
|
70
|
+
* cote plan de controle. Sans lui, les deux journaux ne se rejoignent pas.
|
|
71
|
+
*/
|
|
72
|
+
requestId: z.string().min(1)
|
|
73
|
+
});
|
|
74
|
+
var STATUS_BY_KIND = {
|
|
75
|
+
timeout: 504,
|
|
76
|
+
quota: 507,
|
|
77
|
+
conflict: 409,
|
|
78
|
+
unauthorized: 401,
|
|
79
|
+
"not-found": 404,
|
|
80
|
+
malformed: 422,
|
|
81
|
+
unavailable: 503
|
|
82
|
+
};
|
|
83
|
+
function isRetryable(kind) {
|
|
84
|
+
return kind === "timeout" || kind === "unavailable";
|
|
85
|
+
}
|
|
86
|
+
function describeFailure(body) {
|
|
87
|
+
const detail = body.detail;
|
|
88
|
+
if (detail === void 0) return body.message;
|
|
89
|
+
switch (detail.reason) {
|
|
90
|
+
case "branches-dependantes":
|
|
91
|
+
return `${body.message} \u2014 ${String(detail.branches.length)} branche(s) en dependent : ${detail.branches.join(", ")}. Promouvez-les ou supprimez-les avant d effacer la base parente.`;
|
|
92
|
+
case "base-endormie":
|
|
93
|
+
return `${body.message} \u2014 la base ${detail.ref} dort. Reveillez-la et reessayez.`;
|
|
94
|
+
case "place-insuffisante": {
|
|
95
|
+
const manque = detail.requiredBytes - detail.availableBytes;
|
|
96
|
+
return `${body.message} \u2014 il manque ${String(manque)} octets sur cette machine. Liberez de la place ou placez la base ailleurs.`;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// src/operations.ts
|
|
102
|
+
import { z as z2 } from "zod";
|
|
103
|
+
var resourceRef = z2.string().min(1).max(200);
|
|
104
|
+
var resourceName = z2.string().min(1).max(200);
|
|
105
|
+
var idempotencyKey = z2.string().min(8).max(200);
|
|
106
|
+
var provisioned = z2.object({
|
|
107
|
+
ref: resourceRef,
|
|
108
|
+
/**
|
|
109
|
+
* La chaine de connexion, rendue **une seule fois**.
|
|
110
|
+
*
|
|
111
|
+
* L'agent ne sait pas la relire : il ne conserve pas le mot de passe, il
|
|
112
|
+
* conserve ce que PostgreSQL en garde. Une reprise passe par une rotation,
|
|
113
|
+
* jamais par une relecture — c'est ce qui fait qu'un agent compromis ne
|
|
114
|
+
* livre pas le parc.
|
|
115
|
+
*/
|
|
116
|
+
connectionString: z2.string().min(1),
|
|
117
|
+
/** La region telle que l'agent la connait. Voir la note du module. */
|
|
118
|
+
region: z2.string().min(1)
|
|
119
|
+
});
|
|
120
|
+
var hostedResource = z2.object({
|
|
121
|
+
ref: resourceRef,
|
|
122
|
+
name: resourceName,
|
|
123
|
+
/** L'agent la considere-t-il utilisable ? */
|
|
124
|
+
available: z2.boolean(),
|
|
125
|
+
region: z2.string().min(1),
|
|
126
|
+
/**
|
|
127
|
+
* Endormie ou non.
|
|
128
|
+
*
|
|
129
|
+
* Distinct de `available` : une base endormie est parfaitement saine. Les
|
|
130
|
+
* confondre ferait passer une economie voulue pour une panne, et la
|
|
131
|
+
* reconciliation ouvrirait un incident a chaque client au repos.
|
|
132
|
+
*/
|
|
133
|
+
sleeping: z2.boolean(),
|
|
134
|
+
/**
|
|
135
|
+
* La ressource dont celle-ci est un clone, s'il y en a une.
|
|
136
|
+
*
|
|
137
|
+
* C'est ce qui permet de savoir, avant de demander une suppression, qu'elle
|
|
138
|
+
* sera refusee.
|
|
139
|
+
*/
|
|
140
|
+
parentRef: resourceRef.optional()
|
|
141
|
+
});
|
|
142
|
+
var measuredUsage = z2.object({
|
|
143
|
+
ref: resourceRef,
|
|
144
|
+
/** Octets reellement occupes par le dataset, instantanes compris. */
|
|
145
|
+
storageBytes: z2.number().int().nonnegative(),
|
|
146
|
+
/** Secondes pendant lesquelles le calcul a tourne sur la periode. */
|
|
147
|
+
computeSeconds: z2.number().nonnegative(),
|
|
148
|
+
/** Debut de la periode couverte. */
|
|
149
|
+
from: z2.iso.datetime(),
|
|
150
|
+
/** Fin de la periode couverte. */
|
|
151
|
+
to: z2.iso.datetime(),
|
|
152
|
+
/**
|
|
153
|
+
* Quand la mesure a ete prise.
|
|
154
|
+
*
|
|
155
|
+
* Distinct de `to` : une mesure peut couvrir une periode close il y a une
|
|
156
|
+
* heure. C'est cet horodatage-ci que la regle de peremption regarde.
|
|
157
|
+
*/
|
|
158
|
+
measuredAt: z2.iso.datetime()
|
|
159
|
+
});
|
|
160
|
+
var agentHealth = z2.object({
|
|
161
|
+
nodeId: z2.string().min(1),
|
|
162
|
+
/** Sert-il, oui ou non. Un agent qui doute repond `false`. */
|
|
163
|
+
serving: z2.boolean(),
|
|
164
|
+
/** Bases hebergees, endormies comprises. */
|
|
165
|
+
hosted: z2.number().int().nonnegative(),
|
|
166
|
+
/** Place restante sur le pool. Ce qui decide d'un placement. */
|
|
167
|
+
availableBytes: z2.number().int().nonnegative(),
|
|
168
|
+
/**
|
|
169
|
+
* Depuis quand l'agent tourne.
|
|
170
|
+
*
|
|
171
|
+
* Un agent qui vient de redemarrer et qui sert deja est le signe d'un
|
|
172
|
+
* redemarrage non demande : quelque chose l'a tue.
|
|
173
|
+
*/
|
|
174
|
+
startedAt: z2.iso.datetime(),
|
|
175
|
+
/** Version, pour qu'un parc heterogene se voie. */
|
|
176
|
+
version: z2.string().min(1)
|
|
177
|
+
});
|
|
178
|
+
var createDatabaseRequest = z2.object({
|
|
179
|
+
name: resourceName,
|
|
180
|
+
region: z2.string().min(1),
|
|
181
|
+
idempotencyKey
|
|
182
|
+
});
|
|
183
|
+
var createBranchRequest = z2.object({
|
|
184
|
+
name: resourceName,
|
|
185
|
+
parentRef: resourceRef,
|
|
186
|
+
idempotencyKey
|
|
187
|
+
});
|
|
188
|
+
var OPERATIONS = [
|
|
189
|
+
{
|
|
190
|
+
id: "createDatabase",
|
|
191
|
+
method: "POST",
|
|
192
|
+
path: "/v1/databases",
|
|
193
|
+
request: createDatabaseRequest,
|
|
194
|
+
response: provisioned,
|
|
195
|
+
mutating: true
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
id: "createBranch",
|
|
199
|
+
method: "POST",
|
|
200
|
+
path: "/v1/databases/:ref/branches",
|
|
201
|
+
request: createBranchRequest,
|
|
202
|
+
response: provisioned,
|
|
203
|
+
mutating: true
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
id: "deleteDatabase",
|
|
207
|
+
method: "DELETE",
|
|
208
|
+
path: "/v1/databases/:ref",
|
|
209
|
+
request: void 0,
|
|
210
|
+
response: z2.object({ deleted: z2.literal(true) }),
|
|
211
|
+
mutating: true
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
id: "suspend",
|
|
215
|
+
method: "POST",
|
|
216
|
+
path: "/v1/databases/:ref/suspend",
|
|
217
|
+
request: void 0,
|
|
218
|
+
response: z2.object({ sleeping: z2.literal(true) }),
|
|
219
|
+
mutating: true
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
id: "resume",
|
|
223
|
+
method: "POST",
|
|
224
|
+
path: "/v1/databases/:ref/resume",
|
|
225
|
+
request: void 0,
|
|
226
|
+
response: z2.object({ sleeping: z2.literal(false) }),
|
|
227
|
+
mutating: true
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
id: "list",
|
|
231
|
+
method: "GET",
|
|
232
|
+
path: "/v1/databases",
|
|
233
|
+
request: void 0,
|
|
234
|
+
response: z2.object({ resources: z2.array(hostedResource) }),
|
|
235
|
+
mutating: false
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
id: "rotateCredentials",
|
|
239
|
+
method: "POST",
|
|
240
|
+
path: "/v1/databases/:ref/credentials",
|
|
241
|
+
request: void 0,
|
|
242
|
+
response: z2.object({ connectionString: z2.string().min(1) }),
|
|
243
|
+
mutating: true
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
id: "usage",
|
|
247
|
+
method: "GET",
|
|
248
|
+
path: "/v1/databases/:ref/usage",
|
|
249
|
+
request: void 0,
|
|
250
|
+
response: measuredUsage,
|
|
251
|
+
mutating: false
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
id: "health",
|
|
255
|
+
method: "GET",
|
|
256
|
+
path: "/v1/health",
|
|
257
|
+
request: void 0,
|
|
258
|
+
response: agentHealth,
|
|
259
|
+
mutating: false
|
|
260
|
+
}
|
|
261
|
+
];
|
|
262
|
+
var errorResponse = failureBody;
|
|
263
|
+
function operation(id) {
|
|
264
|
+
const found = OPERATIONS.find((op) => op.id === id);
|
|
265
|
+
if (found === void 0) throw new Error(`Operation inconnue : ${id}`);
|
|
266
|
+
return found;
|
|
267
|
+
}
|
|
268
|
+
function pathFor(op, ref) {
|
|
269
|
+
if (!op.path.includes(":ref")) return op.path;
|
|
270
|
+
if (ref === void 0) {
|
|
271
|
+
throw new Error(`L operation ${op.id} attend une reference.`);
|
|
272
|
+
}
|
|
273
|
+
return op.path.replace(":ref", encodeURIComponent(ref));
|
|
274
|
+
}
|
|
275
|
+
var IDEMPOTENT_OPERATIONS = OPERATIONS.filter(
|
|
276
|
+
(op) => op.request !== void 0 && "idempotencyKey" in op.request.shape
|
|
277
|
+
).map((op) => op.id);
|
|
278
|
+
export {
|
|
279
|
+
IDEMPOTENT_OPERATIONS,
|
|
280
|
+
OPERATIONS,
|
|
281
|
+
STATUS_BY_KIND,
|
|
282
|
+
agentHealth,
|
|
283
|
+
createBranchRequest,
|
|
284
|
+
createDatabaseRequest,
|
|
285
|
+
describeFailure,
|
|
286
|
+
errorResponse,
|
|
287
|
+
failureBody,
|
|
288
|
+
failureDetail,
|
|
289
|
+
failureKind,
|
|
290
|
+
hostedResource,
|
|
291
|
+
idempotencyKey,
|
|
292
|
+
isRetryable,
|
|
293
|
+
measuredUsage,
|
|
294
|
+
operation,
|
|
295
|
+
pathFor,
|
|
296
|
+
provisioned,
|
|
297
|
+
resourceName,
|
|
298
|
+
resourceRef
|
|
299
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@odoro-cli/fleet-protocol",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Le contrat entre le plan de controle Odoro et un agent de flotte.",
|
|
6
|
+
"license": "UNLICENSED",
|
|
7
|
+
"author": "Odoro",
|
|
8
|
+
"homepage": "https://github.com/ODORO-CLI/odoro-fleet#readme",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/ODORO-CLI/odoro-fleet.git",
|
|
12
|
+
"directory": "packages/protocol"
|
|
13
|
+
},
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=22"
|
|
16
|
+
},
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist"
|
|
22
|
+
],
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"import": "./dist/index.js"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"zod": "^4.5.1"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"tsup": "^8.5.0",
|
|
34
|
+
"typescript": "^5.9.3",
|
|
35
|
+
"vitest": "^3.2.4",
|
|
36
|
+
"zod": "^4.5.1"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "tsup",
|
|
40
|
+
"test": "vitest run",
|
|
41
|
+
"typecheck": "tsc --noEmit"
|
|
42
|
+
}
|
|
43
|
+
}
|