UNPKG

zerokey

Version:

Zero-knowledge cross-domain secret sharing library using ECDH encryption

1 lines 33.1 kB
{"version":3,"sources":["../src/crypto.ts","../src/server.ts"],"names":[],"mappings":"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","file":"server.cjs","sourcesContent":["// Base64url encoding/decoding utilities\n\n/**\n * Encodes an ArrayBuffer to a base64url string.\n *\n * Base64url is a URL-safe variant of base64 encoding that replaces\n * '+' with '-', '/' with '_', and removes padding '=' characters.\n * This makes it suitable for use in URLs and filenames.\n *\n * @param {ArrayBuffer} buffer - The binary data to encode.\n * @returns {string} The base64url-encoded string.\n * @internal\n */\nfunction base64urlEncode(buffer: ArrayBuffer): string {\n const base64 = btoa(String.fromCharCode(...new Uint8Array(buffer)));\n return base64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=/g, '');\n}\n\n/**\n * Decodes a base64url string to an ArrayBuffer.\n *\n * Reverses the base64url encoding by restoring the standard base64\n * characters and padding before decoding to binary data.\n *\n * @param {string} str - The base64url-encoded string to decode.\n * @returns {ArrayBuffer} The decoded binary data.\n * @internal\n */\nfunction base64urlDecode(str: string): ArrayBuffer {\n const base64 = str\n .replace(/-/g, '+')\n .replace(/_/g, '/')\n .padEnd(str.length + ((4 - (str.length % 4)) % 4), '=');\n const binary = atob(base64);\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) {\n bytes[i] = binary.charCodeAt(i);\n }\n return bytes.buffer;\n}\n\n// Type definitions\n\n/**\n * Represents an asymmetric cryptographic key pair for ECDH operations.\n *\n * @interface KeyPair\n * @property {string} publicKey - The public key encoded as a base64url string.\n * This key can be safely shared with others.\n * @property {CryptoKey} privateKey - The private key as a native CryptoKey object.\n * This key must be kept secret and never shared.\n *\n * @example\n * ```typescript\n * const keyPair = await generateKeyPair();\n * console.log(keyPair.publicKey); // Safe to share\n * // keyPair.privateKey is kept secret\n * ```\n */\nexport interface KeyPair {\n publicKey: string;\n privateKey: CryptoKey;\n}\n\n/**\n * Internal structure for storing encrypted data along with the ephemeral public key.\n * Used in hybrid encryption scheme.\n *\n * @interface EncryptedPayload\n * @internal\n */\ninterface EncryptedPayload {\n ephemeralPublicKey: string;\n encryptedData: string;\n}\n\n/**\n * Generates a new ECDH (Elliptic Curve Diffie-Hellman) key pair using the P-256 curve.\n *\n * This function creates a cryptographically secure key pair suitable for key exchange\n * and hybrid encryption operations. The P-256 curve (also known as secp256r1) provides\n * 128-bit security strength.\n *\n * @returns {Promise<KeyPair>} A promise that resolves to a KeyPair object containing\n * the public key (as base64url string) and private key (as CryptoKey).\n *\n * @throws {Error} Throws if the Web Crypto API is not available or key generation fails.\n *\n * @example\n * ```typescript\n * const keyPair = await generateKeyPair();\n * // Store keyPair.publicKey in database or share with others\n * // Keep keyPair.privateKey secure and never transmit it\n * ```\n *\n * @see {@link https://www.w3.org/TR/WebCryptoAPI/#ecdh|W3C Web Crypto API ECDH}\n */\nexport async function generateKeyPair(): Promise<KeyPair> {\n const keyPair = await crypto.subtle.generateKey(\n {\n name: 'ECDH',\n namedCurve: 'P-256'\n },\n true,\n ['deriveKey']\n );\n\n const publicKeyExported = await exportPublicKey(keyPair.publicKey);\n\n return {\n publicKey: publicKeyExported,\n privateKey: keyPair.privateKey\n };\n}\n\n/**\n * Exports a CryptoKey public key to a base64url-encoded string format.\n *\n * This function converts a native CryptoKey object to a portable string format\n * that can be safely transmitted over networks, stored in databases, or shared\n * between different systems. The base64url encoding is URL-safe and doesn't\n * require additional encoding.\n *\n * @param {CryptoKey} publicKey - The public key to export. Must be an ECDH P-256 public key.\n *\n * @returns {Promise<string>} A promise that resolves to the base64url-encoded public key.\n *\n * @throws {Error} Throws if the key export fails or if the provided key is not a valid public key.\n *\n * @example\n * ```typescript\n * const keyPair = await generateKeyPair();\n * const publicKeyString = await exportPublicKey(keyPair.publicKey);\n * // publicKeyString can now be stored or transmitted\n * ```\n */\nexport async function exportPublicKey(publicKey: CryptoKey): Promise<string> {\n const exported = await crypto.subtle.exportKey('raw', publicKey);\n return base64urlEncode(exported);\n}\n\n/**\n * Imports a public key from a base64url-encoded string to a CryptoKey object.\n *\n * This function converts a portable string representation of a public key back\n * into a native CryptoKey object that can be used for cryptographic operations.\n * The key must be a valid ECDH P-256 public key in raw format.\n *\n * @param {string} publicKeyString - The base64url-encoded public key string to import.\n *\n * @returns {Promise<CryptoKey>} A promise that resolves to the imported public key as a CryptoKey object.\n *\n * @throws {Error} Throws if the string is not a valid base64url-encoded key or if import fails.\n *\n * @example\n * ```typescript\n * const publicKeyString = \"your-base64url-encoded-public-key\";\n * const publicKey = await importPublicKey(publicKeyString);\n * // publicKey can now be used for encryption operations\n * ```\n */\nexport async function importPublicKey(publicKeyString: string): Promise<CryptoKey> {\n const keyData = base64urlDecode(publicKeyString);\n return await crypto.subtle.importKey(\n 'raw',\n keyData,\n {\n name: 'ECDH',\n namedCurve: 'P-256'\n },\n false,\n []\n );\n}\n\n/**\n * Serializes a private key to a JSON string for secure storage.\n *\n * This function exports a private key in JWK (JSON Web Key) format, which preserves\n * all key parameters and can be safely stored in secure storage systems. The resulting\n * string contains sensitive key material and must be protected accordingly.\n *\n * @param {CryptoKey} privateKey - The private key to serialize. Must be an ECDH P-256 private key.\n *\n * @returns {Promise<string>} A promise that resolves to the JSON-serialized private key.\n *\n * @throws {Error} Throws if the key export fails or if the provided key is not extractable.\n *\n * @example\n * ```typescript\n * const keyPair = await generateKeyPair();\n * const serialized = await serializePrivateKey(keyPair.privateKey);\n * // Store 'serialized' in secure storage (e.g., encrypted database, secure keychain)\n * ```\n *\n * @security This function returns sensitive key material. Ensure the output is stored\n * securely and never transmitted over insecure channels.\n */\nexport async function serializePrivateKey(privateKey: CryptoKey): Promise<string> {\n const exported = await crypto.subtle.exportKey('jwk', privateKey);\n return JSON.stringify(exported);\n}\n\n/**\n * Deserializes a private key from a JSON string back to a CryptoKey object.\n *\n * This function imports a private key that was previously serialized using\n * serializePrivateKey(). It restores the key to a usable CryptoKey object\n * for cryptographic operations.\n *\n * @param {string} privateKeyString - The JSON-serialized private key string to deserialize.\n *\n * @returns {Promise<CryptoKey>} A promise that resolves to the imported private key as a CryptoKey object.\n *\n * @throws {Error} Throws if the string is not valid JSON or contains an invalid JWK structure.\n *\n * @example\n * ```typescript\n * // Retrieve serialized key from secure storage\n * const serialized = await getFromSecureStorage('privateKey');\n * const privateKey = await deserializePrivateKey(serialized);\n * // privateKey can now be used for decryption operations\n * ```\n *\n * @security Handle the input string with care as it contains sensitive key material.\n */\nexport async function deserializePrivateKey(privateKeyString: string): Promise<CryptoKey> {\n const jwk = JSON.parse(privateKeyString);\n return await crypto.subtle.importKey(\n 'jwk',\n jwk,\n {\n name: 'ECDH',\n namedCurve: 'P-256'\n },\n true,\n ['deriveKey']\n );\n}\n\n/**\n * Derives a shared secret key using ECDH key agreement.\n *\n * This function performs the ECDH key agreement protocol to derive\n * a shared AES-256 key from a private key and a public key. The\n * resulting key can be used for symmetric encryption/decryption.\n *\n * @param {CryptoKey} privateKey - The private key for the ECDH operation.\n * @param {CryptoKey} publicKey - The public key for the ECDH operation.\n * @returns {Promise<CryptoKey>} A promise that resolves to the derived AES-256 key.\n * @internal\n */\nasync function deriveSharedSecret(privateKey: CryptoKey, publicKey: CryptoKey): Promise<CryptoKey> {\n return await crypto.subtle.deriveKey(\n {\n name: 'ECDH',\n public: publicKey\n },\n privateKey,\n {\n name: 'AES-GCM',\n length: 256\n },\n false,\n ['encrypt', 'decrypt']\n );\n}\n\n/**\n * Encrypts a string using AES-GCM with a random IV.\n *\n * AES-GCM provides authenticated encryption, ensuring both confidentiality\n * and integrity of the data. A 96-bit random IV is generated for each\n * encryption operation and prepended to the encrypted data.\n *\n * @param {CryptoKey} key - The AES-256 key for encryption.\n * @param {string} data - The plaintext string to encrypt.\n * @returns {Promise<ArrayBuffer>} A promise that resolves to the combined IV + encrypted data.\n * @internal\n */\nasync function aesEncrypt(key: CryptoKey, data: string): Promise<ArrayBuffer> {\n const encoder = new TextEncoder();\n const iv = crypto.getRandomValues(new Uint8Array(12));\n\n const encrypted = await crypto.subtle.encrypt(\n {\n name: 'AES-GCM',\n iv: iv\n },\n key,\n encoder.encode(data)\n );\n\n // Combine iv and encrypted data\n const combined = new Uint8Array(iv.length + encrypted.byteLength);\n combined.set(iv, 0);\n combined.set(new Uint8Array(encrypted), iv.length);\n\n return combined.buffer;\n}\n\n/**\n * Decrypts data that was encrypted using AES-GCM.\n *\n * Extracts the IV from the beginning of the combined data and uses it\n * along with the provided key to decrypt the remaining encrypted data.\n * AES-GCM also verifies the authenticity of the data during decryption.\n *\n * @param {CryptoKey} key - The AES-256 key for decryption.\n * @param {ArrayBuffer} combinedData - The combined IV + encrypted data.\n * @returns {Promise<string>} A promise that resolves to the decrypted plaintext string.\n * @throws {Error} Throws if decryption fails or authentication tag verification fails.\n * @internal\n */\nasync function aesDecrypt(key: CryptoKey, combinedData: ArrayBuffer): Promise<string> {\n const data = new Uint8Array(combinedData);\n const iv = data.slice(0, 12);\n const encrypted = data.slice(12);\n\n const decrypted = await crypto.subtle.decrypt(\n {\n name: 'AES-GCM',\n iv: iv\n },\n key,\n encrypted\n );\n\n const decoder = new TextDecoder();\n return decoder.decode(decrypted);\n}\n\n/**\n * Encrypts a secret using hybrid encryption (ECDH + AES-GCM).\n *\n * This function implements a hybrid encryption scheme that combines the security of\n * asymmetric encryption with the efficiency of symmetric encryption. It generates\n * an ephemeral key pair, derives a shared secret using ECDH, and then encrypts\n * the data using AES-GCM with the derived key.\n *\n * The encryption process:\n * 1. Generates an ephemeral ECDH key pair\n * 2. Derives a shared AES key using ECDH with the recipient's public key\n * 3. Encrypts the secret using AES-GCM\n * 4. Packages the ephemeral public key with the encrypted data\n *\n * @param {string} secret - The secret data to encrypt. Can be any string value.\n * @param {string} publicKeyString - The recipient's public key as a base64url-encoded string.\n *\n * @returns {Promise<string>} A promise that resolves to a base64url-encoded encrypted payload\n * containing both the ephemeral public key and encrypted data.\n *\n * @throws {Error} Throws if encryption fails or if the public key is invalid.\n *\n * @example\n * ```typescript\n * const recipientPublicKey = \"their-public-key-string\";\n * const secretMessage = \"This is a secret message\";\n *\n * const encrypted = await encryptWithPublicKey(secretMessage, recipientPublicKey);\n * // 'encrypted' can be safely transmitted to the recipient\n * // Only the holder of the corresponding private key can decrypt it\n * ```\n *\n * @see {@link decryptWithPrivateKey} for the corresponding decryption function\n */\nexport async function encryptWithPublicKey(\n secret: string,\n publicKeyString: string\n): Promise<string> {\n // Generate ephemeral keypair for this encryption\n const ephemeralKeyPair = await generateKeyPair();\n\n // Import recipient's public key\n const recipientPublicKey = await importPublicKey(publicKeyString);\n\n // Derive shared secret\n const sharedKey = await deriveSharedSecret(ephemeralKeyPair.privateKey, recipientPublicKey);\n\n // Encrypt the secret with AES-GCM\n const encryptedData = await aesEncrypt(sharedKey, secret);\n\n // Create the final payload: ephemeral public key + encrypted data\n const payload: EncryptedPayload = {\n ephemeralPublicKey: ephemeralKeyPair.publicKey,\n encryptedData: base64urlEncode(encryptedData)\n };\n\n return base64urlEncode(new TextEncoder().encode(JSON.stringify(payload)));\n}\n\n/**\n * Decrypts data that was encrypted using hybrid encryption (ECDH + AES-GCM).\n *\n * This function reverses the encryption performed by encryptWithPublicKey().\n * It extracts the ephemeral public key from the encrypted payload, derives\n * the same shared secret using ECDH, and then decrypts the data using AES-GCM.\n *\n * The decryption process:\n * 1. Extracts the ephemeral public key from the encrypted payload\n * 2. Derives the shared AES key using ECDH with the private key\n * 3. Decrypts the data using AES-GCM with the derived key\n *\n * @param {string} encryptedString - The base64url-encoded encrypted payload containing\n * the ephemeral public key and encrypted data.\n * @param {CryptoKey} privateKey - The recipient's private key for decryption.\n *\n * @returns {Promise<string>} A promise that resolves to the decrypted secret string.\n *\n * @throws {Error} Throws \"Failed to decrypt secret\" if decryption fails for any reason,\n * including invalid encrypted data, wrong private key, or corrupted payload.\n *\n * @example\n * ```typescript\n * const keyPair = await generateKeyPair();\n * // Receive encrypted message from sender\n * const encryptedMessage = \"encrypted-payload-from-sender\";\n *\n * try {\n * const decrypted = await decryptWithPrivateKey(encryptedMessage, keyPair.privateKey);\n * console.log(\"Decrypted message:\", decrypted);\n * } catch (error) {\n * console.error(\"Decryption failed:\", error);\n * }\n * ```\n *\n * @see {@link encryptWithPublicKey} for the corresponding encryption function\n */\nexport async function decryptWithPrivateKey(\n encryptedString: string,\n privateKey: CryptoKey\n): Promise<string> {\n try {\n // Decode the payload\n const payloadBytes = base64urlDecode(encryptedString);\n const payloadText = new TextDecoder().decode(payloadBytes);\n const payload: EncryptedPayload = JSON.parse(payloadText);\n\n // Import ephemeral public key\n const ephemeralPublicKey = await importPublicKey(payload.ephemeralPublicKey);\n\n // Derive shared secret\n const sharedKey = await deriveSharedSecret(privateKey, ephemeralPublicKey);\n\n // Decrypt the data\n const encryptedData = base64urlDecode(payload.encryptedData);\n return await aesDecrypt(sharedKey, encryptedData);\n } catch (error) {\n console.error('Decryption failed:', error);\n throw new Error('Failed to decrypt secret');\n }\n}\n","/**\n * @module server\n *\n * Server-side implementation for the zero-knowledge secret sharing system.\n *\n * This module provides functions to create a secure secret server that can share\n * secrets (like API keys or tokens) with requesting applications without either party\n * having access to both the encryption key and the plaintext secret.\n *\n * The flow works as follows:\n * 1. The requesting app generates an RSA keypair and keeps the private key\n * 2. The app redirects the user to the secret server with the public key\n * 3. The secret server authenticates the user and determines the secret to share\n * 4. The secret is encrypted client-side with the public key\n * 5. The user is redirected back with the encrypted secret in the URL fragment\n * 6. The requesting app decrypts the secret with its private key\n *\n * Security properties:\n * - The secret server never sees the private key\n * - The requesting app never sees the plaintext secret on the server\n * - The encrypted secret is passed via URL fragment (never sent to servers)\n * - CSRF protection via state parameter\n *\n * @example\n * ```typescript\n * // In your secret server's client-side code:\n * import { initSecretServer, setSecret } from 'zerokey/server';\n *\n * // Initialize on page load\n * initSecretServer();\n *\n * // After user authentication, set the secret\n * const apiKey = await getUserApiKey(userId);\n * setSecret(apiKey);\n * ```\n */\nimport { encryptWithPublicKey } from './crypto.js';\n\n/**\n * Stores the secret temporarily if `setSecret()` is called before `initSecretServer()`.\n * This allows the secret to be set before the query parameters are parsed and validated.\n * @internal\n */\nlet pendingSecret: string | null = null;\n\n/**\n * Tracks whether the secret server has been initialized to prevent duplicate initialization.\n * @internal\n */\nlet isInitialized = false;\n\n/**\n * Query parameters expected by the zero-knowledge secret server.\n * These parameters are passed by the requesting application to configure the secret sharing flow.\n */\ninterface QueryParams {\n publicKey: string | null;\n redirect: string | null;\n state: string | null;\n}\n\n/**\n * Validated and required parameters for the zero-knowledge secret sharing flow.\n * These are stored globally after validation for use during the secret transfer process.\n */\ninterface ZerokeyParams {\n publicKey: string;\n redirect: string;\n state: string;\n}\n\n/**\n * Configuration options for the secret server.\n */\ninterface SecretServerOptions {\n /**\n * Optional callback to validate the redirect URL.\n * If provided, must return true for the URL to be accepted.\n * This provides an additional security layer to prevent unauthorized domains\n * from requesting secrets.\n *\n * @param url - The redirect URL to validate\n * @returns True if the URL should be allowed, false otherwise\n *\n * @example\n * // Only allow specific domain\n * validateCallbackUrl: (url) => url.startsWith('https://myapp.com')\n *\n * @example\n * // Allow multiple domains\n * validateCallbackUrl: (url) => {\n * const allowed = ['https://app1.com', 'https://app2.com'];\n * return allowed.some(domain => url.startsWith(domain));\n * }\n */\n validateCallbackUrl?: (url: string) => boolean;\n}\n\n/**\n * Global window extension to store validated zerokey parameters.\n * This allows the parameters to persist between initialization and secret setting.\n */\ndeclare global {\n interface Window {\n zerokeyParams?: ZerokeyParams;\n }\n}\n\n/**\n * Parses URL query parameters from the current window location.\n * Extracts the publicKey, redirect, and state parameters needed for the zero-knowledge secret sharing flow.\n *\n * @returns {QueryParams} An object containing the parsed query parameters\n * @returns {string | null} QueryParams.publicKey - The RSA public key for encrypting the secret\n * @returns {string | null} QueryParams.redirect - The URL to redirect to after encryption\n * @returns {string | null} QueryParams.state - The state parameter for CSRF protection\n *\n * @example\n * // URL: https://secret-server.com?publicKey=...&redirect=https://app.com&state=abc123\n * const params = parseQueryParams();\n * // Returns: { publicKey: \"...\", redirect: \"https://app.com\", state: \"abc123\" }\n */\nfunction parseQueryParams(): QueryParams {\n const params = new URLSearchParams(window.location.search);\n return {\n publicKey: params.get('publicKey'),\n redirect: params.get('redirect'),\n state: params.get('state')\n };\n}\n\n/**\n * Validates that a redirect URL is safe to use.\n * Ensures the URL is properly formatted and uses either HTTP or HTTPS protocol.\n * This prevents potential security issues with malicious redirect URLs.\n *\n * @param {string} url - The redirect URL to validate\n * @returns {boolean} True if the URL is valid and safe, false otherwise\n *\n * @example\n * isValidRedirect('https://app.com/callback'); // returns true\n * isValidRedirect('javascript:alert(1)'); // returns false\n * isValidRedirect('file:///etc/passwd'); // returns false\n */\nfunction isValidRedirect(url: string): boolean {\n try {\n const parsedUrl = new URL(url);\n // Ensure it's http or https\n return parsedUrl.protocol === 'http:' || parsedUrl.protocol === 'https:';\n } catch {\n return false;\n }\n}\n\n/**\n * Encrypts the secret with the provided public key and redirects to the specified URL.\n * The encrypted secret is passed in the URL fragment (hash) to ensure it's never sent to the server.\n * This maintains the zero-knowledge property of the system.\n *\n * @param {string} secret - The plaintext secret to encrypt and transfer\n * @param {string} publicKey - The RSA public key (in PEM format) to encrypt the secret\n * @param {string} redirectUrl - The URL to redirect to after encryption\n * @param {string} state - The state parameter for CSRF protection and request correlation\n * @returns {Promise<void>} A promise that resolves when the redirect is performed\n *\n * @throws {Error} May throw if encryption fails or URL parsing fails\n *\n * @example\n * await performRedirect(\n * 'my-secret-api-key',\n * '-----BEGIN PUBLIC KEY-----\\nMIIBIjANBgk...',\n * 'https://app.com/callback',\n * 'unique-state-123'\n * );\n * // Redirects to: https://app.com/callback#secret=encrypted...&state=unique-state-123\n */\nasync function performRedirect(\n secret: string,\n publicKey: string,\n redirectUrl: string,\n state: string\n): Promise<void> {\n try {\n // Encrypt the secret\n const encryptedSecret = await encryptWithPublicKey(secret, publicKey);\n\n // Build redirect URL with fragment\n const url = new URL(redirectUrl);\n const fragment = new URLSearchParams({\n secret: encryptedSecret,\n state: state\n }).toString();\n\n // Redirect with fragment (never sent to server)\n window.location.href = `${url.origin}${url.pathname}${url.search}#${fragment}`;\n } catch (error) {\n console.error('Failed to encrypt and redirect:', error);\n // Redirect back with error\n const url = new URL(redirectUrl);\n window.location.href = `${url.origin}${url.pathname}${url.search}#error=encryption_failed&state=${state}`;\n }\n}\n\n/**\n * Initializes the zero-knowledge secret server handler.\n * This function should be called when the secret server page loads.\n * It parses the query parameters, validates them, and prepares the system to receive a secret.\n *\n * The function expects the following query parameters:\n * - publicKey: RSA public key for encrypting the secret\n * - redirect: URL to redirect to after processing\n * - state: CSRF protection state parameter\n *\n * If a secret has already been set via `setSecret()` before initialization,\n * it will immediately process and redirect with the encrypted secret.\n *\n * @param {SecretServerOptions} options - Optional configuration for the secret server\n * @param {Function} options.validateCallbackUrl - Optional callback to validate redirect URLs\n * @returns {void}\n *\n * @throws {Error} Logs errors if required parameters are missing or invalid\n *\n * @example\n * // Basic initialization\n * import { initSecretServer } from 'zerokey/server';\n *\n * document.addEventListener('DOMContentLoaded', () => {\n * initSecretServer();\n * });\n *\n * @example\n * // With domain validation for security\n * initSecretServer({\n * validateCallbackUrl: (url) => url.startsWith('https://myapp.com')\n * });\n *\n * @example\n * // Allow multiple trusted domains\n * initSecretServer({\n * validateCallbackUrl: (url) => {\n * const trustedDomains = [\n * 'https://app.example.com',\n * 'https://staging.example.com',\n * 'http://localhost:3000' // for development\n * ];\n * return trustedDomains.some(domain => url.startsWith(domain));\n * }\n * });\n */\nexport function initSecretServer(options: SecretServerOptions = {}): void {\n if (isInitialized) {\n console.warn('Secret server already initialized');\n return;\n }\n\n isInitialized = true;\n\n // Parse parameters on initialization\n const { publicKey, redirect, state } = parseQueryParams();\n\n // Validate required parameters\n if (!publicKey || !redirect || !state) {\n console.error('Missing required parameters');\n return;\n }\n\n if (!isValidRedirect(redirect)) {\n console.error('Invalid redirect URL');\n return;\n }\n\n // Apply custom validation if provided\n if (options.validateCallbackUrl && !options.validateCallbackUrl(redirect)) {\n console.error('Redirect URL failed custom validation');\n return;\n }\n\n // Store parameters for later use\n window.zerokeyParams = { publicKey, redirect, state };\n\n // If secret is already pending, process it\n if (pendingSecret) {\n performRedirect(pendingSecret, publicKey, redirect, state);\n }\n}\n\n/**\n * Sets the secret that will be encrypted and transferred to the requesting application.\n * This function should be called after the user has authenticated and the server\n * has determined what secret (e.g., API key, token) to share.\n *\n * If `initSecretServer()` has already been called and valid parameters are present,\n * this will immediately encrypt the secret and redirect. Otherwise, it stores the\n * secret until initialization occurs.\n *\n * The secret is encrypted client-side using the public key provided in the query parameters,\n * ensuring the secret server never sees the public key and the requesting app never sees\n * the plaintext secret - maintaining zero-knowledge properties.\n *\n * @param {string} secret - The plaintext secret to be encrypted and transferred\n * @returns {void}\n *\n * @throws {Error} Logs an error if the secret is empty\n *\n * @example\n * // After user authentication:\n * const userApiKey = await generateApiKeyForUser(userId);\n * setSecret(userApiKey);\n * // User is automatically redirected with encrypted secret\n *\n * @example\n * // In a form submission handler:\n * document.getElementById('secret-form').addEventListener('submit', (e) => {\n * e.preventDefault();\n * const secret = document.getElementById('secret-input').value;\n * setSecret(secret);\n * });\n */\nexport function setSecret(secret: string): void {\n if (!secret) {\n console.error('Secret cannot be empty');\n return;\n }\n\n pendingSecret = secret;\n\n // If already initialized with params, perform redirect\n if (window.zerokeyParams) {\n const { publicKey, redirect, state } = window.zerokeyParams;\n performRedirect(secret, publicKey, redirect, state);\n }\n}\n"]}