@tangle-network/tcloud
Version:
TypeScript SDK and CLI for Tangle Router, Sandbox, model routing, and agent service calls
283 lines (282 loc) • 11.1 kB
JavaScript
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
let _tangle_network_sandbox = require("@tangle-network/sandbox");
let node_crypto = require("node:crypto");
let _tangle_network_tcloud_attestation = require("@tangle-network/tcloud-attestation");
//#region src/sandbox.ts
const DEFAULT_SANDBOX_URL = "https://sandbox.tangle.tools";
var TCloudSandbox = class {
client;
apiKey;
baseUrl;
timeoutMs;
constructor(config) {
this.apiKey = config.apiKey;
this.baseUrl = (config.baseUrl ?? DEFAULT_SANDBOX_URL).replace(/\/+$/, "");
this.timeoutMs = config.timeoutMs;
this.client = new _tangle_network_sandbox.Sandbox({
apiKey: this.apiKey,
baseUrl: this.baseUrl,
timeoutMs: config.timeoutMs
});
}
/**
* The underlying SDK-compatible client (#44): the full
* `SandboxClient` shape (`create(options?: CreateSandboxOptions)`) that
* agent-runtime's `runLoop` and SDK-native code expect — already
* authenticated to the sandbox gateway. `TCloudSandbox.create` remains the
* convenience wrapper (attestation policy + TEE fallbacks); this exposes
* the raw surface for callers that need complete `CreateSandboxOptions`.
*/
sdkClient() {
return this.client;
}
async create(options) {
const effectiveVerify = shouldVerifyAttestation(options);
const attestationPolicy = buildAttestationPolicy(options);
const createOptions = buildSandboxCreateOptions({
...options,
verify: effectiveVerify
});
const sandbox = await this.client.create(createOptions);
this.attachTeeApiFallbacks(sandbox);
let attestation = attestationFromMetadata(sandbox.metadata);
if ((effectiveVerify || createOptions.confidential?.attestationNonce) && !attestation) {
const getTeeAttestation = sandbox.getTeeAttestation;
if (typeof getTeeAttestation !== "function") throw new Error("Installed @tangle-network/sandbox does not expose TEE attestation fetching");
attestation = (await getTeeAttestation.call(sandbox, createOptions.confidential?.attestationNonce ? { attestationNonce: createOptions.confidential.attestationNonce } : void 0)).attestation;
}
if (effectiveVerify && !attestation) throw new Error("TEE attestation verification requested but no evidence was returned");
const verification = effectiveVerify ? await (0, _tangle_network_tcloud_attestation.verifyAttestationAsync)(attestation, {
...attestationPolicy,
expectedNonce: createOptions.confidential?.attestationNonce
}) : void 0;
if (verification && !verification.valid) throw new Error(`TEE attestation verification failed: ${verification.errors.join("; ")}`);
return {
sandbox,
attestation,
verification,
attestationNonce: createOptions.confidential?.attestationNonce,
attestationStatus: {
requested: Boolean(options.tee),
evidenceReturned: Boolean(attestation),
verified: Boolean(verification?.valid),
nonceBound: Boolean(createOptions.confidential?.attestationNonce && verification?.valid),
errors: verification?.errors ?? []
}
};
}
attachTeeApiFallbacks(sandbox) {
if (typeof sandbox !== "object" || sandbox === null) return;
const target = sandbox;
if (typeof target.id !== "string" || target.id.length === 0) return;
if (typeof target.getTeeAttestation !== "function") target.getTeeAttestation = (options) => this.fetchTeeAttestation(target.id, options?.attestationNonce);
if (typeof target.getTeePublicKey !== "function") target.getTeePublicKey = () => this.fetchTeePublicKey(target.id);
}
async fetchTeeAttestation(sandboxId, attestationNonce) {
const data = await (await this.fetchSandboxApi(`/v1/sandboxes/${encodeURIComponent(sandboxId)}/tee/attestation`, {
method: attestationNonce ? "POST" : "GET",
body: attestationNonce ? JSON.stringify({ attestation_nonce: attestationNonce }) : void 0
})).json();
if (attestationNonce && !data.attestationNonce) data.attestationNonce = attestationNonce;
return data;
}
async fetchTeePublicKey(sandboxId) {
return (await this.fetchSandboxApi(`/v1/sandboxes/${encodeURIComponent(sandboxId)}/tee/public-key`, { method: "GET" })).json();
}
async fetchSandboxApi(path, options) {
const headers = new Headers(options.headers);
headers.set("Authorization", `Bearer ${this.apiKey}`);
if (options.body && !headers.has("Content-Type")) headers.set("Content-Type", "application/json");
const response = await fetch(`${this.baseUrl}${path}`, {
...options,
headers,
signal: options.signal ?? (this.timeoutMs ? AbortSignal.timeout(this.timeoutMs) : void 0)
});
if (!response.ok) {
const body = await response.text();
throw new Error(`Sandbox API ${path} failed with HTTP ${response.status}: ${body}`);
}
return response;
}
};
function createTeeAttestationChallenge(context) {
const randomHex = generateAttestationNonce(32);
if (context == null) return {
nonce: randomHex,
randomHex
};
const contextHashHex = (0, node_crypto.createHash)("sha256").update(typeof context === "string" ? Buffer.from(context) : Buffer.from(context)).digest("hex");
return {
nonce: `${randomHex}${contextHashHex}`,
randomHex,
contextHashHex
};
}
function startTeeAttestationHeartbeat(sandbox, options = {}) {
const getTeeAttestation = sandbox?.getTeeAttestation;
if (typeof getTeeAttestation !== "function") throw new Error("Sandbox does not expose TEE attestation fetching");
const intervalMs = options.intervalMs ?? 6e4;
const sessionId = options.sessionId ?? generateAttestationNonce(16);
const policy = options.tee ? buildAttestationPolicy({
tee: options.tee,
attestationPolicy: options.attestationPolicy
}) : options.attestationPolicy ?? {};
let stopped = false;
let failures = 0;
let sequence = 0;
let latest;
let timer;
let resolveDone;
const done = new Promise((resolve) => {
resolveDone = resolve;
});
const stop = () => {
if (stopped) return;
stopped = true;
if (timer) clearTimeout(timer);
resolveDone?.();
};
const ping = async (context) => {
if (stopped || options.signal?.aborted) throw new Error("TEE attestation heartbeat is stopped");
const nextSequence = sequence + 1;
const boundContext = context ?? `${sessionId}:${nextSequence}`;
const challenge = createTeeAttestationChallenge(boundContext);
const attestation = (await getTeeAttestation.call(sandbox, { attestationNonce: challenge.nonce }))?.attestation;
if (!attestation) throw new Error("TEE attestation heartbeat returned no evidence");
const verification = await (0, _tangle_network_tcloud_attestation.verifyAttestationAsync)(attestation, {
...policy,
expectedNonce: challenge.nonce
});
if (!verification.valid) throw new Error(`TEE attestation heartbeat verification failed: ${verification.errors.join("; ")}`);
const sample = {
sequence: nextSequence,
nonce: challenge.nonce,
context: boundContext,
attestation,
verification,
checkedAt: /* @__PURE__ */ new Date()
};
sequence = nextSequence;
latest = sample;
options.onSuccess?.(sample);
return sample;
};
const schedule = () => {
if (stopped || options.signal?.aborted) {
stop();
return;
}
timer = setTimeout(() => {
loop();
}, intervalMs);
};
const loop = async () => {
try {
await ping();
schedule();
} catch (error) {
failures += 1;
options.onFailure?.(error);
if (options.continueOnFailure) schedule();
else stop();
}
};
options.signal?.addEventListener("abort", stop, { once: true });
if (options.immediate === false) schedule();
else loop();
return {
stop,
ping,
get stopped() {
return stopped;
},
get failures() {
return failures;
},
get latest() {
return latest;
},
done
};
}
function buildSandboxCreateOptions(options) {
if (!options.tee && (options.sealed || options.attestationNonce || options.verify)) throw new Error("TEE options require a tee value");
const attestationNonce = options.attestationNonce === "auto" || shouldVerifyAttestation(options) && !options.attestationNonce ? generateAttestationNonce() : options.attestationNonce;
if (attestationNonce) validateAttestationNonce(attestationNonce);
return {
name: options.name,
environment: options.environment ?? options.image,
sshEnabled: options.ssh || void 0,
git: options.gitUrl ? {
url: options.gitUrl,
ref: options.gitRef
} : void 0,
resources: options.cpu || options.memoryMb || options.diskGb || options.accelerator ? {
cpuCores: options.cpu,
memoryMB: options.memoryMb,
diskGB: options.diskGb,
accelerator: options.accelerator
} : void 0,
backend: options.backend || options.agentProfile ? {
...options.backend ? { type: options.backend } : {},
...options.agentProfile ? { profile: options.agentProfile } : {}
} : void 0,
confidential: options.tee ? {
tee: options.tee,
sealed: options.sealed || void 0,
attestationNonce,
attestationRefresh: Boolean(attestationNonce)
} : void 0
};
}
function shouldVerifyAttestation(options) {
return Boolean(options.tee || options.verify);
}
function buildAttestationPolicy(options) {
if (!options.tee || options.tee === "any") return options.attestationPolicy ?? {};
const requestedTypes = acceptedAttestationTypesForTee(options.tee);
const acceptedTeeTypes = options.attestationPolicy?.acceptedTeeTypes;
if (acceptedTeeTypes?.length && !acceptedTeeTypes.some((type) => requestedTypes.includes(type))) throw new Error(`TEE attestation policy does not accept requested TEE type ${options.tee}`);
return {
...options.attestationPolicy,
acceptedTeeTypes: acceptedTeeTypes?.length ? acceptedTeeTypes.filter((type) => requestedTypes.includes(type)) : requestedTypes
};
}
function acceptedAttestationTypesForTee(tee) {
switch (tee) {
case "phala-dstack": return ["tdx", "phala-dstack"];
case "gcp": return [
"tdx",
"sev-snp",
"gcp"
];
case "azure": return ["sev-snp", "azure"];
default: return [(0, _tangle_network_tcloud_attestation.normalizeTeeType)(tee)];
}
}
function validateAttestationNonce(value) {
const normalized = value.trim().toLowerCase().replace(/^0x/, "");
if (!/^[0-9a-f]+$/.test(normalized)) throw new Error("attestation nonce must be hex");
if (normalized.length % 2 !== 0) throw new Error("attestation nonce must have even hex length");
const bytes = normalized.length / 2;
if (bytes < 32 || bytes > 64) throw new Error(`attestation nonce must be 32-64 bytes, got ${bytes}`);
}
function generateAttestationNonce(bytes = 32) {
return Array.from((0, node_crypto.randomBytes)(bytes)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
}
function attestationFromMetadata(metadata) {
const raw = metadata?.teeAttestationJson;
if (typeof raw !== "string" || raw.trim() === "") return void 0;
try {
return JSON.parse(raw);
} catch {
return;
}
}
//#endregion
exports.TCloudSandbox = TCloudSandbox;
exports.buildSandboxCreateOptions = buildSandboxCreateOptions;
exports.createTeeAttestationChallenge = createTeeAttestationChallenge;
exports.generateAttestationNonce = generateAttestationNonce;
exports.shouldVerifyAttestation = shouldVerifyAttestation;
exports.startTeeAttestationHeartbeat = startTeeAttestationHeartbeat;