@phala/dstack-sdk
Version:
654 lines (647 loc) • 21.7 kB
JavaScript
;
var fs = require('fs');
var sha512 = require('@noble/hashes/sha512');
var http = require('http');
var https = require('https');
var net = require('net');
var sha256 = require('@noble/hashes/sha256');
var utils = require('@noble/hashes/utils');
var sha3 = require('@noble/hashes/sha3');
var secp256k1 = require('@noble/curves/secp256k1');
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
var fs__default = /*#__PURE__*/_interopDefault(fs);
var http__default = /*#__PURE__*/_interopDefault(http);
var https__default = /*#__PURE__*/_interopDefault(https);
var net__default = /*#__PURE__*/_interopDefault(net);
// src/index.ts
var __version__ = "0.5.6";
function send_rpc_request(endpoint, path, payload, timeoutMs) {
return new Promise((resolve, reject) => {
const abortController = new AbortController();
let isCompleted = false;
const safeReject = (error) => {
if (!isCompleted) {
isCompleted = true;
reject(error);
}
};
const safeResolve = (result) => {
if (!isCompleted) {
isCompleted = true;
resolve(result);
}
};
const timeout = setTimeout(() => {
abortController.abort();
safeReject(new Error("request timed out"));
}, timeoutMs || 3e4);
const cleanup = () => {
clearTimeout(timeout);
abortController.signal.removeEventListener("abort", onAbort);
};
const onAbort = () => {
cleanup();
safeReject(new Error("request aborted"));
};
abortController.signal.addEventListener("abort", onAbort);
const isHttp = endpoint.startsWith("http://") || endpoint.startsWith("https://");
if (isHttp) {
const url = new URL(path, endpoint);
const options = {
method: "POST",
headers: {
"Content-Type": "application/json",
"Content-Length": Buffer.byteLength(payload),
"User-Agent": `dstack-sdk-js/${__version__}`
}
};
const req = (url.protocol === "https:" ? https__default.default : http__default.default).request(url, options, (res) => {
let data = "";
res.on("data", (chunk) => {
data += chunk;
});
res.on("end", () => {
cleanup();
try {
const result = JSON.parse(data);
safeResolve(result);
} catch (error) {
safeReject(new Error("failed to parse response"));
}
});
});
req.on("error", (error) => {
cleanup();
safeReject(error);
});
abortController.signal.addEventListener("abort", () => {
req.destroy();
});
req.write(payload);
req.end();
} else {
const client = net__default.default.createConnection({ path: endpoint }, () => {
client.write(`POST ${path} HTTP/1.1\r
`);
client.write(`Host: localhost\r
`);
client.write(`Content-Type: application/json\r
`);
client.write(`Content-Length: ${payload.length}\r
`);
client.write("\r\n");
client.write(payload);
});
let data = "";
let headers = {};
let headersParsed = false;
let contentLength = 0;
let bodyData = "";
client.on("data", (chunk) => {
data += chunk;
if (!headersParsed) {
const headerEndIndex = data.indexOf("\r\n\r\n");
if (headerEndIndex !== -1) {
const headerLines = data.slice(0, headerEndIndex).split("\r\n");
headerLines.forEach((line) => {
const [key, value] = line.split(": ");
if (key && value) {
headers[key.toLowerCase()] = value;
}
});
headersParsed = true;
contentLength = parseInt(headers["content-length"] || "0", 10);
bodyData = data.slice(headerEndIndex + 4);
}
} else {
bodyData += chunk;
}
if (headersParsed && bodyData.length >= contentLength) {
client.end();
}
});
client.on("end", () => {
cleanup();
try {
const result = JSON.parse(bodyData.slice(0, contentLength));
safeResolve(result);
} catch (error) {
safeReject(new Error("failed to parse response"));
}
});
client.on("error", (error) => {
cleanup();
safeReject(error);
});
abortController.signal.addEventListener("abort", () => {
client.destroy();
});
}
});
}
function sortObject(obj) {
if (obj === void 0 || obj === null) {
return obj;
}
if (Array.isArray(obj)) {
return obj.map(sortObject);
} else if (obj && typeof obj === "object" && obj.constructor === Object) {
return Object.keys(obj).sort().reduce((result, key) => {
const value = obj[key];
result[key] = sortObject(value);
return result;
}, {});
}
return obj;
}
function preprocessAppCompose(dic) {
const obj = { ...dic };
if (obj.runner === "bash" && "docker_compose_file" in obj) {
delete obj.docker_compose_file;
} else if (obj.runner === "docker-compose" && "bash_script" in obj) {
delete obj.bash_script;
}
if ("pre_launch_script" in obj && !obj.pre_launch_script) {
delete obj.pre_launch_script;
}
return obj;
}
function toDeterministicJson(dic) {
const ordered = sortObject(dic);
return JSON.stringify(ordered, (key, value) => {
if (typeof value === "number" && (isNaN(value) || !isFinite(value))) {
return null;
}
return value;
});
}
function getComposeHash(app_compose, normalize = false) {
if (normalize) {
app_compose = preprocessAppCompose(app_compose);
}
const manifest_str = toDeterministicJson(app_compose);
return utils.bytesToHex(sha256.sha256(new TextEncoder().encode(manifest_str)));
}
var DEFAULT_MAX_AGE_SECONDS = 300;
function bigintToBeBytes(value, length) {
const bytes = new Uint8Array(length);
for (let i = length - 1; i >= 0; i--) {
bytes[i] = Number(value & 0xffn);
value >>= 8n;
}
return bytes;
}
function hexToBytes(hex) {
if (hex.startsWith("0x") || hex.startsWith("0X")) hex = hex.slice(2);
if (hex.length % 2 !== 0 || !/^[0-9a-fA-F]*$/.test(hex)) return null;
const bytes = new Uint8Array(hex.length / 2);
for (let i = 0; i < hex.length; i += 2) {
bytes[i / 2] = parseInt(hex.substr(i, 2), 16);
}
return bytes;
}
function bytesToHex2(bytes) {
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
}
function concat(...parts) {
const total = parts.reduce((n, p) => n + p.length, 0);
const out = new Uint8Array(total);
let offset = 0;
for (const part of parts) {
out.set(part, offset);
offset += part.length;
}
return out;
}
function recoverSigner(messageHash, signature) {
try {
const sigBytes = signature.slice(0, 64);
const recovery = signature[64];
const recoveredPubKey = secp256k1.secp256k1.Signature.fromCompact(sigBytes).addRecoveryBit(recovery).recoverPublicKey(messageHash);
return "0x" + bytesToHex2(recoveredPubKey.toRawBytes(true));
} catch (error) {
console.error("signature verification failed:", error);
return null;
}
}
function verifyEnvEncryptPublicKey(publicKey, signature, appId, timestamp, options) {
if (signature.length !== 65) return null;
const ts = typeof timestamp === "bigint" ? timestamp : BigInt(timestamp);
const maxAge = options?.maxAgeSeconds ?? DEFAULT_MAX_AGE_SECONDS;
const now = BigInt(Math.floor(Date.now() / 1e3));
const age = now - ts;
if (age < -60n) {
console.error("timestamp is too far in the future");
return null;
}
if (age > BigInt(maxAge)) {
console.error(`timestamp is too old: ${age}s > ${maxAge}s`);
return null;
}
const appIdBytes = hexToBytes(appId);
if (!appIdBytes) return null;
const prefix = new TextEncoder().encode("dstack-env-encrypt-pubkey");
const separator = new TextEncoder().encode(":");
const timestampBytes = bigintToBeBytes(ts, 8);
const message = concat(
prefix,
separator,
appIdBytes,
timestampBytes,
publicKey
);
return recoverSigner(sha3.keccak_256(message), signature);
}
function verifyEnvEncryptPublicKeyLegacy(publicKey, signature, appId) {
if (signature.length !== 65) return null;
const appIdBytes = hexToBytes(appId);
if (!appIdBytes) return null;
const prefix = new TextEncoder().encode("dstack-env-encrypt-pubkey");
const separator = new TextEncoder().encode(":");
const message = concat(prefix, separator, appIdBytes, publicKey);
return recoverSigner(sha3.keccak_256(message), signature);
}
// src/index.ts
function to_hex(data) {
if (typeof data === "string") {
return Buffer.from(data).toString("hex");
}
if (data instanceof Uint8Array) {
return Buffer.from(data).toString("hex");
}
return data.toString("hex");
}
function x509key_to_uint8array(pem, max_length) {
const content = pem.replace(/-----BEGIN PRIVATE KEY-----/, "").replace(/-----END PRIVATE KEY-----/, "").replace(/\n/g, "");
const binaryDer = atob(content);
if (!max_length) {
max_length = binaryDer.length;
}
const result = new Uint8Array(max_length);
for (let i = 0; i < max_length; i++) {
result[i] = binaryDer.charCodeAt(i);
}
return result;
}
function replay_rtmr(history) {
const INIT_MR = "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000";
if (history.length === 0) {
return INIT_MR;
}
let mr = Buffer.from(INIT_MR, "hex");
for (const content of history) {
let contentBuffer = Buffer.from(content, "hex");
if (contentBuffer.length < 48) {
const padding = Buffer.alloc(48 - contentBuffer.length, 0);
contentBuffer = Buffer.concat([contentBuffer, padding]);
}
mr = Buffer.from(sha512.sha384(Buffer.concat([mr, contentBuffer])));
}
return mr.toString("hex");
}
function reply_rtmrs(event_log) {
const rtmrs = [];
for (let idx = 0; idx < 4; idx++) {
const history = event_log.filter((event) => event.imr === idx).map((event) => event.digest);
rtmrs[idx] = replay_rtmr(history);
}
return rtmrs;
}
var SECP256K1_ALGORITHMS = /* @__PURE__ */ new Set(["secp256k1", "k256", ""]);
var DstackClient = class {
constructor(endpoint = void 0) {
if (endpoint === void 0) {
if (process.env.DSTACK_SIMULATOR_ENDPOINT) {
console.warn(`Using simulator endpoint: ${process.env.DSTACK_SIMULATOR_ENDPOINT}`);
endpoint = process.env.DSTACK_SIMULATOR_ENDPOINT;
} else {
const socketPaths = [
"/var/run/dstack.sock",
"/run/dstack.sock",
"/var/run/dstack/dstack.sock",
"/run/dstack/dstack.sock"
];
endpoint = socketPaths.find((p) => fs__default.default.existsSync(p)) ?? socketPaths[0];
}
}
if (endpoint.startsWith("/") && !fs__default.default.existsSync(endpoint)) {
throw new Error(`Unix socket file ${endpoint} does not exist`);
}
this.endpoint = endpoint;
}
async ensureAlgorithmSupported(algorithm) {
if (SECP256K1_ALGORITHMS.has(algorithm)) return;
try {
await this.version();
} catch {
throw new Error(`algorithm "${algorithm}" is not supported: OS version too old (Version RPC unavailable)`);
}
}
async ensureTlsKeyOptionsSupported(featureNames) {
try {
await this.version();
} catch {
throw new Error(`TLS key options [${featureNames.join(", ")}] are not supported: OS version too old (Version RPC unavailable)`);
}
}
async getKey(path = "", purpose = "", algorithm = "secp256k1") {
await this.ensureAlgorithmSupported(algorithm);
const payload = JSON.stringify({
path,
purpose,
algorithm
});
const result = await send_rpc_request(this.endpoint, "/GetKey", payload);
return Object.freeze({
key: new Uint8Array(Buffer.from(result.key, "hex")),
signature_chain: result.signature_chain.map((sig) => new Uint8Array(Buffer.from(sig, "hex"))),
__name__: "GetKeyResponse"
});
}
async getTlsKey(options = {}) {
const {
subject = "",
altNames = [],
usageRaTls = false,
usageServerAuth = true,
usageClientAuth = false,
notBefore,
notAfter,
withAppInfo
} = options;
const newFeatures = [];
if (notBefore !== void 0) newFeatures.push("notBefore");
if (notAfter !== void 0) newFeatures.push("notAfter");
if (withAppInfo !== void 0) newFeatures.push("withAppInfo");
if (newFeatures.length > 0) {
await this.ensureTlsKeyOptionsSupported(newFeatures);
}
let raw = {
subject,
usage_ra_tls: usageRaTls,
usage_server_auth: usageServerAuth,
usage_client_auth: usageClientAuth
};
if (altNames && altNames.length) {
raw["alt_names"] = altNames;
}
if (notBefore !== void 0) {
raw["not_before"] = notBefore;
}
if (notAfter !== void 0) {
raw["not_after"] = notAfter;
}
if (withAppInfo !== void 0) {
raw["with_app_info"] = withAppInfo;
}
const payload = JSON.stringify(raw);
const result = await send_rpc_request(this.endpoint, "/GetTlsKey", payload);
const asUint8Array = (length) => x509key_to_uint8array(result.key, length);
return Object.freeze({
...result,
asUint8Array,
__name__: "GetTlsKeyResponse"
});
}
async getQuote(report_data) {
let hex = to_hex(report_data);
if (hex.length > 128) {
throw new Error(`Report data is too large, it should be less than 64 bytes.`);
}
const payload = JSON.stringify({ report_data: hex });
const result = await send_rpc_request(this.endpoint, "/GetQuote", payload);
if ("error" in result) {
const err = result["error"];
throw new Error(err);
}
Object.defineProperty(result, "replayRtmrs", {
get: () => () => reply_rtmrs(JSON.parse(result.event_log)),
enumerable: true,
configurable: false
});
return Object.freeze(result);
}
async attest(report_data) {
let hex = to_hex(report_data);
if (hex.length > 128) {
throw new Error(`Report data is too large, it should be less than 64 bytes.`);
}
const payload = JSON.stringify({ report_data: hex });
const result = await send_rpc_request(this.endpoint, "/Attest", payload);
if ("error" in result) {
const err = result["error"];
throw new Error(err);
}
return Object.freeze({
__name__: "AttestResponse",
attestation: result.attestation
});
}
async info() {
const result = await send_rpc_request(this.endpoint, "/Info", "{}");
return Object.freeze({
...result,
tcb_info: JSON.parse(result.tcb_info)
});
}
/**
* Query the guest-agent version.
*
* Returns the version on OS >= 0.5.7.
* Throws on older OS versions that lack the Version RPC.
*/
async version() {
const result = await send_rpc_request(this.endpoint, "/Version", "{}");
return Object.freeze({
...result,
__name__: "VersionResponse"
});
}
async isReachable() {
try {
await send_rpc_request(this.endpoint, "/Info", "{}", 500);
return true;
} catch (error) {
return false;
}
}
/**
* Emit an event. This extends the event to RTMR3 on TDX platform.
*
* Requires dstack OS 0.5.0 or later.
*
* @param event The event name
* @param payload The event data as string or Buffer or Uint8Array
*/
async emitEvent(event, payload) {
if (!event) {
throw new Error("Event name cannot be empty");
}
const hexPayload = to_hex(payload);
await send_rpc_request(
this.endpoint,
"/EmitEvent",
JSON.stringify({
event,
payload: hexPayload
})
);
}
/**
* Signs a payload using a derived key.
* @param algorithm The algorithm to use (e.g., "ed25519", "secp256k1", "secp256k1_prehashed")
* @param data The data to sign. If algorithm is "secp256k1_prehashed", this must be a 32-byte hash.
* @returns A SignResponse containing the signature, signature chain, and public key.
*/
async sign(algorithm, data) {
const hexData = to_hex(data);
if (algorithm === "secp256k1_prehashed" && hexData.length !== 64) {
throw new Error(`Pre-hashed signing requires a 32-byte digest, but received ${hexData.length / 2} bytes`);
}
const payload = JSON.stringify({
algorithm,
data: hexData
});
const result = await send_rpc_request(this.endpoint, "/Sign", payload);
return Object.freeze({
signature: new Uint8Array(Buffer.from(result.signature, "hex")),
signature_chain: result.signature_chain.map((sig) => new Uint8Array(Buffer.from(sig, "hex"))),
public_key: new Uint8Array(Buffer.from(result.public_key, "hex")),
__name__: "SignResponse"
});
}
/**
* Verifies a payload signature.
* @param algorithm The algorithm to use (e.g., "ed25519", "secp256k1", "secp256k1_prehashed")
* @param data The data that was signed.
* @param signature The signature to verify.
* @param publicKey The public key to use for verification.
* @returns A VerifyResponse indicating if the signature is valid.
*/
async verify(algorithm, data, signature, publicKey) {
const payload = JSON.stringify({
algorithm,
data: to_hex(data),
signature: to_hex(signature),
public_key: to_hex(publicKey)
});
const result = await send_rpc_request(this.endpoint, "/Verify", payload);
return Object.freeze({
...result,
__name__: "VerifyResponse"
});
}
//
// Legacy methods for backward compatibility with a warning to notify users about migrating to new methods.
// These methods don't mean fully compatible as past, but we keep them here until next major version.
//
/**
* @deprecated Use getKey instead.
* @param path The path to the key.
* @param subject The subject of the key.
* @param altNames The alternative names of the key.
* @returns The key.
*/
async deriveKey(path, subject, altNames) {
throw new Error("deriveKey is deprecated, please use getKey instead.");
}
/**
* @deprecated Use getQuote instead.
* @param report_data The report data.
* @param hash_algorithm The hash algorithm.
* @returns The quote.
*/
async tdxQuote(report_data, hash_algorithm) {
console.warn("tdxQuote is deprecated, please use getQuote instead");
if (hash_algorithm !== "raw") {
throw new Error("tdxQuote only supports raw hash algorithm.");
}
return this.getQuote(report_data);
}
};
var TappdClient = class extends DstackClient {
constructor(endpoint = void 0) {
if (endpoint === void 0) {
if (process.env.TAPPD_SIMULATOR_ENDPOINT) {
console.warn(`Using tappd endpoint: ${process.env.TAPPD_SIMULATOR_ENDPOINT}`);
endpoint = process.env.TAPPD_SIMULATOR_ENDPOINT;
} else {
const socketPaths = [
"/var/run/tappd.sock",
"/run/tappd.sock",
"/var/run/dstack/tappd.sock",
"/run/dstack/tappd.sock"
];
endpoint = socketPaths.find((p) => fs__default.default.existsSync(p)) ?? socketPaths[0];
}
}
console.warn("TappdClient is deprecated, please use DstackClient instead");
super(endpoint);
}
/**
* @deprecated Use getKey instead.
* @param path The path to the key.
* @param subject The subject of the key.
* @param altNames The alternative names of the key.
* @returns The key.
*/
async deriveKey(path, subject, alt_names) {
console.warn("deriveKey is deprecated, please use getKey instead");
let raw = { path: path || "", subject: subject || path || "" };
if (alt_names && alt_names.length) {
raw["alt_names"] = alt_names;
}
const payload = JSON.stringify(raw);
const result = await send_rpc_request(this.endpoint, "/prpc/Tappd.DeriveKey", payload);
const asUint8Array = (length) => x509key_to_uint8array(result.key, length);
return Object.freeze({
...result,
asUint8Array,
__name__: "GetTlsKeyResponse"
});
}
/**
* @deprecated Use getQuote instead.
* @param report_data The report data.
* @param hash_algorithm The hash algorithm.
* @returns The quote.
*/
async tdxQuote(report_data, hash_algorithm) {
console.warn("tdxQuote is deprecated, please use getQuote instead");
let hex = to_hex(report_data);
if (hash_algorithm === "raw") {
if (hex.length > 128) {
throw new Error(`Report data is too large, it should less then 64 bytes when hash_algorithm is raw.`);
}
if (hex.length < 128) {
hex = hex.padStart(128, "0");
}
}
const payload = JSON.stringify({ report_data: hex, hash_algorithm });
const result = await send_rpc_request(this.endpoint, "/prpc/Tappd.TdxQuote", payload);
if ("error" in result) {
const err = result["error"];
throw new Error(err);
}
Object.defineProperty(result, "replayRtmrs", {
get: () => () => reply_rtmrs(JSON.parse(result.event_log)),
enumerable: true,
configurable: false
});
return Object.freeze(result);
}
async isReachable() {
try {
await send_rpc_request(this.endpoint, "/prpc/Tappd.Info", "{}", 500);
return true;
} catch (error) {
return false;
}
}
};
exports.DstackClient = DstackClient;
exports.TappdClient = TappdClient;
exports.getComposeHash = getComposeHash;
exports.to_hex = to_hex;
exports.verifyEnvEncryptPublicKey = verifyEnvEncryptPublicKey;
exports.verifyEnvEncryptPublicKeyLegacy = verifyEnvEncryptPublicKeyLegacy;