@hsuite/smart-engines-sdk 3.0.2 → 3.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.js CHANGED
@@ -1014,6 +1014,7 @@ zod.z.object({
1014
1014
  // src/discovery/index.ts
1015
1015
  var discovery_exports = {};
1016
1016
  __export(discovery_exports, {
1017
+ ClusterDiscoveryClient: () => ClusterDiscoveryClient,
1017
1018
  MIRROR_NODE_URLS: () => MIRROR_NODE_URLS,
1018
1019
  MirrorNodeClient: () => MirrorNodeClient,
1019
1020
  MirrorNodeError: () => MirrorNodeError,
@@ -1386,6 +1387,185 @@ function normalizeRegistryEntry(raw) {
1386
1387
  return null;
1387
1388
  }
1388
1389
 
1390
+ // src/discovery/cluster-discovery.ts
1391
+ var ClusterDiscoveryClient = class {
1392
+ bootstrap;
1393
+ cacheTtlMs;
1394
+ fetchTimeoutMs;
1395
+ allowInsecure;
1396
+ trustAnchorClient;
1397
+ cache = null;
1398
+ constructor(config) {
1399
+ if (!config.bootstrap || config.bootstrap.length === 0) {
1400
+ throw new Error("ClusterDiscoveryClient: bootstrap must list at least one seed URL");
1401
+ }
1402
+ if (!config.allowInsecure) {
1403
+ for (const seed of config.bootstrap) {
1404
+ if (!seed.startsWith("https://")) {
1405
+ throw new Error(
1406
+ `ClusterDiscoveryClient: bootstrap seed "${seed}" is not HTTPS. Set allowInsecure=true for local development only.`
1407
+ );
1408
+ }
1409
+ }
1410
+ }
1411
+ this.bootstrap = config.bootstrap;
1412
+ this.cacheTtlMs = config.cacheTtlMs ?? 6e4;
1413
+ this.fetchTimeoutMs = config.fetchTimeoutMs ?? 5e3;
1414
+ this.allowInsecure = config.allowInsecure ?? false;
1415
+ this.trustAnchorClient = config.trustAnchor ? new ValidatorDiscoveryClient(config.trustAnchor) : null;
1416
+ }
1417
+ /**
1418
+ * Fetch the active-cluster set, with caching.
1419
+ *
1420
+ * Tries each bootstrap seed in order until one returns HTTP 200 with a
1421
+ * parseable cluster list. If all seeds fail, throws — callers can
1422
+ * handle by falling back to a previously-cached value if they want,
1423
+ * or by initializing with multiple seeds.
1424
+ */
1425
+ async getClusters(forceRefresh = false) {
1426
+ if (!forceRefresh && this.cache && this.isCacheValid()) {
1427
+ return this.cache.clusters;
1428
+ }
1429
+ const fetched = await this.fetchFromFirstAvailableSeed();
1430
+ const verified = this.trustAnchorClient ? await this.verifyAgainstTrustAnchor(fetched) : fetched;
1431
+ this.cache = { clusters: verified, lastUpdated: Date.now() };
1432
+ return verified;
1433
+ }
1434
+ /**
1435
+ * Random pick over the active-cluster set. Returns `null` when no
1436
+ * cluster passes verification (empty registry / all-rejected anchor).
1437
+ *
1438
+ * Uses `crypto.getRandomValues` when available (browsers, Deno, modern
1439
+ * Node) for cryptographic-strength randomness; `Math.random` is a
1440
+ * fallback for older runtimes where `globalThis.crypto` is undefined.
1441
+ * Discovery isn't cryptographically sensitive — load distribution is
1442
+ * the goal here.
1443
+ */
1444
+ async getRandomCluster(forceRefresh = false) {
1445
+ const clusters = await this.getClusters(forceRefresh);
1446
+ if (clusters.length === 0) return null;
1447
+ return clusters[this.pickRandomIndex(clusters.length)];
1448
+ }
1449
+ /**
1450
+ * Convenience wrapper returning just the gateway URL of a random
1451
+ * active cluster. The single most-used SDK entry point — most
1452
+ * downstream callers want `new SmartEngineClient({ baseUrl: ... })`
1453
+ * and don't care about the rest of the metadata.
1454
+ */
1455
+ async getRandomGatewayUrl(forceRefresh = false) {
1456
+ const cluster = await this.getRandomCluster(forceRefresh);
1457
+ return cluster?.endpoints.gatewayUrl ?? null;
1458
+ }
1459
+ /** Per-clusterId lookup, useful for "stick the SDK to cluster X" flows. */
1460
+ async getClusterById(clusterId, forceRefresh = false) {
1461
+ const clusters = await this.getClusters(forceRefresh);
1462
+ return clusters.find((c) => c.clusterId === clusterId) ?? null;
1463
+ }
1464
+ clearCache() {
1465
+ this.cache = null;
1466
+ }
1467
+ isCacheValid() {
1468
+ if (!this.cache) return false;
1469
+ return Date.now() - this.cache.lastUpdated < this.cacheTtlMs;
1470
+ }
1471
+ async fetchFromFirstAvailableSeed() {
1472
+ const errors = [];
1473
+ for (const seed of this.bootstrap) {
1474
+ try {
1475
+ return await this.fetchClustersFromSeed(seed);
1476
+ } catch (err) {
1477
+ errors.push(`${seed}: ${err.message}`);
1478
+ }
1479
+ }
1480
+ throw new Error(
1481
+ `ClusterDiscoveryClient: no bootstrap seed reachable. Attempts:
1482
+ ${errors.join("\n ")}`
1483
+ );
1484
+ }
1485
+ async fetchClustersFromSeed(seed) {
1486
+ const url = `${seed.replace(/\/$/, "")}/api/v3/discovery/clusters`;
1487
+ const ctrl = new AbortController();
1488
+ const timer = setTimeout(() => ctrl.abort(), this.fetchTimeoutMs);
1489
+ try {
1490
+ const res = await fetch(url, { signal: ctrl.signal });
1491
+ if (!res.ok) {
1492
+ throw new Error(`HTTP ${res.status}`);
1493
+ }
1494
+ const body = await res.json();
1495
+ if (!Array.isArray(body.clusters)) {
1496
+ throw new Error("response missing clusters[]");
1497
+ }
1498
+ return body.clusters.map((c) => this.normalizeClusterEntry(c)).filter((c) => c !== null);
1499
+ } finally {
1500
+ clearTimeout(timer);
1501
+ }
1502
+ }
1503
+ normalizeClusterEntry(raw) {
1504
+ if (typeof raw.clusterId !== "string" || !raw.clusterId) return null;
1505
+ if (!raw.endpoints || typeof raw.endpoints !== "object") return null;
1506
+ const ep = raw.endpoints;
1507
+ if (typeof ep.gatewayUrl !== "string" || !ep.gatewayUrl) return null;
1508
+ if (!this.allowInsecure && !ep.gatewayUrl.startsWith("https://")) {
1509
+ return null;
1510
+ }
1511
+ const nodeIds = Array.isArray(raw.nodeIds) ? raw.nodeIds.filter((n) => typeof n === "string") : [];
1512
+ return {
1513
+ clusterId: raw.clusterId,
1514
+ endpoints: {
1515
+ clusterId: raw.clusterId,
1516
+ gatewayUrl: ep.gatewayUrl,
1517
+ harborUrl: typeof ep.harborUrl === "string" ? ep.harborUrl : void 0,
1518
+ natsUrl: typeof ep.natsUrl === "string" ? ep.natsUrl : void 0,
1519
+ publicIp: typeof ep.publicIp === "string" ? ep.publicIp : void 0,
1520
+ region: typeof ep.region === "string" ? ep.region : void 0
1521
+ },
1522
+ nodeIds
1523
+ };
1524
+ }
1525
+ /**
1526
+ * Cross-check the HTTP-fetched cluster set against the HCS validator
1527
+ * registry. Clusters whose nodeIds are not on-chain are dropped.
1528
+ *
1529
+ * Two failure modes are deliberately silent here:
1530
+ * - the HCS read itself throws → original list is returned
1531
+ * un-verified. Trust-anchor is advisory; a network partition
1532
+ * between SDK and Hedera mirror shouldn't strand the SDK.
1533
+ * - the HCS read returns empty (mirror node lag, wrong topicId)
1534
+ * → original list is returned. Better to keep working off
1535
+ * possibly-stale-but-real data than reject every cluster.
1536
+ *
1537
+ * Both behaviors are why this is called "trust *anchor*", not
1538
+ * "trust gate". Crypto-strong gating is a follow-up.
1539
+ */
1540
+ async verifyAgainstTrustAnchor(clusters) {
1541
+ if (!this.trustAnchorClient || clusters.length === 0) return clusters;
1542
+ let onChainNodeIds;
1543
+ try {
1544
+ const validators = await this.trustAnchorClient.getValidators();
1545
+ if (validators.length === 0) return clusters;
1546
+ onChainNodeIds = new Set(validators.map((v) => v.nodeId));
1547
+ } catch {
1548
+ return clusters;
1549
+ }
1550
+ return clusters.filter((c) => {
1551
+ if (c.nodeIds.length === 0) {
1552
+ return false;
1553
+ }
1554
+ return c.nodeIds.some((id) => onChainNodeIds.has(id));
1555
+ });
1556
+ }
1557
+ pickRandomIndex(n) {
1558
+ if (n <= 1) return 0;
1559
+ const g = globalThis.crypto;
1560
+ if (g?.getRandomValues) {
1561
+ const buf = new Uint32Array(1);
1562
+ g.getRandomValues(buf);
1563
+ return buf[0] % n;
1564
+ }
1565
+ return Math.floor(Math.random() * n);
1566
+ }
1567
+ };
1568
+
1389
1569
  // src/auth/index.ts
1390
1570
  var auth_exports = {};
1391
1571
  __export(auth_exports, {
@@ -1725,14 +1905,14 @@ function createHttpClient(config) {
1725
1905
  throw new SdkHttpError(`Network error: ${err.message}`, 0, error);
1726
1906
  }
1727
1907
  }
1728
- async function upload(path, file, filename, metadata) {
1908
+ async function upload(path, file, filename, metadata, fieldName = "file") {
1729
1909
  const url = `${config.baseUrl}${path}`;
1730
1910
  const controller = new AbortController();
1731
1911
  const timeoutId = setTimeout(() => controller.abort(), timeout * 2);
1732
1912
  try {
1733
1913
  const formData = new FormData();
1734
1914
  const blob = file instanceof Blob ? file : new Blob([new Uint8Array(file)]);
1735
- formData.append("file", blob, filename);
1915
+ formData.append(fieldName, blob, filename);
1736
1916
  if (metadata) {
1737
1917
  for (const [key, value] of Object.entries(metadata)) {
1738
1918
  formData.append(key, value);
@@ -1776,7 +1956,7 @@ function createHttpClient(config) {
1776
1956
  get: (path) => request("GET", path),
1777
1957
  put: (path, body) => request("PUT", path, body),
1778
1958
  delete: (path) => request("DELETE", path),
1779
- upload,
1959
+ upload: ((path, file, filename, metadata, fieldName) => upload(path, file, filename, metadata, fieldName)),
1780
1960
  setAuthToken
1781
1961
  };
1782
1962
  return client;
@@ -1784,6 +1964,17 @@ function createHttpClient(config) {
1784
1964
  function encodePathParam(param) {
1785
1965
  return encodeURIComponent(param).replace(/%2F/gi, "");
1786
1966
  }
1967
+ function isRuleRejected(err) {
1968
+ if (!(err instanceof SdkHttpError)) return false;
1969
+ if (err.statusCode !== 403) return false;
1970
+ const d = err.details;
1971
+ if (d === null || typeof d !== "object") return false;
1972
+ const obj = d;
1973
+ if (obj.error !== "rule_rejected") return false;
1974
+ if (typeof obj.reason !== "string") return false;
1975
+ if (!Array.isArray(obj.ruleAtoms)) return false;
1976
+ return obj.ruleAtoms.every((a) => typeof a === "string");
1977
+ }
1787
1978
 
1788
1979
  // src/subscription/index.ts
1789
1980
  var subscription_exports = {};
@@ -2207,6 +2398,167 @@ var SnapshotsClient = class {
2207
2398
  }
2208
2399
  };
2209
2400
 
2401
+ // src/historical-balance/historical-balance-client.ts
2402
+ var DEFAULT_TIMEOUT_MS = 3e4;
2403
+ var ENDPOINT_PATH = "/api/v3/historical-balance/query";
2404
+ var HistoricalBalanceClientError = class extends Error {
2405
+ constructor(message, statusCode, details) {
2406
+ super(message);
2407
+ this.statusCode = statusCode;
2408
+ this.details = details;
2409
+ this.name = "HistoricalBalanceClientError";
2410
+ }
2411
+ statusCode;
2412
+ details;
2413
+ };
2414
+ var HistoricalBalanceClient = class _HistoricalBalanceClient {
2415
+ // Standalone-mode fields (set when the client builds its own fetch).
2416
+ baseUrl;
2417
+ authToken;
2418
+ apiKey;
2419
+ timeoutMs;
2420
+ fetchImpl;
2421
+ // Sub-client-mode field (set when wired via SmartEngineClient).
2422
+ http;
2423
+ constructor(config) {
2424
+ if ("http" in config) {
2425
+ this.http = config.http;
2426
+ this.timeoutMs = DEFAULT_TIMEOUT_MS;
2427
+ return;
2428
+ }
2429
+ if (!config.baseUrl) {
2430
+ throw new HistoricalBalanceClientError(
2431
+ "HistoricalBalanceClient: baseUrl is required for standalone construction",
2432
+ 400
2433
+ );
2434
+ }
2435
+ this.baseUrl = config.baseUrl.replace(/\/+$/, "");
2436
+ this.authToken = config.authToken;
2437
+ this.apiKey = config.apiKey;
2438
+ this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
2439
+ this.fetchImpl = config.fetchImpl;
2440
+ }
2441
+ /**
2442
+ * Factory used by `SmartEngineClient` to wire this sub-client onto the
2443
+ * parent's shared `HttpClient` (which already carries auth + baseUrl + a
2444
+ * `/api/v3` prefix). Routes through `/historical-balance/query` since the
2445
+ * parent's HttpClient is rooted at `/api/v3`.
2446
+ */
2447
+ static fromHttp(http) {
2448
+ return new _HistoricalBalanceClient({ http });
2449
+ }
2450
+ /**
2451
+ * Query a point-in-time balance.
2452
+ *
2453
+ * The validator returns the on-chain bigint as a base-10 decimal string.
2454
+ * Callers needing arithmetic precision should wrap the result:
2455
+ *
2456
+ * ```ts
2457
+ * const { balance } = await client.historicalBalance.getBalance({ ... });
2458
+ * const value = BigInt(balance);
2459
+ * ```
2460
+ */
2461
+ async getBalance(params) {
2462
+ this.validateParams(params);
2463
+ if (this.http) {
2464
+ return this.http.post("/historical-balance/query", params);
2465
+ }
2466
+ return this.standaloneFetch(params);
2467
+ }
2468
+ validateParams(params) {
2469
+ if (!params || typeof params !== "object") {
2470
+ throw new HistoricalBalanceClientError(
2471
+ "HistoricalBalanceClient.getBalance: params object is required",
2472
+ 400
2473
+ );
2474
+ }
2475
+ const { chain, entityId, account, atTimestamp } = params;
2476
+ if (chain !== "hedera" && chain !== "xrpl" && chain !== "polkadot") {
2477
+ throw new HistoricalBalanceClientError(
2478
+ `HistoricalBalanceClient.getBalance: unsupported chain "${String(chain)}" (expected hedera | xrpl | polkadot)`,
2479
+ 400
2480
+ );
2481
+ }
2482
+ if (typeof entityId !== "string" || entityId.length === 0) {
2483
+ throw new HistoricalBalanceClientError(
2484
+ "HistoricalBalanceClient.getBalance: entityId must be a non-empty string",
2485
+ 400
2486
+ );
2487
+ }
2488
+ if (typeof account !== "string" || account.length === 0) {
2489
+ throw new HistoricalBalanceClientError(
2490
+ "HistoricalBalanceClient.getBalance: account must be a non-empty string",
2491
+ 400
2492
+ );
2493
+ }
2494
+ if (typeof atTimestamp !== "number" || !Number.isInteger(atTimestamp) || atTimestamp <= 0) {
2495
+ throw new HistoricalBalanceClientError(
2496
+ "HistoricalBalanceClient.getBalance: atTimestamp must be a positive integer (unix seconds)",
2497
+ 400
2498
+ );
2499
+ }
2500
+ }
2501
+ async standaloneFetch(params) {
2502
+ const url = `${this.baseUrl}${ENDPOINT_PATH}`;
2503
+ const headers = {
2504
+ "Content-Type": "application/json",
2505
+ Accept: "application/json"
2506
+ };
2507
+ if (this.authToken) headers.Authorization = `Bearer ${this.authToken}`;
2508
+ if (this.apiKey) headers["X-API-Key"] = this.apiKey;
2509
+ const fetchFn = this.fetchImpl ?? (typeof fetch !== "undefined" ? fetch : void 0);
2510
+ if (!fetchFn) {
2511
+ throw new HistoricalBalanceClientError(
2512
+ "HistoricalBalanceClient: no fetch implementation available (provide fetchImpl)",
2513
+ 500
2514
+ );
2515
+ }
2516
+ const controller = new AbortController();
2517
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
2518
+ let response;
2519
+ try {
2520
+ response = await fetchFn(url, {
2521
+ method: "POST",
2522
+ headers,
2523
+ body: JSON.stringify(params),
2524
+ signal: controller.signal
2525
+ });
2526
+ } catch (err) {
2527
+ clearTimeout(timer);
2528
+ const message = err instanceof Error ? err.message : String(err);
2529
+ throw new HistoricalBalanceClientError(
2530
+ `HistoricalBalanceClient: transport error: ${message}`,
2531
+ 0,
2532
+ err
2533
+ );
2534
+ }
2535
+ clearTimeout(timer);
2536
+ let body;
2537
+ const text = await response.text();
2538
+ if (text.length > 0) {
2539
+ try {
2540
+ body = JSON.parse(text);
2541
+ } catch (err) {
2542
+ const message = err instanceof Error ? err.message : String(err);
2543
+ throw new HistoricalBalanceClientError(
2544
+ `HistoricalBalanceClient: non-JSON response (status=${response.status}): ${message}`,
2545
+ response.status,
2546
+ { raw: text }
2547
+ );
2548
+ }
2549
+ }
2550
+ if (!response.ok) {
2551
+ const detail = body && typeof body === "object" && "message" in body ? String(body.message) : `HTTP ${response.status}`;
2552
+ throw new HistoricalBalanceClientError(
2553
+ `HistoricalBalanceClient: ${detail}`,
2554
+ response.status,
2555
+ body
2556
+ );
2557
+ }
2558
+ return body;
2559
+ }
2560
+ };
2561
+
2210
2562
  // src/settlement/index.ts
2211
2563
  var settlement_exports = {};
2212
2564
  __export(settlement_exports, {
@@ -2245,6 +2597,75 @@ var SettlementClient = class {
2245
2597
  }
2246
2598
  };
2247
2599
 
2600
+ // src/governance/index.ts
2601
+ var governance_exports = {};
2602
+ __export(governance_exports, {
2603
+ GovernanceClient: () => GovernanceClient
2604
+ });
2605
+
2606
+ // src/governance/governance-client.ts
2607
+ var GovernanceClient = class {
2608
+ constructor(http) {
2609
+ this.http = http;
2610
+ }
2611
+ http;
2612
+ /**
2613
+ * Dry-run a governance proposal/vote/etc. against `GovernanceMolecule.validate(...)`.
2614
+ *
2615
+ * Returns whatever `ValidationResult` the molecule produced. A `false`
2616
+ * `isValid` is **not** an error — it is a successful "this proposal is
2617
+ * invalid" outcome and is surfaced via the normal return value.
2618
+ *
2619
+ * HTTP errors (400 malformed body, 500 unexpected) bubble up as
2620
+ * `SdkHttpError` via the shared `HttpClient` layer.
2621
+ */
2622
+ async simulate(params) {
2623
+ return this.http.post("/governance/simulate", params);
2624
+ }
2625
+ };
2626
+
2627
+ // src/personhood/index.ts
2628
+ var personhood_exports = {};
2629
+ __export(personhood_exports, {
2630
+ PERSONHOOD_VERIFIER_NOT_CONFIGURED: () => PERSONHOOD_VERIFIER_NOT_CONFIGURED,
2631
+ PersonhoodClient: () => PersonhoodClient,
2632
+ isPersonhoodVerifierNotConfigured: () => isPersonhoodVerifierNotConfigured
2633
+ });
2634
+
2635
+ // src/personhood/personhood-client.ts
2636
+ var PERSONHOOD_VERIFIER_NOT_CONFIGURED = "personhood_verifier_not_configured";
2637
+ var PersonhoodClient = class {
2638
+ constructor(http) {
2639
+ this.http = http;
2640
+ }
2641
+ http;
2642
+ /**
2643
+ * Verify a personhood proof for `candidate`.
2644
+ *
2645
+ * Returns the issued cert on accept, `null` on clean rejection. All
2646
+ * other failure modes (validation, transport, 5xx) propagate as
2647
+ * `SdkHttpError`.
2648
+ */
2649
+ async verify(params) {
2650
+ const result = await this.http.post(
2651
+ "/personhood/verify",
2652
+ {
2653
+ candidate: params.candidate,
2654
+ proof: params.proof
2655
+ }
2656
+ );
2657
+ if (result === void 0) return null;
2658
+ return result;
2659
+ }
2660
+ };
2661
+ function isPersonhoodVerifierNotConfigured(err) {
2662
+ if (!(err instanceof SdkHttpError)) return false;
2663
+ if (err.statusCode !== 503) return false;
2664
+ const d = err.details;
2665
+ if (d === null || typeof d !== "object") return false;
2666
+ return d.error === PERSONHOOD_VERIFIER_NOT_CONFIGURED;
2667
+ }
2668
+
2248
2669
  // src/client.ts
2249
2670
  var SmartEngineClient = class _SmartEngineClient {
2250
2671
  baseUrl;
@@ -2265,8 +2686,14 @@ var SmartEngineClient = class _SmartEngineClient {
2265
2686
  transactions;
2266
2687
  /** Token holder snapshot generation and retrieval */
2267
2688
  snapshots;
2689
+ /** Historical balance archive reads (chain-native bigint, returned as decimal string) */
2690
+ historicalBalance;
2268
2691
  /** Cross-chain settlement operations */
2269
2692
  settlement;
2693
+ /** Governance proposal dry-run (simulate-only) */
2694
+ governance;
2695
+ /** Personhood verification (HPP one-human-one-member) */
2696
+ personhood;
2270
2697
  constructor(config) {
2271
2698
  this.allowInsecure = config.allowInsecure ?? false;
2272
2699
  this.baseUrl = validateClientUrl(config.baseUrl, this.allowInsecure);
@@ -2287,7 +2714,10 @@ var SmartEngineClient = class _SmartEngineClient {
2287
2714
  this.ipfs = new IPFSClient(this.http);
2288
2715
  this.transactions = new TransactionsClient(this.txHttp);
2289
2716
  this.snapshots = new SnapshotsClient(this.http);
2717
+ this.historicalBalance = HistoricalBalanceClient.fromHttp(this.http);
2290
2718
  this.settlement = new SettlementClient(this.http);
2719
+ this.governance = new GovernanceClient(this.http);
2720
+ this.personhood = new PersonhoodClient(this.http);
2291
2721
  }
2292
2722
  /**
2293
2723
  * Connect to the smart-engines network with auto-discovery and authentication
@@ -2332,6 +2762,67 @@ var SmartEngineClient = class _SmartEngineClient {
2332
2762
  });
2333
2763
  return { client, validator, session };
2334
2764
  }
2765
+ /**
2766
+ * Connect to the smart-engines network via the **service-registry**
2767
+ * (PR-1 of the cluster-discovery arc). Preferred over
2768
+ * {@link connectToNetwork} once the validator pods in the target network
2769
+ * have published their cluster endpoints — the SDK auto-balances across
2770
+ * the active cluster set and rides permissionless cluster join/leave
2771
+ * without code edits.
2772
+ *
2773
+ * Fallback ladder (per `docs/ops/HANDOFF-service-registry-distribution-layer.md` §6):
2774
+ * 1. HTTP fetch `/api/v3/discovery/clusters` from each bootstrap seed.
2775
+ * 2. (Optional) HCS trust-anchor membership cross-check.
2776
+ * 3. Random-pick over the verified set.
2777
+ *
2778
+ * @example
2779
+ * ```ts
2780
+ * const { client, cluster, session } = await SmartEngineClient.connectToCluster({
2781
+ * bootstrap: ['https://sn1.testnet.hsuite.network', 'https://sn2.testnet.hsuite.network'],
2782
+ * chain: 'xrpl',
2783
+ * address: '...',
2784
+ * publicKey: '...',
2785
+ * signFn: async (challenge) => sign(challenge),
2786
+ * });
2787
+ * ```
2788
+ */
2789
+ static async connectToCluster(config) {
2790
+ const allowInsecure = config.allowInsecure ?? false;
2791
+ const discovery = new ClusterDiscoveryClient({
2792
+ bootstrap: config.bootstrap,
2793
+ allowInsecure,
2794
+ trustAnchor: config.trustAnchor ? {
2795
+ network: config.trustAnchor.network,
2796
+ registryTopicId: config.trustAnchor.registryTopicId,
2797
+ mirrorNodeUrl: config.trustAnchor.mirrorNodeUrl,
2798
+ allowInsecure
2799
+ } : void 0
2800
+ });
2801
+ const cluster = await discovery.getRandomCluster();
2802
+ if (!cluster) {
2803
+ throw new SmartEngineError2(
2804
+ "No active clusters available via bootstrap seeds. Check bootstrap URLs and network reachability.",
2805
+ 503
2806
+ );
2807
+ }
2808
+ const gatewayUrl = cluster.endpoints.gatewayUrl;
2809
+ validateClientUrl(gatewayUrl, allowInsecure);
2810
+ const auth = new ValidatorAuthClient({ security: { allowInsecure } });
2811
+ const session = await auth.authenticateWithSigner(
2812
+ gatewayUrl,
2813
+ config.chain,
2814
+ config.address,
2815
+ config.publicKey,
2816
+ config.signFn,
2817
+ config.metadata
2818
+ );
2819
+ const client = new _SmartEngineClient({
2820
+ baseUrl: gatewayUrl,
2821
+ authToken: session.token,
2822
+ allowInsecure
2823
+ });
2824
+ return { client, cluster, session };
2825
+ }
2335
2826
  /** Get the current validator URL */
2336
2827
  getBaseUrl() {
2337
2828
  return this.baseUrl;
@@ -3619,49 +4110,101 @@ var DeploymentClient = class {
3619
4110
  }
3620
4111
  http;
3621
4112
  /**
3622
- * Create (deploy) a new app
4113
+ * Step 1 allocate appId + receive ephemeral push credentials for
4114
+ * the cluster's per-tenant Harbor project.
4115
+ *
4116
+ * Each app gets its own Harbor project (`hsuite-customers-<appId>`)
4117
+ * isolated by Harbor's RBAC. The push robot returned in
4118
+ * `registry.{username, password}` is scoped to that project only —
4119
+ * it cannot read or write any other tenant's images.
4120
+ *
4121
+ * **Single-use secret discipline:** `registry.password` is returned
4122
+ * exactly once and is NOT persisted server-side. Store it locally
4123
+ * for the `docker push` and discard. To rotate, call `init` again
4124
+ * (issues a new robot under the same project).
4125
+ *
4126
+ * Use the credentials to `docker login` + `docker push`, then call
4127
+ * {@link deploy} with the pushed image tag.
3623
4128
  */
3624
- async create(request) {
3625
- return this.http.post("/api/deployment/apps", request);
4129
+ async init(request) {
4130
+ return this.http.post("/api/deployment/apps/init", request);
3626
4131
  }
3627
4132
  /**
3628
- * List all deployed apps
4133
+ * Step 3 (optional) — upload the SPA tarball.
4134
+ *
4135
+ * The tarball is content-addressed (SHA-256) and mounted read-only
4136
+ * into the customer's pod alongside the backend container. Returns
4137
+ * the hash + size so the caller can verify the upload.
4138
+ */
4139
+ async uploadFrontend(appId, bundle, filename = "bundle.tar.gz") {
4140
+ return this.http.upload(
4141
+ `/api/deployment/apps/${encodeURIComponent(appId)}/frontend`,
4142
+ bundle,
4143
+ filename,
4144
+ void 0,
4145
+ "bundle"
4146
+ );
4147
+ }
4148
+ /**
4149
+ * Step 4 — reconcile the runtime to k8s.
4150
+ *
4151
+ * Returns immediately with `status: 'deploying'`. Poll {@link status}
4152
+ * until `runtime.runtimeState === 'RUNNING'` for the URL to be live.
4153
+ */
4154
+ async deploy(appId, request) {
4155
+ return this.http.post(`/api/deployment/apps/${encodeURIComponent(appId)}/deploy`, request);
4156
+ }
4157
+ /**
4158
+ * Roll back to a previously-deployed image tag (must exist in
4159
+ * `runtime.deploymentHistory[]`).
4160
+ */
4161
+ async rollback(appId, request) {
4162
+ return this.http.post(`/api/deployment/apps/${encodeURIComponent(appId)}/rollback`, request);
4163
+ }
4164
+ /**
4165
+ * Live combined lifecycle + runtime status of an app.
4166
+ */
4167
+ async status(appId) {
4168
+ return this.http.get(`/api/deployment/apps/${encodeURIComponent(appId)}/status`);
4169
+ }
4170
+ /**
4171
+ * List all deployed apps for the authenticated developer.
3629
4172
  */
3630
4173
  async list() {
3631
4174
  return this.http.get("/api/deployment/apps");
3632
4175
  }
3633
4176
  /**
3634
- * Get app details
4177
+ * Get app details.
3635
4178
  */
3636
4179
  async get(appId) {
3637
4180
  return this.http.get(`/api/deployment/apps/${encodeURIComponent(appId)}`);
3638
4181
  }
3639
4182
  /**
3640
- * Update app configuration
4183
+ * Update app configuration. Runtime effect lands in PR-H.
3641
4184
  */
3642
4185
  async update(appId, updates) {
3643
4186
  return this.http.put(`/api/deployment/apps/${encodeURIComponent(appId)}`, updates);
3644
4187
  }
3645
4188
  /**
3646
- * Delete an app
4189
+ * Delete an app. Runtime effect (namespace teardown) lands in PR-H.
3647
4190
  */
3648
4191
  async delete(appId) {
3649
4192
  return this.http.delete(`/api/deployment/apps/${encodeURIComponent(appId)}`);
3650
4193
  }
3651
4194
  /**
3652
- * Suspend an app
4195
+ * Suspend an app. Runtime effect (scale to zero) lands in PR-H.
3653
4196
  */
3654
4197
  async suspend(appId) {
3655
4198
  return this.http.post(`/api/deployment/apps/${encodeURIComponent(appId)}/suspend`, {});
3656
4199
  }
3657
4200
  /**
3658
- * Resume a suspended app
4201
+ * Resume a suspended app. Runtime effect (scale back up) lands in PR-H.
3659
4202
  */
3660
4203
  async resume(appId) {
3661
4204
  return this.http.post(`/api/deployment/apps/${encodeURIComponent(appId)}/resume`, {});
3662
4205
  }
3663
4206
  /**
3664
- * Get deployment statistics
4207
+ * Get deployment statistics.
3665
4208
  */
3666
4209
  async getStats() {
3667
4210
  return this.http.get("/api/deployment/stats");
@@ -3791,6 +4334,78 @@ var AgentsClient = class {
3791
4334
  }
3792
4335
  };
3793
4336
 
4337
+ // src/baas/customer-session/index.ts
4338
+ var CustomerSessionClient = class {
4339
+ constructor(baseUrl, timeoutMs = 3e4) {
4340
+ this.baseUrl = baseUrl;
4341
+ this.timeoutMs = timeoutMs;
4342
+ }
4343
+ baseUrl;
4344
+ timeoutMs;
4345
+ /**
4346
+ * Step 1: ask the host to issue a fresh challenge for the customer to sign.
4347
+ */
4348
+ async challenge(input) {
4349
+ return this.fetch("POST", "/api/customer-session/challenge", input);
4350
+ }
4351
+ /**
4352
+ * Step 2: submit the customer's signed challenge. On success returns a
4353
+ * short-lived bearer JWT scoped to {appId, chain, address}.
4354
+ */
4355
+ async verify(req) {
4356
+ return this.fetch("POST", "/api/customer-session/verify", req);
4357
+ }
4358
+ /**
4359
+ * Validate a customer bearer + return the decoded session info. Used by
4360
+ * smart-app backends to authorise incoming customer requests.
4361
+ */
4362
+ async validate(bearer) {
4363
+ return this.fetch("GET", "/api/customer-session/validate", void 0, bearer);
4364
+ }
4365
+ /**
4366
+ * Revoke a customer session. Idempotent.
4367
+ */
4368
+ async end(bearer) {
4369
+ return this.fetch(
4370
+ "POST",
4371
+ "/api/customer-session/end",
4372
+ void 0,
4373
+ bearer
4374
+ );
4375
+ }
4376
+ async fetch(method, path, body, bearer) {
4377
+ const url = `${this.baseUrl}${path}`;
4378
+ const controller = new AbortController();
4379
+ const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
4380
+ try {
4381
+ const headers = { "Content-Type": "application/json" };
4382
+ if (bearer) headers["Authorization"] = `Bearer ${bearer}`;
4383
+ const init = { method, headers, signal: controller.signal };
4384
+ if (body !== void 0) init.body = JSON.stringify(body);
4385
+ const response = await fetch(url, init);
4386
+ clearTimeout(timeoutId);
4387
+ if (!response.ok) {
4388
+ const errBody = await response.json().catch(() => ({}));
4389
+ const err = new Error(
4390
+ errBody.message ?? `customer-session ${path} failed: ${response.status} ${response.statusText}`
4391
+ );
4392
+ err.status = response.status;
4393
+ throw err;
4394
+ }
4395
+ const text = await response.text();
4396
+ if (!text) return void 0;
4397
+ return JSON.parse(text);
4398
+ } catch (error) {
4399
+ clearTimeout(timeoutId);
4400
+ const e = error;
4401
+ if (e.name === "AbortError") {
4402
+ throw new Error(`customer-session ${path} timeout`);
4403
+ }
4404
+ throw error;
4405
+ }
4406
+ }
4407
+ };
4408
+
3794
4409
  // src/baas/client.ts
3795
4410
  var BaasClient = class {
3796
4411
  hostUrl;
@@ -3815,6 +4430,8 @@ var BaasClient = class {
3815
4430
  deployment;
3816
4431
  /** Autonomous smart agent management */
3817
4432
  agents;
4433
+ /** Customer→smart-app session bridge (TokenGate Face B). */
4434
+ customerSession;
3818
4435
  constructor(config) {
3819
4436
  this.allowInsecure = config.allowInsecure ?? false;
3820
4437
  this.hostUrl = validateUrl2(config.hostUrl, this.allowInsecure);
@@ -3834,6 +4451,7 @@ var BaasClient = class {
3834
4451
  this.messaging = new MessagingClient(this.http, getAppId);
3835
4452
  this.deployment = new DeploymentClient(this.http);
3836
4453
  this.agents = new AgentsClient(this.http);
4454
+ this.customerSession = new CustomerSessionClient(baseUrlWithPrefix, this.timeout);
3837
4455
  }
3838
4456
  /** Set the app ID (for newly registered apps) */
3839
4457
  setAppId(appId) {
@@ -3860,7 +4478,7 @@ var BaasClient = class {
3860
4478
  requireAppId() {
3861
4479
  if (!this.appId) {
3862
4480
  throw new BaasError(
3863
- "App ID required. Either provide appId in config or call register() first.",
4481
+ "App ID required. Provide appId in config (e.g. from a prior deployment.init() call).",
3864
4482
  400
3865
4483
  );
3866
4484
  }
@@ -3907,12 +4525,6 @@ var BaasClient = class {
3907
4525
  }
3908
4526
  this.authToken = null;
3909
4527
  }
3910
- // ========== App Registration ==========
3911
- /** Register a new app on the BaaS host */
3912
- async register(request) {
3913
- this.requireAuth();
3914
- return this.post("/api/deployment/apps", request);
3915
- }
3916
4528
  // ========== HTTP Helpers ==========
3917
4529
  requireAuth() {
3918
4530
  if (!this.authToken) {
@@ -4007,6 +4619,7 @@ exports.CapabilityNotEnabledError = CapabilityNotEnabledError;
4007
4619
  exports.CapabilityValidationError = CapabilityValidationError;
4008
4620
  exports.CircuitBreaker = CircuitBreaker;
4009
4621
  exports.CircuitBreakerOpenError = CircuitBreakerOpenError;
4622
+ exports.CustomerSessionClient = CustomerSessionClient;
4010
4623
  exports.DEFAULT_CIRCUIT_BREAKER_CONFIG = DEFAULT_CIRCUIT_BREAKER_CONFIG;
4011
4624
  exports.DEFAULT_RETRY_CONFIG = DEFAULT_RETRY_CONFIG;
4012
4625
  exports.DatabaseClient = DatabaseClient;
@@ -4015,11 +4628,16 @@ exports.DnsClient = DnsClient;
4015
4628
  exports.DomainsClient = DomainsClient;
4016
4629
  exports.ErrorCode = ErrorCode;
4017
4630
  exports.FunctionsClient = FunctionsClient;
4631
+ exports.GovernanceClient = GovernanceClient;
4632
+ exports.HistoricalBalanceClient = HistoricalBalanceClient;
4633
+ exports.HistoricalBalanceClientError = HistoricalBalanceClientError;
4018
4634
  exports.IPFSClient = IPFSClient;
4019
4635
  exports.MIRROR_NODE_URLS = MIRROR_NODE_URLS;
4020
4636
  exports.MessagingClient = MessagingClient;
4021
4637
  exports.MirrorNodeClient = MirrorNodeClient;
4022
4638
  exports.MirrorNodeError = MirrorNodeError;
4639
+ exports.PERSONHOOD_VERIFIER_NOT_CONFIGURED = PERSONHOOD_VERIFIER_NOT_CONFIGURED;
4640
+ exports.PersonhoodClient = PersonhoodClient;
4023
4641
  exports.RateLimiter = RateLimiter;
4024
4642
  exports.RoutingClient = RoutingClient;
4025
4643
  exports.SdkHttpError = SdkHttpError;
@@ -4043,6 +4661,10 @@ exports.createHttpClient = createHttpClient;
4043
4661
  exports.createResilientFetchWithBreaker = createResilientFetchWithBreaker;
4044
4662
  exports.discovery = discovery_exports;
4045
4663
  exports.encodePathParam = encodePathParam;
4664
+ exports.governance = governance_exports;
4665
+ exports.isPersonhoodVerifierNotConfigured = isPersonhoodVerifierNotConfigured;
4666
+ exports.isRuleRejected = isRuleRejected;
4667
+ exports.personhood = personhood_exports;
4046
4668
  exports.resilientFetch = resilientFetch;
4047
4669
  exports.settlement = settlement_exports;
4048
4670
  exports.subscription = subscription_exports;