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