@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/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
- constructor(status, message, retryAfter) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@empereur-rouge/pms-sdk",
3
- "version": "0.5.0",
3
+ "version": "0.7.1",
4
4
  "description": "TypeScript SDK for PMS (Planetary Monetary System) — wallet management, transactions, NFTs, and DAG interactions",
5
5
  "author": "empereur-rouge",
6
6
  "license": "MIT",