@empereur-rouge/pms-sdk 0.5.0 → 0.7.1
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/README.md +76 -0
- package/dist/index.cjs +318 -5
- package/dist/index.d.cts +453 -2
- package/dist/index.d.ts +453 -2
- package/dist/index.js +316 -5
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -378,7 +378,25 @@ var DEFAULT_CONFIG = {
|
|
|
378
378
|
enableRacing: true,
|
|
379
379
|
retries: 2,
|
|
380
380
|
perAttemptTimeoutMs: 4e3,
|
|
381
|
-
retryBaseDelayMs: 100
|
|
381
|
+
retryBaseDelayMs: 100,
|
|
382
|
+
// Vide par défaut : les méthodes admin lèvent une erreur explicite si
|
|
383
|
+
// appelées sans jeton (voir PmsClient.requireAdminToken).
|
|
384
|
+
adminToken: ""
|
|
385
|
+
};
|
|
386
|
+
var ErrorCode = {
|
|
387
|
+
/** En-tête d'authentification manquant (admin requis). */
|
|
388
|
+
MissingAuth: 1001,
|
|
389
|
+
/** Jeton d'authentification invalide. */
|
|
390
|
+
InvalidAuth: 1002,
|
|
391
|
+
/**
|
|
392
|
+
* Proposition de gouvernance rejetée (timelock non écoulé, proposition
|
|
393
|
+
* non en attente, ou identifiant inconnu). HTTP 409.
|
|
394
|
+
*/
|
|
395
|
+
GovernanceRejected: 3071,
|
|
396
|
+
/** Émission native (mint) désactivée. HTTP 503. */
|
|
397
|
+
MintDisabled: 5031,
|
|
398
|
+
/** Erreur interne générique (dette : handler non encore migré). */
|
|
399
|
+
Internal: 9999
|
|
382
400
|
};
|
|
383
401
|
|
|
384
402
|
// src/crypto.ts
|
|
@@ -577,6 +595,204 @@ var PmsClient = class {
|
|
|
577
595
|
return this.fetch("/v1/reserves/latest", void 0, { idempotent: true });
|
|
578
596
|
}
|
|
579
597
|
// ═══════════════════════════════════════════════════════════════════════
|
|
598
|
+
// Gouvernance — lectures publiques (GET, sans authentification)
|
|
599
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
600
|
+
/**
|
|
601
|
+
* Liste les propositions de gouvernance EN ATTENTE (statut `pending`).
|
|
602
|
+
*
|
|
603
|
+
* Lecture publique — aucun `adminToken` requis.
|
|
604
|
+
*
|
|
605
|
+
* @returns Le tableau des propositions en attente (vide si aucune).
|
|
606
|
+
*
|
|
607
|
+
* @example
|
|
608
|
+
* ```typescript
|
|
609
|
+
* const pending = await client.getGovernancePending();
|
|
610
|
+
* for (const p of pending) {
|
|
611
|
+
* console.log(`${p.proposal_id} enacts after ${new Date(p.enact_after_ms)}`);
|
|
612
|
+
* }
|
|
613
|
+
* ```
|
|
614
|
+
*/
|
|
615
|
+
async getGovernancePending() {
|
|
616
|
+
const res = await this.fetch(
|
|
617
|
+
"/v1/governance/pending",
|
|
618
|
+
void 0,
|
|
619
|
+
{ idempotent: true }
|
|
620
|
+
);
|
|
621
|
+
return res.pending ?? [];
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Liste les propositions de gouvernance TERMINÉES (enactées ou annulées).
|
|
625
|
+
*
|
|
626
|
+
* Lecture publique — aucun `adminToken` requis.
|
|
627
|
+
*
|
|
628
|
+
* @returns Le tableau de l'historique des propositions (vide si aucune).
|
|
629
|
+
*/
|
|
630
|
+
async getGovernanceHistory() {
|
|
631
|
+
const res = await this.fetch(
|
|
632
|
+
"/v1/governance/history",
|
|
633
|
+
void 0,
|
|
634
|
+
{ idempotent: true }
|
|
635
|
+
);
|
|
636
|
+
return res.history ?? [];
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* Liste les blocs de gouvernance ancrés dans le DAG (un par proposition,
|
|
640
|
+
* enactment, ou annulation).
|
|
641
|
+
*
|
|
642
|
+
* Lecture publique — aucun `adminToken` requis.
|
|
643
|
+
*
|
|
644
|
+
* @returns `{ count, blocks }` — le nombre et la liste des blocs.
|
|
645
|
+
*/
|
|
646
|
+
async getGovernanceBlocks() {
|
|
647
|
+
return this.fetch(
|
|
648
|
+
"/v1/governance/blocks",
|
|
649
|
+
void 0,
|
|
650
|
+
{ idempotent: true }
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
654
|
+
// Gouvernance — actions ADMIN (POST, requièrent adminToken)
|
|
655
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
656
|
+
/**
|
|
657
|
+
* Soumet une proposition de gouvernance ancrée dans le DAG.
|
|
658
|
+
*
|
|
659
|
+
* Méthode ADMIN — requiert `adminToken` dans la config du client
|
|
660
|
+
* (envoyé en `Authorization: Bearer <adminToken>`). Lève une erreur
|
|
661
|
+
* explicite avant tout appel réseau si le jeton est absent.
|
|
662
|
+
*
|
|
663
|
+
* @param req.update - Mise à jour de config à proposer.
|
|
664
|
+
* @param req.tier - Palier de gouvernance (minuscules, voir
|
|
665
|
+
* [`GovernanceTier`]).
|
|
666
|
+
* @param req.reason - Raison libre (optionnelle).
|
|
667
|
+
* @returns `{ status, proposal_id, block_id, tier, announced_at_ms,
|
|
668
|
+
* enact_after_ms }`.
|
|
669
|
+
*
|
|
670
|
+
* @example
|
|
671
|
+
* ```typescript
|
|
672
|
+
* const res = await client.proposeGovernance({
|
|
673
|
+
* update: { SetEmissionCorridor: { ceiling_bps: 1500, floor_bps: 0, target_bps: 1000, epoch_duration_sec: 86400 } },
|
|
674
|
+
* tier: "constitution",
|
|
675
|
+
* reason: "raise emission ceiling for Q3",
|
|
676
|
+
* });
|
|
677
|
+
* console.log(res.proposal_id, "enacts after", new Date(res.enact_after_ms));
|
|
678
|
+
* ```
|
|
679
|
+
*/
|
|
680
|
+
async proposeGovernance(req) {
|
|
681
|
+
return this.fetchAdmin("proposeGovernance", "/admin/governance/propose", {
|
|
682
|
+
method: "POST",
|
|
683
|
+
body: JSON.stringify({
|
|
684
|
+
update: req.update,
|
|
685
|
+
tier: req.tier,
|
|
686
|
+
reason: req.reason ?? ""
|
|
687
|
+
})
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* Enacte une proposition de gouvernance dont le timelock est écoulé.
|
|
692
|
+
*
|
|
693
|
+
* Méthode ADMIN — requiert `adminToken`. Le moteur rejette (HTTP 409,
|
|
694
|
+
* code [`ErrorCode.GovernanceRejected`] = 3071) si le timelock n'est pas
|
|
695
|
+
* écoulé, si la proposition n'est pas en attente, ou si l'identifiant est
|
|
696
|
+
* inconnu.
|
|
697
|
+
*
|
|
698
|
+
* @param proposalId - Identifiant de la proposition à enacter.
|
|
699
|
+
* @param reason - Raison libre (optionnelle).
|
|
700
|
+
* @returns `{ status, proposal_id, block_id }`.
|
|
701
|
+
*/
|
|
702
|
+
async enactProposal(proposalId, reason) {
|
|
703
|
+
return this.fetchAdmin(
|
|
704
|
+
"enactProposal",
|
|
705
|
+
`/admin/governance/enact/${encodeURIComponent(proposalId)}`,
|
|
706
|
+
{
|
|
707
|
+
method: "POST",
|
|
708
|
+
body: JSON.stringify({ reason: reason ?? "" })
|
|
709
|
+
}
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
/**
|
|
713
|
+
* Annule une proposition de gouvernance en attente.
|
|
714
|
+
*
|
|
715
|
+
* Méthode ADMIN — requiert `adminToken`. Rejet HTTP 409 / code 3071 si la
|
|
716
|
+
* proposition n'est pas en attente ou si l'identifiant est inconnu.
|
|
717
|
+
*
|
|
718
|
+
* @param proposalId - Identifiant de la proposition à annuler.
|
|
719
|
+
* @param reason - Raison libre (optionnelle).
|
|
720
|
+
* @returns `{ status, proposal_id, block_id }`.
|
|
721
|
+
*/
|
|
722
|
+
async cancelProposal(proposalId, reason) {
|
|
723
|
+
return this.fetchAdmin(
|
|
724
|
+
"cancelProposal",
|
|
725
|
+
`/admin/governance/cancel/${encodeURIComponent(proposalId)}`,
|
|
726
|
+
{
|
|
727
|
+
method: "POST",
|
|
728
|
+
body: JSON.stringify({ reason: reason ?? "" })
|
|
729
|
+
}
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* Applique (ou propose) une mise à jour de configuration.
|
|
734
|
+
*
|
|
735
|
+
* Méthode ADMIN — requiert `adminToken`. Le moteur distingue DEUX issues
|
|
736
|
+
* selon le sens du changement, et le SDK les EXPOSE explicitement via une
|
|
737
|
+
* union discriminée sur `applied` — un 202 n'est JAMAIS traité comme un
|
|
738
|
+
* succès appliqué :
|
|
739
|
+
* - **HTTP 200** (durcissement) → `{ applied: true, ... }` : la mise à
|
|
740
|
+
* jour est instantanément en vigueur.
|
|
741
|
+
* - **HTTP 202** (assouplissement) → `{ applied: false, proposalId,
|
|
742
|
+
* enactAfterMs, ... }` : la mise à jour est placée sous timelock de
|
|
743
|
+
* gouvernance, PAS encore appliquée. Elle s'auto-enacte à l'expiration
|
|
744
|
+
* du timelock, ou via [`enactProposal`].
|
|
745
|
+
*
|
|
746
|
+
* @param update - Mise à jour de config (envoyée brute, sans wrapper).
|
|
747
|
+
* @returns Une [`ConfigChangeResult`] discriminée sur `applied`.
|
|
748
|
+
*
|
|
749
|
+
* @example
|
|
750
|
+
* ```typescript
|
|
751
|
+
* const r = await client.setConfig({ SetMintEnabled: { enabled: false } });
|
|
752
|
+
* if (r.applied) {
|
|
753
|
+
* console.log("appliqué instantanément:", r.updateApplied);
|
|
754
|
+
* } else {
|
|
755
|
+
* console.log("timelocké, enact après", new Date(r.enactAfterMs));
|
|
756
|
+
* }
|
|
757
|
+
* ```
|
|
758
|
+
*/
|
|
759
|
+
async setConfig(update) {
|
|
760
|
+
const { status, body } = await this.fetchAdminWithStatus(
|
|
761
|
+
"setConfig",
|
|
762
|
+
"/admin/config",
|
|
763
|
+
{
|
|
764
|
+
method: "POST",
|
|
765
|
+
body: JSON.stringify(update)
|
|
766
|
+
}
|
|
767
|
+
);
|
|
768
|
+
const isApplied = status === 200 && body.status !== "proposed";
|
|
769
|
+
if (isApplied) {
|
|
770
|
+
return {
|
|
771
|
+
applied: true,
|
|
772
|
+
status: "applied",
|
|
773
|
+
mode: body.mode ?? "",
|
|
774
|
+
updateApplied: body.update_applied ?? "",
|
|
775
|
+
tier: body.tier ?? "",
|
|
776
|
+
proposalId: body.proposal_id ?? "",
|
|
777
|
+
proposalBlockId: body.proposal_block_id ?? "",
|
|
778
|
+
enactBlockId: body.enact_block_id ?? "",
|
|
779
|
+
config: body.config ?? null
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
return {
|
|
783
|
+
applied: false,
|
|
784
|
+
status: "proposed",
|
|
785
|
+
mode: body.mode ?? "",
|
|
786
|
+
update: body.update ?? "",
|
|
787
|
+
tier: body.tier ?? "",
|
|
788
|
+
proposalId: body.proposal_id ?? "",
|
|
789
|
+
proposalBlockId: body.proposal_block_id ?? "",
|
|
790
|
+
announcedAtMs: body.announced_at_ms ?? 0,
|
|
791
|
+
enactAfterMs: body.enact_after_ms ?? 0,
|
|
792
|
+
message: body.message ?? ""
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
580
796
|
// Wallet API (Custodial — Server-Side)
|
|
581
797
|
// ═══════════════════════════════════════════════════════════════════════
|
|
582
798
|
/**
|
|
@@ -1399,6 +1615,62 @@ var PmsClient = class {
|
|
|
1399
1615
|
// ═══════════════════════════════════════════════════════════════════════
|
|
1400
1616
|
// Helper HTTP
|
|
1401
1617
|
// ═══════════════════════════════════════════════════════════════════════
|
|
1618
|
+
/**
|
|
1619
|
+
* @internal
|
|
1620
|
+
* Garantit qu'un `adminToken` est configuré avant un appel admin.
|
|
1621
|
+
* Lève une erreur claire (sans appel réseau) sinon.
|
|
1622
|
+
*/
|
|
1623
|
+
requireAdminToken(method) {
|
|
1624
|
+
const token = this.config.adminToken;
|
|
1625
|
+
if (!token || token.trim() === "") {
|
|
1626
|
+
throw new Error(
|
|
1627
|
+
`PmsClient.${method}() requires an admin token. Set it via new PmsClient({ ..., adminToken: '<token>' }). Admin methods send it as 'Authorization: Bearer <adminToken>'. Public reads (getGovernancePending/History/Blocks) do NOT need it.`
|
|
1628
|
+
);
|
|
1629
|
+
}
|
|
1630
|
+
return token;
|
|
1631
|
+
}
|
|
1632
|
+
/**
|
|
1633
|
+
* @internal
|
|
1634
|
+
* Effectue un appel admin authentifié (en-tête `Authorization: Bearer`).
|
|
1635
|
+
*
|
|
1636
|
+
* Les écritures admin ne sont PAS idempotentes — pas de retry automatique
|
|
1637
|
+
* (single-attempt via la branche non-idempotente de `fetchUrl`). L'auth est
|
|
1638
|
+
* injectée par appel (pas dans le bloc d'en-têtes partagé de `fetchOnce`)
|
|
1639
|
+
* pour éviter de fuiter le jeton admin vers des nœuds découverts/seeds lors
|
|
1640
|
+
* du racing — cf. `submitBlockRacing`. `method` ne sert qu'au libellé de
|
|
1641
|
+
* l'erreur "jeton manquant".
|
|
1642
|
+
*/
|
|
1643
|
+
async fetchAdmin(method, path, init) {
|
|
1644
|
+
return this.fetchUrl(this.config.nodeUrl, path, this.withAdminAuth(method, init));
|
|
1645
|
+
}
|
|
1646
|
+
/**
|
|
1647
|
+
* @internal
|
|
1648
|
+
* Variante d'appel admin qui expose le STATUT HTTP en plus du corps parsé.
|
|
1649
|
+
* Nécessaire pour [`setConfig`], qui doit distinguer 200 (applied) de 202
|
|
1650
|
+
* (proposed) — deux corps de réponse différents pour deux issues. Délègue à
|
|
1651
|
+
* la même primitive `fetchOnceWithStatus` que tous les autres appels, donc
|
|
1652
|
+
* hérite du merge de signal et du tagging d'abort sans duplication.
|
|
1653
|
+
*/
|
|
1654
|
+
async fetchAdminWithStatus(method, path, init) {
|
|
1655
|
+
const url = `${this.config.nodeUrl.replace(/\/$/, "")}/${path.replace(/^\//, "")}`;
|
|
1656
|
+
const auth = this.withAdminAuth(method, init);
|
|
1657
|
+
return this.fetchOnceWithStatus(url, auth, this.config.timeout, auth.signal ?? void 0);
|
|
1658
|
+
}
|
|
1659
|
+
/**
|
|
1660
|
+
* @internal
|
|
1661
|
+
* Vérifie la présence du jeton admin (sinon erreur claire, sans réseau) et
|
|
1662
|
+
* injecte l'en-tête `Authorization: Bearer <token>` dans un RequestInit.
|
|
1663
|
+
*/
|
|
1664
|
+
withAdminAuth(method, init) {
|
|
1665
|
+
const token = this.requireAdminToken(method);
|
|
1666
|
+
return {
|
|
1667
|
+
...init,
|
|
1668
|
+
headers: {
|
|
1669
|
+
...init?.headers,
|
|
1670
|
+
"Authorization": `Bearer ${token}`
|
|
1671
|
+
}
|
|
1672
|
+
};
|
|
1673
|
+
}
|
|
1402
1674
|
async fetch(path, init, opts) {
|
|
1403
1675
|
return this.fetchUrl(this.config.nodeUrl, path, init, opts);
|
|
1404
1676
|
}
|
|
@@ -1406,8 +1678,26 @@ var PmsClient = class {
|
|
|
1406
1678
|
* Single-attempt HTTP request. Throws a tagged error on transient conditions
|
|
1407
1679
|
* (network failure, per-attempt abort, 5xx) so the retry loop can decide
|
|
1408
1680
|
* whether to retry. 4xx and other non-2xx are thrown as fatal HttpError.
|
|
1681
|
+
*
|
|
1682
|
+
* Thin wrapper over `fetchOnceWithStatus` that discards the HTTP status —
|
|
1683
|
+
* every caller that only needs the parsed body goes through here.
|
|
1409
1684
|
*/
|
|
1410
1685
|
async fetchOnce(url, init, attemptTimeoutMs, outerSignal) {
|
|
1686
|
+
return (await this.fetchOnceWithStatus(url, init, attemptTimeoutMs, outerSignal)).body;
|
|
1687
|
+
}
|
|
1688
|
+
/**
|
|
1689
|
+
* Single-attempt HTTP request returning BOTH the numeric HTTP status and the
|
|
1690
|
+
* parsed body. The shared fetch primitive: applies the default headers
|
|
1691
|
+
* (`Content-Type`, public `X-API-Key`), merges the caller's signal with the
|
|
1692
|
+
* per-attempt timeout, tags per-attempt aborts as `PerAttemptTimeoutError`
|
|
1693
|
+
* (so the retry loop can act on them), and constructs `HttpError` on non-2xx.
|
|
1694
|
+
*
|
|
1695
|
+
* NOTE: admin `Authorization: Bearer` is NOT injected here — it is added
|
|
1696
|
+
* per-admin-call by `withAdminAuth` so the admin token never leaks to
|
|
1697
|
+
* discovered/seed nodes during `submitBlockRacing`. Only the public,
|
|
1698
|
+
* scoped `X-API-Key` is broadcast.
|
|
1699
|
+
*/
|
|
1700
|
+
async fetchOnceWithStatus(url, init, attemptTimeoutMs, outerSignal) {
|
|
1411
1701
|
const attemptController = new AbortController();
|
|
1412
1702
|
const attemptTimer = setTimeout(() => attemptController.abort(), attemptTimeoutMs);
|
|
1413
1703
|
const signal = mergeSignalsAny(attemptController.signal, outerSignal);
|
|
@@ -1426,14 +1716,14 @@ var PmsClient = class {
|
|
|
1426
1716
|
if (!res.ok) {
|
|
1427
1717
|
const text2 = await res.text().catch(() => "");
|
|
1428
1718
|
const retryAfter = res.headers?.get?.("Retry-After") ?? null;
|
|
1429
|
-
throw new HttpError(res.status, `HTTP ${res.status} (${url}): ${text2}`, retryAfter);
|
|
1719
|
+
throw new HttpError(res.status, `HTTP ${res.status} (${url}): ${text2}`, retryAfter, text2);
|
|
1430
1720
|
}
|
|
1431
1721
|
const text = await res.text();
|
|
1432
1722
|
if (!text) {
|
|
1433
|
-
return {};
|
|
1723
|
+
return { status: res.status, body: {} };
|
|
1434
1724
|
}
|
|
1435
1725
|
try {
|
|
1436
|
-
return JSON.parse(text);
|
|
1726
|
+
return { status: res.status, body: JSON.parse(text) };
|
|
1437
1727
|
} catch {
|
|
1438
1728
|
throw new Error(`Invalid JSON response: ${text.substring(0, 50)}...`);
|
|
1439
1729
|
}
|
|
@@ -1496,11 +1786,30 @@ var PmsClient = class {
|
|
|
1496
1786
|
var HttpError = class extends Error {
|
|
1497
1787
|
status;
|
|
1498
1788
|
retryAfter;
|
|
1499
|
-
|
|
1789
|
+
/** Raw response body text (may be empty). */
|
|
1790
|
+
body;
|
|
1791
|
+
/** Stable numeric error code parsed from `{code,message}`, if present. */
|
|
1792
|
+
code;
|
|
1793
|
+
/** Engine-provided message from `{code,message}`, if present. */
|
|
1794
|
+
engineMessage;
|
|
1795
|
+
constructor(status, message, retryAfter, body = "") {
|
|
1500
1796
|
super(message);
|
|
1501
1797
|
this.name = "HttpError";
|
|
1502
1798
|
this.status = status;
|
|
1503
1799
|
this.retryAfter = retryAfter;
|
|
1800
|
+
this.body = body;
|
|
1801
|
+
if (body) {
|
|
1802
|
+
try {
|
|
1803
|
+
const parsed = JSON.parse(body);
|
|
1804
|
+
if (parsed && typeof parsed === "object" && typeof parsed.code === "number") {
|
|
1805
|
+
this.code = parsed.code;
|
|
1806
|
+
if (typeof parsed.message === "string") {
|
|
1807
|
+
this.engineMessage = parsed.message;
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
} catch {
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1504
1813
|
}
|
|
1505
1814
|
};
|
|
1506
1815
|
var PerAttemptTimeoutError = class extends Error {
|
|
@@ -1569,6 +1878,8 @@ function mergeAbortSignals(a, b) {
|
|
|
1569
1878
|
}
|
|
1570
1879
|
export {
|
|
1571
1880
|
DAY_MS,
|
|
1881
|
+
ErrorCode,
|
|
1882
|
+
HttpError,
|
|
1572
1883
|
PmsClient,
|
|
1573
1884
|
PmsWallet,
|
|
1574
1885
|
decryptPayload,
|
package/package.json
CHANGED