UNPKG

@turnkey/iframe-stamper

Version:

Iframe-based stamper for @turnkey/http

461 lines (458 loc) 22.3 kB
'use strict'; /// <reference lib="dom" /> // Header name for an API key stamp const stampHeaderName = "X-Stamp"; // Set of constants for event types expected to be sent and received between a parent page and its iframe. exports.IframeEventType = void 0; (function (IframeEventType) { // Event sent by the iframe to its parent to indicate readiness. // Value: the iframe public key IframeEventType["PublicKeyReady"] = "PUBLIC_KEY_READY"; // Event sent by the parent to inject a credential bundle (for recovery or auth) into the iframe. // Value: the bundle to inject IframeEventType["InjectCredentialBundle"] = "INJECT_CREDENTIAL_BUNDLE"; // Event sent by the parent to inject a private key export bundle into the iframe. // Value: the bundle to inject // Key Format (optional): the key format to encode the private key in after it's exported and decrypted: HEXADECIMAL or SOLANA. Defaults to HEXADECIMAL. // Public Key (optional): the public key of the exported private key. Required when the key format is SOLANA. IframeEventType["InjectKeyExportBundle"] = "INJECT_KEY_EXPORT_BUNDLE"; // Event sent by the parent to inject a wallet export bundle into the iframe. // Value: the bundle to inject IframeEventType["InjectWalletExportBundle"] = "INJECT_WALLET_EXPORT_BUNDLE"; // Event sent by the parent to inject an import bundle into the iframe. // Value: the bundle to inject IframeEventType["InjectImportBundle"] = "INJECT_IMPORT_BUNDLE"; // Event sent by the parent to extract an encrypted wallet bundle from the iframe. // Value: none IframeEventType["ExtractWalletEncryptedBundle"] = "EXTRACT_WALLET_ENCRYPTED_BUNDLE"; // Event sent by the parent to extract an encrypted private key bundle from the iframe. // Value: none // Key Format (optional): the key format to decode the private key in before it's encrypted for import: HEXADECIMAL or SOLANA. Defaults to HEXADECIMAL. IframeEventType["ExtractKeyEncryptedBundle"] = "EXTRACT_KEY_ENCRYPTED_BUNDLE"; // Event sent by the parent to apply settings on the iframe. // Value: the settings to apply in JSON string format. IframeEventType["ApplySettings"] = "APPLY_SETTINGS"; // Event sent by the iframe to its parent when `InjectBundle` is successful // Value: true (boolean) IframeEventType["BundleInjected"] = "BUNDLE_INJECTED"; // Event sent by the iframe to its parent when `ExtractEncryptedBundle` is successful // Value: the bundle encrypted in the iframe IframeEventType["EncryptedBundleExtracted"] = "ENCRYPTED_BUNDLE_EXTRACTED"; // Event sent by the iframe to its parent when `ApplySettings` is successful // Value: true (boolean) IframeEventType["SettingsApplied"] = "SETTINGS_APPLIED"; // Event sent by the iframe to its parent when `signTransaction` is successful // Value: true (boolean) IframeEventType["TransactionSigned"] = "TRANSACTION_SIGNED"; // Event sent by the iframe to its parent when `signMessage` is successful // Value: true (boolean) IframeEventType["MessageSigned"] = "MESSAGE_SIGNED"; // Event sent by the iframe to its parent when `clearEmbeddedPrivateKey` is successful // Value: true (boolean) IframeEventType["EmbeddedPrivateKeyCleared"] = "EMBEDDED_PRIVATE_KEY_CLEARED"; // Event sent by the parent page to request a signature // Value: payload to sign IframeEventType["StampRequest"] = "STAMP_REQUEST"; // Event sent by the iframe to communicate the result of a stamp operation. // Value: signed payload IframeEventType["Stamp"] = "STAMP"; // Event sent by the parent to establish secure communication via MessageChannel API. // Value: MessageChannel port IframeEventType["TurnkeyInitMessageChannel"] = "TURNKEY_INIT_MESSAGE_CHANNEL"; // Event sent by the parent to get the iframe target embedded key's public key. // Value: the iframe public key IframeEventType["GetEmbeddedPublicKey"] = "GET_EMBEDDED_PUBLIC_KEY"; // Event sent by the parent to clear the iframe's embedded key. // Value: none IframeEventType["ClearEmbeddedKey"] = "RESET_EMBEDDED_KEY"; // Event sent by the parent to initialize a new embedded key. // Value: none IframeEventType["InitEmbeddedKey"] = "INIT_EMBEDDED_KEY"; // Event sent by the parent page to request a signature for a transaction. // Value: payload to sign IframeEventType["SignTransaction"] = "SIGN_TRANSACTION"; // Event sent by the parent page to request a signature for a message. // Value: payload to sign IframeEventType["SignMessage"] = "SIGN_MESSAGE"; // Event sent by the parent page to request that the iframe embedded private key is cleared from memory. // Value: none IframeEventType["clearEmbeddedPrivateKey"] = "CLEAR_EMBEDDED_PRIVATE_KEY"; // Event sent by the parent to set an override for the iframe's embedded key. IframeEventType["SetEmbeddedKeyOverride"] = "SET_EMBEDDED_KEY_OVERRIDE"; // Event sent by the iframe to clear the injected decryption key and use the default embedded key instead. IframeEventType["ResetToDefaultEmbeddedKey"] = "RESET_TO_DEFAULT_EMBEDDED_KEY"; // Event sent by the iframe to communicate an error // Value: serialized error IframeEventType["Error"] = "ERROR"; })(exports.IframeEventType || (exports.IframeEventType = {})); // Set of constants for private key formats. These formats map to the encoding type used on a private key before encrypting and importing it // or after exporting it and decrypting it. exports.KeyFormat = void 0; (function (KeyFormat) { // 64 hexadecimal digits. Key format used by MetaMask, MyEtherWallet, Phantom, Ledger, and Trezor for Ethereum and Tron keys KeyFormat["Hexadecimal"] = "HEXADECIMAL"; // Key format used by Phantom and Solflare for Solana keys KeyFormat["Solana"] = "SOLANA"; // Wallet Import Format used by Main Net Bitcoin wallets like Electrum and Bitcoin Core KeyFormat["BitcoinMainNetWIF"] = "BITCOIN_MAINNET_WIF"; // Wallet Import Format used by Test Net Bitcoin wallets like Electrum and Bitcoin Core KeyFormat["BitcoinTestNetWIF"] = "BITCOIN_TESTNET_WIF"; // Bech32 format used by modern Bitcoin wallets & Sui wallets KeyFormat["SuiBech32"] = "SUI_BECH32"; })(exports.KeyFormat || (exports.KeyFormat = {})); exports.MessageType = void 0; (function (MessageType) { MessageType["Ethereum"] = "ETHEREUM"; MessageType["Solana"] = "SOLANA"; })(exports.MessageType || (exports.MessageType = {})); exports.TransactionType = void 0; (function (TransactionType) { TransactionType["Ethereum"] = "ETHEREUM"; TransactionType["Solana"] = "SOLANA"; })(exports.TransactionType || (exports.TransactionType = {})); function generateUUID() { return crypto.randomUUID(); } /** * Stamper to use with `@turnkey/http`'s `TurnkeyClient` * Creating a stamper inserts an iframe in the current page. */ class IframeStamper { /** * Creates a new iframe stamper. This function _does not_ insert the iframe in the DOM. * Call `.init()` to insert the iframe element in the DOM. * @param {TIframeStamperConfig} config - Configuration object for the iframe stamper * @throws {Error} When running in non-browser environment * @throws {Error} When MessageChannel is not supported * @throws {Error} When iframeContainer is not provided * @throws {Error} When iframe element with the same ID already exists */ constructor(config) { if (typeof window === "undefined") { throw new Error("Cannot initialize iframe in non-browser environment"); } if (typeof MessageChannel === "undefined") { throw new Error("Cannot initialize iframe without MessageChannel support"); } if (!config.iframeContainer) { throw new Error("Iframe container cannot be found"); } this.container = config.iframeContainer; if (this.container.querySelector(`#${config.iframeElementId}`)) { throw new Error(`Iframe element with ID ${config.iframeElementId} already exists`); } let iframe = window.document.createElement("iframe"); // See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#sandbox // We do not need any other permission than running scripts for import/export/auth frames. iframe.setAttribute("sandbox", "allow-scripts allow-same-origin"); iframe.id = config.iframeElementId; iframe.src = config.iframeUrl; if (config.clearClipboardOnPaste ?? true) { iframe.allow = "clipboard-write"; // Clipboard will clear when pasting in the iframe } this.iframe = iframe; const iframeUrl = new URL(config.iframeUrl); this.iframeOrigin = iframeUrl.origin; // This is populated once the iframe is ready. Call `.init()` to kick off DOM insertion! this.iframePublicKey = null; /** * The MessageChannel API is used to establish secure communication between two execution contexts. * In this case, the parent page and the iframe. * See https://developer.mozilla.org/en-US/docs/Web/API/MessageChannel */ this.messageChannel = new MessageChannel(); // Initialize a pending requests tracker this.pendingRequests = new Map(); } /** * Handles incoming messages from the iframe via MessageChannel * @param {MessageEvent} event - Message event from the iframe * @returns {void} */ onMessageHandler(event) { const { type, value, requestId } = event.data || {}; // Handle messages without requestId (like PUBLIC_KEY_READY) if (!requestId) { if (type === exports.IframeEventType.PublicKeyReady) { this.iframePublicKey = value; return; } return; } const pendingRequest = this.pendingRequests.get(requestId); if (!pendingRequest) { console.warn(`Received response for unknown request: ${requestId}`); return; } // Remove from pending requests this.pendingRequests.delete(requestId); if (type === exports.IframeEventType.Error) { pendingRequest.reject(new Error(value)); return; } // Handle specific response types switch (type) { case exports.IframeEventType.Stamp: pendingRequest.resolve({ stampHeaderName, stampHeaderValue: value, }); break; default: pendingRequest.resolve(value); } } /** * Inserts the iframe on the page and returns a promise resolving to the iframe's public key * @param {number} [dangerouslyOverrideIframeKeyTtl] - Optional TTL override for the iframe's embedded key (default 48 hours). Only use this if you are intentional about the security implications. * @returns {Promise<string>} The iframe's public key * @throws {Error} When contentWindow or contentWindow.postMessage does not exist */ async init(dangerouslyOverrideIframeKeyTtl) { return new Promise((resolve, reject) => { this.container.appendChild(this.iframe); this.iframe.addEventListener("load", () => { if (!this.iframe.contentWindow?.postMessage) { reject(new Error("contentWindow or contentWindow.postMessage does not exist")); return; } this.iframe.contentWindow.postMessage({ type: exports.IframeEventType.TurnkeyInitMessageChannel, dangerouslyOverrideIframeKeyTtl: dangerouslyOverrideIframeKeyTtl, }, this.iframeOrigin, [this.messageChannel.port2]); }); this.messageChannel.port1.onmessage = (event) => { // Handle initial PublicKeyReady event if (event.data?.type === exports.IframeEventType.PublicKeyReady) { this.iframePublicKey = event.data.value; resolve(event.data.value); } // Handle all other messages this.onMessageHandler(event); }; }); } /** * Removes the iframe from the DOM and cleans up all resources * @returns {void} */ clear() { this.messageChannel?.port1?.close(); this.messageChannel?.port2?.close(); this.iframe.remove(); this.pendingRequests.clear(); } /** * Returns the public key, or `null` if the underlying iframe isn't properly initialized. * @returns {string | null} The iframe's public key or null */ publicKey() { return this.iframePublicKey; } /** * Returns the public key, or `null` if the underlying iframe isn't properly initialized. * This differs from the above in that it reaches out to the live iframe to see if an embedded key exists. * @returns {Promise<string | null>} The embedded public key or null */ async getEmbeddedPublicKey() { const publicKey = await this.createRequest(exports.IframeEventType.GetEmbeddedPublicKey); this.iframePublicKey = publicKey; return publicKey; } /** * Clears the embedded key within an iframe. * @returns {Promise<null>} Returns null on success */ async clearEmbeddedKey() { await this.createRequest(exports.IframeEventType.ClearEmbeddedKey); this.iframePublicKey = ""; return null; } /** * Creates a new embedded key within an iframe. If an embedded key already exists, this will return it. * This is primarily to be used in conjunction with `clearEmbeddedKey()`: after an embedded key is cleared, * this can be used to create a new one. * @returns {Promise<string | null>} The newly created embedded public key */ async initEmbeddedKey() { const publicKey = await this.createRequest(exports.IframeEventType.InitEmbeddedKey); this.iframePublicKey = publicKey; return publicKey; } /** * Generic function to abstract away request creation * @template T * @param {IframeEventType} type - The type of iframe event to send * @param {any} [payload={}] - Optional payload data to send with the request * @returns {Promise<T>} Promise resolving to the expected response shape */ createRequest(type, payload = {}) { return new Promise((resolve, reject) => { const requestId = generateUUID(); this.pendingRequests.set(requestId, { resolve, reject, requestId, }); this.messageChannel.port1.postMessage({ type, requestId, ...payload, }); }); } /** * Function to inject a new credential into the iframe * The bundle should be encrypted to the iframe's initial public key * Encryption should be performed with HPKE (RFC 9180). * This is used during recovery and auth flows. * @param {string} bundle - The encrypted credential bundle to inject * @returns {Promise<boolean>} Returns true on successful injection */ async injectCredentialBundle(bundle) { return this.createRequest(exports.IframeEventType.InjectCredentialBundle, { value: bundle, }); } /** * Function to inject an export bundle into the iframe * The bundle should be encrypted to the iframe's initial public key * Encryption should be performed with HPKE (RFC 9180). * The key format to encode the private key in after it's exported and decrypted: HEXADECIMAL or SOLANA. Defaults to HEXADECIMAL. * This is used during the private key export flow. * @param {string} bundle - The encrypted export bundle to inject * @param {string} organizationId - The organization ID * @param {KeyFormat} keyFormat - [Optional] The key format (HEXADECIMAL or SOLANA). Defaults to HEXADECIMAL * @param {string} address - [Optional] Address corresponding to the key bundle (case sensitive) * @returns {Promise<boolean>} Returns true on successful injection */ async injectKeyExportBundle(bundle, organizationId, keyFormat, address) { return this.createRequest(exports.IframeEventType.InjectKeyExportBundle, { value: bundle, keyFormat, organizationId, address, }); } /** * Function to inject an export bundle into the iframe * The bundle should be encrypted to the iframe's initial public key * Encryption should be performed with HPKE (RFC 9180). * This is used during the wallet export flow. * @param {string} bundle - The encrypted wallet export bundle to inject * @param {string} organizationId - The organization ID * @returns {Promise<boolean>} Returns true on successful injection */ async injectWalletExportBundle(bundle, organizationId) { return this.createRequest(exports.IframeEventType.InjectWalletExportBundle, { value: bundle, organizationId, }); } /** * Function to inject an import bundle into the iframe * This is used to initiate either the wallet import flow or the private key import flow. * @param {string} bundle - The import bundle to inject * @param {string} organizationId - The organization ID * @param {string} userId - The user ID * @returns {Promise<boolean>} Returns true on successful injection */ async injectImportBundle(bundle, organizationId, userId) { return this.createRequest(exports.IframeEventType.InjectImportBundle, { value: bundle, organizationId, userId, }); } /** * Function to extract an encrypted bundle from the iframe * The bundle should be encrypted to Turnkey's Signer enclave's initial public key * Encryption should be performed with HPKE (RFC 9180). * This is used during the wallet import flow. * @returns {Promise<string>} The encrypted wallet bundle */ async extractWalletEncryptedBundle() { return this.createRequest(exports.IframeEventType.ExtractWalletEncryptedBundle); } /** * Function to extract an encrypted bundle from the iframe * The bundle should be encrypted to Turnkey's Signer enclave's initial public key * Encryption should be performed with HPKE (RFC 9180). * The key format to encode the private key in before it's encrypted and imported: HEXADECIMAL or SOLANA. Defaults to HEXADECIMAL. * This is used during the private key import flow. * @param {KeyFormat} [keyFormat] - The key format (HEXADECIMAL or SOLANA). Defaults to HEXADECIMAL * @returns {Promise<string>} The encrypted key bundle */ async extractKeyEncryptedBundle(keyFormat) { return this.createRequest(exports.IframeEventType.ExtractKeyEncryptedBundle, { keyFormat }); } /** * Function to apply settings on allowed parameters in the iframe * This is used to style the HTML element used for plaintext in wallet and private key import. * @param {TIframeSettings} settings - The settings object containing styles to apply * @returns {Promise<boolean>} Returns true on successful application */ async applySettings(settings) { return this.createRequest(exports.IframeEventType.ApplySettings, { value: JSON.stringify(settings), }); } /** * Function to sign a payload with the underlying iframe * @param {string} payload - The payload to sign * @returns {Promise<TStamp>} Object containing stamp header name and value * @throws {Error} When iframe public key is null (init() not called/awaited) */ async stamp(payload) { if (this.iframePublicKey === null) { throw new Error("null iframe public key. Have you called/awaited .init()?"); } return this.createRequest(exports.IframeEventType.StampRequest, { value: payload, }); } /** * Function to sign a message using an embedded private key in-memory within an iframe * Returns the signed message string * @param {TSignableMessage} message - The message to sign with type (Ethereum or Solana) * @param {string} address - [Optional] Address to sign with * @returns {Promise<string>} The signed message string */ async signMessage(message, address) { return this.createRequest(exports.IframeEventType.SignMessage, { value: JSON.stringify(message), address, }); } /** * Function to sign a transaction using an embedded private key in-memory within an iframe * Returns the signed, serialized transaction payload * @param {TSignableTransaction} transaction - The transaction to sign with type (Ethereum or Solana) * @param {string} address - [Optional] Address to sign with * @returns {Promise<string>} The signed, serialized transaction payload */ async signTransaction(transaction, address) { return this.createRequest(exports.IframeEventType.SignTransaction, { value: JSON.stringify(transaction), address, }); } /** * Function to clear the iframe's in-memory embedded private key. For now, we assume that there will be only one private key at most. * @returns {Promise<boolean>} Returns true on successful clearing */ async clearEmbeddedPrivateKey() { return this.createRequest(exports.IframeEventType.clearEmbeddedPrivateKey); } async setEmbeddedKeyOverride(organizationId, bundle) { return this.createRequest(exports.IframeEventType.SetEmbeddedKeyOverride, { organizationId, value: bundle, }); } async resetToDefaultEmbeddedKey() { return this.createRequest(exports.IframeEventType.ResetToDefaultEmbeddedKey); } } exports.IframeStamper = IframeStamper; //# sourceMappingURL=index.js.map