@provablehq/sdk
Version:
A Software Development Kit (SDK) for Zero-Knowledge Transactions
1 lines • 714 kB
Source Map (JSON)
{"version":3,"file":"browser.cjs","sources":["../../src/utils/logger.ts","../../src/keys/keystore/error.ts","../../src/keys/verifier/interface.ts","../../src/keys/verifier/memory.ts","../../src/keys/keystore/indexeddb.ts","../../src/security.ts","../../src/account.ts","../../src/utils/utils.ts","../../src/models/provingResponse.ts","../../src/constants.ts","../../src/network-client.ts","../../src/models/record-scanner/error.ts","../../src/keys/provider/memory.ts","../../src/keys/keystore/interface.ts","../../src/keys/provider/offline.ts","../../src/record-provider.ts","../../src/record-scanner.ts","../../src/integrations/sealance/merkle-tree.ts","../../src/integrations/circle/hook-data.ts","../../src/program-manager.ts","../../src/models/external-signing.ts","../../src/external-signing.ts","../../src/browser.ts"],"sourcesContent":["/**\n * Log levels in ascending severity. Each level includes all levels above it.\n * \"silent\" suppresses all output. \"debug\" enables everything.\n *\n * @example\n * import { setLogLevel } from \"@provablehq/sdk\";\n * setLogLevel(\"silent\"); // suppress all SDK logging\n */\nexport type LogLevel = \"silent\" | \"error\" | \"warn\" | \"info\" | \"debug\";\n\nconst LEVEL_PRIORITY: Record<LogLevel, number> = {\n silent: 0,\n error: 1,\n warn: 2,\n info: 3,\n debug: 4,\n};\n\nlet currentLevel: LogLevel = \"info\";\n\n/**\n * Set the SDK log level. Levels are hierarchical:\n * - \"silent\" — suppress all SDK logging\n * - \"error\" — only errors\n * - \"warn\" — errors + warnings\n * - \"info\" — errors + warnings + info (default)\n * - \"debug\" — everything including debug traces\n *\n * This controls both TS-side and WASM-side logging. WASM log calls\n * (e.g., \"Loading program\", \"Executing program\") are silenced when\n * the level is below \"info\".\n */\nexport function setLogLevel(level: LogLevel): void {\n currentLevel = level;\n import(\"../wasm.js\")\n .then(({ setWasmLogLevel }) => setWasmLogLevel(LEVEL_PRIORITY[level]))\n .catch(() => {}); // WASM not loaded yet — will use default level (info)\n}\n\n/** Returns the current SDK log level. */\nexport function getLogLevel(): LogLevel {\n return currentLevel;\n}\n\nfunction shouldLog(level: LogLevel): boolean {\n return LEVEL_PRIORITY[level] <= LEVEL_PRIORITY[currentLevel];\n}\n\n/**\n * SDK logger object following console.log/warn/error/debug syntax.\n * Respects the current log level set via setLogLevel().\n */\nexport const logger = {\n log(...args: unknown[]): void {\n if (shouldLog(\"info\")) console.log(...args);\n },\n warn(...args: unknown[]): void {\n if (shouldLog(\"warn\")) console.warn(...args);\n },\n error(...args: unknown[]): void {\n if (shouldLog(\"error\")) console.error(...args);\n },\n debug(...args: unknown[]): void {\n if (shouldLog(\"debug\")) console.debug(...args);\n },\n};\n","/**\n * Reason code for invalid locator validation failures.\n * - `\"reserved_name\"`: A locator component is empty or `\".\"`.\n * - `\"path_traversal\"`: A locator component contains `..`.\n * - `\"path_separator\"`: A locator component contains a path separator (`/`, `\\`) or null byte.\n * - `\"negative_value\"`: A numeric locator field (edition, amendment, recordInputPosition) is not a non-negative integer.\n */\nexport type InvalidLocatorReason =\n | \"reserved_name\"\n | \"path_traversal\"\n | \"path_separator\"\n | \"negative_value\";\n\n/**\n * Error thrown when a key locator is invalid for filesystem use.\n * Used to prevent path traversal and other filesystem injection when deriving paths from locators.\n *\n * @extends Error\n */\nexport class InvalidLocatorError extends Error {\n /**\n * @param message - Human-readable description of the validation failure.\n * @param locator - The invalid locator string that failed validation.\n * @param reason - Machine-readable reason code for the failure.\n */\n constructor(\n message: string,\n public readonly locator: string,\n public readonly reason: InvalidLocatorReason\n ) {\n super(message);\n this.name = \"InvalidLocatorError\";\n Object.setPrototypeOf(this, InvalidLocatorError.prototype);\n }\n}\n","/**\n * Fingerprint for a proving and verifying key that includes the checksum of the bytes and size in number of bytes.\n *\n * @property {string} checksum - SHA-256 checksum of the key bytes.\n * @property {number} size - Size of the key in number of bytes.\n */\nexport interface KeyFingerprint {\n checksum: string;\n size: number;\n}\n\n\n/**\n * Options for verifying the integrity of proving and verifying keys. An identifier to allow the interface to find key metadata and/or the desired metadata to verify must be passed in.\n *\n * @property {Uint8Array} keyBytes - The raw bytes of the cryptographic key.\n * @property {string} [locator] - Optional identifier or path indicating where the key or KeyFingerprint might be stored.\n * @property {KeyFingerprint} [fingerprint] - Optional metadata containing the key's expected checksum and size for verification purposes.\n */\nexport interface KeyMetadata {\n keyBytes: Uint8Array;\n locator?: string;\n fingerprint?: KeyFingerprint;\n}\n\n/**\n * Error thrown when there is a mismatch between expected and actual key metadata.\n * This can occur during verification of either the checksum or size of a key.\n *\n * @extends Error\n */\nexport class KeyVerificationError extends Error {\n /**\n * Creates a new KeyVerificationError instance (error.name is \"ChecksumMismatchError\").\n *\n * @param {string} locator - The key locator where the mismatch occurred.\n * @param {\"checksum\" | \"size\"} field - The field that failed verification (either \"checksum\" or \"size\").\n * @param {string} expected - The expected value of the field.\n * @param {string} actual - The actual value encountered.\n */\n constructor(\n public readonly locator: string,\n public readonly field: \"checksum\" | \"size\",\n public readonly expected: string,\n public readonly actual: string\n ) {\n super(\n `Key verification ${locator} ${field} mismatch: expected ${expected}, got ${actual}`\n );\n this.name = \"ChecksumMismatchError\";\n Object.setPrototypeOf(this, KeyVerificationError.prototype);\n }\n}\n\n/**\n * Computes the SHA-256 checksum of a given set of bytes.\n *\n * @param {Uint8Array} bytes - The bytes to compute the checksum of.\n */\nexport async function sha256Hex(bytes: Uint8Array): Promise<string> {\n const hash = await crypto.subtle.digest(\"SHA-256\", bytes as BufferSource);\n return Array.from(new Uint8Array(hash))\n .map((b) => b.toString(16).padStart(2, \"0\"))\n .join(\"\");\n}\n\n/**\n * Verifies key-pair metadata (checksums and sizes) against raw bytes in order to ensure the correct keypair is used.\n * Implementations throw {@link KeyVerificationError} when verification fails.\n */\nexport interface KeyVerifier {\n /**\n * Computes and optionally stores key metadata. If keyFingerprint is provided, verifies against it.\n * @param {KeyMetadata} keyMetadata - Object containing key bytes and optional verification data.\n * @throws {KeyVerificationError} When provided keyFingerprint doesn't match computed values.\n * @returns {Promise<KeyFingerprint>} Computed key metadata.\n */\n computeKeyMetadata(keyMetadata: KeyMetadata): Promise<KeyFingerprint>;\n\n /**\n * Verifies prover bytes against key metadata (size + checksum).\n *\n * @param {KeyMetadata} keyMetadata - Object containing the key bytes, an optional locator and optional metadata for verification.\n * @throws {KeyVerificationError} when size or checksum does not match.\n * @returns {Promise<void>} Promise that resolves when verification succeeds.\n */\n verifyKeyBytes(keyMetadata: KeyMetadata): Promise<void>;\n}","import { KeyVerificationError, KeyVerifier, KeyFingerprint, KeyMetadata, sha256Hex } from \"./interface.js\";\n\n/**\n * In-memory implementation of KeyVerifier that stores and verifies key fingerprints.\n * Provides functionality to compute and verify cryptographic checksums of keys, storing them\n * in memory for subsequent verification. This implementation is primarily used for testing\n * and development purposes where persistence is not required.\n * \n * Key features:\n * - Computes SHA-256 checksums and sizes for key bytes.\n * - Stores key fingerprints in memory using string locators.\n * - Verifies key bytes against stored or provided fingerprints.\n *\n * @implements {KeyVerifier}\n */\nexport class MemKeyVerifier implements KeyVerifier {\n private keyStore: Record<string, KeyFingerprint> = {};\n\n /**\n * Computes and optionally stores key metadata. If a keyFingerprint is provided, this function will verify the computed checksum against it before storing it.\n *\n * @param {KeyMetadata} keyMetadata - Object containing key bytes and optional verification data.\n * @throws {KeyVerificationError} When provided keyFingerprint doesn't match computed values.\n * @returns {Promise<KeyFingerprint>} Computed key metadata.\n */\n async computeKeyMetadata(keyMetadata: KeyMetadata): Promise<KeyFingerprint> {\n // Compute the metadata from the key bytes\n const computedFingerprint: KeyFingerprint = {\n checksum: await sha256Hex(keyMetadata.keyBytes),\n size: keyMetadata.keyBytes.length\n };\n\n // If a KeyFingerprint is provided, verify it matches computed values.\n if (keyMetadata.fingerprint) {\n if (keyMetadata.fingerprint.size !== computedFingerprint.size) {\n throw new KeyVerificationError(\n keyMetadata.locator ? keyMetadata.locator : \"\",\n \"size\",\n String(keyMetadata.fingerprint.size),\n String(computedFingerprint.size)\n );\n }\n if (keyMetadata.fingerprint.checksum !== computedFingerprint.checksum) {\n throw new KeyVerificationError(\n keyMetadata.locator ? keyMetadata.locator : \"\",\n \"checksum\",\n keyMetadata.fingerprint.checksum,\n computedFingerprint.checksum\n );\n }\n }\n\n // If locator is provided, store only the fingerprint\n if (keyMetadata.locator) {\n this.keyStore[keyMetadata.locator] = computedFingerprint;\n }\n\n return computedFingerprint;\n }\n\n /**\n * Verifies key bytes against stored or provided metadata. Follows a priority verification scheme:\n * 1. If KeyFingerprint is provided in the metadata, this method verifies against that first.\n * 2. If a locator is provided, attempts to verify against stored fingerprint.\n * 3. If neither is available, throws an error.\n *\n * @param {KeyMetadata} keyMetadata - Object containing the key bytes and optional verification metadata.\n * @throws {Error} When neither fingerprint nor valid locator is provided for verification.\n * @throws {KeyVerificationError} When size or checksum verification fails.\n * @returns {Promise<void>} Promise that resolves when verification succeeds.\n */\n async verifyKeyBytes(keyMetadata: KeyMetadata): Promise<void> {\n if (!keyMetadata.keyBytes) {\n throw new Error(\"Key bytes must be provided for verification.\");\n }\n\n // Compute the fingerprint for the provided bytes.\n const computedFingerprint = await this.computeKeyMetadata({\n keyBytes: keyMetadata.keyBytes\n });\n\n // Determine which fingerprint to verify against.\n let fingerprintToVerify: KeyFingerprint | undefined;\n\n if (keyMetadata.fingerprint) {\n // If a key fingerprint is provided, use it.\n fingerprintToVerify = keyMetadata.fingerprint;\n } else if (keyMetadata.locator) {\n // Otherwise try to get stored fingerprint by locator.\n fingerprintToVerify = this.keyStore[keyMetadata.locator];\n }\n\n if (!fingerprintToVerify) {\n throw new Error(\"Either fingerprint or a valid locator must be provided for verification.\");\n }\n\n // Verify the key size.\n if (fingerprintToVerify.size !== computedFingerprint.size) {\n throw new KeyVerificationError(\n keyMetadata.locator || \"\",\n \"size\",\n String(fingerprintToVerify.size),\n String(computedFingerprint.size)\n );\n }\n\n // Verify the key checksum.\n if (fingerprintToVerify.checksum !== computedFingerprint.checksum) {\n throw new KeyVerificationError(\n keyMetadata.locator || \"\",\n \"checksum\",\n fingerprintToVerify.checksum,\n computedFingerprint.checksum\n );\n }\n }\n}","import { FunctionKeyPair } from \"../../models/keyPair.js\";\nimport { KeyFingerprint } from \"../verifier/interface.js\";\nimport { InvalidLocatorError } from \"./error.js\";\nimport { KeyLocator, KeyStore, ProvingKeyLocator, VerifyingKeyLocator } from \"./interface.js\";\nimport { MemKeyVerifier } from \"../verifier/memory.js\";\nimport { ProvingKey, VerifyingKey } from \"../../wasm.js\";\n\n/** Shape of each record stored in the IndexedDB object store. */\ninterface StoredKey {\n /** Serialized locator string (also the primary key). */\n locator: string;\n /** Raw key bytes. */\n bytes: Uint8Array;\n /** SHA-256 fingerprint computed at write time. */\n metadata: KeyFingerprint;\n}\n\n/**\n * Browser-compatible {@link KeyStore} backed by IndexedDB.\n *\n * This is the browser counterpart to {@link LocalFileKeyStore} (which requires Node.js `fs`).\n * It persists proving and verifying keys across page reloads and browser sessions using the\n * IndexedDB API available in all modern browsers and Web Workers.\n *\n * **Environment**: Browser / Web Worker only. Instantiating this class is safe in any\n * environment, but the first IndexedDB operation will reject with a clear error when\n * `globalThis.indexedDB` is not available (e.g., Node.js, SSR contexts). Guard your\n * imports accordingly if you bundle for server-side rendering.\n *\n * **Security**: Keys are stored as plaintext `Uint8Array` in IndexedDB. Any script\n * running on the same origin — including scripts injected via XSS — can read the stored\n * proving and verifying keys. Do **not** use this keystore for data that must remain\n * confidential under an XSS-capable adversary; encrypt sensitive material at the\n * application layer before persisting, or use an in-memory store.\n *\n * @example\n * ```ts\n * import { IndexedDBKeyStore, ProgramManager } from \"@provablehq/sdk\";\n *\n * const keyStore = new IndexedDBKeyStore();\n * const pm = new ProgramManager();\n * pm.setKeyStore(keyStore);\n * // Keys synthesized during execution are now cached in IndexedDB\n * // and reloaded automatically on subsequent runs.\n * ```\n */\nexport class IndexedDBKeyStore implements KeyStore {\n private readonly dbName: string;\n private readonly storeName = \"keys\";\n private readonly keyVerifier = new MemKeyVerifier();\n private dbPromise: Promise<IDBDatabase> | null = null;\n\n /**\n * @param dbName IndexedDB database name. Defaults to `\"aleo-keystore\"`.\n */\n constructor(dbName = \"aleo-keystore\") {\n this.dbName = dbName;\n }\n\n // -------------------------------------------------------\n // IndexedDB helpers\n // -------------------------------------------------------\n\n /** Opens (or creates) the database, returning a cached promise. */\n private openDB(): Promise<IDBDatabase> {\n if (this.dbPromise) return this.dbPromise;\n // Env check sits outside the Promise constructor so a failure doesn't\n // poison the cache; a later call (e.g. after a polyfill loads) can retry.\n if (typeof indexedDB === \"undefined\") {\n return Promise.reject(\n new Error(\n \"IndexedDBKeyStore requires a browser or Web Worker environment: \" +\n \"`indexedDB` is not defined. If you are in Node.js or an SSR \" +\n \"context, use LocalFileKeyStore or an in-memory key provider instead.\",\n ),\n );\n }\n this.dbPromise = new Promise<IDBDatabase>((resolve, reject) => {\n const request = indexedDB.open(this.dbName, 1);\n request.onupgradeneeded = () => {\n const db = request.result;\n if (!db.objectStoreNames.contains(this.storeName)) {\n db.createObjectStore(this.storeName, { keyPath: \"locator\" });\n }\n };\n request.onsuccess = () => resolve(request.result);\n request.onerror = () => {\n this.dbPromise = null;\n reject(request.error);\n };\n request.onblocked = () => {\n this.dbPromise = null;\n reject(new DOMException(\"Database open blocked\", \"AbortError\"));\n };\n });\n return this.dbPromise;\n }\n\n /** Runs a single read-write transaction and returns the request result. */\n private async tx<T>(\n mode: IDBTransactionMode,\n fn: (store: IDBObjectStore) => IDBRequest<T>,\n ): Promise<T> {\n const db = await this.openDB();\n return new Promise<T>((resolve, reject) => {\n const txn = db.transaction(this.storeName, mode);\n const store = txn.objectStore(this.storeName);\n const req = fn(store);\n let result: T;\n req.onsuccess = () => { result = req.result; };\n txn.oncomplete = () => resolve(result);\n txn.onerror = () => reject(txn.error);\n txn.onabort = () => reject(txn.error ?? new DOMException(\"Transaction aborted\", \"AbortError\"));\n });\n }\n\n // -------------------------------------------------------\n // Locator serialization (mirrors LocalFileKeyStore)\n // -------------------------------------------------------\n\n private validateComponent(value: string, label: string): void {\n if (value === \"\" || value === \".\") {\n throw new InvalidLocatorError(\n `KeyLocator ${label} must not be empty or \".\" (got \"${value}\")`,\n value,\n \"reserved_name\",\n );\n }\n if (value.includes(\"..\")) {\n throw new InvalidLocatorError(\n `KeyLocator ${label} must not contain \"..\" (got \"${value}\")`,\n value,\n \"path_traversal\",\n );\n }\n if (value.includes(\"/\") || value.includes(\"\\\\\") || value.includes(\"\\0\")) {\n throw new InvalidLocatorError(\n `KeyLocator ${label} must not contain path separators or null bytes (got \"${value}\")`,\n value,\n \"path_separator\",\n );\n }\n }\n\n private validateNonNegative(value: number, label: string): void {\n if (!Number.isInteger(value) || value < 0) {\n throw new InvalidLocatorError(\n `KeyLocator ${label} must be a non-negative integer (got ${value})`,\n String(value),\n \"negative_value\",\n );\n }\n }\n\n private serializeLocator(locator: KeyLocator): string {\n this.validateComponent(locator.program, \"program\");\n this.validateComponent(locator.functionName, \"functionName\");\n this.validateComponent(locator.network, \"network\");\n this.validateNonNegative(locator.edition, \"edition\");\n this.validateNonNegative(locator.amendment, \"amendment\");\n const base = `${locator.program}.${locator.functionName}.e${locator.edition}.a${locator.amendment}.${locator.network}.${locator.keyType}`;\n if (locator.keyType === \"translation\") {\n this.validateComponent(locator.recordName, \"recordName\");\n this.validateNonNegative(locator.recordInputPosition, \"recordInputPosition\");\n return `${base}.${locator.recordName}.${locator.recordInputPosition}`;\n }\n return base;\n }\n\n private checksumToFingerprint(checksum: string | undefined, keyBytes: Uint8Array): KeyFingerprint | undefined {\n if (!checksum) return undefined;\n return { checksum, size: keyBytes.length };\n }\n\n // -------------------------------------------------------\n // KeyStore interface\n // -------------------------------------------------------\n\n async getKeyBytes(locator: KeyLocator): Promise<Uint8Array | null> {\n const key = this.serializeLocator(locator);\n const record = await this.tx<StoredKey | undefined>(\"readonly\", (store) => store.get(key));\n if (!record) return null;\n\n const fingerprint =\n this.checksumToFingerprint(locator.checksum, record.bytes) ?? record.metadata;\n if (fingerprint) {\n await this.keyVerifier.verifyKeyBytes({\n keyBytes: record.bytes,\n locator: key,\n fingerprint,\n });\n }\n\n return record.bytes;\n }\n\n async getProvingKey(locator: ProvingKeyLocator): Promise<ProvingKey | null> {\n const bytes = await this.getKeyBytes(locator);\n if (!bytes) return null;\n return ProvingKey.fromBytes(bytes);\n }\n\n async getVerifyingKey(locator: VerifyingKeyLocator): Promise<VerifyingKey | null> {\n const bytes = await this.getKeyBytes(locator);\n if (!bytes) return null;\n return VerifyingKey.fromBytes(bytes);\n }\n\n async setKeys(\n proverLocator: ProvingKeyLocator,\n verifierLocator: VerifyingKeyLocator,\n keys: FunctionKeyPair,\n ): Promise<void> {\n const proverKey = this.serializeLocator(proverLocator);\n const verifierKey = this.serializeLocator(verifierLocator);\n\n const [provingKey, verifyingKey] = keys;\n const [provingKeyBytes, verifyingKeyBytes] = [\n provingKey.toBytes(),\n verifyingKey.toBytes(),\n ];\n\n const [proverFingerprint, verifierFingerprint] = await Promise.all([\n this.keyVerifier.computeKeyMetadata({\n keyBytes: provingKeyBytes,\n locator: proverKey,\n fingerprint: this.checksumToFingerprint(proverLocator.checksum, provingKeyBytes),\n }),\n this.keyVerifier.computeKeyMetadata({\n keyBytes: verifyingKeyBytes,\n locator: verifierKey,\n fingerprint: this.checksumToFingerprint(verifierLocator.checksum, verifyingKeyBytes),\n }),\n ]);\n\n const proverRecord: StoredKey = { locator: proverKey, bytes: provingKeyBytes, metadata: proverFingerprint };\n const verifierRecord: StoredKey = { locator: verifierKey, bytes: verifyingKeyBytes, metadata: verifierFingerprint };\n\n // Write both in a single transaction for atomicity.\n const db = await this.openDB();\n await new Promise<void>((resolve, reject) => {\n const txn = db.transaction(this.storeName, \"readwrite\");\n const store = txn.objectStore(this.storeName);\n store.put(proverRecord);\n store.put(verifierRecord);\n txn.oncomplete = () => resolve();\n txn.onerror = () => reject(txn.error);\n txn.onabort = () => reject(txn.error ?? new DOMException(\"Transaction aborted\", \"AbortError\"));\n });\n }\n\n async setKeyBytes(keyBytes: Uint8Array, locator: KeyLocator): Promise<void> {\n const key = this.serializeLocator(locator);\n\n const computedMetadata = await this.keyVerifier.computeKeyMetadata({\n keyBytes,\n locator: key,\n fingerprint: this.checksumToFingerprint(locator.checksum, keyBytes),\n });\n\n const record: StoredKey = { locator: key, bytes: keyBytes, metadata: computedMetadata };\n await this.tx(\"readwrite\", (store) => store.put(record));\n }\n\n async getKeyMetadata(locator: KeyLocator): Promise<KeyFingerprint | null> {\n const key = this.serializeLocator(locator);\n const record = await this.tx<StoredKey | undefined>(\"readonly\", (store) => store.get(key));\n return record?.metadata ?? null;\n }\n\n async has(locator: KeyLocator): Promise<boolean> {\n const key = this.serializeLocator(locator);\n const count = await this.tx<number>(\"readonly\", (store) => store.count(IDBKeyRange.only(key)));\n return count > 0;\n }\n\n async delete(locator: KeyLocator): Promise<void> {\n const key = this.serializeLocator(locator);\n await this.tx(\"readwrite\", (store) => store.delete(key));\n }\n\n async clear(): Promise<void> {\n await this.tx(\"readwrite\", (store) => store.clear());\n }\n}\n","import { cryptoBoxSeal } from \"@serenity-kit/noble-sodium\";\nimport { base64 } from \"@scure/base\";\nimport { ViewKey, Authorization, ProvingRequest } from \"./wasm.js\";\n\n/**\n * Encrypt an authorization with a cryptobox X25519 public key (libsodium-compatible wire format).\n *\n * @param {string} publicKey The cryptobox X25519 public key to encrypt with (encoded in RFC 4648 standard Base64).\n * @param {Authorization} authorization the authorization to encrypt.\n *\n * @returns {string} the encrypted authorization in RFC 4648 standard Base64.\n */\nexport function encryptAuthorization(publicKey: string, authorization: Authorization): string {\n // Zeroize the intermediate plaintext bytes regardless of success or\n // failure — same pattern as encryptRegistrationRequest.\n const bytes = authorization.toBytesLe();\n try {\n return encryptMessage(publicKey, bytes);\n } finally {\n zeroizeBytes(bytes);\n }\n}\n\n/**\n * Encrypt a ProvingRequest with a cryptobox X25519 public key (libsodium-compatible wire format).\n *\n * @param {string} publicKey The cryptobox X25519 public key to encrypt with (encoded in RFC 4648 standard Base64).\n * @param {ProvingRequest} provingRequest the ProvingRequest to encrypt.\n *\n * @returns {string} the encrypted ProvingRequest in RFC 4648 standard Base64.\n */\nexport function encryptProvingRequest(publicKey: string, provingRequest: ProvingRequest): string {\n // Zeroize the intermediate plaintext bytes regardless of success or\n // failure — same pattern as encryptRegistrationRequest.\n const bytes = provingRequest.toBytesLe();\n try {\n return encryptMessage(publicKey, bytes);\n } finally {\n zeroizeBytes(bytes);\n }\n}\n\n/**\n * Serialize a ProvingRequest for later encryption with\n * `encryptSerializedProvingRequest`.\n *\n * Useful when the request may have to be encrypted more than once: the\n * delegated proving service's one-time keys are single-use, so a client that\n * needs to resend a request (e.g. after the service rejected a spent or\n * unknown `key_id`) can keep the serialized form and re-encrypt it for a\n * fresh key instead of rebuilding and re-signing the request.\n *\n * The returned buffer contains the plaintext request (including the signed\n * authorization and its inputs) and is owned by the caller: keep it only as\n * long as a resend may still be needed, then overwrite it with\n * `zeroizeBytes(serialized)`. Zeroization in JavaScript is best-effort (see\n * `zeroizeBytes`), and transient copies made inside the wasm boundary by\n * `toBytesLe` are outside its reach.\n *\n * @param {ProvingRequest} provingRequest the ProvingRequest to serialize.\n *\n * @returns {Uint8Array} the serialized ProvingRequest bytes.\n */\nexport function serializeProvingRequest(provingRequest: ProvingRequest): Uint8Array {\n return provingRequest.toBytesLe();\n}\n\n/**\n * Encrypt an already-serialized ProvingRequest (as produced by\n * `serializeProvingRequest`) with a cryptobox X25519 public key\n * (libsodium-compatible wire format).\n *\n * Produces exactly the same ciphertext format as `encryptProvingRequest` —\n * `encryptSerializedProvingRequest(pk, serializeProvingRequest(req))` and\n * `encryptProvingRequest(pk, req)` are interchangeable from the proving\n * service's point of view.\n *\n * The input buffer is not mutated and deliberately not zeroized here: the\n * caller keeps ownership so the same bytes can be re-encrypted for another\n * one-time key (retry). Call `zeroizeBytes(serializedProvingRequest)` once\n * no resend can be needed anymore.\n *\n * @param {string} publicKey The cryptobox X25519 public key to encrypt with (encoded in RFC 4648 standard Base64).\n * @param {Uint8Array} serializedProvingRequest the serialized ProvingRequest bytes.\n *\n * @returns {string} the encrypted ProvingRequest in RFC 4648 standard Base64.\n */\nexport function encryptSerializedProvingRequest(\n publicKey: string,\n serializedProvingRequest: Uint8Array,\n): string {\n return encryptMessage(publicKey, serializedProvingRequest);\n}\n\n/**\n * Encrypt a view key with a cryptobox X25519 public key (libsodium-compatible wire format).\n *\n * @param {string} publicKey The cryptobox X25519 public key to encrypt with (encoded in RFC 4648 standard Base64).\n * @param {ViewKey} viewKey the view key to encrypt.\n *\n * @returns {string} the encrypted view key in RFC 4648 standard Base64.\n */\nexport function encryptViewKey(publicKey: string, viewKey: ViewKey): string {\n // Zeroize the intermediate plaintext bytes regardless of success or\n // failure — same pattern as encryptRegistrationRequest.\n const bytes = viewKey.toBytesLe();\n try {\n return encryptMessage(publicKey, bytes);\n } finally {\n zeroizeBytes(bytes);\n }\n}\n\n/**\n * Encrypt a record scanner registration request.\n *\n * @param {string} publicKey The cryptobox X25519 public key to encrypt with (encoded in RFC 4648 standard Base64).\n * @param {ViewKey} viewKey the view key to encrypt.\n * @param {number} start the start height of the registration request.\n *\n * @returns {string} the encrypted view key in RFC 4648 standard Base64.\n */\nexport function encryptRegistrationRequest(publicKey: string, viewKey: ViewKey, start: number): string {\n // Turn the view key into a Uint8Array.\n const vk_bytes: Uint8Array = viewKey.toBytesLe();\n // Create a new array to hold the original bytes and the 4-byte start height.\n const bytes = new Uint8Array(vk_bytes.length + 4);\n\n // Copy existing bytes.\n bytes.set(vk_bytes, 0);\n\n // Write the 4-byte number in LE format at the end of the array.\n const view = new DataView(bytes.buffer);\n view.setUint32(vk_bytes.length, start, true);\n\n // Encrypt the encoded bytes, ensuring sensitive intermediate\n // byte arrays are zeroized regardless of success or failure.\n try {\n return encryptMessage(publicKey, bytes);\n } finally {\n zeroizeBytes(vk_bytes);\n zeroizeBytes(bytes);\n }\n}\n\n/**\n * Best-effort zeroization of a byte array by overwriting all bytes with zeros.\n * Use this to clear sensitive data (e.g., key bytes) from memory when working\n * with Uint8Array representations of keys or other secrets.\n *\n * This is best-effort in JavaScript — the JIT compiler could theoretically\n * elide the fill if the array is never read again (though current engines\n * do not). For deterministic zeroization of key material, use\n * `Account.destroy()` or call `.free()` on key objects (PrivateKey, ViewKey,\n * ComputeKey, GraphKey) whose Rust Drop implementations zeroize memory\n * before deallocation.\n *\n * Note: This cannot zeroize JavaScript strings, which are immutable and managed\n * by the garbage collector. Prefer using byte array representations of sensitive\n * data over strings whenever possible.\n *\n * @param {Uint8Array} bytes The byte array to zeroize\n *\n * @example\n * const keyBytes = privateKey.toBytesLe();\n * // ... use keyBytes ...\n * zeroizeBytes(keyBytes); // Overwrite with zeros when done\n */\nexport function zeroizeBytes(bytes: Uint8Array): void {\n bytes.fill(0);\n}\n\n/**\n * Encrypt arbitrary bytes with a cryptobox public key using the libsodium\n * `crypto_box_seal` wire format.\n *\n * The implementation is delegated to `@serenity-kit/noble-sodium`, which\n * composes the primitive steps of `crypto_box_seal` — ephemeral X25519\n * keypair, blake2b-24 nonce derivation over `epk || rpk`, HSalsa20 key\n * derivation from the ECDH shared secret, and XSalsa20-Poly1305 AEAD — on\n * top of the audited `@noble/*` primitives. Output is byte-identical to\n * libsodium's `crypto_box_seal` for any given ephemeral key, so ciphertexts\n * are decryptable by any libsodium-compatible backend (e.g. `sodiumoxide`,\n * `libsodium-sys`) with no changes.\n *\n * @param {string} publicKey The cryptobox X25519 public key to encrypt with (encoded in RFC 4648 standard Base64).\n * @param {Uint8Array} message the bytes to encrypt.\n *\n * @returns {string} the encrypted bytes in RFC 4648 standard Base64.\n */\nfunction encryptMessage(publicKey: string, message: Uint8Array): string {\n const publicKeyBytes = base64.decode(publicKey);\n const ciphertext = cryptoBoxSeal({ message, publicKey: publicKeyBytes });\n return base64.encode(ciphertext);\n}\n","import { logger } from \"./utils/logger.js\";\nimport {\n Address,\n ComputeKey,\n EncryptionToolkit,\n Field,\n Group,\n PrivateKey,\n Signature,\n ViewKey,\n PrivateKeyCiphertext,\n RecordCiphertext,\n RecordPlaintext,\n} from \"./wasm.js\";\nimport { zeroizeBytes } from \"./security.js\";\n\ninterface AccountParam {\n privateKey?: string | PrivateKey;\n seed?: Uint8Array;\n}\n\n/**\n * Key Management class. Enables the creation of a new Aleo Account, importation of an existing account from\n * an existing private key or seed, and message signing and verification functionality. An Aleo Account is generated\n * from a randomly generated seed (number) from which an account private key, view key, and a public account address are\n * derived. The private key lies at the root of an Aleo account. It is a highly sensitive secret and should be protected\n * as it allows for creation of Aleo Program executions and arbitrary value transfers. The View Key allows for decryption\n * of a user's activity on the blockchain. The Address is the public address to which other users of Aleo can send Aleo\n * credits and other records to. This class should only be used in environments where the safety of the underlying key\n * material can be assured.\n *\n * When an Account is no longer needed, call {@link destroy} to securely zeroize and free all sensitive key material\n * from WASM memory. Alternatively, use `[Symbol.dispose]()` with the `using` declaration in ES2024+ environments.\n *\n * @example\n * import { Account } from \"@provablehq/sdk/testnet.js\";\n *\n * // Create a new account\n * const myRandomAccount = new Account();\n *\n * // Create an account from a randomly generated seed\n * const seed = new Uint8Array([94, 91, 52, 251, 240, 230, 226, 35, 117, 253, 224, 210, 175, 13, 205, 120, 155, 214, 7, 169, 66, 62, 206, 50, 188, 40, 29, 122, 40, 250, 54, 18]);\n * const mySeededAccount = new Account({seed: seed});\n *\n * // Create an account from an existing private key\n * const myExistingAccount = new Account({privateKey: process.env.privateKey});\n *\n * // Sign a message\n * const hello_world = Uint8Array.from([104, 101, 108, 108, 111 119, 111, 114, 108, 100]);\n * const signature = myRandomAccount.sign(hello_world);\n *\n * // Verify a signature\n * assert(myRandomAccount.verify(hello_world, signature));\n *\n * // Securely destroy the account when done\n * myRandomAccount.destroy();\n */\nexport class Account {\n _privateKey: PrivateKey;\n _viewKey: ViewKey;\n _computeKey: ComputeKey;\n _address: Address;\n private _destroyed = false;\n\n constructor(params: AccountParam = {}) {\n try {\n this._privateKey = this.privateKeyFromParams(params);\n } catch (e) {\n logger.error(\"Wrong parameter\", e);\n throw new Error(\"Wrong Parameter\");\n }\n this._viewKey = ViewKey.from_private_key(this._privateKey);\n this._computeKey = ComputeKey.from_private_key(this._privateKey);\n this._address = Address.from_private_key(this._privateKey);\n }\n\n /**\n * Attempts to create an account from a private key ciphertext\n * @param {PrivateKeyCiphertext | string} ciphertext The encrypted private key ciphertext or its string representation\n * @param {string} password The password used to decrypt the private key ciphertext\n * @returns {Account} A new Account instance created from the decrypted private key\n *\n * @example\n * import { Account } from \"@provablehq/sdk/testnet.js\";\n *\n * // Create an account object from a previously encrypted ciphertext and password.\n * const account = Account.fromCiphertext(process.env.ciphertext, process.env.password);\n */\n public static fromCiphertext(ciphertext: PrivateKeyCiphertext | string, password: string): Account {\n try {\n ciphertext = (typeof ciphertext === \"string\") ? PrivateKeyCiphertext.fromString(ciphertext) : ciphertext;\n const _privateKey = PrivateKey.fromPrivateKeyCiphertext(ciphertext, password);\n try {\n return new Account({ privateKey: _privateKey });\n } finally {\n _privateKey.free(); // Zeroize + free the temporary; Account owns its own clone\n }\n } catch(e) {\n throw new Error(\"Wrong password or invalid ciphertext\");\n }\n }\n\n /**\n * Validates whether the given input is a valid Aleo address.\n * @param {string | Uint8Array} address The address to validate, either as a string or bytes\n * @returns {boolean} True if the address is valid, false otherwise\n *\n * @example\n * import { Account } from \"@provablehq/sdk/testnet.js\";\n *\n * const isValid = Account.isValidAddress(\"aleo1rhgdu77hgyqd3xjj8ucu3jj9r2krwz6mnzyd80gncr5fxcwlh5rsvzp9px\");\n * console.log(isValid); // true\n *\n * const isInvalid = Account.isValidAddress(\"invalid_address\");\n * console.log(isInvalid); // false\n */\n public static isValidAddress(address: string | Uint8Array): boolean {\n return Address.isValid(address);\n }\n\n /**\n * Creates a PrivateKey from the provided parameters.\n * @param {AccountParam} params The parameters containing either a private key string, PrivateKey object, or a seed\n * @returns {PrivateKey} A PrivateKey instance derived from the provided parameters\n */\n private privateKeyFromParams(params: AccountParam): PrivateKey {\n if (params.seed) {\n return PrivateKey.from_seed_unchecked(params.seed);\n }\n if (params.privateKey) {\n if (typeof params.privateKey === 'string') {\n return PrivateKey.from_string(params.privateKey);\n }\n // Clone the PrivateKey WASM object via byte serialization to avoid\n // creating an immutable JS string of the private key.\n const bytes = params.privateKey.toBytesLe();\n try {\n return PrivateKey.fromBytesLe(bytes);\n } finally {\n zeroizeBytes(bytes);\n }\n }\n return new PrivateKey();\n }\n\n /**\n * Throws an error if this account has been destroyed.\n */\n private assertNotDestroyed(): void {\n if (this._destroyed) {\n throw new Error(\"Account has been destroyed. Create a new Account instance.\");\n }\n }\n\n /**\n * Returns the PrivateKey associated with the account.\n * @returns {PrivateKey} The private key of the account\n *\n * @example\n * import { Account } from \"@provablehq/sdk/testnet.js\";\n *\n * const account = new Account();\n * const privateKey = account.privateKey();\n */\n privateKey(): PrivateKey {\n this.assertNotDestroyed();\n return this._privateKey;\n }\n\n /**\n * Returns the ViewKey associated with the account.\n * @returns {ViewKey} The view key of the account\n *\n * @example\n * import { Account } from \"@provablehq/sdk/testnet.js\";\n *\n * const account = new Account();\n * const viewKey = account.viewKey();\n */\n viewKey(): ViewKey {\n this.assertNotDestroyed();\n return this._viewKey;\n }\n\n /**\n * Returns the ComputeKey associated with the account.\n * @returns {ComputeKey} The compute key of the account\n *\n * @example\n * import { Account } from \"@provablehq/sdk/testnet.js\";\n *\n * const account = new Account();\n * const computeKey = account.computeKey();\n */\n computeKey(): ComputeKey {\n this.assertNotDestroyed();\n return this._computeKey;\n }\n\n /**\n * Returns the Aleo address associated with the account.\n * @returns {Address} The public address of the account\n *\n * @example\n * import { Account } from \"@provablehq/sdk/testnet.js\";\n *\n * const account = new Account();\n * const address = account.address();\n */\n address(): Address {\n this.assertNotDestroyed();\n return this._address;\n }\n\n /**\n * Deep clones the Account via byte serialization of the private key,\n * avoiding creation of immutable JS string representations of the private key.\n * @returns {Account} A new Account instance with the same private key\n *\n * @example\n * import { Account } from \"@provablehq/sdk/testnet.js\";\n *\n * const account = new Account();\n * const clonedAccount = account.clone();\n */\n clone(): Account {\n this.assertNotDestroyed();\n return new Account({ privateKey: this._privateKey });\n }\n\n /**\n * Returns the address of the account in a string representation.\n *\n * @returns {string} The string representation of the account address\n */\n toString(): string {\n this.assertNotDestroyed();\n return this.address().to_string()\n }\n\n /**\n * Encrypts the account's private key with a password.\n *\n * @param {string} password Password to encrypt the private key.\n * @returns {PrivateKeyCiphertext} The encrypted private key ciphertext\n *\n * @example\n * import { Account } from \"@provablehq/sdk/testnet.js\";\n *\n * const account = new Account();\n * const ciphertext = account.encryptAccount(\"password\");\n * process.env.ciphertext = ciphertext.toString();\n */\n encryptAccount(password: string): PrivateKeyCiphertext {\n this.assertNotDestroyed();\n return this._privateKey.toCiphertext(password);\n }\n\n /**\n * Decrypts an encrypted record string into a plaintext record object.\n *\n * @param {string} ciphertext A string representing the ciphertext of a record.\n * @returns {RecordPlaintext} The decrypted record plaintext\n *\n * @example\n * // Import the AleoNetworkClient and Account classes\n * import { AleoNetworkClient, Account } from \"@provablehq/sdk/testnet.js\";\n *\n * // Create a connection to the Aleo network and an account\n * const networkClient = new AleoNetworkClient(\"https://api.provable.com/v2\");\n * const account = Account.fromCiphertext(process.env.ciphertext!, process.env.password!);\n *\n * // Get the record ciphertexts from a transaction.\n * const transaction = await networkClient.getTransactionObject(\"at1fjy6s9md2v4rgcn3j3q4qndtfaa2zvg58a4uha0rujvrn4cumu9qfazxdd\");\n * const records = transaction.records();\n *\n * // Decrypt any records the account owns.\n * const decryptedRecords = [];\n * for (const record of records) {\n * if (account.decryptRecord(record)) {\n * decryptedRecords.push(record);\n * }\n * }\n */\n decryptRecord(ciphertext: string): RecordPlaintext {\n this.assertNotDestroyed();\n return this._viewKey.decrypt(ciphertext) as unknown as RecordPlaintext;\n }\n\n /**\n * Decrypts an array of Record ciphertext strings into an array of record plaintext objects.\n *\n * @param {string[]} ciphertexts An array of strings representing the ciphertexts of records.\n * @returns {RecordPlaintext[]} An array of decrypted record plaintexts\n *\n * @example\n * // Import the AleoNetworkClient and Account classes\n * import { AleoNetworkClient, Account } from \"@provablehq/sdk/testnet.js\";\n *\n * // Create a connection to the Aleo network and an account\n * const networkClient = new AleoNetworkClient(\"https://api.provable.com/v2\");\n * const account = Account.fromCiphertext(process.env.ciphertext!, process.env.password!);\n *\n * // Get the record ciphertexts from a transaction.\n * const transaction = await networkClient.getTransactionObject(\"at1fjy6s9md2v4rgcn3j3q4qndtfaa2zvg58a4uha0rujvrn4cumu9qfazxdd\");\n * const records = transaction.records();\n *\n * // Decrypt any records the account owns. If the account owns no records, the array will be empty.\n * const decryptedRecords = account.decryptRecords(records);\n */\n decryptRecords(ciphertexts: string[]): RecordPlaintext[] {\n this.assertNotDestroyed();\n return ciphertexts.map((ciphertext) => this._viewKey.decrypt(ciphertext) as unknown as RecordPlaintext);\n }\n\n /**\n * Generates a record view key from the account owner's view key and the record ciphertext.\n * This key can be used to decrypt the record without revealing the account's view key.\n * @param {RecordCiphertext | string} recordCiphertext The record ciphertext to generate the view key for\n * @returns {Field} The record view key\n *\n * @example\n * // Import the Account class\n * import { Account } from \"@provablehq/sdk/testnet.js\";\n *\n * // Create an account object from a previously encrypted ciphertext and password.\n * const account = Account.fromCiphertext(process.env.ciphertext!, process.env.password!);\n *\n * // Generate a record view key from the account's view key and a record ciphertext\n * const recordCiphertext = RecordCiphertext.fromString(\"your_record_ciphertext_here\");\n * const recordViewKey = account.generateRecordViewKey(recordCiphertext);\n */\n generateRecordViewKey(recordCiphertext: RecordCiphertext | string): Field {\n this.assertNotDestroyed();\n if (typeof recordCiphertext === 'string') {\n recordCiphertext = RecordCiphertext.fromString(recordCiphertext);\n }\n if (!(recordCiphertext.isOwner(this._viewKey))) {\n throw new Error(\"The record ciphertext does not belong to this account\");\n }\n return EncryptionToolkit.generateRecordViewKey(this._viewKey, recordCiphertext);\n }\n\n /**\n * Generates a transition view key from the account owner's view key and the transition public key.\n * This key can be used to decrypt the private inputs and outputs of a the transition without\n * revealing the account's view key.\n * @param {string | Group} tpk The transition public key\n * @returns {Field} The transition view key\n *\n * @example\n * // Import the Account class\n * import { Account } from \"@provablehq/sdk/testnet.js\";\n *\n * // Generate a transition view key from the account's view key and a transition public key\n * const tpk = Group.fromString(\"your_transition_public_key_here\");\n *\n * const transitionViewKey = account.generateTransitionViewKey(tpk);\n */\n generateTransitionViewKey(tpk: string | Group): Field {\n this.assertNotDestroyed();\n if (typeof tpk === 'string') {\n tpk = Group.fromString(tpk);\n }\n return EncryptionToolkit.generateTvk(this._viewKey, tpk);\n }\n\n /**\n * Determines whether the account owns a ciphertext record.\n * @param {RecordCiphertext | string} ciphertext The record ciphertext to check ownership of\n * @returns {boolean} True if the account owns the record, false otherwise\n *\n * @example\n * // Import the AleoNetworkClient and Account classes\n * import { AleoNetworkClient, Account } from \"@provablehq/sdk/testnet.js\";\n *\n * // Create a connection to the Aleo network and an account\n * const networkClient = new AleoNetworkClient(\"https://api.provable.com/v2\");\n * const account = Account.fromCiphertext(process.env.ciphertext!, process.env.password!);\n *\n * // Get the record ciphertexts from a transaction and check ownership of them.\n * const transaction = await networkClient.getTransactionObject(\"at1fjy6s9md2v4rgcn3j3q4qndtfaa2zvg58a4uha0rujvrn4cumu9qfazxdd\");\n * const records = transaction.records();\n *\n * // Check if the account owns any of the record ciphertexts present in the transaction.\n * const ownedRecords = [];\n * for (const record of records) {\n * if (account.ownsRecordCiphertext(record)) {\n * ownedRecords.push(record);\n * }\n * }\n */\n ownsRecordCiphertext(ciphertext: RecordCiphertext | string): boolean {\n this.assertNotDestroyed();\n if (typeof ciphertext === 'string') {\n try {\n const ciphertextObject = RecordCiphertext.fromString(ciphertext);\n return ciphertextObject.isOwner(this._viewKey);\n }\n catch (e) {\n return false;\n }\n }\n else {\n return ciphertext.isOwner(this._viewKey);\n }\n }\n\n /**\n * Signs a message with the account's private key.\n * Returns a Signature.\n *\n * @param {Uint8Array} message Message to be signed.\n * @retur