diff --git a/package-lock.json b/package-lock.json index d51f54d..2376fd9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,16 @@ { "name": "opengradient-sdk", - "version": "2.0.0", + "version": "2.1.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "opengradient-sdk", - "version": "2.0.0", + "version": "2.1.0", "license": "MIT", "dependencies": { + "undici": "^6.21.0", + "viem": "^2.21.0", "x402-fetch": "^1.2.0" }, "devDependencies": { @@ -10571,6 +10573,15 @@ "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==", "license": "MIT" }, + "node_modules/undici": { + "version": "6.25.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.25.0.tgz", + "integrity": "sha512-ZgpWDC5gmNiuY9CnLVXEH8rl50xhRCuLNA97fAUnKi8RRuV4E6KG31pDTsLVUKnohJE0I3XDrTeEydAXRw47xg==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, "node_modules/undici-types": { "version": "6.20.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", @@ -18350,6 +18361,11 @@ "resolved": "https://registry.npmjs.org/uncrypto/-/uncrypto-0.1.3.tgz", "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==" }, + "undici": { + "version": "6.25.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.25.0.tgz", + "integrity": "sha512-ZgpWDC5gmNiuY9CnLVXEH8rl50xhRCuLNA97fAUnKi8RRuV4E6KG31pDTsLVUKnohJE0I3XDrTeEydAXRw47xg==" + }, "undici-types": { "version": "6.20.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", diff --git a/package.json b/package.json index 3ccaaa2..c2b2382 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "opengradient-sdk", - "version": "2.0.0", + "version": "2.1.0", "description": "Official TypeScript SDK for OpenGradient TEE LLM chat and completion", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -52,6 +52,8 @@ "testEnvironment": "node" }, "dependencies": { + "undici": "^6.21.0", + "viem": "^2.21.0", "x402-fetch": "^1.2.0" } } diff --git a/src/abi/teeRegistry.ts b/src/abi/teeRegistry.ts new file mode 100644 index 0000000..af49fcf --- /dev/null +++ b/src/abi/teeRegistry.ts @@ -0,0 +1,40 @@ +/** + * Minimal ABI for the on-chain TEE Registry contract. + * + * Only the read-only methods needed to discover active TEE endpoints and + * fetch their pinned TLS certificates are included. + */ +export const TEE_REGISTRY_ABI = [ + { + inputs: [{ internalType: "uint8", name: "teeType", type: "uint8" }], + name: "getActiveTEEs", + outputs: [ + { + components: [ + { internalType: "address", name: "owner", type: "address" }, + { internalType: "address", name: "paymentAddress", type: "address" }, + { internalType: "string", name: "endpoint", type: "string" }, + { internalType: "bytes", name: "publicKey", type: "bytes" }, + { internalType: "bytes", name: "tlsCertificate", type: "bytes" }, + { internalType: "bytes32", name: "pcrHash", type: "bytes32" }, + { internalType: "uint8", name: "teeType", type: "uint8" }, + { internalType: "bool", name: "enabled", type: "bool" }, + { internalType: "uint256", name: "registeredAt", type: "uint256" }, + { internalType: "uint256", name: "lastHeartbeatAt", type: "uint256" }, + ], + internalType: "struct TEERegistry.TEEInfo[]", + name: "", + type: "tuple[]", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "bytes32", name: "teeId", type: "bytes32" }], + name: "isTEEActive", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, +] as const; diff --git a/src/client.ts b/src/client.ts index dbf294f..c7ba7a7 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1,9 +1,15 @@ import { LLM } from "./llm"; import { ClientConfig } from "./types"; +import { + RegistryTEEConnection, + StaticTEEConnection, + type TEEConnection, +} from "./teeConnection"; +import { TEERegistry } from "./teeRegistry"; import { DEFAULT_NETWORK_FILTER, - DEFAULT_OPENGRADIENT_LLM_SERVER_URL, - DEFAULT_OPENGRADIENT_LLM_STREAMING_SERVER_URL, + DEFAULT_OG_RPC_URL, + DEFAULT_TEE_REGISTRY_ADDRESS, } from "./defaults"; /** @@ -12,6 +18,11 @@ import { * Provides access to LLM chat and completion via OpenGradient's TEE * (Trusted Execution Environment) with x402 payment protocol. * + * By default, the TEE endpoint is resolved from the on-chain TEE registry and + * the TLS certificate is pinned to the value stored at registration time. + * Pass `llmServerUrl` to override with a hardcoded URL (development / + * self-hosted TEE servers; TLS verification is disabled). + * * Usage: * const client = new Client({ privateKey: "0x..." }); * const result = await client.llm.chat({ @@ -29,14 +40,27 @@ export class Client { : `0x${config.privateKey}` ) as `0x${string}`; + let connection: TEEConnection; + if (config.llmServerUrl) { + connection = new StaticTEEConnection(config.llmServerUrl); + } else { + const registry = new TEERegistry( + config.rpcUrl ?? DEFAULT_OG_RPC_URL, + config.teeRegistryAddress ?? DEFAULT_TEE_REGISTRY_ADDRESS, + ); + connection = new RegistryTEEConnection(registry); + } + this.llm = new LLM({ privateKey, network: config.network ?? DEFAULT_NETWORK_FILTER, maxPaymentValue: config.maxPaymentValue, - serverUrl: config.llmServerUrl ?? DEFAULT_OPENGRADIENT_LLM_SERVER_URL, - streamingServerUrl: - config.llmStreamingServerUrl ?? - DEFAULT_OPENGRADIENT_LLM_STREAMING_SERVER_URL, + connection, }); } + + /** Tear down dispatchers and any background refresh timers. */ + async close(): Promise { + await this.llm.close(); + } } diff --git a/src/defaults.ts b/src/defaults.ts index 74ba4e9..e3b713d 100644 --- a/src/defaults.ts +++ b/src/defaults.ts @@ -1,14 +1,14 @@ /** - * Default OpenGradient TEE LLM server URL. + * Default RPC URL for the chain hosting the on-chain TEE registry. */ -export const DEFAULT_OPENGRADIENT_LLM_SERVER_URL = - "https://llmogevm.opengradient.ai"; +export const DEFAULT_OG_RPC_URL = "https://ogevmdevnet.opengradient.ai"; /** - * Default OpenGradient TEE LLM streaming server URL. + * Default address of the on-chain TEERegistry contract used to discover + * verified TEE LLM endpoints and their pinned TLS certificates. */ -export const DEFAULT_OPENGRADIENT_LLM_STREAMING_SERVER_URL = - "https://llmogevm.opengradient.ai"; +export const DEFAULT_TEE_REGISTRY_ADDRESS = + "0x4e72238852f3c918f4E4e57AeC9280dDB0c80248"; /** * Default x402 settlement network. OpenGradient settles in OPG on Base. diff --git a/src/index.ts b/src/index.ts index 1bab9fb..1955ba4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -17,10 +17,24 @@ export type { ToolFunction, } from "./types"; +export { + TEERegistry, + TEE_TYPE_LLM_PROXY, + TEE_TYPE_VALIDATOR, +} from "./teeRegistry"; +export type { TEEEndpoint } from "./teeRegistry"; + +export { + RegistryTEEConnection, + StaticTEEConnection, + buildPinnedAgent, +} from "./teeConnection"; +export type { ActiveTEE, TEEConnection } from "./teeConnection"; + export { DEFAULT_NETWORK_FILTER, - DEFAULT_OPENGRADIENT_LLM_SERVER_URL, - DEFAULT_OPENGRADIENT_LLM_STREAMING_SERVER_URL, + DEFAULT_OG_RPC_URL, + DEFAULT_TEE_REGISTRY_ADDRESS, DEFAULT_OG_FAUCET_URL, DEFAULT_HUB_SIGNUP_URL, DEFAULT_BLOCKCHAIN_EXPLORER, diff --git a/src/llm.ts b/src/llm.ts index bbb86e1..eeafa00 100644 --- a/src/llm.ts +++ b/src/llm.ts @@ -1,4 +1,5 @@ import { createSigner, wrapFetchWithPayment } from "x402-fetch"; +import type { Agent } from "undici"; import { ChatParams, CompletionParams, @@ -8,6 +9,7 @@ import { TextGenerationOutput, X402SettlementMode, } from "./types"; +import type { ActiveTEE, TEEConnection } from "./teeConnection"; const X402_PLACEHOLDER_API_KEY = "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"; @@ -17,8 +19,8 @@ export interface LLMConfig { privateKey: `0x${string}`; network: string; maxPaymentValue?: bigint; - serverUrl: string; - streamingServerUrl: string; + /** Resolves the active TEE endpoint and TLS dispatcher. */ + connection: TEEConnection; } /** @@ -27,18 +29,20 @@ export interface LLMConfig { * Provides chat and completion access to LLMs hosted in OpenGradient's TEE * (Trusted Execution Environment) with x402 payment protocol support. * - * Usage: - * const client = new Client({ privateKey }); - * const result = await client.llm.chat({ - * model: TEE_LLM.CLAUDE_3_5_HAIKU, - * messages: [{ role: "user", content: "Hello" }], - * }); + * The TEE endpoint is normally resolved from the on-chain TEE registry, with + * the TLS certificate pinned to the value stored at registration time. Pass + * `llmServerUrl` on the `Client` to override with a hardcoded URL. */ export class LLM { - private fetchWithPayment?: typeof fetch; + private signerPromise?: Promise; constructor(private readonly config: LLMConfig) {} + /** Tear down dispatchers and any background refresh timers. */ + async close(): Promise { + await this.config.connection.close(); + } + /** * Perform a (non-chat) completion via the TEE LLM server. */ @@ -60,8 +64,8 @@ export class LLM { }; if (stopSequence && stopSequence.length) payload.stop = stopSequence; - const response = await this.post( - `${trimSlash(this.config.serverUrl)}/v1/completions`, + const { response } = await this.requestWithRetry( + "/v1/completions", payload, x402SettlementMode, ); @@ -95,8 +99,8 @@ export class LLM { params: ChatParams, ): Promise { const payload = this.buildChatPayload(params, false); - const response = await this.post( - `${trimSlash(this.config.serverUrl)}/v1/chat/completions`, + const { response } = await this.requestWithRetry( + "/v1/chat/completions", payload, params.x402SettlementMode ?? X402SettlementMode.SETTLE_BATCH, ); @@ -125,8 +129,8 @@ export class LLM { private async *chatStream(params: ChatParams): AsyncIterable { const payload = this.buildChatPayload(params, true); - const response = await this.post( - `${trimSlash(this.config.streamingServerUrl)}/v1/chat/completions`, + const { response } = await this.requestWithRetry( + "/v1/chat/completions", payload, params.x402SettlementMode ?? X402SettlementMode.SETTLE_BATCH, ); @@ -196,28 +200,69 @@ export class LLM { return payload; } - private async getFetch(): Promise { - if (!this.fetchWithPayment) { - const signer = await createSigner( + private async getSigner(): Promise { + if (!this.signerPromise) { + this.signerPromise = createSigner( this.config.network, this.config.privateKey, ); - this.fetchWithPayment = wrapFetchWithPayment( - fetch, - signer, - this.config.maxPaymentValue, - ) as typeof fetch; } - return this.fetchWithPayment; + return this.signerPromise; } - private async post( - url: string, + /** + * Build a paid fetch that injects the TEE's pinned TLS dispatcher into every + * request (including x402 payment retries). + */ + private async buildPaidFetch(dispatcher: Agent): Promise { + const signer = await this.getSigner(); + const baseFetch: typeof fetch = ((input: any, init?: any) => + fetch(input, { ...(init ?? {}), dispatcher } as any)) as typeof fetch; + return wrapFetchWithPayment( + baseFetch, + signer as any, + this.config.maxPaymentValue, + ) as typeof fetch; + } + /** + * Send a request, lazily resolving the TEE endpoint. On a connection-level + * failure the TEE is re-resolved from the registry and the request is + * retried once. + */ + private async requestWithRetry( + path: string, body: Record, settlementMode: X402SettlementMode, - ): Promise { - const paidFetch = await this.getFetch(); + ): Promise<{ response: Response; tee: ActiveTEE }> { + this.config.connection.ensureRefreshLoop(); + try { + return await this.sendOnce(path, body, settlementMode); + } catch (e) { + if (e instanceof OpenGradientError && e.statusCode !== undefined) { + // Server responded with a non-2xx — don't retry. + throw e; + } + try { + await this.config.connection.reconnect(); + } catch (reconnectErr) { + throw new OpenGradientError( + `TEE LLM request failed and registry refresh failed: ${String(reconnectErr)}`, + ); + } + return await this.sendOnce(path, body, settlementMode); + } + } + + private async sendOnce( + path: string, + body: Record, + settlementMode: X402SettlementMode, + ): Promise<{ response: Response; tee: ActiveTEE }> { + const tee = await this.config.connection.ensureConnected(); + const url = `${trimSlash(tee.endpoint)}${path}`; + const paidFetch = await this.buildPaidFetch(tee.dispatcher); + let response: Response; try { response = await paidFetch(url, { @@ -240,7 +285,7 @@ export class LLM { response.status, ); } - return response; + return { response, tee }; } } diff --git a/src/teeConnection.ts b/src/teeConnection.ts new file mode 100644 index 0000000..2cf93d4 --- /dev/null +++ b/src/teeConnection.ts @@ -0,0 +1,195 @@ +import { Agent } from "undici"; +import { + TEE_TYPE_LLM_PROXY, + TEERegistry, + type TEEEndpoint, +} from "./teeRegistry"; + +/** Snapshot of the currently connected TEE. */ +export interface ActiveTEE { + endpoint: string; + /** + * Undici dispatcher pinned to the TEE's TLS certificate (or to skip + * verification for static/self-hosted endpoints). Pass via the `dispatcher` + * init option to `fetch`. + */ + dispatcher: Agent; + teeId?: string; + paymentAddress?: string; +} + +function derToPem(der: Uint8Array): string { + const b64 = Buffer.from(der).toString("base64"); + const wrapped = b64.match(/.{1,64}/g)?.join("\n") ?? b64; + return `-----BEGIN CERTIFICATE-----\n${wrapped}\n-----END CERTIFICATE-----\n`; +} + +/** + * Build an undici Agent that trusts *only* the given DER-encoded certificate. + * + * Hostname verification is disabled because TEE servers are typically + * addressed by IP while the cert may be issued for a different hostname; the + * pinned certificate itself is the trust anchor. + */ +export function buildPinnedAgent(der: Uint8Array): Agent { + const pem = derToPem(der); + return new Agent({ + connect: { + ca: pem, + rejectUnauthorized: true, + // Skip hostname check — the pinned cert is the trust anchor. + checkServerIdentity: () => undefined, + }, + }); +} + +/** Common interface for static and registry-backed TEE connections. */ +export interface TEEConnection { + /** + * Resolve the active TEE, performing any required network/registry lookups. + * Safe to call repeatedly; idempotent after the first successful call. + */ + ensureConnected(): Promise; + /** Force a fresh resolution of the TEE (e.g. after a connection failure). */ + reconnect(): Promise; + /** Start the optional background TEE refresh loop, if applicable. */ + ensureRefreshLoop(): void; + /** Tear down all dispatchers and timers. */ + close(): Promise; +} + +const REFRESH_INTERVAL_MS = 5 * 60 * 1000; + +/** + * TEE connection with a hardcoded endpoint URL. + * + * No registry lookup, no background refresh. TLS certificate verification is + * disabled because self-hosted TEE servers typically use self-signed certs. + */ +export class StaticTEEConnection implements TEEConnection { + private active: ActiveTEE; + + constructor(endpoint: string) { + this.active = { + endpoint, + dispatcher: new Agent({ connect: { rejectUnauthorized: false } }), + }; + } + + async ensureConnected(): Promise { + return this.active; + } + + ensureRefreshLoop(): void { + /* no-op — static connections don't refresh */ + } + + async reconnect(): Promise { + const old = this.active.dispatcher; + this.active = { + endpoint: this.active.endpoint, + dispatcher: new Agent({ connect: { rejectUnauthorized: false } }), + }; + try { + await old.close(); + } catch { + /* ignore */ + } + } + + async close(): Promise { + try { + await this.active.dispatcher.close(); + } catch { + /* ignore */ + } + } +} + +/** + * TEE connection resolved from the on-chain registry. + * + * Handles TLS certificate pinning and (optional) background health checks + * with automatic failover when the current TEE becomes unavailable. + */ +export class RegistryTEEConnection implements TEEConnection { + private active: ActiveTEE | null = null; + private connecting: Promise | null = null; + private refreshTimer: NodeJS.Timeout | null = null; + + constructor(private readonly registry: TEERegistry) {} + + async ensureConnected(): Promise { + if (this.active) return this.active; + if (!this.connecting) this.connecting = this.connect(); + try { + this.active = await this.connecting; + return this.active; + } finally { + this.connecting = null; + } + } + + async reconnect(): Promise { + const old = this.active?.dispatcher; + this.active = await this.connect(); + try { + await old?.close(); + } catch { + /* ignore */ + } + } + + ensureRefreshLoop(): void { + if (this.refreshTimer) return; + this.refreshTimer = setInterval(() => { + void this.runHealthCheck(); + }, REFRESH_INTERVAL_MS); + if (typeof this.refreshTimer.unref === "function") { + this.refreshTimer.unref(); + } + } + + async close(): Promise { + if (this.refreshTimer) { + clearInterval(this.refreshTimer); + this.refreshTimer = null; + } + try { + await this.active?.dispatcher.close(); + } catch { + /* ignore */ + } + } + + private async connect(): Promise { + let tee: TEEEndpoint | null; + try { + tee = await this.registry.getLLMTEE(); + } catch (e) { + throw new Error( + `Failed to fetch LLM TEE endpoint from registry: ${String(e)}`, + ); + } + if (!tee) { + throw new Error("No active LLM proxy TEE found in the registry."); + } + return { + endpoint: tee.endpoint, + dispatcher: buildPinnedAgent(tee.tlsCertDer), + teeId: tee.teeId, + paymentAddress: tee.paymentAddress, + }; + } + + private async runHealthCheck(): Promise { + if (!this.active) return; + try { + const tees = await this.registry.getActiveTEEsByType(TEE_TYPE_LLM_PROXY); + if (tees.some((t) => t.teeId === this.active!.teeId)) return; + await this.reconnect(); + } catch { + /* swallow & retry next cycle */ + } + } +} diff --git a/src/teeRegistry.ts b/src/teeRegistry.ts new file mode 100644 index 0000000..a12415e --- /dev/null +++ b/src/teeRegistry.ts @@ -0,0 +1,124 @@ +import { + createPublicClient, + http, + keccak256, + type Address, + type Hex, + type PublicClient, +} from "viem"; +import { TEE_REGISTRY_ABI } from "./abi/teeRegistry"; + +/** TEE types as defined in the registry contract. */ +export const TEE_TYPE_LLM_PROXY = 0; +export const TEE_TYPE_VALIDATOR = 1; + +/** A verified TEE with its endpoint URL and TLS certificate from the registry. */ +export interface TEEEndpoint { + /** keccak256 of the TEE's public key. */ + teeId: string; + /** HTTPS endpoint URL of the TEE. */ + endpoint: string; + /** DER-encoded X.509 certificate bytes as stored in the registry. */ + tlsCertDer: Uint8Array; + /** Wallet address that receives x402 payments for this TEE. */ + paymentAddress: string; +} + +interface RawTEEInfo { + owner: Address; + paymentAddress: Address; + endpoint: string; + publicKey: Hex; + tlsCertificate: Hex; + pcrHash: Hex; + teeType: number; + enabled: boolean; + registeredAt: bigint; + lastHeartbeatAt: bigint; +} + +function hexToBytes(hex: Hex): Uint8Array { + const cleaned = hex.startsWith("0x") ? hex.slice(2) : hex; + if (cleaned.length === 0) return new Uint8Array(0); + const out = new Uint8Array(cleaned.length / 2); + for (let i = 0; i < out.length; i++) { + out[i] = parseInt(cleaned.slice(i * 2, i * 2 + 2), 16); + } + return out; +} + +/** + * Queries the on-chain TEE Registry contract to retrieve verified TEE + * endpoints and their TLS certificates. + * + * Instead of blindly trusting the TLS certificate presented by a TEE server + * (TOFU), this class fetches the certificate that was submitted and verified + * during TEE registration. Any certificate that does not match the one stored + * in the registry should be rejected. + */ +export class TEERegistry { + private readonly client: PublicClient; + private readonly address: Address; + + /** + * @param rpcUrl - RPC endpoint for the chain where the registry is deployed. + * @param registryAddress - Address of the deployed TEERegistry contract. + */ + constructor(rpcUrl: string, registryAddress: string) { + this.client = createPublicClient({ transport: http(rpcUrl) }); + this.address = registryAddress as Address; + } + + /** + * Return all active TEEs of the given type with their endpoints and TLS certs. + * + * Uses the contract's `getActiveTEEs(teeType)` which returns only TEEs that + * are enabled, have a valid (non-revoked) PCR, and a fresh heartbeat — all in + * a single on-chain call. + */ + async getActiveTEEsByType(teeType: number): Promise { + let teeInfos: readonly RawTEEInfo[]; + try { + teeInfos = (await this.client.readContract({ + address: this.address, + abi: TEE_REGISTRY_ABI, + functionName: "getActiveTEEs", + args: [teeType], + })) as readonly RawTEEInfo[]; + } catch (e) { + // eslint-disable-next-line no-console + console.warn( + `Failed to fetch active TEEs from registry (type=${teeType}): ${String(e)}`, + ); + return []; + } + + const out: TEEEndpoint[] = []; + for (const tee of teeInfos) { + if ( + !tee.endpoint || + !tee.tlsCertificate || + tee.tlsCertificate === "0x" + ) { + continue; + } + out.push({ + teeId: keccak256(tee.publicKey), + endpoint: tee.endpoint, + tlsCertDer: hexToBytes(tee.tlsCertificate), + paymentAddress: tee.paymentAddress, + }); + } + return out; + } + + /** + * Return a random active LLM proxy TEE from the registry, or `null` if none + * are available. + */ + async getLLMTEE(): Promise { + const tees = await this.getActiveTEEsByType(TEE_TYPE_LLM_PROXY); + if (tees.length === 0) return null; + return tees[Math.floor(Math.random() * tees.length)]; + } +} diff --git a/src/types.ts b/src/types.ts index 3ca9b0e..c5bbd12 100644 --- a/src/types.ts +++ b/src/types.ts @@ -134,10 +134,13 @@ export interface StreamChunk { export interface ClientConfig { /** EVM private key (hex string, with or without 0x prefix). */ privateKey: string; - /** Override the OpenGradient TEE LLM server URL. */ + /** + * Override with a hardcoded TEE LLM server URL (dev / self-hosted). When + * set, the on-chain TEE registry is bypassed and TLS verification is + * disabled. Leave unset to discover an active TEE via the registry with + * its TLS certificate pinned to the registered value. + */ llmServerUrl?: string; - /** Override the OpenGradient TEE LLM streaming server URL. */ - llmStreamingServerUrl?: string; /** Override the x402 settlement network. Defaults to `base`. */ network?: string; /** @@ -145,6 +148,10 @@ export interface ClientConfig { * request. Defaults to the x402-fetch default (0.10 USDC, i.e. `100_000n`). */ maxPaymentValue?: bigint; + /** Override the RPC URL used to query the on-chain TEE registry. */ + rpcUrl?: string; + /** Override the deployed TEERegistry contract address. */ + teeRegistryAddress?: string; } export class OpenGradientError extends Error {