JavaScript / TypeScript client for the dstack guest agent. Derive deterministic keys, generate TDX attestation quotes, issue TLS certificates, sign / verify payloads, and encrypt environment variables for KMS-managed deployments — all against the guest agent socket inside a confidential VM (CVM).
npm install @phala/dstack-sdk @noble/hashes@noble/hashes is the only required peer dependency (used by the core for sha256 / sha384). Install the matching peer when you import a submodule:
| Import path | Extra peer dependency |
|---|---|
@phala/dstack-sdk/viem |
viem |
@phala/dstack-sdk/solana |
@solana/web3.js |
@phala/dstack-sdk/encrypt-env-vars |
@noble/curves |
@phala/dstack-sdk/verify-env-encrypt-public-key |
@noble/curves |
Breaking change in 0.5.8. Prior releases listed
@solana/web3.js,viem, and@noble/curvesunderoptionalDependencies, so npm installed them automatically. They are now opt-in peers — install them yourself when you use the corresponding submodule.
Node 18+ supported. Tested through Node 24.
import { DstackClient } from '@phala/dstack-sdk'
const client = new DstackClient()
const key = await client.getKey('wallet/eth')
console.log(Buffer.from(key.key).toString('hex'))
const quote = await client.getQuote('app-state-snapshot')
console.log(quote.quote)
console.log(quote.replayRtmrs())The constructor probes /var/run/dstack.sock, then /run/dstack.sock, then the /var/run/dstack/ and /run/dstack/ variants. Pass an explicit endpoint for HTTP or for a non-default socket:
const client = new DstackClient('http://localhost:8090') // simulator
const client = new DstackClient('/run/dstack/dstack.sock') // custom pathDSTACK_SIMULATOR_ENDPOINT overrides the default when set.
Derive a deterministic key. Same (app_id, path, purpose, algorithm) always returns the same key; different apps deriving on the same path get different keys.
const eth = await client.getKey('wallet/ethereum') // secp256k1 (default)
const sol = await client.getKey('wallet/solana', 'mainnet', 'ed25519') // ed25519Returns { key: Uint8Array, signature_chain: Uint8Array[] }. The signature chain proves the key was derived inside a genuine TEE.
algorithm: 'secp256k1' (default), 'k256' (alias), or 'ed25519'. ed25519 requires guest agent ≥ 0.5.7.
Generate a fresh random TLS keypair plus certificate chain. Every call returns a new key — use getKey for deterministic material.
const tls = await client.getTlsKey({
subject: 'api.example.com',
altNames: ['localhost', '127.0.0.1'],
usageRaTls: true, // embed TDX quote in cert extension
})Options: subject, altNames, usageRaTls, usageServerAuth (default true), usageClientAuth (default false), and — on guest agent ≥ 0.5.7 — notBefore, notAfter (Unix seconds), withAppInfo. The client probes version() before sending the new options and throws a clear error on older agents instead of silently dropping them.
Returns { key: string, certificate_chain: string[], asUint8Array(maxLength?) }. key is PEM-encoded.
Generate a raw TDX quote. reportData is up to 64 bytes (string, Buffer, or Uint8Array).
const quote = await client.getQuote('user:alice:nonce123')
quote.quote // hex-encoded TDX quote
quote.event_log // JSON string of measured events
quote.replayRtmrs() // recompute RTMR[0..3] from the event logVersioned dstack attestation that works across TDX / GCP / Nitro providers. Preferred for cross-platform verifiers.
const { attestation } = await client.attest('app-state-snapshot')App identity and TCB metadata.
const info = await client.info()
info.app_id // application identifier
info.instance_id // CVM instance identifier
info.tcb_info // parsed { mrtd, rtmr0..3, event_log, ... }
info.compose_hash
info.cloud_vendor // e.g. "Google" (guest agent ≥ 0.5.7)
info.cloud_product // e.g. "Google Compute Engine" (guest agent ≥ 0.5.7)Returns { version, rev } of the guest agent. Throws on agents older than 0.5.7 (the RPC didn't exist).
Sign data with a derived key. The SDK rejects mismatched input early — secp256k1_prehashed requires a 32-byte digest.
const res = await client.sign('ed25519', 'hello dstack')
res.signature // Uint8Array
res.public_key // Uint8Array
res.signature_chain // Uint8Array[] — proves the signing key came from this TEEAlgorithms: ed25519, secp256k1, secp256k1_prehashed. Requires guest agent ≥ 0.5.7.
const ok = await client.verify('ed25519', 'hello dstack', res.signature, res.public_key)
ok.valid // booleanExtends RTMR3 with a custom event. The event becomes part of the next quote's event log and cannot be removed.
await client.emitEvent('config_loaded', 'v1.0.0')Requires guest agent ≥ 0.5.0.
Sub-500ms probe against /Info. Returns a boolean and never throws — useful for liveness checks.
import { toViemAccountSecure } from '@phala/dstack-sdk/viem'
import { createWalletClient, http } from 'viem'
import { mainnet } from 'viem/chains'
const key = await client.getKey('wallet/ethereum')
const account = toViemAccountSecure(key)
const wallet = createWalletClient({ account, chain: mainnet, transport: http() })toViemAccountSecure hashes the derived key with SHA-256 before passing it to viem's privateKeyToAccount. The unhashed alternative toViemAccount is kept for migration only and emits a warning.
import { toKeypairSecure } from '@phala/dstack-sdk/solana'
const key = await client.getKey('wallet/solana')
const keypair = toKeypairSecure(key)
console.log(keypair.publicKey.toBase58())Same pattern as the Ethereum helper. toKeypair is the unhashed legacy variant.
import { getComposeHash, type AppCompose } from '@phala/dstack-sdk/get-compose-hash'
const compose: AppCompose = {
manifest_version: 2,
name: 'my-app',
runner: 'docker-compose',
docker_compose_file: '...',
kms_enabled: true,
}
const hash = getComposeHash(compose)
const normalized = getComposeHash(compose, true) // strip bash_script/docker_compose_file overlapPure function — no TEE call required. Produces the canonical SHA-256 used by the on-chain KMS allowlist.
The full deployment flow mirrors vmm-cli.py: fetch the env-encrypt public key from KMS, verify its signature locally, then ECIES-encrypt the env vars against it.
import {
verifyEnvEncryptPublicKey,
verifyEnvEncryptPublicKeyLegacy,
} from '@phala/dstack-sdk'
import { encryptEnvVars, type EnvVar } from '@phala/dstack-sdk/encrypt-env-vars'
const response = await fetch(`${kmsUrl}/prpc/GetAppEnvEncryptPubKey?json`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ app_id: appId }),
}).then(r => r.json())
const publicKey = Buffer.from(response.public_key, 'hex')
// Prefer v1 (timestamp-protected against replay)
let signer = response.signature_v1
? verifyEnvEncryptPublicKey(
publicKey,
Buffer.from(response.signature_v1, 'hex'),
appId,
BigInt(response.timestamp),
)
: null
// Fall back to legacy signature on older KMS
if (!signer && response.signature) {
signer = verifyEnvEncryptPublicKeyLegacy(
publicKey,
Buffer.from(response.signature, 'hex'),
appId,
)
}
if (!signer) throw new Error('KMS signature did not verify')
const envs: EnvVar[] = [
{ key: 'DATABASE_URL', value: 'postgresql://…' },
{ key: 'API_KEY', value: 'sk-test-1234' },
]
const encrypted = await encryptEnvVars(envs, response.public_key)Verify functions return the signer's compressed public key (hex) on success, or null on failure. Check the signer against your trusted-signer whitelist before encrypting.
| Feature | Minimum guest agent |
|---|---|
getKey, getTlsKey, getQuote, info |
0.3.x |
emitEvent |
0.5.0 |
attest, sign, verify, version, ed25519 keys, info.cloud_vendor / cloud_product, getTlsKey notBefore / notAfter / withAppInfo |
0.5.7 |
The SDK's release versions track guest agent versions — 0.5.8-x targets dstack 0.5.7+.
Run the standalone simulator instead of a real TDX host:
cd dstack/sdk/simulator
./build.sh
./dstack-simulator
export DSTACK_SIMULATOR_ENDPOINT=http://localhost:8090Then point new DstackClient() at the simulator (it picks up DSTACK_SIMULATOR_ENDPOINT automatically).
TappdClient and its deriveKey / tdxQuote methods are deprecated but still exported. Replace them with DstackClient and the new methods:
| Old | New |
|---|---|
new TappdClient() |
new DstackClient() |
client.deriveKey(path, subject) |
client.getTlsKey({ subject }) |
client.tdxQuote(data) |
client.getQuote(data) |
/var/run/tappd.sock |
/var/run/dstack.sock |
toViemAccount and toKeypair are kept for the same reason; prefer their Secure variants in new code.
Apache-2.0