UNPKG

@neardefi/shade-agent-js

Version:

A library for creating Shade Agent agents in JavaScript and TypeScript

1,230 lines (1,218 loc) 39.1 kB
var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { ShadeClient: () => ShadeClient, addSensitive: () => addSensitive, sanitize: () => sanitize, toThrowable: () => toThrowable }); module.exports = __toCommonJS(index_exports); // src/utils/near.ts var import_providers = require("@near-js/providers"); var import_accounts = require("@near-js/accounts"); var import_tokens = require("@near-js/tokens"); var import_transactions = require("@near-js/transactions"); // src/utils/errors.ts var import_deep_redact = require("@hackylabs/deep-redact/index.ts"); var import_crypto = require("@near-js/crypto"); var import_signers = require("@near-js/signers"); var REDACTED = "[REDACTED]"; var UNSANITISABLE = "[unsanitisable]"; var SHADE_REDACT_KEYS = [ // NEAR "privateKey", "private_key", "secretKey", "secret_key", "extendedSecretKey", "signer", "key", "keyPair", "agentPrivateKey", "agentPrivateKeys", // BIP39 / mnemonics / entropy "mnemonic", "mnemonicPhrase", "seedPhrase", "seed_phrase", "seed", "entropy", // ethers / EVM internals "signingKey", "signing_key", "_signingKey", "_privateKey", // BIP32 hierarchical "xprv", "xpriv", "masterKey", "master_key", // Encrypted keystores "keystore", // API / OAuth / generic auth credentials "apiKey", "api_key", "apiSecret", "api_secret", "accessToken", "access_token", "refreshToken", "refresh_token", "bearerToken", "bearer_token", "authToken", "auth_token", "token", "clientSecret", "client_secret", "sessionToken", "session_token", "webhookSecret", "webhook_secret", "authorization", "cookie", // Passwords "password", "passwd", "passphrase" ]; var SHADE_REDACT_PATTERNS = [ // Any string containing a sensitive keyword → whole string redacted. { pattern: /privateKey|private_key|secretKey|secret_key|extendedSecretKey|agentPrivateKeys?/i, replacer: () => REDACTED }, // NEAR canonical secret-key string form → surgical substring replacement. { pattern: /ed25519:[^\s]+/, replacer: (v) => v.replace(/ed25519:[^\s]+/g, REDACTED) }, { pattern: /secp256k1:[^\s]+/, replacer: (v) => v.replace(/secp256k1:[^\s]+/g, REDACTED) }, // PEM private key blocks (TLS / SSH / PGP). { pattern: /-----BEGIN[\s\S]*?PRIVATE KEY-----[\s\S]*?-----END[\s\S]*?PRIVATE KEY-----/, replacer: () => REDACTED }, // JSON Web Tokens (RFC 7519). Three base64url segments separated by // dots; first two start with "eyJ" (base64 of `{"`). Very specific // shape, near-zero false-positive risk. { pattern: /\beyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/, replacer: (v) => v.replace( /\beyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g, REDACTED ) }, // HTTP Authorization header (RFC 6750/7235): "Bearer ...", "Basic ...", // "Token ...". Common when a library attaches request headers to an // error. Replaces just the credential, preserves the scheme. { pattern: /\b(?:Bearer|Basic|Token)\s+[A-Za-z0-9_.\-+/=]+/i, replacer: (v) => v.replace( /\b(Bearer|Basic|Token)\s+[A-Za-z0-9_.\-+/=]+/gi, `$1 ${REDACTED}` ) } ]; var activeKeys = [...SHADE_REDACT_KEYS]; var activePatterns = [...SHADE_REDACT_PATTERNS]; var deepRedact = build(activeKeys, activePatterns); function build(keys, patterns) { return new import_deep_redact.DeepRedact({ serialise: false, blacklistedKeys: keys, stringTests: patterns, types: ["string", "object"] }); } function stripStatefulFlags(re) { if (!/[gy]/.test(re.flags)) return re; return new RegExp(re.source, re.flags.replace(/[gy]/g, "")); } function addSensitive(opts) { if (opts.keys?.length) activeKeys = [...activeKeys, ...opts.keys]; if (opts.patterns?.length) { const safe = opts.patterns.map((p) => { const original = p.pattern; const stripped = stripStatefulFlags(original); if (stripped === original) return p; return { pattern: stripped, // Caller's replacer keeps access to the original (flagged) regex. replacer: (v) => p.replacer(v, original) }; }); activePatterns = [...activePatterns, ...safe]; } deepRedact = build(activeKeys, activePatterns); } function sanitizeError(error) { try { let sanitisedCause; let hasCause = false; if ("cause" in error) { try { sanitisedCause = sanitize( error.cause ); } catch { sanitisedCause = UNSANITISABLE; } hasCause = true; } let sanitisedErrors; if (error instanceof AggregateError) { try { const arr = error.errors; if (Array.isArray(arr)) { sanitisedErrors = arr.map((e) => { try { return sanitize(e); } catch { return UNSANITISABLE; } }); } else { sanitisedErrors = UNSANITISABLE; } } catch { sanitisedErrors = UNSANITISABLE; } } const own = { name: error.name, message: error.message ?? "" }; for (const k of Object.getOwnPropertyNames(error)) { if (k === "cause" || k === "errors") continue; if (k in own) continue; try { const v = error[k]; own[k] = sanitize(v); } catch { own[k] = UNSANITISABLE; } } const sanitised = deepRedact.redact(own); const msg = String(sanitised.message ?? ""); const out = new Error(msg || "An error occurred"); for (const [k, v] of Object.entries(sanitised)) { if (k === "message") continue; try { Object.defineProperty(out, k, { value: v, enumerable: true, writable: true, configurable: true }); } catch { } } if (hasCause) { try { Object.defineProperty(out, "cause", { value: sanitisedCause, enumerable: true, writable: true, configurable: true }); } catch { } } if (sanitisedErrors !== void 0) { try { Object.defineProperty(out, "errors", { value: sanitisedErrors, enumerable: true, writable: true, configurable: true }); } catch { } } return out; } catch { return new Error("An error occurred"); } } function sanitize(value) { try { if (value === null || value === void 0 || typeof value === "number" || typeof value === "boolean" || typeof value === "symbol" || typeof value === "bigint") { return value; } if (typeof value === "string") { return String(deepRedact.redact(value)); } if (value instanceof Error) { return sanitizeError(value); } if (typeof value === "object") { const result = deepRedact.redact(value); return typeof result === "object" && result !== null ? result : {}; } return value; } catch { return UNSANITISABLE; } } function safeStringify(value) { try { const seen = /* @__PURE__ */ new WeakSet(); const json = JSON.stringify(value, (_k, v) => { if (typeof v === "bigint") return `${v}n`; if (typeof v === "object" && v !== null) { if (seen.has(v)) return "[CIRCULAR]"; seen.add(v); } return v; }); return json ?? ""; } catch { try { return String(value); } catch { return ""; } } } function toThrowable(error) { const result = sanitize(error); if (result instanceof Error) return result; if (typeof result === "object" && result !== null) { return new Error(safeStringify(result) || "An error occurred"); } const cleaned = String(deepRedact.redact(String(result))); return new Error(cleaned || "An error occurred"); } function genericError(message) { return new Error(message); } function defaultRetryable(e) { if (e instanceof TypeError) { return e.cause !== void 0; } if (e instanceof RangeError || e instanceof ReferenceError || e instanceof SyntaxError || e instanceof URIError) { return false; } const status = e?.status; if (typeof status === "number" && status >= 400 && status < 500) { return status === 408 || status === 429; } return true; } var DEFAULT_DELAYS = [250, 500, 1e3]; function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } function getDelay(delayMs, index) { if (typeof delayMs === "number") return delayMs; const arr = delayMs ?? DEFAULT_DELAYS; return arr[Math.min(index, arr.length - 1)] ?? 0; } async function withRetry(fn, opts = {}) { const attempts = Math.max(1, opts.attempts ?? 3); const retryable = opts.retryable ?? defaultRetryable; let lastError; for (let i = 0; i < attempts; i++) { try { return await fn(); } catch (e) { lastError = e; const isLast = i === attempts - 1; if (isLast || !retryable(e)) { throw toThrowable(e); } await sleep(getDelay(opts.delayMs, i)); } } throw toThrowable(lastError); } function safeParseKeyPair(secret) { try { return import_crypto.KeyPair.fromString(secret); } catch { throw genericError("Failed to parse key"); } } function safeParseSigner(secret) { try { return import_signers.KeyPairSigner.fromSecretKey(secret); } catch { throw genericError("Failed to parse key"); } } // src/utils/near.ts function createDefaultProvider(networkId) { try { return new import_providers.JsonRpcProvider( { url: networkId === "testnet" ? "https://test.rpc.fastnear.com" : "https://free.rpc.fastnear.com" }, { retries: 3, backoff: 2, wait: 1e3 } ); } catch (error) { throw toThrowable(error); } } function createAccountObject(accountId, provider, signer) { try { return new import_accounts.Account(accountId, provider, signer); } catch (error) { throw toThrowable(error); } } async function internalFundAgent(agentAccountId, sponsorAccountId, sponsorPrivateKey, amount, provider) { try { const signer = safeParseSigner(sponsorPrivateKey); const account = new import_accounts.Account(sponsorAccountId, provider, signer); await account.transfer({ token: import_tokens.NEAR, amount: import_tokens.NEAR.toUnits(amount), receiverId: agentAccountId }); } catch (error) { throw toThrowable(error); } } async function addKeysToAccount(account, secrets) { try { const actions = secrets.map((secretKey) => { const keyPair = safeParseKeyPair(secretKey); return import_transactions.actionCreators.addKey( keyPair.getPublicKey(), import_transactions.actionCreators.fullAccessKey() ); }); await account.signAndSendTransaction({ receiverId: account.accountId, actions, throwOnFailure: true }); } catch (error) { throw toThrowable(error); } } async function removeKeysFromAccount(account, secrets) { try { const actions = secrets.map((secretKey) => { const keyPair = safeParseKeyPair(secretKey); return import_transactions.actionCreators.deleteKey(keyPair.getPublicKey()); }); await account.signAndSendTransaction({ receiverId: account.accountId, actions, throwOnFailure: true }); } catch (error) { throw toThrowable(error); } } // src/utils/tee.ts var import_fs = require("fs"); var import_dstack_sdk = require("@phala/dstack-sdk"); // src/utils/attestation-transform.ts function hexToBytes(hexStr) { if (!hexStr || hexStr === "") { return []; } try { return Array.from(Buffer.from(hexStr, "hex")); } catch (error) { throw toThrowable(error); } } function bytesToHex(bytes) { if (bytes.length === 0) { return ""; } return Buffer.from(bytes).toString("hex"); } function transformQuote(quoteHex) { try { const cleanedHex = quoteHex.replace(/^0x/, ""); return Array.from(Buffer.from(cleanedHex, "hex")); } catch (error) { throw toThrowable(error); } } function transformCollateral(rawCollateral) { try { return { pck_crl_issuer_chain: rawCollateral.pck_crl_issuer_chain || "", root_ca_crl: hexToBytes(rawCollateral.root_ca_crl), pck_crl: hexToBytes(rawCollateral.pck_crl), tcb_info_issuer_chain: rawCollateral.tcb_info_issuer_chain || "", tcb_info: rawCollateral.tcb_info || "", tcb_info_signature: hexToBytes(rawCollateral.tcb_info_signature), qe_identity_issuer_chain: rawCollateral.qe_identity_issuer_chain || "", qe_identity: rawCollateral.qe_identity || "", qe_identity_signature: hexToBytes(rawCollateral.qe_identity_signature) }; } catch (error) { throw toThrowable(error); } } function transformTcbInfo(dstackTcbInfo) { try { return { mrtd: dstackTcbInfo.mrtd || "", rtmr0: dstackTcbInfo.rtmr0 || "", rtmr1: dstackTcbInfo.rtmr1 || "", rtmr2: dstackTcbInfo.rtmr2 || "", rtmr3: dstackTcbInfo.rtmr3 || "", os_image_hash: dstackTcbInfo.os_image_hash || "", compose_hash: dstackTcbInfo.compose_hash || "", device_id: dstackTcbInfo.device_id || "", app_compose: dstackTcbInfo.app_compose || "", event_log: (dstackTcbInfo.event_log || []).map( (event) => ({ imr: event.imr, event_type: event.event_type, digest: event.digest, event: event.event, event_payload: event.event_payload }) ) }; } catch (error) { throw toThrowable(error); } } function attestationForContract(attestation) { try { return { quote: attestation.quote, collateral: { pck_crl_issuer_chain: attestation.collateral.pck_crl_issuer_chain, root_ca_crl: bytesToHex(attestation.collateral.root_ca_crl), pck_crl: bytesToHex(attestation.collateral.pck_crl), tcb_info_issuer_chain: attestation.collateral.tcb_info_issuer_chain, tcb_info: attestation.collateral.tcb_info, tcb_info_signature: bytesToHex( attestation.collateral.tcb_info_signature ), qe_identity_issuer_chain: attestation.collateral.qe_identity_issuer_chain, qe_identity: attestation.collateral.qe_identity, qe_identity_signature: bytesToHex( attestation.collateral.qe_identity_signature ) }, tcb_info: attestation.tcb_info }; } catch (error) { throw toThrowable(error); } } function getFakeAttestationInternal() { const ZERO_48_HEX = "0".repeat(96); const ZERO_32_HEX = "0".repeat(64); return { quote: [], collateral: { pck_crl_issuer_chain: "", root_ca_crl: [], pck_crl: [], tcb_info_issuer_chain: "", tcb_info: "", tcb_info_signature: [], qe_identity_issuer_chain: "", qe_identity: "", qe_identity_signature: [] }, tcb_info: { mrtd: ZERO_48_HEX, rtmr0: ZERO_48_HEX, rtmr1: ZERO_48_HEX, rtmr2: ZERO_48_HEX, rtmr3: ZERO_48_HEX, os_image_hash: "", compose_hash: ZERO_32_HEX, device_id: ZERO_32_HEX, app_compose: "", event_log: [] } }; } function getFakeAttestation() { return attestationForContract(getFakeAttestationInternal()); } // src/utils/collateral-freshness.ts var import_asn1 = __toESM(require("asn1.js"), 1); var MAX_COLLATERAL_AGE_MS = 7 * 24 * 60 * 60 * 1e3; var FUTURE_TIMESTAMP_GRACE_MS = 5 * 60 * 1e3; var FreshnessError = class extends Error { constructor(message, details) { super(message); this.name = "FreshnessError"; this.field = details.field; this.kind = details.kind; this.issuedAt = details.issuedAt; this.elapsedMs = details.elapsedMs; this.limitMs = details.limitMs; } }; var Time = import_asn1.default.define("Time", function() { this.choice({ utcTime: this.utctime(), generalTime: this.gentime() }); }); var TBSCertList = import_asn1.default.define("TBSCertList", function() { this.seq().obj( this.key("version").int().optional(), this.key("signature").any(), this.key("issuer").any(), this.key("thisUpdate").use(Time) // nextUpdate / revokedCertificates / crlExtensions ignored ); }); var CertificateList = import_asn1.default.define("CertificateList", function() { this.seq().obj( this.key("tbsCertList").use(TBSCertList), this.key("signatureAlgorithm").any(), this.key("signature").bitstr() ); }); function parseIssueDateFromJson(field, raw) { let parsed; try { parsed = JSON.parse(raw); } catch { throw new FreshnessError( `Failed to JSON.parse ${field} for freshness check`, { field, kind: "json-parse" } ); } if (typeof parsed.issueDate !== "string") { throw new FreshnessError( `Missing or non-string issueDate in ${field}`, { field, kind: "issue-date-rfc3339" } ); } const date = new Date(parsed.issueDate); if (Number.isNaN(date.getTime())) { throw new FreshnessError( `Unparseable issueDate in ${field}`, { field, kind: "issue-date-rfc3339" } ); } return date; } function parsePckCrlThisUpdate(pckCrlBytes) { if (!pckCrlBytes || pckCrlBytes.length === 0) { throw new FreshnessError("PCK CRL is empty", { field: "pck_crl", kind: "crl-parse" }); } let decoded; try { decoded = CertificateList.decode(Buffer.from(pckCrlBytes), "der"); } catch { throw new FreshnessError("Failed to decode PCK CRL as DER X.509 v2 CRL", { field: "pck_crl", kind: "crl-parse" }); } const thisUpdateMs = decoded?.tbsCertList?.thisUpdate?.value; if (typeof thisUpdateMs !== "number" || Number.isNaN(thisUpdateMs)) { throw new FreshnessError("PCK CRL thisUpdate is missing or unparseable", { field: "pck_crl", kind: "crl-parse" }); } return new Date(thisUpdateMs); } function checkWithinWindow(field, issuedAt, now) { const elapsedMs = now.getTime() - issuedAt.getTime(); if (elapsedMs > MAX_COLLATERAL_AGE_MS) { throw new FreshnessError( `${field} is stale: ${elapsedMs}ms old, limit ${MAX_COLLATERAL_AGE_MS}ms`, { field, kind: "stale", issuedAt, elapsedMs, limitMs: MAX_COLLATERAL_AGE_MS } ); } if (-elapsedMs > FUTURE_TIMESTAMP_GRACE_MS) { throw new FreshnessError( `${field} is timestamped ${-elapsedMs}ms in the future, grace ${FUTURE_TIMESTAMP_GRACE_MS}ms`, { field, kind: "future-timestamp", issuedAt, elapsedMs, limitMs: FUTURE_TIMESTAMP_GRACE_MS } ); } } function checkCollateralFreshness(collateral, now) { const tcbIssuedAt = parseIssueDateFromJson("tcb_info", collateral.tcb_info); checkWithinWindow("tcb_info", tcbIssuedAt, now); const qeIssuedAt = parseIssueDateFromJson( "qe_identity", collateral.qe_identity ); checkWithinWindow("qe_identity", qeIssuedAt, now); const crlIssuedAt = parsePckCrlThisUpdate(collateral.pck_crl); checkWithinWindow("pck_crl", crlIssuedAt, now); } // src/utils/tee.ts async function getDstackClient() { if (!(0, import_fs.existsSync)("/var/run/dstack.sock")) { return void 0; } try { const client = new import_dstack_sdk.DstackClient(); await client.info(); return client; } catch { return void 0; } } async function internalGetAttestation(dstackClient, agentAccountId, keysDerivedWithRandom) { if (!dstackClient || !keysDerivedWithRandom) { return getFakeAttestation(); } try { const info = await withRetry(() => dstackClient.info()); const dstackTcbInfo = info.tcb_info; const accountIdBytes = Buffer.from(agentAccountId, "hex"); const reportData = Buffer.alloc(64); accountIdBytes.copy(reportData, 0); const quoteResponse = await withRetry( () => dstackClient.getQuote(reportData) ); const quote_hex = quoteResponse.quote; const quote = transformQuote(quote_hex); const formData = new FormData(); formData.append("hex", quote_hex.replace(/^0x/, "")); const collateralUrl = "https://cloud-api.phala.network/api/v1/attestations/verify"; const collateral = await withRetry(async () => { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 3e4); try { const response = await fetch(collateralUrl, { method: "POST", body: formData, signal: controller.signal }); if (!response.ok) { const body = (await response.text().catch(() => "")).slice(0, 500); throw Object.assign( new Error( `Failed to fetch quote collateral from Phala (HTTP ${response.status} ${response.statusText})${body ? `: ${body}` : ""}` ), { status: response.status } ); } const resHelper = await response.json(); return transformCollateral(resHelper.quote_collateral); } finally { clearTimeout(timeoutId); } }); checkCollateralFreshness(collateral, /* @__PURE__ */ new Date()); const tcb_info = transformTcbInfo(dstackTcbInfo); const attestation = { quote, collateral, tcb_info }; return attestationForContract(attestation); } catch (error) { throw toThrowable(error); } } // src/utils/agent.ts var import_node_crypto = require("crypto"); var import_near_seed_phrase = require("near-seed-phrase"); var import_crypto2 = require("@near-js/crypto"); var import_accounts2 = require("@near-js/accounts"); async function generateAgent(dstackClient, derivationPath) { try { const { hash, usedRandom } = deriveHash(dstackClient, derivationPath); const seedInfo = (0, import_near_seed_phrase.generateSeedPhrase)(hash); const accountId = Buffer.from(import_crypto2.PublicKey.from(seedInfo.publicKey).data).toString("hex").toLowerCase(); return { accountId, agentPrivateKey: seedInfo.secretKey, derivedWithRandom: usedRandom }; } catch (error) { throw toThrowable(error); } } function deriveHashFromPath(derivationPath) { return (0, import_node_crypto.createHash)("sha256").update(Buffer.from(derivationPath)).digest(); } function deriveHashFromRandom() { return Buffer.from(crypto.getRandomValues(new Uint8Array(32))); } function deriveHash(dstackClient, derivationPath) { if (!dstackClient && derivationPath) { return { hash: deriveHashFromPath(derivationPath), usedRandom: false }; } return { hash: deriveHashFromRandom(), usedRandom: true }; } async function manageKeySetup(agentAccount, numAdditionalKeys, dstackClient, derivationPath, keysDerivedWithRandom) { try { const keysOnAccount = await agentAccount.getAccessKeyList(); const numKeysOnAccount = keysOnAccount.keys.length; const numExistingAdditionalKeys = numKeysOnAccount - 1; const inRandomMode = !!dstackClient || !derivationPath; if (inRandomMode && numExistingAdditionalKeys > 0) { throw genericError( `Account has ${numExistingAdditionalKeys} additional key(s) that cannot be reused in random-derivation mode. Set a derivationPath (local) or use a fresh account.` ); } const numKeysToDerive = Math.max( numAdditionalKeys, numExistingAdditionalKeys ); const { keys, allDerivedWithRandom } = await deriveAdditionalKeys( numKeysToDerive, dstackClient, derivationPath ); if (allDerivedWithRandom !== keysDerivedWithRandom) { throw genericError( "First key and additional keys disagree on derivation method. Something went wrong with the key derivation." ); } if (numExistingAdditionalKeys < numAdditionalKeys) { const keysToAdd = keys.slice( numExistingAdditionalKeys, numAdditionalKeys ); await addKeysToAccount(agentAccount, keysToAdd); } else if (numExistingAdditionalKeys > numAdditionalKeys) { const excessKeys = keys.slice(numAdditionalKeys); await removeKeysFromAccount(agentAccount, excessKeys); } const keysToSave = keys.slice(0, numAdditionalKeys); return { keysToSave }; } catch (error) { throw toThrowable(error); } } async function deriveAdditionalKeys(numKeys, dstackClient, derivationPath) { try { const keyPromises = Array.from({ length: numKeys }, async (_, index) => { const i = index + 1; const keyDerivationPath = derivationPath ? `${derivationPath}-${i}` : void 0; const { hash, usedRandom } = deriveHash(dstackClient, keyDerivationPath); const seedInfo = (0, import_near_seed_phrase.generateSeedPhrase)(hash); return { key: seedInfo.secretKey, usedRandom }; }); const results = await Promise.all(keyPromises); return { keys: results.map((r) => r.key), allDerivedWithRandom: results.every((r) => r.usedRandom) }; } catch (error) { throw toThrowable(error); } } function getAgentSigner(agentPrivateKeys, currentKeyIndex) { try { if (agentPrivateKeys.length === 0) { throw genericError("No agent keys available"); } if (agentPrivateKeys.length === 1) { return { signer: safeParseSigner(agentPrivateKeys[0]), keyIndex: 0 }; } currentKeyIndex++; if (currentKeyIndex > agentPrivateKeys.length - 1) { currentKeyIndex = 0; } return { signer: safeParseSigner(agentPrivateKeys[currentKeyIndex]), keyIndex: currentKeyIndex }; } catch (error) { throw toThrowable(error); } } async function ensureKeysSetup(agentAccountId, agentPrivateKeys, rpc, numKeys, dstackClient, derivationPath, keysDerivedWithRandom, keysChecked) { try { if (keysChecked) { return { keysToAdd: [], wasChecked: true }; } const signer = safeParseSigner(agentPrivateKeys[0]); const agentAccount = new import_accounts2.Account(agentAccountId, rpc, signer); const { keysToSave } = await manageKeySetup( agentAccount, numKeys - 1, dstackClient, derivationPath, keysDerivedWithRandom ); return { keysToAdd: keysToSave, wasChecked: true }; } catch (error) { throw toThrowable(error); } } // src/utils/validation.ts async function validateShadeConfig(config) { if (config.networkId === void 0) { config.networkId = "testnet"; } if (config.networkId !== "testnet" && config.networkId !== "mainnet") { throw genericError("networkId must be either 'testnet' or 'mainnet'"); } if (config.sponsor) { if (!config.sponsor.accountId || config.sponsor.accountId.trim() === "") { throw genericError( "sponsor.accountId is required when sponsor is provided" ); } if (!config.sponsor.privateKey || config.sponsor.privateKey.trim() === "") { throw genericError( "sponsor.privateKey is required when sponsor is provided" ); } } if (config.numKeys === void 0) { config.numKeys = 1; } if (!Number.isInteger(config.numKeys) || config.numKeys < 1 || config.numKeys > 100) { throw genericError("numKeys must be an integer between 1 and 100"); } try { if (!config.rpc) { config.rpc = createDefaultProvider(config.networkId); } const rpcNetworkId = await config.rpc.getNetworkId(); if (rpcNetworkId !== config.networkId) { throw genericError( `Network ID mismatch: config.networkId is "${config.networkId}" but RPC provider is connected to "${rpcNetworkId}"` ); } } catch (error) { throw toThrowable(error); } } // src/api.ts var import_tokens2 = require("@near-js/tokens"); var DEFAULT_REGISTER_DEPOSIT_YOCTO = "5000000000000000000000"; var ShadeClient = class _ShadeClient { // true if the number of keys have been checked (happens on the first call), false otherwise // Private constructor so only `create()` can be used to create an instance constructor(config, dstackClient, accountId, agentPrivateKeys, keysDerivedWithRandom) { this.config = config; this.dstackClient = dstackClient; this.agentAccountId = accountId; this.agentPrivateKeys = agentPrivateKeys; this.currentKeyIndex = 0; this.keysDerivedWithRandom = keysDerivedWithRandom; this.keysChecked = false; } /** * Creates a new ShadeClient instance asynchronously * @param config - Configuration object for the Shade client (see ShadeConfig interface for details) * @returns Promise that resolves to a ShadeClient instance * @throws Error if configuration is invalid, network ID mismatch, or key generation fails */ static async create(config) { try { await validateShadeConfig(config); const dstackClient = await getDstackClient(); const agentPrivateKeys = []; const { accountId, agentPrivateKey, derivedWithRandom } = await generateAgent(dstackClient, config.derivationPath); agentPrivateKeys.push(agentPrivateKey); return new _ShadeClient( config, dstackClient, accountId, agentPrivateKeys, derivedWithRandom ); } catch (error) { throw toThrowable(error); } } /** * Gets the NEAR account ID of the agent * @returns The agent's account ID */ accountId() { return this.agentAccountId; } /** * Gets the NEAR balance of the agent account in human readable format (e.g. 1 = one NEAR) * @returns Promise that resolves to the account balance in NEAR tokens, if the agent account does not exist, returns 0 * @throws Error if network request fails */ async balance() { const account = createAccountObject(this.agentAccountId, this.config.rpc); try { const balance = await account.getBalance(); return parseFloat(import_tokens2.NEAR.toDecimal(balance)); } catch (error) { const err = error; if (err?.type === "AccountDoesNotExist") { return 0; } throw toThrowable(error); } } /** * Registers the agent in the agent contract. * * @param params * @param params.deposit Attached deposit in yoctoNEAR when storage is required or when `forceDeposit` is `true` (defaults to `5000000000000000000000` — 0.005 NEAR) * @param params.forceDeposit If `true`, always attach `deposit` (or the default) and skip `get_agent`. If `false`, attach no deposit and skip `get_agent`. If omitted, use `get_agent` to decide. * @returns Promise that resolves to true if registration was successful * @throws Error if agentContractId is not configured, if fetching attestation fails, or if the contract call fails */ async register(params) { if (!this.config.agentContractId) { throw genericError("agentContractId is required for registering the agent"); } try { const contractAttestation = await internalGetAttestation( this.dstackClient, this.agentAccountId, this.keysDerivedWithRandom ); let depositYocto; if (params?.forceDeposit === false) { depositYocto = 0n; } else if (params?.forceDeposit === true) { depositYocto = BigInt( params.deposit ?? DEFAULT_REGISTER_DEPOSIT_YOCTO ); } else { const existing = await this.view({ methodName: "get_agent", args: { account_id: this.agentAccountId } }); const alreadyRegistered = existing !== null && existing !== void 0; depositYocto = alreadyRegistered ? 0n : BigInt(params?.deposit ?? DEFAULT_REGISTER_DEPOSIT_YOCTO); } return await this.call({ methodName: "register_agent", args: { attestation: contractAttestation }, deposit: depositYocto, gas: BigInt("300000000000000") // 300 TGas }); } catch (error) { throw toThrowable(error); } } /** * Call a view function on the agent contract and return the result * @param params * @param params.methodName The method that will be called * @param params.args Arguments as a valid JSON Object * @param params.blockQuery (optional) Block reference for the query * @returns A promise that resolves with the result of the view function call * @throws Error if agentContractId is not configured or if RPC call fails */ async view(params) { if (!this.config.agentContractId) { throw genericError("agentContractId is required for view calls"); } try { return await this.config.rpc.callFunction( this.config.agentContractId, params.methodName, params.args, params.blockQuery ); } catch (error) { throw toThrowable(error); } } /** * Call a function on the agent contract and return the result * @param params * @param params.methodName The method that will be called * @param params.args Arguments, either as a valid JSON Object or a raw Uint8Array * @param params.deposit (optional) Amount of NEAR Tokens to attach to the call * @param params.gas (optional) Amount of GAS to use attach to the call * @param params.waitUntil (optional) Transaction finality to wait for * @returns A promise that resolves with the result of the contract function call * @throws Error if agentContractId is not configured, if key derivation fails, or if transaction fails */ async call(params) { if (!this.config.agentContractId) { throw genericError("agentContractId is required for call functions"); } try { const { keysToAdd, wasChecked } = await ensureKeysSetup( this.agentAccountId, this.agentPrivateKeys, this.config.rpc, this.config.numKeys, this.dstackClient, this.config.derivationPath, this.keysDerivedWithRandom, this.keysChecked ); this.agentPrivateKeys.push(...keysToAdd); if (wasChecked) { this.keysChecked = true; } const { signer, keyIndex } = getAgentSigner( this.agentPrivateKeys, this.currentKeyIndex ); this.currentKeyIndex = keyIndex; const account = createAccountObject( this.agentAccountId, this.config.rpc, signer ); return await account.callFunction({ contractId: this.config.agentContractId, methodName: params.methodName, args: params.args, gas: params.gas, deposit: params.deposit, waitUntil: params.waitUntil }); } catch (error) { throw toThrowable(error); } } /** * Gets the TEE attestation for the agent in contract format (ready to be sent to the contract) * @returns Promise that resolves to the contract-formatted attestation object * @throws Error if fetching quote collateral fails (network errors, HTTP errors, timeouts) */ async getAttestation() { try { return await internalGetAttestation( this.dstackClient, this.agentAccountId, this.keysDerivedWithRandom ); } catch (error) { throw toThrowable(error); } } /** * Funds the agent account with NEAR tokens from the sponsor account * @param fundAmount - Amount of NEAR tokens to transfer to the agent account in human readable format (e.g. 1 = one NEAR) * @returns Promise that resolves when funding is complete * @throws Error if sponsor is not configured or if transfer fails after retries */ async fund(fundAmount) { if (!this.config.sponsor) { throw genericError("sponsor is required for funding the agent account"); } try { await internalFundAgent( this.agentAccountId, this.config.sponsor.accountId, this.config.sponsor.privateKey, fundAmount, this.config.rpc ); } catch (error) { throw toThrowable(error); } } /** * Gets the agent's private keys (use with caution) * @param params - Must pass `{ acknowledgeRisk: true }` * @returns Array of private key strings */ getPrivateKeys(params) { if (!params.acknowledgeRisk) { throw genericError( "WARNING: Exporting private keys from the library is a risky operation, you may accidentally leak them from the TEE. Do not use the keys to sign transactions other than to the agent contract. Please acknowledge the risk by setting acknowledgeRisk to true." ); } console.log( "WARNING: Exporting private keys from the library is a risky operation, you may accidentally leak them from the TEE. Do not use the keys to sign transactions other than to the agent contract." ); return this.agentPrivateKeys; } /** * Checks if the agent is whitelisted for local mode * @returns Promise that resolves to true if the agent is whitelisted, false if the agent is not whitelisted, or null if the agent contract requires TEE * @throws Error if agentContractId is not configured or if view call fails */ async isWhitelisted() { if (!this.config.agentContractId) { throw genericError( "agentContractId is required for checking if the agent is whitelisted" ); } try { const res = await this.view({ methodName: "get_contract_info", args: {} }); if (res.requires_tee) { return null; } const whitelisted_agents = await this.view({ methodName: "get_whitelisted_agents_for_local", args: {} }); return whitelisted_agents.includes(this.agentAccountId); } catch (error) { throw toThrowable(error); } } }; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { ShadeClient, addSensitive, sanitize, toThrowable });