@tangle-network/tcloud 0.4.1 → 0.4.3

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.
@@ -335,7 +335,7 @@ interface GatewayOptions {
335
335
  */
336
336
  interface BridgeOptions {
337
337
  /** Which harness to drive. Picks the backend on the bridge. */
338
- harness: 'claude-code' | 'claudish' | 'codex' | 'opencode' | 'kimi-code' | 'openai' | 'anthropic' | 'moonshot' | 'zai';
338
+ harness: 'claude-code' | 'claudish' | 'codex' | 'opencode' | 'kimi-code' | 'sandbox' | 'openai' | 'anthropic' | 'moonshot' | 'zai';
339
339
  /** Model id inside the harness (e.g. `sonnet`, `kimi-for-coding`, `gpt-5-codex`). Omit for harness default. */
340
340
  model?: string;
341
341
  /** Router-issued unlock token. Required unless operator has disabled the gate. */
@@ -590,6 +590,8 @@ declare class PrivateRouter {
590
590
  private usage;
591
591
  private currentIndex;
592
592
  private totalRequests;
593
+ /** Slug of the most-recently-selected operator. Populated on each selectOperator() hit. */
594
+ private _lastSelectedSlug;
593
595
  constructor(config?: Partial<PrivateRouterConfig>);
594
596
  /** Set the available operator pool */
595
597
  setOperators(operators: OperatorInfo[]): void;
@@ -614,6 +616,8 @@ declare class PrivateRouter {
614
616
  private minExposure;
615
617
  private latencyAware;
616
618
  private recordUsage;
619
+ /** Slug of the most-recently-selected operator (null before the first call). */
620
+ get lastSelectedSlug(): string | null;
617
621
  private getLastUsedOperator;
618
622
  private peekNextOperator;
619
623
  }
@@ -623,6 +627,35 @@ declare class PrivateRouter {
623
627
  * Shared between CLI and SDK.
624
628
  */
625
629
 
630
+ /** Rotation knobs for {@link TCloudClient.rotating}. */
631
+ interface RotatingRoutingConfig {
632
+ /** Router strategy. Defaults to `'min-exposure'`. */
633
+ strategy?: Extract<RoutingStrategy, 'min-exposure' | 'round-robin' | 'random'>;
634
+ /**
635
+ * Pre-seed the operator pool. When omitted the client fetches
636
+ * `/api/operators` on the first call (TTL-cached).
637
+ */
638
+ pool?: OperatorInfo[];
639
+ /** Minimum distinct operators required before routing proceeds. */
640
+ minOperators?: number;
641
+ /** Max requests per operator before forced rotation. */
642
+ maxRequestsPerOperator?: number;
643
+ /** Exclude specific operator slugs. */
644
+ excludeOperators?: string[];
645
+ /** Prefer specific regions (others kept as fallback). */
646
+ preferRegions?: string[];
647
+ }
648
+ /** Configuration accepted by {@link TCloudClient.rotating}. */
649
+ type RotatingClientConfig = Omit<TCloudConfig, 'routing'> & {
650
+ routing?: RotatingRoutingConfig;
651
+ };
652
+ /** Rotation stats surfaced by {@link TCloudClient.getRotationStats}. */
653
+ interface RotationStats {
654
+ /** Per-operator call counter. */
655
+ callsByOperator: Record<string, number>;
656
+ /** Slug of the most-recently-selected operator, if any. */
657
+ currentOperator: string | null;
658
+ }
626
659
  declare class TCloudClient {
627
660
  readonly baseURL: string;
628
661
  readonly platformURL: string;
@@ -675,6 +708,28 @@ declare class TCloudClient {
675
708
  /** Optional config passthrough (timeout, retry, etc). */
676
709
  config?: Omit<TCloudConfig, 'apiKey' | 'baseURL'>;
677
710
  }): TCloudClient;
711
+ /**
712
+ * Build a client that rotates which operator serves each call. Mirrors
713
+ * {@link TCloudClient.shielded} in shape: returns a standard `TCloudClient`
714
+ * that behaves identically for the OpenAI-compatible surface but
715
+ * dispatches each chat/completions/embeddings request through a
716
+ * {@link PrivateRouter} — different operator per call per the chosen
717
+ * strategy.
718
+ *
719
+ * ```ts
720
+ * const tcloud = TCloudClient.rotating({
721
+ * apiKey: process.env.TCLOUD_API_KEY,
722
+ * routing: { strategy: 'min-exposure' },
723
+ * })
724
+ * await tcloud.ask('hello')
725
+ * tcloud.getRotationStats() // { callsByOperator: { ... }, currentOperator: '…' }
726
+ * ```
727
+ *
728
+ * Rotation is meaningful only for stateless calls. Sandbox-harness
729
+ * sessions bind to a single operator for the lifetime of the session;
730
+ * `rotating()` clients refuse to dispatch them — see {@link bridge}.
731
+ */
732
+ static rotating(config?: RotatingClientConfig): TCloudClient;
678
733
  constructor(config?: TCloudConfig);
679
734
  /** Set the SpendAuth signer for private mode */
680
735
  setSpendAuthSigner(fn: () => Promise<SpendAuth>): void;
@@ -754,8 +809,19 @@ declare class TCloudClient {
754
809
  *
755
810
  * Sessions persist across process restarts — use the same `resume` id
756
811
  * to land on the same CLI conversation (context intact, no replay tax).
812
+ *
813
+ * Guard: clients built via {@link TCloudClient.rotating} cannot dispatch
814
+ * sandbox-harness sessions (rotation rotates per call; a sandbox session
815
+ * binds to one operator). Attempting `bridge({ harness: 'sandbox' })` on
816
+ * a rotating client throws.
757
817
  */
758
818
  bridge(cfg: BridgeOptions): BridgeSession;
819
+ /**
820
+ * Rotation stats — populated only on clients created via
821
+ * {@link TCloudClient.rotating}. Non-rotating clients return an empty
822
+ * counter and `currentOperator: null`.
823
+ */
824
+ getRotationStats(): RotationStats;
759
825
  /** Convenience: send a single message and get the text response */
760
826
  ask(message: string, modelOrOptions?: string | Partial<ChatOptions>): Promise<string>;
761
827
  /** Convenience: send a single message and get the full completion (with usage) */
@@ -1149,4 +1215,4 @@ declare class TCloudError extends Error {
1149
1215
  constructor(status: number, message: string);
1150
1216
  }
1151
1217
 
1152
- export { type ApiKeyInfo as A, type BatchJobResponse as B, type ChatCompletion as C, type RoutingStrategy as D, type EmbeddingOptions as E, type FineTuningJob as F, type GatewayOptions as G, type SpendAuth as H, type ImageGenerateOptions as I, type JobEvent as J, type SpendingLimits as K, TCloudError as L, type Model as M, type TierConfig as N, type Operator as O, type PricingTier as P, type TranscriptionResponse as Q, type RerankOptions as R, type ShieldedConfig as S, TCloudClient as T, type UpdateKeyOptions as U, type VideoGenerateOptions as V, type VideoResponse as W, type WatchJobOptions as X, type TCloudConfig as a, type AvatarGenerateRequest as b, type AvatarGenerateResponse as c, type AvatarJobStatus as d, type AvatarResult as e, type BatchRequest as f, type BridgeOptions as g, BridgeSession as h, type ChatCompletionChunk as i, type ChatMessage as j, type ChatOptions as k, type CompletionOptions as l, type CompletionResponse as m, type CreateKeyOptions as n, type CreatedKey as o, type CreditBalance as p, type EmbeddingResponse as q, type FineTuningJobOptions as r, type ImageResponse as s, type OperatorInfo as t, type PrivacyConfig as u, PrivateRouter as v, type PrivateRouterConfig as w, type RerankResponse as x, type RetryConfig as y, type RoutingConfig as z };
1218
+ export { type ApiKeyInfo as A, type BatchJobResponse as B, type ChatCompletion as C, type RotatingRoutingConfig as D, type EmbeddingOptions as E, type FineTuningJob as F, type GatewayOptions as G, type RotationStats as H, type ImageGenerateOptions as I, type JobEvent as J, type RoutingConfig as K, type RoutingStrategy as L, type Model as M, type SpendAuth as N, type Operator as O, type PricingTier as P, type SpendingLimits as Q, type RotatingClientConfig as R, type ShieldedConfig as S, TCloudClient as T, TCloudError as U, type TierConfig as V, type TranscriptionResponse as W, type UpdateKeyOptions as X, type VideoGenerateOptions as Y, type VideoResponse as Z, type WatchJobOptions as _, type TCloudConfig as a, type AvatarGenerateRequest as b, type AvatarGenerateResponse as c, type AvatarJobStatus as d, type AvatarResult as e, type BatchRequest as f, type BridgeOptions as g, BridgeSession as h, type ChatCompletionChunk as i, type ChatMessage as j, type ChatOptions as k, type CompletionOptions as l, type CompletionResponse as m, type CreateKeyOptions as n, type CreatedKey as o, type CreditBalance as p, type EmbeddingResponse as q, type FineTuningJobOptions as r, type ImageResponse as s, type OperatorInfo as t, type PrivacyConfig as u, PrivateRouter as v, type PrivateRouterConfig as w, type RerankOptions as x, type RerankResponse as y, type RetryConfig as z };
package/dist/index.cjs CHANGED
@@ -35,10 +35,23 @@ __export(index_exports, {
35
35
  TCloud: () => TCloud,
36
36
  TCloudClient: () => TCloudClient,
37
37
  TCloudError: () => TCloudError,
38
+ TCloudSandbox: () => TCloudSandbox,
39
+ assertAttestation: () => import_tcloud_attestation2.assertAttestation,
40
+ createSevSnpHardwareVerifier: () => import_tcloud_attestation2.createSevSnpHardwareVerifier,
38
41
  createShieldedClient: () => createShieldedClient,
42
+ createTdxHardwareVerifier: () => import_tcloud_attestation2.createTdxHardwareVerifier,
43
+ createTeeAttestationChallenge: () => createTeeAttestationChallenge,
39
44
  estimateCost: () => estimateCost,
45
+ generateAttestationNonce: () => generateAttestationNonce,
40
46
  generateWallet: () => generateWallet,
41
- signSpendAuth: () => signSpendAuth
47
+ normalizeTeeType: () => import_tcloud_attestation2.normalizeTeeType,
48
+ parseAttestation: () => import_tcloud_attestation2.parseAttestation,
49
+ parseSevSnpReport: () => import_tcloud_attestation2.parseSevSnpReport,
50
+ signSpendAuth: () => signSpendAuth,
51
+ startTeeAttestationHeartbeat: () => startTeeAttestationHeartbeat,
52
+ toHex: () => import_tcloud_attestation2.toHex,
53
+ verifyAttestation: () => import_tcloud_attestation2.verifyAttestation,
54
+ verifyAttestationAsync: () => import_tcloud_attestation2.verifyAttestationAsync
42
55
  });
43
56
  module.exports = __toCommonJS(index_exports);
44
57
 
@@ -54,6 +67,8 @@ var PrivateRouter = class {
54
67
  usage = /* @__PURE__ */ new Map();
55
68
  currentIndex = 0;
56
69
  totalRequests = 0;
70
+ /** Slug of the most-recently-selected operator. Populated on each selectOperator() hit. */
71
+ _lastSelectedSlug = null;
57
72
  constructor(config = {}) {
58
73
  this.config = {
59
74
  strategy: config.strategy || "round-robin",
@@ -199,6 +214,11 @@ var PrivateRouter = class {
199
214
  requestCount: (existing?.requestCount || 0) + 1,
200
215
  lastUsedAt: Date.now()
201
216
  });
217
+ this._lastSelectedSlug = op.slug;
218
+ }
219
+ /** Slug of the most-recently-selected operator (null before the first call). */
220
+ get lastSelectedSlug() {
221
+ return this._lastSelectedSlug;
202
222
  }
203
223
  getLastUsedOperator() {
204
224
  let latest = null;
@@ -256,6 +276,7 @@ var PrivateRouter = class {
256
276
  };
257
277
 
258
278
  // src/client.ts
279
+ var ROTATING_MARKER = "__tcloudRotating";
259
280
  var DEFAULT_BASE_URL = "https://router.tangle.tools/v1";
260
281
  var SDK_VERSION = "0.4.0";
261
282
  async function proxiedFetch(privacy, url, init, streaming) {
@@ -353,6 +374,54 @@ var TCloudClient = class _TCloudClient {
353
374
  const baseURL = opts.url.replace(/\/+$/, "") + "/v1";
354
375
  return new _TCloudClient({ ...opts.config ?? {}, apiKey: opts.bearer, baseURL });
355
376
  }
377
+ /**
378
+ * Build a client that rotates which operator serves each call. Mirrors
379
+ * {@link TCloudClient.shielded} in shape: returns a standard `TCloudClient`
380
+ * that behaves identically for the OpenAI-compatible surface but
381
+ * dispatches each chat/completions/embeddings request through a
382
+ * {@link PrivateRouter} — different operator per call per the chosen
383
+ * strategy.
384
+ *
385
+ * ```ts
386
+ * const tcloud = TCloudClient.rotating({
387
+ * apiKey: process.env.TCLOUD_API_KEY,
388
+ * routing: { strategy: 'min-exposure' },
389
+ * })
390
+ * await tcloud.ask('hello')
391
+ * tcloud.getRotationStats() // { callsByOperator: { ... }, currentOperator: '…' }
392
+ * ```
393
+ *
394
+ * Rotation is meaningful only for stateless calls. Sandbox-harness
395
+ * sessions bind to a single operator for the lifetime of the session;
396
+ * `rotating()` clients refuse to dispatch them — see {@link bridge}.
397
+ */
398
+ static rotating(config = {}) {
399
+ const routing = config.routing ?? {};
400
+ const strategy = routing.strategy ?? "min-exposure";
401
+ const { routing: _omit, ...base } = config;
402
+ const client = new _TCloudClient(base);
403
+ const router = new PrivateRouter({
404
+ strategy,
405
+ minOperators: routing.minOperators ?? 1,
406
+ maxRequestsPerOperator: routing.maxRequestsPerOperator,
407
+ excludeOperators: routing.excludeOperators,
408
+ preferRegions: routing.preferRegions
409
+ });
410
+ if (routing.pool && routing.pool.length > 0) {
411
+ router.setOperators(routing.pool);
412
+ client._cachedOperators = routing.pool;
413
+ client._operatorsCachedAt = Date.now();
414
+ }
415
+ ;
416
+ client.privateRouter = router;
417
+ Object.defineProperty(client, ROTATING_MARKER, {
418
+ value: true,
419
+ enumerable: false,
420
+ configurable: false,
421
+ writable: false
422
+ });
423
+ return client;
424
+ }
356
425
  constructor(config = {}) {
357
426
  this.baseURL = (config.baseURL || DEFAULT_BASE_URL).replace(/\/$/, "");
358
427
  this.platformURL = (config.platformURL || DEFAULT_PLATFORM_URL).replace(/\/$/, "");
@@ -668,10 +737,38 @@ var TCloudClient = class _TCloudClient {
668
737
  *
669
738
  * Sessions persist across process restarts — use the same `resume` id
670
739
  * to land on the same CLI conversation (context intact, no replay tax).
740
+ *
741
+ * Guard: clients built via {@link TCloudClient.rotating} cannot dispatch
742
+ * sandbox-harness sessions (rotation rotates per call; a sandbox session
743
+ * binds to one operator). Attempting `bridge({ harness: 'sandbox' })` on
744
+ * a rotating client throws.
671
745
  */
672
746
  bridge(cfg) {
747
+ if (cfg.harness === "sandbox" && this[ROTATING_MARKER] === true) {
748
+ throw new Error(
749
+ "TCloudClient.rotating() cannot dispatch sandbox-harness sessions.\nSandbox sessions bind to a single operator; rotation is meaningful only for\nstateless calls. Use TCloudClient.shielded() + AgentProfile.confidential.tee\nfor privacy-preserving sandbox execution instead."
750
+ );
751
+ }
673
752
  return new BridgeSession(this, cfg);
674
753
  }
754
+ /**
755
+ * Rotation stats — populated only on clients created via
756
+ * {@link TCloudClient.rotating}. Non-rotating clients return an empty
757
+ * counter and `currentOperator: null`.
758
+ */
759
+ getRotationStats() {
760
+ if (!this.privateRouter) {
761
+ return { callsByOperator: {}, currentOperator: null };
762
+ }
763
+ const callsByOperator = {};
764
+ for (const row of this.privateRouter.getStats().operatorBreakdown) {
765
+ callsByOperator[row.slug] = row.requests;
766
+ }
767
+ return {
768
+ callsByOperator,
769
+ currentOperator: this.privateRouter.lastSelectedSlug
770
+ };
771
+ }
675
772
  /** Convenience: send a single message and get the text response */
676
773
  async ask(message, modelOrOptions) {
677
774
  const options = typeof modelOrOptions === "string" ? { model: modelOrOptions } : modelOrOptions;
@@ -1173,6 +1270,8 @@ var BridgeSession = class _BridgeSession {
1173
1270
  this.client = client;
1174
1271
  this.cfg = cfg;
1175
1272
  }
1273
+ client;
1274
+ cfg;
1176
1275
  /** Full chat completion (non-streaming). */
1177
1276
  async chat(options) {
1178
1277
  return this.client.chat({ ...options, bridge: this.cfg });
@@ -1242,6 +1341,7 @@ var TCloudError = class extends Error {
1242
1341
  this.status = status;
1243
1342
  this.name = "TCloudError";
1244
1343
  }
1344
+ status;
1245
1345
  };
1246
1346
 
1247
1347
  // src/shielded.ts
@@ -1497,7 +1597,321 @@ async function replenishDirect(fundingKey, tokenAddress, amount, commitment, spe
1497
1597
  }
1498
1598
  }
1499
1599
 
1600
+ // src/sandbox.ts
1601
+ var import_sandbox = require("@tangle-network/sandbox");
1602
+ var import_node_crypto = require("crypto");
1603
+ var import_tcloud_attestation = require("@tangle-network/tcloud-attestation");
1604
+ var DEFAULT_SANDBOX_URL = "https://sandbox.tangle.tools";
1605
+ var TCloudSandbox = class {
1606
+ client;
1607
+ apiKey;
1608
+ baseUrl;
1609
+ timeoutMs;
1610
+ constructor(config) {
1611
+ this.apiKey = config.apiKey;
1612
+ this.baseUrl = (config.baseUrl ?? DEFAULT_SANDBOX_URL).replace(/\/+$/, "");
1613
+ this.timeoutMs = config.timeoutMs;
1614
+ this.client = new import_sandbox.Sandbox({
1615
+ apiKey: this.apiKey,
1616
+ baseUrl: this.baseUrl,
1617
+ timeoutMs: config.timeoutMs
1618
+ });
1619
+ }
1620
+ async create(options) {
1621
+ const effectiveVerify = shouldVerifyAttestation(options);
1622
+ const attestationPolicy = buildAttestationPolicy(options);
1623
+ const createOptions = buildSandboxCreateOptions({
1624
+ ...options,
1625
+ verify: effectiveVerify
1626
+ });
1627
+ const sandbox = await this.client.create(createOptions);
1628
+ this.attachTeeApiFallbacks(sandbox);
1629
+ let attestation = attestationFromMetadata(sandbox.metadata);
1630
+ if ((effectiveVerify || createOptions.confidential?.attestationNonce) && !attestation) {
1631
+ const getTeeAttestation = sandbox.getTeeAttestation;
1632
+ if (typeof getTeeAttestation !== "function") {
1633
+ throw new Error("Installed @tangle-network/sandbox does not expose TEE attestation fetching");
1634
+ }
1635
+ attestation = (await getTeeAttestation.call(
1636
+ sandbox,
1637
+ createOptions.confidential?.attestationNonce ? { attestationNonce: createOptions.confidential.attestationNonce } : void 0
1638
+ )).attestation;
1639
+ }
1640
+ if (effectiveVerify && !attestation) {
1641
+ throw new Error("TEE attestation verification requested but no evidence was returned");
1642
+ }
1643
+ const verification = effectiveVerify ? await (0, import_tcloud_attestation.verifyAttestationAsync)(attestation, {
1644
+ ...attestationPolicy,
1645
+ expectedNonce: createOptions.confidential?.attestationNonce
1646
+ }) : void 0;
1647
+ if (verification && !verification.valid) {
1648
+ throw new Error(`TEE attestation verification failed: ${verification.errors.join("; ")}`);
1649
+ }
1650
+ return {
1651
+ sandbox,
1652
+ attestation,
1653
+ verification,
1654
+ attestationNonce: createOptions.confidential?.attestationNonce,
1655
+ attestationStatus: {
1656
+ requested: Boolean(options.tee),
1657
+ evidenceReturned: Boolean(attestation),
1658
+ verified: Boolean(verification?.valid),
1659
+ nonceBound: Boolean(createOptions.confidential?.attestationNonce && verification?.valid),
1660
+ errors: verification?.errors ?? []
1661
+ }
1662
+ };
1663
+ }
1664
+ attachTeeApiFallbacks(sandbox) {
1665
+ if (typeof sandbox !== "object" || sandbox === null) return;
1666
+ const target = sandbox;
1667
+ if (typeof target.id !== "string" || target.id.length === 0) return;
1668
+ if (typeof target.getTeeAttestation !== "function") {
1669
+ target.getTeeAttestation = (options) => this.fetchTeeAttestation(target.id, options?.attestationNonce);
1670
+ }
1671
+ if (typeof target.getTeePublicKey !== "function") {
1672
+ target.getTeePublicKey = () => this.fetchTeePublicKey(target.id);
1673
+ }
1674
+ }
1675
+ async fetchTeeAttestation(sandboxId, attestationNonce) {
1676
+ const response = await this.fetchSandboxApi(
1677
+ `/v1/sandboxes/${encodeURIComponent(sandboxId)}/tee/attestation`,
1678
+ {
1679
+ method: attestationNonce ? "POST" : "GET",
1680
+ body: attestationNonce ? JSON.stringify({ attestation_nonce: attestationNonce }) : void 0
1681
+ }
1682
+ );
1683
+ const data = await response.json();
1684
+ if (attestationNonce && !data.attestationNonce) {
1685
+ data.attestationNonce = attestationNonce;
1686
+ }
1687
+ return data;
1688
+ }
1689
+ async fetchTeePublicKey(sandboxId) {
1690
+ const response = await this.fetchSandboxApi(
1691
+ `/v1/sandboxes/${encodeURIComponent(sandboxId)}/tee/public-key`,
1692
+ { method: "GET" }
1693
+ );
1694
+ return response.json();
1695
+ }
1696
+ async fetchSandboxApi(path, options) {
1697
+ const headers = new Headers(options.headers);
1698
+ headers.set("Authorization", `Bearer ${this.apiKey}`);
1699
+ if (options.body && !headers.has("Content-Type")) {
1700
+ headers.set("Content-Type", "application/json");
1701
+ }
1702
+ const response = await fetch(`${this.baseUrl}${path}`, {
1703
+ ...options,
1704
+ headers,
1705
+ signal: options.signal ?? (this.timeoutMs ? AbortSignal.timeout(this.timeoutMs) : void 0)
1706
+ });
1707
+ if (!response.ok) {
1708
+ const body = await response.text();
1709
+ throw new Error(`Sandbox API ${path} failed with HTTP ${response.status}: ${body}`);
1710
+ }
1711
+ return response;
1712
+ }
1713
+ };
1714
+ function createTeeAttestationChallenge(context) {
1715
+ const randomHex = generateAttestationNonce(32);
1716
+ if (context == null) {
1717
+ return { nonce: randomHex, randomHex };
1718
+ }
1719
+ const contextHashHex = (0, import_node_crypto.createHash)("sha256").update(typeof context === "string" ? Buffer.from(context) : Buffer.from(context)).digest("hex");
1720
+ return {
1721
+ nonce: `${randomHex}${contextHashHex}`,
1722
+ randomHex,
1723
+ contextHashHex
1724
+ };
1725
+ }
1726
+ function startTeeAttestationHeartbeat(sandbox, options = {}) {
1727
+ const getTeeAttestation = sandbox?.getTeeAttestation;
1728
+ if (typeof getTeeAttestation !== "function") {
1729
+ throw new Error("Sandbox does not expose TEE attestation fetching");
1730
+ }
1731
+ const intervalMs = options.intervalMs ?? 6e4;
1732
+ const sessionId = options.sessionId ?? generateAttestationNonce(16);
1733
+ const policy = options.tee ? buildAttestationPolicy({ tee: options.tee, attestationPolicy: options.attestationPolicy }) : options.attestationPolicy ?? {};
1734
+ let stopped = false;
1735
+ let failures = 0;
1736
+ let sequence = 0;
1737
+ let latest;
1738
+ let timer;
1739
+ let resolveDone;
1740
+ const done = new Promise((resolve) => {
1741
+ resolveDone = resolve;
1742
+ });
1743
+ const stop = () => {
1744
+ if (stopped) return;
1745
+ stopped = true;
1746
+ if (timer) clearTimeout(timer);
1747
+ resolveDone?.();
1748
+ };
1749
+ const ping = async (context) => {
1750
+ if (stopped || options.signal?.aborted) {
1751
+ throw new Error("TEE attestation heartbeat is stopped");
1752
+ }
1753
+ const nextSequence = sequence + 1;
1754
+ const boundContext = context ?? `${sessionId}:${nextSequence}`;
1755
+ const challenge = createTeeAttestationChallenge(boundContext);
1756
+ const response = await getTeeAttestation.call(sandbox, {
1757
+ attestationNonce: challenge.nonce
1758
+ });
1759
+ const attestation = response?.attestation;
1760
+ if (!attestation) {
1761
+ throw new Error("TEE attestation heartbeat returned no evidence");
1762
+ }
1763
+ const verification = await (0, import_tcloud_attestation.verifyAttestationAsync)(attestation, {
1764
+ ...policy,
1765
+ expectedNonce: challenge.nonce
1766
+ });
1767
+ if (!verification.valid) {
1768
+ throw new Error(`TEE attestation heartbeat verification failed: ${verification.errors.join("; ")}`);
1769
+ }
1770
+ const sample = {
1771
+ sequence: nextSequence,
1772
+ nonce: challenge.nonce,
1773
+ context: boundContext,
1774
+ attestation,
1775
+ verification,
1776
+ checkedAt: /* @__PURE__ */ new Date()
1777
+ };
1778
+ sequence = nextSequence;
1779
+ latest = sample;
1780
+ options.onSuccess?.(sample);
1781
+ return sample;
1782
+ };
1783
+ const schedule = () => {
1784
+ if (stopped || options.signal?.aborted) {
1785
+ stop();
1786
+ return;
1787
+ }
1788
+ timer = setTimeout(() => {
1789
+ void loop();
1790
+ }, intervalMs);
1791
+ };
1792
+ const loop = async () => {
1793
+ try {
1794
+ await ping();
1795
+ schedule();
1796
+ } catch (error) {
1797
+ failures += 1;
1798
+ options.onFailure?.(error);
1799
+ if (options.continueOnFailure) {
1800
+ schedule();
1801
+ } else {
1802
+ stop();
1803
+ }
1804
+ }
1805
+ };
1806
+ options.signal?.addEventListener("abort", stop, { once: true });
1807
+ if (options.immediate === false) {
1808
+ schedule();
1809
+ } else {
1810
+ void loop();
1811
+ }
1812
+ return {
1813
+ stop,
1814
+ ping,
1815
+ get stopped() {
1816
+ return stopped;
1817
+ },
1818
+ get failures() {
1819
+ return failures;
1820
+ },
1821
+ get latest() {
1822
+ return latest;
1823
+ },
1824
+ done
1825
+ };
1826
+ }
1827
+ function buildSandboxCreateOptions(options) {
1828
+ if (!options.tee && (options.sealed || options.attestationNonce || options.verify)) {
1829
+ throw new Error("TEE options require a tee value");
1830
+ }
1831
+ const shouldGenerateNonce = options.attestationNonce === "auto" || shouldVerifyAttestation(options) && !options.attestationNonce;
1832
+ const attestationNonce = shouldGenerateNonce ? generateAttestationNonce() : options.attestationNonce;
1833
+ if (attestationNonce) {
1834
+ validateAttestationNonce(attestationNonce);
1835
+ }
1836
+ return {
1837
+ name: options.name,
1838
+ environment: options.environment ?? options.image,
1839
+ sshEnabled: options.ssh || void 0,
1840
+ git: options.gitUrl ? {
1841
+ url: options.gitUrl,
1842
+ ref: options.gitRef
1843
+ } : void 0,
1844
+ resources: options.cpu || options.memoryMb || options.diskGb ? {
1845
+ cpuCores: options.cpu,
1846
+ memoryMB: options.memoryMb,
1847
+ diskGB: options.diskGb
1848
+ } : void 0,
1849
+ backend: options.backend ? { type: options.backend } : void 0,
1850
+ confidential: options.tee ? {
1851
+ tee: options.tee,
1852
+ sealed: options.sealed || void 0,
1853
+ attestationNonce,
1854
+ attestationRefresh: Boolean(attestationNonce)
1855
+ } : void 0
1856
+ };
1857
+ }
1858
+ function shouldVerifyAttestation(options) {
1859
+ return Boolean(options.tee || options.verify);
1860
+ }
1861
+ function buildAttestationPolicy(options) {
1862
+ if (!options.tee || options.tee === "any") return options.attestationPolicy ?? {};
1863
+ const requestedTypes = acceptedAttestationTypesForTee(options.tee);
1864
+ const acceptedTeeTypes = options.attestationPolicy?.acceptedTeeTypes;
1865
+ if (acceptedTeeTypes?.length && !acceptedTeeTypes.some((type) => requestedTypes.includes(type))) {
1866
+ throw new Error(
1867
+ `TEE attestation policy does not accept requested TEE type ${options.tee}`
1868
+ );
1869
+ }
1870
+ return {
1871
+ ...options.attestationPolicy,
1872
+ acceptedTeeTypes: acceptedTeeTypes?.length ? acceptedTeeTypes.filter((type) => requestedTypes.includes(type)) : requestedTypes
1873
+ };
1874
+ }
1875
+ function acceptedAttestationTypesForTee(tee) {
1876
+ switch (tee) {
1877
+ case "phala-dstack":
1878
+ return ["tdx", "phala-dstack"];
1879
+ case "gcp":
1880
+ return ["tdx", "sev-snp", "gcp"];
1881
+ case "azure":
1882
+ return ["sev-snp", "azure"];
1883
+ default:
1884
+ return [(0, import_tcloud_attestation.normalizeTeeType)(tee)];
1885
+ }
1886
+ }
1887
+ function validateAttestationNonce(value) {
1888
+ const normalized = value.trim().toLowerCase().replace(/^0x/, "");
1889
+ if (!/^[0-9a-f]+$/.test(normalized)) {
1890
+ throw new Error("attestation nonce must be hex");
1891
+ }
1892
+ if (normalized.length % 2 !== 0) {
1893
+ throw new Error("attestation nonce must have even hex length");
1894
+ }
1895
+ const bytes = normalized.length / 2;
1896
+ if (bytes < 32 || bytes > 64) {
1897
+ throw new Error(`attestation nonce must be 32-64 bytes, got ${bytes}`);
1898
+ }
1899
+ }
1900
+ function generateAttestationNonce(bytes = 32) {
1901
+ return Array.from((0, import_node_crypto.randomBytes)(bytes)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
1902
+ }
1903
+ function attestationFromMetadata(metadata) {
1904
+ const raw = metadata?.teeAttestationJson;
1905
+ if (typeof raw !== "string" || raw.trim() === "") return void 0;
1906
+ try {
1907
+ return JSON.parse(raw);
1908
+ } catch {
1909
+ return void 0;
1910
+ }
1911
+ }
1912
+
1500
1913
  // src/index.ts
1914
+ var import_tcloud_attestation2 = require("@tangle-network/tcloud-attestation");
1501
1915
  var TCloud = class _TCloud extends TCloudClient {
1502
1916
  constructor(config) {
1503
1917
  super(config);
@@ -1526,6 +1940,21 @@ var TCloud = class _TCloud extends TCloudClient {
1526
1940
  static shielded(config) {
1527
1941
  return createShieldedClient(config);
1528
1942
  }
1943
+ /**
1944
+ * Create a client that rotates operators per call.
1945
+ * See {@link TCloudClient.rotating} for semantics.
1946
+ *
1947
+ * ```ts
1948
+ * const client = TCloud.rotating({
1949
+ * apiKey: process.env.TCLOUD_API_KEY,
1950
+ * routing: { strategy: 'min-exposure' },
1951
+ * })
1952
+ * const stats = client.getRotationStats()
1953
+ * ```
1954
+ */
1955
+ static rotating(config) {
1956
+ return TCloudClient.rotating(config);
1957
+ }
1529
1958
  /**
1530
1959
  * Generate a new ephemeral wallet (without creating a client).
1531
1960
  *
@@ -1543,8 +1972,21 @@ var TCloud = class _TCloud extends TCloudClient {
1543
1972
  TCloud,
1544
1973
  TCloudClient,
1545
1974
  TCloudError,
1975
+ TCloudSandbox,
1976
+ assertAttestation,
1977
+ createSevSnpHardwareVerifier,
1546
1978
  createShieldedClient,
1979
+ createTdxHardwareVerifier,
1980
+ createTeeAttestationChallenge,
1547
1981
  estimateCost,
1982
+ generateAttestationNonce,
1548
1983
  generateWallet,
1549
- signSpendAuth
1984
+ normalizeTeeType,
1985
+ parseAttestation,
1986
+ parseSevSnpReport,
1987
+ signSpendAuth,
1988
+ startTeeAttestationHeartbeat,
1989
+ toHex,
1990
+ verifyAttestation,
1991
+ verifyAttestationAsync
1550
1992
  });
package/dist/index.d.cts CHANGED
@@ -1,7 +1,9 @@
1
1
  import { ShieldedWallet, generateWallet } from './shielded.cjs';
2
2
  export { createShieldedClient, estimateCost, signSpendAuth } from './shielded.cjs';
3
- import { T as TCloudClient, a as TCloudConfig } from './client-C547pugt.cjs';
4
- export { A as ApiKeyInfo, b as AvatarGenerateRequest, c as AvatarGenerateResponse, d as AvatarJobStatus, e as AvatarResult, B as BatchJobResponse, f as BatchRequest, g as BridgeOptions, h as BridgeSession, C as ChatCompletion, i as ChatCompletionChunk, j as ChatMessage, k as ChatOptions, l as CompletionOptions, m as CompletionResponse, n as CreateKeyOptions, o as CreatedKey, p as CreditBalance, E as EmbeddingOptions, q as EmbeddingResponse, F as FineTuningJob, r as FineTuningJobOptions, G as GatewayOptions, I as ImageGenerateOptions, s as ImageResponse, J as JobEvent, M as Model, O as Operator, t as OperatorInfo, P as PricingTier, u as PrivacyConfig, v as PrivateRouter, w as PrivateRouterConfig, R as RerankOptions, x as RerankResponse, y as RetryConfig, z as RoutingConfig, D as RoutingStrategy, S as ShieldedConfig, H as SpendAuth, K as SpendingLimits, L as TCloudError, N as TierConfig, Q as TranscriptionResponse, U as UpdateKeyOptions, V as VideoGenerateOptions, W as VideoResponse, X as WatchJobOptions } from './client-C547pugt.cjs';
3
+ import { T as TCloudClient, a as TCloudConfig, R as RotatingClientConfig } from './client-DkQugNHH.cjs';
4
+ export { A as ApiKeyInfo, b as AvatarGenerateRequest, c as AvatarGenerateResponse, d as AvatarJobStatus, e as AvatarResult, B as BatchJobResponse, f as BatchRequest, g as BridgeOptions, h as BridgeSession, C as ChatCompletion, i as ChatCompletionChunk, j as ChatMessage, k as ChatOptions, l as CompletionOptions, m as CompletionResponse, n as CreateKeyOptions, o as CreatedKey, p as CreditBalance, E as EmbeddingOptions, q as EmbeddingResponse, F as FineTuningJob, r as FineTuningJobOptions, G as GatewayOptions, I as ImageGenerateOptions, s as ImageResponse, J as JobEvent, M as Model, O as Operator, t as OperatorInfo, P as PricingTier, u as PrivacyConfig, v as PrivateRouter, w as PrivateRouterConfig, x as RerankOptions, y as RerankResponse, z as RetryConfig, D as RotatingRoutingConfig, H as RotationStats, K as RoutingConfig, L as RoutingStrategy, S as ShieldedConfig, N as SpendAuth, Q as SpendingLimits, U as TCloudError, V as TierConfig, W as TranscriptionResponse, X as UpdateKeyOptions, Y as VideoGenerateOptions, Z as VideoResponse, _ as WatchJobOptions } from './client-DkQugNHH.cjs';
5
+ export { TCloudSandbox, TCloudSandboxAttestationStatus, TCloudSandboxConfig, TCloudSandboxCreateOptions, TCloudSandboxCreateResult, TCloudSandboxTee, TCloudTeeAttestationChallenge, TCloudTeeAttestationHeartbeat, TCloudTeeAttestationHeartbeatOptions, TCloudTeeAttestationHeartbeatSample, createTeeAttestationChallenge, generateAttestationNonce, startTeeAttestationHeartbeat } from './sandbox.cjs';
6
+ export { AsyncAttestationPolicy, AsyncHardwareVerifier, AttestationPolicy, AttestationVerificationResult, HardwareVerifier, HardwareVerifierResult, ParsedAttestation, SevSnpReport, SevSnpVerifierOptions, TeeType, assertAttestation, createSevSnpHardwareVerifier, createTdxHardwareVerifier, normalizeTeeType, parseAttestation, parseSevSnpReport, toHex, verifyAttestation, verifyAttestationAsync } from '@tangle-network/tcloud-attestation';
5
7
  export { AgentProfile, AgentProfileCapabilities, AgentProfileMcpServer, AgentProfileModelHints, AgentProfilePermissionValue, AgentProfilePrompt, AgentProfileResources } from '@tangle-network/sandbox';
6
8
  import 'viem';
7
9
 
@@ -36,6 +38,19 @@ declare class TCloud extends TCloudClient {
36
38
  wallet: ShieldedWallet;
37
39
  stopAutoReplenish: () => void;
38
40
  };
41
+ /**
42
+ * Create a client that rotates operators per call.
43
+ * See {@link TCloudClient.rotating} for semantics.
44
+ *
45
+ * ```ts
46
+ * const client = TCloud.rotating({
47
+ * apiKey: process.env.TCLOUD_API_KEY,
48
+ * routing: { strategy: 'min-exposure' },
49
+ * })
50
+ * const stats = client.getRotationStats()
51
+ * ```
52
+ */
53
+ static rotating(config?: RotatingClientConfig): TCloudClient;
39
54
  /**
40
55
  * Generate a new ephemeral wallet (without creating a client).
41
56
  *
@@ -47,4 +62,4 @@ declare class TCloud extends TCloudClient {
47
62
  static generateWallet: typeof generateWallet;
48
63
  }
49
64
 
50
- export { ShieldedWallet, TCloud, TCloudClient, TCloudConfig, generateWallet };
65
+ export { RotatingClientConfig, ShieldedWallet, TCloud, TCloudClient, TCloudConfig, generateWallet };