diff --git a/packages/auth0/README.md b/packages/auth0/README.md new file mode 100644 index 00000000..077eaa5c --- /dev/null +++ b/packages/auth0/README.md @@ -0,0 +1,99 @@ +# @fast-auth/auth0 + +The Auth0 half of FastAuth: a PostLogin action that turns a signing request into a user +approval, and the forms that render it. + +## How it fits together + +FastAuth's guarantee is that the MPC only ever signs bytes Auth0 certified after showing them +to the user. The on-chain guard enforces one half of that — it checks that the bytes handed to +the MPC equal the `fatxn` claim in the access token, and nothing else. It never inspects them. + +The other half lives here. The action is what decides *what* those bytes are allowed to be and +*how* they are described to the user before the token is minted. + +``` +/authorize? → action decodes + validates → form renders for approval + → approved: fatxn = the exact bytes + → denied: no token +``` + +Because the guard compares byte for byte, the action must place the payload in `fatxn` +unmodified. Any normalisation here would make every resulting signature fail on-chain. + +## Payload types + +The action accepts exactly one payload per request, on the `/authorize` query string: + +| Query key | Contents | Form rendered | +| --------------- | ------------------------------------- | ---------------------- | +| `transaction` | Borsh-encoded NEAR `Transaction` | `TRANSACTION_FORM` | +| `delegateAction`| Borsh-encoded `DelegateAction` | `DELEGATE_ACTION_FORM` | +| `nep413` | Borsh-encoded NEP-413 message payload | `NEP413_FORM` | + +Sending more than one is rejected: with several present, precedence would decide silently which +one lands in `fatxn`, and a caller could display one payload while a different one gets signed. + +A payload that fails to decode is denied rather than rendered. If the screen cannot describe it, +the user cannot meaningfully approve it. + +## Action secrets + +Configured under **Actions → Library → your action → Secrets**. Despite the name, only the +audience is remotely sensitive — the form entries are identifiers Auth0 generates on import, and +they differ per tenant, which is why they are configuration rather than constants in the code. + +| Secret | Value | +| ---------------------- | -------------------------------------------------------- | +| `ONCHAIN_AUDIENCE` | Identifier of the signing API. A request carrying a payload is only honoured for this audience, and a request *for* this audience without a payload is denied. | +| `TRANSACTION_FORM` | Form id for transaction approvals. | +| `DELEGATE_ACTION_FORM` | Form id for delegate action approvals. | +| `NEP413_FORM` | Form id for NEP-413 message approvals. | + +## Deploying a form + +Forms are built from source rather than edited in the dashboard: `build.js` inlines each +component's JS, CSS and schema into a template and emits a single importable file. + +```bash +pnpm build # writes src/forms/
/_form.json +``` + +Then, per form: + +1. **Auth0 Dashboard → Forms → Create Form → Import from JSON**, and upload the generated file. +2. Copy the id Auth0 assigns to the created form. +3. Paste it into the matching action secret above. + +Repeat per tenant — ids are not portable between staging and production. + +Re-importing after a code change creates a *new* form with a new id, so remember to update the +secret, or the action will keep rendering the previous version. + +## Local development + +```bash +pnpm playground # serves the forms at http://localhost:5174 +``` + +The playground renders the same components against mock payloads (`playground/payloads.js`) +with a shim standing in for the helpers that `build.js` inlines in production. It is the fastest +way to iterate on an approval screen without touching a tenant. + +```bash +pnpm test # action decoding, handler dispatch, and form rendering +``` + +The fixtures encode payloads with NEAR's production encoders, and the NEP-413 fixtures use a +borsh schema transcribed from the NEP rather than imported from the action — so a change to the +action's schema breaks the round-trip instead of silently agreeing with itself. + +## Adding a form + +1. `src/forms//_form_base.json` — the template. Custom components carry + `"$source": ""`, resolved against the form's own directory, or against the forms root + when the path contains a `/` (e.g. `shared/image`). +2. `src/forms//details/index.js` and `index.css` — the component. Rendering helpers live in + `shared/helpers` and are available as `__auth0FormHelpers`. +3. Register the form in the `FORMS` array in `build.js`. +4. Add the corresponding secret and render it from the action. diff --git a/packages/auth0/src/actions/authorize-app.action.js b/packages/auth0/src/actions/authorize-app.action.js index e4279d10..58fcc910 100644 --- a/packages/auth0/src/actions/authorize-app.action.js +++ b/packages/auth0/src/actions/authorize-app.action.js @@ -5,6 +5,19 @@ const { deserialize } = require("borsh"); const TRANSACTION_KEY = "transaction"; const DELEGATE_ACTION_KEY = "delegateAction"; +const NEP413_KEY = "nep413"; + +// NEP-413 CONSTANTS + +/** + * Domain-separation tag mandated by NEP-413 (2^31 + 413). It is prepended to every signed + * off-chain message so the bytes can never be reinterpreted as a NEAR transaction, whose + * borsh encoding starts with the signerId length — a small u32. Verifying this tag is what + * keeps the new payload type from becoming a way to smuggle transaction bytes past the user. + * + * https://github.com/near/NEPs/blob/master/neps/nep-0413.md#how-to-ensure-the-message-is-not-a-transaction + */ +const NEP413_PREFIX_TAG = 2147484061; // SCHEMA definitions const SCHEMA = new (class BorshSchema { @@ -177,6 +190,20 @@ const SCHEMA = new (class BorshSchema { signature: this.Signature, }, }; + /** + * NEP-413 off-chain message payload. Field order is normative — it must match the + * serializer the client signs with, or the recovered bytes will not match `fatxn`. + * https://github.com/near/NEPs/blob/master/neps/nep-0413.md#input-interface + */ + NEP413Payload = { + struct: { + tag: "u32", + message: "string", + nonce: { array: { type: "u8", len: 32 } }, + recipient: "string", + callbackUrl: { option: "string" }, + }, + }; })(); // UTILS @@ -203,6 +230,76 @@ function decodeDelegateAction(encodedDelegateAction) { return delegateAction; } +/** + * Decode and validate a NEP-413 payload arriving on the authorize query string. + * + * The guard contract only verifies that `fatxn` equals the bytes the MPC is asked to sign — it + * never inspects them — so this function is the only place that establishes *what* the user is + * being asked to approve. Two checks carry that weight: + * + * 1. The borsh payload must deserialize cleanly under the NEP-413 schema, and the domain tag + * must be exactly NEP413_PREFIX_TAG so the bytes cannot also be a valid NEAR transaction. + * Never trust the caller to have set the tag. + * 2. There must be a message to show. Signing something the approval screen cannot display + * would defeat the consent guarantee the whole flow rests on. + * + * The recipient is not restricted. Under NEP-413 it names the application a message is addressed + * to, and the standard's protection is that the user sees it — which is why the approval screen + * always renders it. Restricting it here would be the wrong granularity anyway: a recipient + * belongs to an application, not to the tenant that hosts many of them. + * @param {string} encodedPayload Comma-separated byte string from the query. + * @returns {{payload: object, message: object|null}} The payload, plus the message parsed as JSON when it is JSON. + * @throws {Error} With a user-facing reason when any check fails. + */ +function decodeNep413Payload(encodedPayload) { + const bytes = Uint8Array.from(String(encodedPayload).split(",").map((value) => Number(value))); + + let payload; + try { + payload = deserialize(SCHEMA.NEP413Payload, bytes); + } catch (e) { + throw new Error("Payload is not a valid NEP-413 message"); + } + + if (payload.tag !== NEP413_PREFIX_TAG) { + throw new Error("Payload is missing the NEP-413 domain tag"); + } + + if (typeof payload.message !== "string" || payload.message.length === 0) { + throw new Error("NEP-413 message is empty"); + } + + // A JSON message may be a structured payload the approval screen can render richly (NEAR + // Intents being the case we know about). Plain-text messages are equally valid and are + // shown verbatim, so failing to parse is not an error. + let message = null; + try { + const parsed = JSON.parse(payload.message); + if (parsed && typeof parsed === "object") message = parsed; + } catch (e) { + message = null; + } + + return { payload, message }; +} + +function stringifyIntents(intents) { + return JSON.stringify(intents, (_, value) => (typeof value === "bigint" ? value.toString() : value), 2); +} + +/** + * Extract the NEAR Intents body from a decoded message, when the message is one. + * + * Recognising it is what lets the approval screen show "transfer 1 USDC to X" instead of a wall + * of JSON. Anything else is not an error — it is just a message that gets displayed as text. + * @param {object|null} message The parsed message, or null when it was not JSON. + * @returns {Array|null} The intents array, or null when this is not an intents message. + */ +function extractIntents(message) { + if (!message || !Array.isArray(message.intents) || message.intents.length === 0) return null; + return message.intents; +} + function stringifyActions(actions) { return JSON.stringify( actions, @@ -230,7 +327,9 @@ exports.onExecutePostLogin = async (event, api) => { const isOnchainAudience = event.resource_server?.identifier === onchainAudience; const hasTxParams = TRANSACTION_KEY in query; const hasDelegateParams = DELEGATE_ACTION_KEY in query; - const hasSigningPayload = hasTxParams || hasDelegateParams; + const hasNep413Params = NEP413_KEY in query; + const payloadCount = [hasTxParams, hasDelegateParams, hasNep413Params].filter(Boolean).length; + const hasSigningPayload = payloadCount > 0; if (isOnchainAudience && !hasSigningPayload) { return api.access.deny("Signing audience requested without transaction payload"); @@ -240,6 +339,12 @@ exports.onExecutePostLogin = async (event, api) => { } if (!isOnchainAudience) return; + // Exactly one payload may be present. Accepting several and silently picking by precedence + // would let a caller show the user one payload while a different one lands in `fatxn`. + if (payloadCount > 1) { + return api.access.deny("Only one signing payload may be requested at a time"); + } + // Strip OIDC profile scopes from the issued access token. // // @auth0/auth0-spa-js unions per-call scope with its built-in default ("openid profile email"), @@ -280,7 +385,7 @@ exports.onExecutePostLogin = async (event, api) => { "fatxn", query.transaction.split(",").map((value) => Number(value)), ); - } else { + } else if (hasDelegateParams) { const delegateAction = decodeDelegateAction(query.delegateAction); api.prompt.render(event.secrets.DELEGATE_ACTION_FORM, { fields: { @@ -295,6 +400,38 @@ exports.onExecutePostLogin = async (event, api) => { "fatxn", query.delegateAction.split(",").map((value) => Number(value)), ); + } else { + let decoded; + try { + decoded = decodeNep413Payload(query.nep413); + } catch (error) { + // A payload we cannot decode is a payload we cannot show the user. Signing it would + // break the consent guarantee the whole flow rests on, so refuse instead. + return api.access.deny(error.message); + } + + const { payload, message } = decoded; + const intents = extractIntents(message); + + api.prompt.render(event.secrets.NEP413_FORM, { + fields: { + ...branding, + // Always shown: per NEP-413 the recipient is the user's protection against a + // message being relayed to a third party, so it must be on screen either way. + recipient: payload.recipient, + callbackUrl: payload.callbackUrl ?? "", + // The raw message is always passed through. The form prefers the structured + // intents view when present and falls back to showing this verbatim. + message: payload.message, + signerId: (message && message.signer_id) ?? "", + deadline: (message && message.deadline) ?? "", + intents: intents ? stringifyIntents(intents) : "", + }, + }); + api.accessToken.setCustomClaim( + "fatxn", + query.nep413.split(",").map((value) => Number(value)), + ); } }; @@ -317,5 +454,9 @@ exports.onContinuePostLogin = async (event, api) => { // `onContinuePostLogin`; extra exports are inert in production. exports.parseTransaction = parseTransaction; exports.decodeDelegateAction = decodeDelegateAction; +exports.decodeNep413Payload = decodeNep413Payload; +exports.extractIntents = extractIntents; exports.stringifyActions = stringifyActions; +exports.stringifyIntents = stringifyIntents; exports.SCHEMA = SCHEMA; +exports.NEP413_PREFIX_TAG = NEP413_PREFIX_TAG; diff --git a/packages/auth0/src/forms/build.js b/packages/auth0/src/forms/build.js index 71656405..897d24b2 100644 --- a/packages/auth0/src/forms/build.js +++ b/packages/auth0/src/forms/build.js @@ -32,6 +32,7 @@ const HELPERS_PATH = path.join(FORMS_DIR, "shared", "helpers", "index.js"); const FORMS = [ { name: "transaction", base: "transaction_form_base.json", out: "transaction_form.json" }, { name: "delegate_action", base: "delegate_action_form_base.json", out: "delegate_action_form.json" }, + { name: "nep413", base: "nep413_form_base.json", out: "nep413_form.json" }, ]; function readHelpersPreamble() { diff --git a/packages/auth0/src/forms/delegate_action/delegate_action_form.json b/packages/auth0/src/forms/delegate_action/delegate_action_form.json index d73d335b..da241da7 100644 --- a/packages/auth0/src/forms/delegate_action/delegate_action_form.json +++ b/packages/auth0/src/forms/delegate_action/delegate_action_form.json @@ -24,7 +24,7 @@ "sensitive": false, "config": { "schema": {}, - "code": "(function () {\n// --- BEGIN __auth0FormHelpers (auto-inlined from shared/helpers/index.js) ---\n/**\n * Shared helpers for Auth0 form custom components.\n *\n * Runs in two environments:\n * - Auth0 form runtime: build.js inlines this file into each component's `config.code`,\n * exposing `__auth0FormHelpers` as a top-level variable in the same scope as the component.\n * - Node (tests / playground via require): consumed as a CommonJS module via module.exports.\n *\n * Keep this file free of `require` and ES imports so it can be concatenated as plain text.\n */\n\nfunction ensureBufferPolyfill() {\n if (typeof Buffer !== \"undefined\") return;\n if (typeof globalThis === \"undefined\") return;\n globalThis.Buffer = {\n from: function (data) {\n if (Array.isArray(data)) return new Uint8Array(data);\n if (typeof data === \"string\") return new TextEncoder().encode(data);\n return data;\n },\n };\n}\n\nconst BASE58_ALPHABET = \"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz\";\n\nfunction base58Encode(bytes) {\n let result = \"\";\n let num = BigInt(0);\n for (const byte of bytes) {\n num = (num << BigInt(8)) + BigInt(byte);\n }\n while (num > BigInt(0)) {\n const remainder = num % BigInt(58);\n num = num / BigInt(58);\n result = BASE58_ALPHABET[Number(remainder)] + result;\n }\n for (const byte of bytes) {\n if (byte === 0) result = BASE58_ALPHABET[0] + result;\n else break;\n }\n return result;\n}\n\n/**\n * Format a yocto-NEAR BigInt string as NEAR with up to 8 decimals.\n * \"1000000000000000000000000\" -> \"1\"\n * \"1500000000000000000000000\" -> \"1.5\"\n * \"1\" -> \"0.00000000\" trimmed -> \"0.0\"\n */\nfunction yoctoToNear(bigIntStr) {\n const trimmed = String(bigIntStr).replace(/^0+/, \"\") || \"0\";\n const length = trimmed.length;\n\n if (length <= 24) {\n const zerosNeeded = 24 - length;\n const withZeros = \"0\".repeat(zerosNeeded) + trimmed;\n const decimals = withZeros.slice(0, 8).replace(/0+$/, \"\") || \"0\";\n return `0.${decimals}`;\n }\n\n const splitPos = length - 24;\n const integerPart = trimmed.slice(0, splitPos);\n const decimalPart = trimmed.slice(splitPos, splitPos + 8);\n const trimmedDecimals = decimalPart.replace(/0+$/, \"\");\n if (trimmedDecimals === \"\") return integerPart;\n return `${integerPart}.${trimmedDecimals}`;\n}\n\nfunction formatPublicKey(publicKey) {\n if (publicKey && publicKey.secp256k1Key !== undefined) {\n return `secp256k1:${base58Encode(publicKey.secp256k1Key.data)}`;\n }\n if (publicKey && publicKey.ed25519Key !== undefined) {\n return `ed25519:${base58Encode(publicKey.ed25519Key.data)}`;\n }\n return \"\";\n}\n\n/**\n * Return the canonical action type discriminator (the single non-undefined key of the action).\n * Useful for dispatch and for tests that don't need DOM rendering.\n */\nfunction getActionType(action) {\n if (!action || typeof action !== \"object\") return null;\n for (const key of Object.keys(action)) {\n if (action[key] !== undefined) return key;\n }\n return null;\n}\n\n// --- DOM helpers (require document/window — jsdom in tests, real DOM in browser) ---\n\nfunction createTextContent(label, value, link = false) {\n const textContent = document.createElement(\"div\");\n textContent.classList.add(\"text-content\");\n\n const labelElement = document.createElement(\"div\");\n labelElement.classList.add(\"label\");\n labelElement.textContent = label;\n\n const valueElement = document.createElement(\"div\");\n valueElement.classList.add(\"value\");\n valueElement.textContent = value;\n\n if (link) {\n valueElement.classList.add(\"link\");\n valueElement.setAttribute(\"href\", link);\n valueElement.setAttribute(\"target\", \"_blank\");\n }\n\n textContent.appendChild(labelElement);\n textContent.appendChild(valueElement);\n return textContent;\n}\n\nfunction createDescription(text) {\n const node = document.createElement(\"p\");\n node.classList.add(\"action-description\");\n node.textContent = text;\n return node;\n}\n\nfunction createAccordion(label, content, showWarning = false) {\n const accordion = document.createElement(\"div\");\n accordion.classList.add(\"accordion\");\n\n const header = document.createElement(\"div\");\n header.classList.add(\"accordion-header\");\n\n const headerContent = document.createElement(\"div\");\n headerContent.classList.add(\"accordion-header-content\");\n\n const leftContent = document.createElement(\"div\");\n leftContent.classList.add(\"accordion-left-content\");\n\n if (showWarning) {\n const warningIcon = document.createElement(\"span\");\n warningIcon.classList.add(\"warning-icon\");\n warningIcon.innerHTML = \"⚠️\";\n leftContent.appendChild(warningIcon);\n }\n\n const labelElement = document.createElement(\"span\");\n labelElement.classList.add(\"accordion-header-label\");\n labelElement.textContent = label;\n leftContent.appendChild(labelElement);\n\n const expandIcon = document.createElement(\"span\");\n expandIcon.classList.add(\"expand-icon\");\n expandIcon.innerHTML = \"+\";\n\n headerContent.appendChild(leftContent);\n headerContent.appendChild(expandIcon);\n header.appendChild(headerContent);\n\n const contentElement = document.createElement(\"div\");\n contentElement.classList.add(\"accordion-content\");\n contentElement.appendChild(content);\n\n accordion.appendChild(header);\n accordion.appendChild(contentElement);\n\n header.addEventListener(\"click\", function () {\n contentElement.classList.toggle(\"open\");\n expandIcon.innerHTML = contentElement.classList.contains(\"open\") ? \"−\" : \"+\";\n });\n\n return accordion;\n}\n\n// --- Action content factories ---\n\nfunction createAccountContent() {\n return createDescription(\"By approving this request, a new account will be created.\");\n}\n\nfunction deployContractContent(action) {\n ensureBufferPolyfill();\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the compiled smart contract will be deployed.\");\n const codeLen = action.deployContract && action.deployContract.code ? `${action.deployContract.code.length} bytes` : null;\n container.appendChild(text);\n container.appendChild(createTextContent(\"Code\", codeLen));\n return container;\n}\n\n/**\n * Decode functionCall `args` (a byte array / Uint8Array) to a readable string. NEAR contract\n * args are almost always UTF-8 JSON, so decode the bytes and pretty-print when they parse as\n * JSON; otherwise fall back to the raw decoded text. TextDecoder is used directly because it\n * exists in the browser and the Auth0 form runtime (unlike Buffer, which the runtime lacks).\n */\nfunction decodeFunctionCallArgs(args) {\n if (args == null) return \"\";\n let text;\n try {\n if (typeof TextDecoder !== \"undefined\") {\n text = new TextDecoder().decode(Uint8Array.from(args));\n } else if (typeof Buffer !== \"undefined\" && typeof Buffer.from === \"function\") {\n text = Buffer.from(args).toString(\"utf8\");\n } else {\n text = String.fromCharCode.apply(null, Array.from(args));\n }\n } catch (e) {\n return \"\";\n }\n try {\n return JSON.stringify(JSON.parse(text), null, 2);\n } catch (e) {\n return text;\n }\n}\n\n/**\n * Format a yocto-NEAR amount for display. In transaction mode (showYoctoConversion) amounts at or\n * above 0.00000000001 NEAR are shown as NEAR; smaller amounts and delegate-action mode stay in\n * yoctoNEAR so no precision is hidden.\n */\nfunction formatNearAmount(value, options) {\n const showYoctoConversion = !!(options && options.showYoctoConversion);\n if (showYoctoConversion && value != null && BigInt(value) >= BigInt(\"10000000000000000\")) {\n return `${yoctoToNear(value.toString())} NEAR`;\n }\n return `${value != null ? value.toString() : \"0\"} yoctoNEAR`;\n}\n\nfunction functionCallContent(action, options) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following function will be called.\");\n\n const fc = action.functionCall || {};\n container.appendChild(text);\n container.appendChild(createTextContent(\"Method Name\", fc.methodName));\n container.appendChild(createTextContent(\"Args\", decodeFunctionCallArgs(fc.args)));\n container.appendChild(createTextContent(\"Gas\", fc.gas != null ? fc.gas.toString() : \"\"));\n container.appendChild(createTextContent(\"Deposit\", formatNearAmount(fc.deposit, options)));\n return container;\n}\n\nfunction transferContent(action, options) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following amount will be transferred to the receiver.\");\n\n const deposit = action.transfer ? action.transfer.deposit : undefined;\n container.appendChild(text);\n container.appendChild(createTextContent(\"Deposit\", formatNearAmount(deposit, options)));\n return container;\n}\n\nfunction stakeContent(action, options) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following amount will be staked to the public key.\");\n const stake = action.stake || {};\n container.appendChild(text);\n container.appendChild(createTextContent(\"Stake\", formatNearAmount(stake.stake, options)));\n container.appendChild(createTextContent(\"Public Key\", formatPublicKey(stake.publicKey)));\n return container;\n}\n\nfunction addKeyContent(action) {\n const container = document.createElement(\"div\");\n const ak = action.addKey || {};\n const accessKey = ak.accessKey || {};\n const permission = accessKey.permission || {};\n\n if (permission.fullAccess !== undefined) {\n const warning = document.createElement(\"div\");\n warning.classList.add(\"warning-callout\");\n warning.textContent = \"This key will have full access to your account. Only approve if you trust the recipient.\";\n container.appendChild(warning);\n }\n\n container.appendChild(createDescription(\"By approving this request, the following access key will be added to your account.\"));\n container.appendChild(createTextContent(\"Public Key\", formatPublicKey(ak.publicKey)));\n container.appendChild(createTextContent(\"Nonce\", accessKey.nonce != null ? accessKey.nonce.toString() : \"\"));\n\n if (permission.fullAccess === undefined) {\n container.appendChild(createTextContent(\"Permission\", JSON.stringify(permission.functionCall, null, 2)));\n }\n return container;\n}\n\nfunction deleteKeyContent(action) {\n const container = document.createElement(\"div\");\n const dk = action.deleteKey || {};\n\n const warning = document.createElement(\"div\");\n warning.classList.add(\"warning-callout\");\n warning.textContent = \"Once deleted, the key cannot be recovered.\";\n\n container.appendChild(warning);\n container.appendChild(\n createDescription(\"This action will delete an access key from your account. Only approve if you trust the recipient.\"),\n );\n container.appendChild(createTextContent(\"Public Key\", formatPublicKey(dk.publicKey)));\n return container;\n}\n\nfunction deleteAccountContent(action) {\n const container = document.createElement(\"div\");\n\n const warning = document.createElement(\"div\");\n warning.classList.add(\"warning-callout\");\n warning.textContent = \"Once deleted, the account cannot be recovered.\";\n\n container.appendChild(warning);\n container.appendChild(createDescription(\"By approving this request, the account will be deleted.\"));\n container.appendChild(createTextContent(\"Beneficiary ID\", action.deleteAccount && action.deleteAccount.beneficiaryId));\n return container;\n}\n\nfunction signedDelegateContent(action) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following delegate action will be executed.\");\n\n const sd = action.signedDelegate || {};\n let serialized;\n try {\n serialized = JSON.stringify(\n sd.delegateAction,\n function (_, value) {\n return typeof value === \"bigint\" ? value.toString() : value;\n },\n 2,\n );\n } catch (e) {\n serialized = \"\";\n }\n container.appendChild(text);\n container.appendChild(createTextContent(\"Delegate Action\", serialized));\n return container;\n}\n\nfunction deployGlobalContractContent(action) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the compiled smart contract will be deployed.\");\n const codeLen =\n action.deployGlobalContract && action.deployGlobalContract.code ? `${action.deployGlobalContract.code.length} bytes` : null;\n container.appendChild(text);\n container.appendChild(createTextContent(\"Code\", codeLen));\n return container;\n}\n\nfunction useGlobalContractContent(action) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following global contract will be used.\");\n let identifier;\n try {\n identifier = JSON.stringify(action.useGlobalContract && action.useGlobalContract.contractIdentifier);\n } catch (e) {\n identifier = \"\";\n }\n container.appendChild(text);\n container.appendChild(createTextContent(\"Contract Identifier\", identifier));\n return container;\n}\n\nconst ACTION_DISPATCH = {\n createAccount: {\n label: \"CreateAccount\",\n warn: false,\n render: function () {\n return createAccountContent();\n },\n },\n deployContract: {\n label: \"DeployContract\",\n warn: true,\n render: function (a) {\n return deployContractContent(a);\n },\n },\n functionCall: {\n label: \"FunctionCall\",\n warn: false,\n render: function (a, opts) {\n return functionCallContent(a, opts);\n },\n },\n transfer: {\n label: \"Transfer\",\n warn: false,\n render: function (a, opts) {\n return transferContent(a, opts);\n },\n },\n stake: {\n label: \"Stake\",\n warn: false,\n render: function (a, opts) {\n return stakeContent(a, opts);\n },\n },\n addKey: {\n label: \"AddKey\",\n warn: false,\n render: function (a) {\n return addKeyContent(a);\n },\n warnsWhen: function (a) {\n return !!(\n a.addKey &&\n a.addKey.accessKey &&\n a.addKey.accessKey.permission &&\n a.addKey.accessKey.permission.fullAccess !== undefined\n );\n },\n },\n deleteKey: {\n label: \"DeleteKey\",\n warn: true,\n render: function (a) {\n return deleteKeyContent(a);\n },\n },\n deleteAccount: {\n label: \"DeleteAccount\",\n warn: true,\n render: function (a) {\n return deleteAccountContent(a);\n },\n },\n signedDelegate: {\n label: \"SignedDelegate\",\n warn: false,\n render: function (a) {\n return signedDelegateContent(a);\n },\n },\n deployGlobalContract: {\n label: \"DeployGlobalContract\",\n warn: true,\n render: function (a) {\n return deployGlobalContractContent(a);\n },\n },\n useGlobalContract: {\n label: \"UseGlobalContract\",\n warn: false,\n render: function (a) {\n return useGlobalContractContent(a);\n },\n },\n};\n\nfunction handleNearAction(action, options) {\n const actionKey = getActionType(action);\n const entry = actionKey ? ACTION_DISPATCH[actionKey] : null;\n if (!entry) {\n const unknown = document.createElement(\"div\");\n unknown.textContent = `Unknown action: ${actionKey || \"(empty)\"}`;\n return createAccordion(\"Unknown\", unknown, true);\n }\n const showWarning = entry.warnsWhen ? entry.warnsWhen(action) : !!entry.warn;\n return createAccordion(entry.label, entry.render(action, options), showWarning);\n}\n\n/**\n * Build the full details DOM tree for a form custom field.\n *\n * @param {object} params\n * @param {Array<{label: string, value: string|undefined}>} params.fields - top-level fields like Signer/Receiver/MaxBlockHeight.\n * @param {string} params.actions - JSON string with the actions array (as Auth0 form fields deliver them).\n * @param {{ showYoctoConversion?: boolean }} [params.options]\n */\nfunction renderDetails(params) {\n ensureBufferPolyfill();\n const box = document.createElement(\"div\");\n box.classList.add(\"box\");\n\n for (const field of params.fields || []) {\n if (field.value === undefined || field.value === null || field.value === \"\") continue;\n box.appendChild(createTextContent(field.label, field.value));\n }\n\n const actionsContainer = document.createElement(\"div\");\n actionsContainer.classList.add(\"actions-container\");\n const actionsLabel = document.createElement(\"div\");\n actionsLabel.classList.add(\"label\");\n actionsLabel.textContent = \"Actions\";\n actionsContainer.appendChild(actionsLabel);\n\n let parsedActions = [];\n try {\n parsedActions = JSON.parse(params.actions || \"[]\");\n } catch (e) {\n const errorNode = document.createElement(\"div\");\n errorNode.classList.add(\"warning-callout\");\n errorNode.textContent = \"Failed to parse actions payload.\";\n actionsContainer.appendChild(errorNode);\n box.appendChild(actionsContainer);\n return box;\n }\n\n for (const action of parsedActions) {\n actionsContainer.appendChild(handleNearAction(action, params.options));\n }\n\n box.appendChild(actionsContainer);\n return box;\n}\n\nvar __auth0FormHelpers = {\n ensureBufferPolyfill: ensureBufferPolyfill,\n base58Encode: base58Encode,\n yoctoToNear: yoctoToNear,\n formatNearAmount: formatNearAmount,\n decodeFunctionCallArgs: decodeFunctionCallArgs,\n formatPublicKey: formatPublicKey,\n getActionType: getActionType,\n createTextContent: createTextContent,\n createDescription: createDescription,\n createAccordion: createAccordion,\n createAccountContent: createAccountContent,\n deployContractContent: deployContractContent,\n functionCallContent: functionCallContent,\n transferContent: transferContent,\n stakeContent: stakeContent,\n addKeyContent: addKeyContent,\n deleteKeyContent: deleteKeyContent,\n deleteAccountContent: deleteAccountContent,\n signedDelegateContent: signedDelegateContent,\n deployGlobalContractContent: deployGlobalContractContent,\n useGlobalContractContent: useGlobalContractContent,\n handleNearAction: handleNearAction,\n renderDetails: renderDetails,\n};\n\nif (typeof module !== \"undefined\" && module.exports) {\n module.exports = __auth0FormHelpers;\n}\n\n// --- END __auth0FormHelpers ---\n\n\nreturn (\n/**\n * Custom field that renders two app avatars (left = our wallet, right = requesting app)\n * separated by a small circle with a transfer/icon glyph in the middle.\n *\n * Params (configured per-form in _form_base.json):\n * - leftImageUrl: URL for the left avatar (our wallet/identity logo)\n * - rightImageUrl: URL for the right avatar (the requesting app's logo, e.g. {{ fields.imageUrl }})\n * - iconUrl: URL for the center circle icon\n *\n * If `rightImageUrl` is empty/missing (e.g. the client has no logo_uri configured), the\n * right avatar is hidden so we don't render a broken-image placeholder.\n */\nfunction AuthorizeAppImage(context) {\n return {\n /** Invoked once when the field is created */\n init() {\n const layout = document.createElement(\"div\");\n layout.classList.add(\"layout\");\n\n const { leftImageUrl, rightImageUrl, iconUrl } = context.custom.getParams();\n\n const leftAvatar = document.createElement(\"img\");\n leftAvatar.classList.add(\"avatar\");\n leftAvatar.setAttribute(\"alt\", \"\");\n leftAvatar.setAttribute(\"src\", leftImageUrl);\n\n layout.appendChild(leftAvatar);\n\n const trimmedRight = (rightImageUrl || \"\").trim();\n if (trimmedRight) {\n const circle = document.createElement(\"div\");\n circle.classList.add(\"circle\");\n\n const icon = document.createElement(\"img\");\n icon.classList.add(\"icon\");\n icon.setAttribute(\"alt\", \"\");\n icon.setAttribute(\"src\", iconUrl);\n circle.appendChild(icon);\n\n const rightAvatar = document.createElement(\"img\");\n rightAvatar.classList.add(\"avatar\");\n rightAvatar.setAttribute(\"alt\", \"\");\n rightAvatar.setAttribute(\"src\", trimmedRight);\n\n layout.appendChild(circle);\n layout.appendChild(rightAvatar);\n }\n\n return layout;\n },\n\n /** Returns a list of URLs that the SDK guarantees that will loaded before init() is invoked */\n getScripts() {\n return [];\n },\n\n /** Invoked when field has to be blocked */\n block() {},\n\n /** Invoked when field has to be unblocked */\n unblock() {},\n\n /** Invoked when the SDK needs to get the value (possibly several times) */\n getValue() {},\n };\n}\n\n);\n})()\n", + "code": "(function () {\n// --- BEGIN __auth0FormHelpers (auto-inlined from shared/helpers/index.js) ---\n/**\n * Shared helpers for Auth0 form custom components.\n *\n * Runs in two environments:\n * - Auth0 form runtime: build.js inlines this file into each component's `config.code`,\n * exposing `__auth0FormHelpers` as a top-level variable in the same scope as the component.\n * - Node (tests / playground via require): consumed as a CommonJS module via module.exports.\n *\n * Keep this file free of `require` and ES imports so it can be concatenated as plain text.\n */\n\nfunction ensureBufferPolyfill() {\n if (typeof Buffer !== \"undefined\") return;\n if (typeof globalThis === \"undefined\") return;\n globalThis.Buffer = {\n from: function (data) {\n if (Array.isArray(data)) return new Uint8Array(data);\n if (typeof data === \"string\") return new TextEncoder().encode(data);\n return data;\n },\n };\n}\n\nconst BASE58_ALPHABET = \"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz\";\n\nfunction base58Encode(bytes) {\n let result = \"\";\n let num = BigInt(0);\n for (const byte of bytes) {\n num = (num << BigInt(8)) + BigInt(byte);\n }\n while (num > BigInt(0)) {\n const remainder = num % BigInt(58);\n num = num / BigInt(58);\n result = BASE58_ALPHABET[Number(remainder)] + result;\n }\n for (const byte of bytes) {\n if (byte === 0) result = BASE58_ALPHABET[0] + result;\n else break;\n }\n return result;\n}\n\n/**\n * Format a yocto-NEAR BigInt string as NEAR with up to 8 decimals.\n * \"1000000000000000000000000\" -> \"1\"\n * \"1500000000000000000000000\" -> \"1.5\"\n * \"1\" -> \"0.00000000\" trimmed -> \"0.0\"\n */\nfunction yoctoToNear(bigIntStr) {\n const trimmed = String(bigIntStr).replace(/^0+/, \"\") || \"0\";\n const length = trimmed.length;\n\n if (length <= 24) {\n const zerosNeeded = 24 - length;\n const withZeros = \"0\".repeat(zerosNeeded) + trimmed;\n const decimals = withZeros.slice(0, 8).replace(/0+$/, \"\") || \"0\";\n return `0.${decimals}`;\n }\n\n const splitPos = length - 24;\n const integerPart = trimmed.slice(0, splitPos);\n const decimalPart = trimmed.slice(splitPos, splitPos + 8);\n const trimmedDecimals = decimalPart.replace(/0+$/, \"\");\n if (trimmedDecimals === \"\") return integerPart;\n return `${integerPart}.${trimmedDecimals}`;\n}\n\nfunction formatPublicKey(publicKey) {\n if (publicKey && publicKey.secp256k1Key !== undefined) {\n return `secp256k1:${base58Encode(publicKey.secp256k1Key.data)}`;\n }\n if (publicKey && publicKey.ed25519Key !== undefined) {\n return `ed25519:${base58Encode(publicKey.ed25519Key.data)}`;\n }\n return \"\";\n}\n\n/**\n * Return the canonical action type discriminator (the single non-undefined key of the action).\n * Useful for dispatch and for tests that don't need DOM rendering.\n */\nfunction getActionType(action) {\n if (!action || typeof action !== \"object\") return null;\n for (const key of Object.keys(action)) {\n if (action[key] !== undefined) return key;\n }\n return null;\n}\n\n// --- DOM helpers (require document/window — jsdom in tests, real DOM in browser) ---\n\nfunction createTextContent(label, value, link = false) {\n const textContent = document.createElement(\"div\");\n textContent.classList.add(\"text-content\");\n\n const labelElement = document.createElement(\"div\");\n labelElement.classList.add(\"label\");\n labelElement.textContent = label;\n\n const valueElement = document.createElement(\"div\");\n valueElement.classList.add(\"value\");\n valueElement.textContent = value;\n\n if (link) {\n valueElement.classList.add(\"link\");\n valueElement.setAttribute(\"href\", link);\n valueElement.setAttribute(\"target\", \"_blank\");\n }\n\n textContent.appendChild(labelElement);\n textContent.appendChild(valueElement);\n return textContent;\n}\n\nfunction createDescription(text) {\n const node = document.createElement(\"p\");\n node.classList.add(\"action-description\");\n node.textContent = text;\n return node;\n}\n\nfunction createAccordion(label, content, showWarning = false) {\n const accordion = document.createElement(\"div\");\n accordion.classList.add(\"accordion\");\n\n const header = document.createElement(\"div\");\n header.classList.add(\"accordion-header\");\n\n const headerContent = document.createElement(\"div\");\n headerContent.classList.add(\"accordion-header-content\");\n\n const leftContent = document.createElement(\"div\");\n leftContent.classList.add(\"accordion-left-content\");\n\n if (showWarning) {\n const warningIcon = document.createElement(\"span\");\n warningIcon.classList.add(\"warning-icon\");\n warningIcon.innerHTML = \"⚠️\";\n leftContent.appendChild(warningIcon);\n }\n\n const labelElement = document.createElement(\"span\");\n labelElement.classList.add(\"accordion-header-label\");\n labelElement.textContent = label;\n leftContent.appendChild(labelElement);\n\n const expandIcon = document.createElement(\"span\");\n expandIcon.classList.add(\"expand-icon\");\n expandIcon.innerHTML = \"+\";\n\n headerContent.appendChild(leftContent);\n headerContent.appendChild(expandIcon);\n header.appendChild(headerContent);\n\n const contentElement = document.createElement(\"div\");\n contentElement.classList.add(\"accordion-content\");\n contentElement.appendChild(content);\n\n accordion.appendChild(header);\n accordion.appendChild(contentElement);\n\n header.addEventListener(\"click\", function () {\n contentElement.classList.toggle(\"open\");\n expandIcon.innerHTML = contentElement.classList.contains(\"open\") ? \"−\" : \"+\";\n });\n\n return accordion;\n}\n\n// --- Action content factories ---\n\nfunction createAccountContent() {\n return createDescription(\"By approving this request, a new account will be created.\");\n}\n\nfunction deployContractContent(action) {\n ensureBufferPolyfill();\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the compiled smart contract will be deployed.\");\n const codeLen = action.deployContract && action.deployContract.code ? `${action.deployContract.code.length} bytes` : null;\n container.appendChild(text);\n container.appendChild(createTextContent(\"Code\", codeLen));\n return container;\n}\n\n/**\n * Decode functionCall `args` (a byte array / Uint8Array) to a readable string. NEAR contract\n * args are almost always UTF-8 JSON, so decode the bytes and pretty-print when they parse as\n * JSON; otherwise fall back to the raw decoded text. TextDecoder is used directly because it\n * exists in the browser and the Auth0 form runtime (unlike Buffer, which the runtime lacks).\n */\nfunction decodeFunctionCallArgs(args) {\n if (args == null) return \"\";\n let text;\n try {\n if (typeof TextDecoder !== \"undefined\") {\n text = new TextDecoder().decode(Uint8Array.from(args));\n } else if (typeof Buffer !== \"undefined\" && typeof Buffer.from === \"function\") {\n text = Buffer.from(args).toString(\"utf8\");\n } else {\n text = String.fromCharCode.apply(null, Array.from(args));\n }\n } catch (e) {\n return \"\";\n }\n try {\n return JSON.stringify(JSON.parse(text), null, 2);\n } catch (e) {\n return text;\n }\n}\n\n/**\n * Format a yocto-NEAR amount for display. In transaction mode (showYoctoConversion) amounts at or\n * above 0.00000000001 NEAR are shown as NEAR; smaller amounts and delegate-action mode stay in\n * yoctoNEAR so no precision is hidden.\n */\nfunction formatNearAmount(value, options) {\n const showYoctoConversion = !!(options && options.showYoctoConversion);\n if (showYoctoConversion && value != null && BigInt(value) >= BigInt(\"10000000000000000\")) {\n return `${yoctoToNear(value.toString())} NEAR`;\n }\n return `${value != null ? value.toString() : \"0\"} yoctoNEAR`;\n}\n\nfunction functionCallContent(action, options) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following function will be called.\");\n\n const fc = action.functionCall || {};\n container.appendChild(text);\n container.appendChild(createTextContent(\"Method Name\", fc.methodName));\n container.appendChild(createTextContent(\"Args\", decodeFunctionCallArgs(fc.args)));\n container.appendChild(createTextContent(\"Gas\", fc.gas != null ? fc.gas.toString() : \"\"));\n container.appendChild(createTextContent(\"Deposit\", formatNearAmount(fc.deposit, options)));\n return container;\n}\n\nfunction transferContent(action, options) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following amount will be transferred to the receiver.\");\n\n const deposit = action.transfer ? action.transfer.deposit : undefined;\n container.appendChild(text);\n container.appendChild(createTextContent(\"Deposit\", formatNearAmount(deposit, options)));\n return container;\n}\n\nfunction stakeContent(action, options) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following amount will be staked to the public key.\");\n const stake = action.stake || {};\n container.appendChild(text);\n container.appendChild(createTextContent(\"Stake\", formatNearAmount(stake.stake, options)));\n container.appendChild(createTextContent(\"Public Key\", formatPublicKey(stake.publicKey)));\n return container;\n}\n\nfunction addKeyContent(action) {\n const container = document.createElement(\"div\");\n const ak = action.addKey || {};\n const accessKey = ak.accessKey || {};\n const permission = accessKey.permission || {};\n\n if (permission.fullAccess !== undefined) {\n const warning = document.createElement(\"div\");\n warning.classList.add(\"warning-callout\");\n warning.textContent = \"This key will have full access to your account. Only approve if you trust the recipient.\";\n container.appendChild(warning);\n }\n\n container.appendChild(createDescription(\"By approving this request, the following access key will be added to your account.\"));\n container.appendChild(createTextContent(\"Public Key\", formatPublicKey(ak.publicKey)));\n container.appendChild(createTextContent(\"Nonce\", accessKey.nonce != null ? accessKey.nonce.toString() : \"\"));\n\n if (permission.fullAccess === undefined) {\n container.appendChild(createTextContent(\"Permission\", JSON.stringify(permission.functionCall, null, 2)));\n }\n return container;\n}\n\nfunction deleteKeyContent(action) {\n const container = document.createElement(\"div\");\n const dk = action.deleteKey || {};\n\n const warning = document.createElement(\"div\");\n warning.classList.add(\"warning-callout\");\n warning.textContent = \"Once deleted, the key cannot be recovered.\";\n\n container.appendChild(warning);\n container.appendChild(\n createDescription(\"This action will delete an access key from your account. Only approve if you trust the recipient.\"),\n );\n container.appendChild(createTextContent(\"Public Key\", formatPublicKey(dk.publicKey)));\n return container;\n}\n\nfunction deleteAccountContent(action) {\n const container = document.createElement(\"div\");\n\n const warning = document.createElement(\"div\");\n warning.classList.add(\"warning-callout\");\n warning.textContent = \"Once deleted, the account cannot be recovered.\";\n\n container.appendChild(warning);\n container.appendChild(createDescription(\"By approving this request, the account will be deleted.\"));\n container.appendChild(createTextContent(\"Beneficiary ID\", action.deleteAccount && action.deleteAccount.beneficiaryId));\n return container;\n}\n\nfunction signedDelegateContent(action) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following delegate action will be executed.\");\n\n const sd = action.signedDelegate || {};\n let serialized;\n try {\n serialized = JSON.stringify(\n sd.delegateAction,\n function (_, value) {\n return typeof value === \"bigint\" ? value.toString() : value;\n },\n 2,\n );\n } catch (e) {\n serialized = \"\";\n }\n container.appendChild(text);\n container.appendChild(createTextContent(\"Delegate Action\", serialized));\n return container;\n}\n\nfunction deployGlobalContractContent(action) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the compiled smart contract will be deployed.\");\n const codeLen =\n action.deployGlobalContract && action.deployGlobalContract.code ? `${action.deployGlobalContract.code.length} bytes` : null;\n container.appendChild(text);\n container.appendChild(createTextContent(\"Code\", codeLen));\n return container;\n}\n\nfunction useGlobalContractContent(action) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following global contract will be used.\");\n let identifier;\n try {\n identifier = JSON.stringify(action.useGlobalContract && action.useGlobalContract.contractIdentifier);\n } catch (e) {\n identifier = \"\";\n }\n container.appendChild(text);\n container.appendChild(createTextContent(\"Contract Identifier\", identifier));\n return container;\n}\n\nconst ACTION_DISPATCH = {\n createAccount: {\n label: \"CreateAccount\",\n warn: false,\n render: function () {\n return createAccountContent();\n },\n },\n deployContract: {\n label: \"DeployContract\",\n warn: true,\n render: function (a) {\n return deployContractContent(a);\n },\n },\n functionCall: {\n label: \"FunctionCall\",\n warn: false,\n render: function (a, opts) {\n return functionCallContent(a, opts);\n },\n },\n transfer: {\n label: \"Transfer\",\n warn: false,\n render: function (a, opts) {\n return transferContent(a, opts);\n },\n },\n stake: {\n label: \"Stake\",\n warn: false,\n render: function (a, opts) {\n return stakeContent(a, opts);\n },\n },\n addKey: {\n label: \"AddKey\",\n warn: false,\n render: function (a) {\n return addKeyContent(a);\n },\n warnsWhen: function (a) {\n return !!(\n a.addKey &&\n a.addKey.accessKey &&\n a.addKey.accessKey.permission &&\n a.addKey.accessKey.permission.fullAccess !== undefined\n );\n },\n },\n deleteKey: {\n label: \"DeleteKey\",\n warn: true,\n render: function (a) {\n return deleteKeyContent(a);\n },\n },\n deleteAccount: {\n label: \"DeleteAccount\",\n warn: true,\n render: function (a) {\n return deleteAccountContent(a);\n },\n },\n signedDelegate: {\n label: \"SignedDelegate\",\n warn: false,\n render: function (a) {\n return signedDelegateContent(a);\n },\n },\n deployGlobalContract: {\n label: \"DeployGlobalContract\",\n warn: true,\n render: function (a) {\n return deployGlobalContractContent(a);\n },\n },\n useGlobalContract: {\n label: \"UseGlobalContract\",\n warn: false,\n render: function (a) {\n return useGlobalContractContent(a);\n },\n },\n};\n\nfunction handleNearAction(action, options) {\n const actionKey = getActionType(action);\n const entry = actionKey ? ACTION_DISPATCH[actionKey] : null;\n if (!entry) {\n const unknown = document.createElement(\"div\");\n unknown.textContent = `Unknown action: ${actionKey || \"(empty)\"}`;\n return createAccordion(\"Unknown\", unknown, true);\n }\n const showWarning = entry.warnsWhen ? entry.warnsWhen(action) : !!entry.warn;\n return createAccordion(entry.label, entry.render(action, options), showWarning);\n}\n\n/**\n * Build the full details DOM tree for a form custom field.\n *\n * @param {object} params\n * @param {Array<{label: string, value: string|undefined}>} params.fields - top-level fields like Signer/Receiver/MaxBlockHeight.\n * @param {string} params.actions - JSON string with the actions array (as Auth0 form fields deliver them).\n * @param {{ showYoctoConversion?: boolean }} [params.options]\n */\nfunction renderDetails(params) {\n ensureBufferPolyfill();\n const box = document.createElement(\"div\");\n box.classList.add(\"box\");\n\n for (const field of params.fields || []) {\n if (field.value === undefined || field.value === null || field.value === \"\") continue;\n box.appendChild(createTextContent(field.label, field.value));\n }\n\n const actionsContainer = document.createElement(\"div\");\n actionsContainer.classList.add(\"actions-container\");\n const actionsLabel = document.createElement(\"div\");\n actionsLabel.classList.add(\"label\");\n actionsLabel.textContent = \"Actions\";\n actionsContainer.appendChild(actionsLabel);\n\n let parsedActions = [];\n try {\n parsedActions = JSON.parse(params.actions || \"[]\");\n } catch (e) {\n const errorNode = document.createElement(\"div\");\n errorNode.classList.add(\"warning-callout\");\n errorNode.textContent = \"Failed to parse actions payload.\";\n actionsContainer.appendChild(errorNode);\n box.appendChild(actionsContainer);\n return box;\n }\n\n for (const action of parsedActions) {\n actionsContainer.appendChild(handleNearAction(action, params.options));\n }\n\n box.appendChild(actionsContainer);\n return box;\n}\n\n// --- NEP-413 intent content factories ---\n\n/**\n * Render the token map of a transfer intent (`{ \"\": \"\" }`).\n *\n * Amounts stay in the token's smallest unit: the form has no token metadata, so converting\n * would mean guessing decimals — and a wrong guess here understates what the user is\n * approving. Showing the raw amount alongside its token id is honest and unambiguous.\n */\nfunction intentTokensContent(tokens) {\n const container = document.createElement(\"div\");\n if (!tokens || typeof tokens !== \"object\") return container;\n for (const tokenId of Object.keys(tokens)) {\n container.appendChild(createTextContent(tokenId, String(tokens[tokenId])));\n }\n return container;\n}\n\nfunction transferIntentContent(intent) {\n const container = document.createElement(\"div\");\n container.appendChild(createDescription(\"By approving this request, the following tokens will be transferred.\"));\n container.appendChild(createTextContent(\"Receiver ID\", intent.receiver_id));\n\n const tokensLabel = document.createElement(\"div\");\n tokensLabel.classList.add(\"label\");\n tokensLabel.textContent = \"Tokens\";\n container.appendChild(tokensLabel);\n container.appendChild(intentTokensContent(intent.tokens));\n return container;\n}\n\n/**\n * Fallback for intent kinds this form does not model explicitly. It is deliberately shown\n * with a warning: the user is approving something the UI cannot describe in plain terms.\n */\nfunction unknownIntentContent(intent) {\n const container = document.createElement(\"div\");\n container.appendChild(createDescription(\"This request contains an intent type this app does not recognize. Review it carefully.\"));\n let serialized;\n try {\n serialized = JSON.stringify(intent, null, 2);\n } catch (e) {\n serialized = \"\";\n }\n container.appendChild(createTextContent(\"Intent\", serialized));\n return container;\n}\n\nconst INTENT_DISPATCH = {\n transfer: {\n label: \"Transfer\",\n warn: false,\n render: function (i) {\n return transferIntentContent(i);\n },\n },\n};\n\nfunction handleIntent(intent) {\n const kind = intent && typeof intent === \"object\" ? intent.intent : null;\n const entry = kind ? INTENT_DISPATCH[kind] : null;\n if (!entry) {\n return createAccordion(kind ? `Unknown: ${kind}` : \"Unknown\", unknownIntentContent(intent), true);\n }\n return createAccordion(entry.label, entry.render(intent), !!entry.warn);\n}\n\n/**\n * Render the message body of a NEP-413 request verbatim.\n *\n * This is the default view, and the one that matters most: NEP-413 messages are arbitrary\n * strings meant to be read by a human (\"Sign in to example.com\"), so showing the text exactly\n * as it will be signed is the honest presentation. Pretty-printing is applied only when the\n * message is JSON, purely for legibility.\n */\nfunction messageContent(message) {\n const container = document.createElement(\"div\");\n let display = message;\n try {\n const parsed = JSON.parse(message);\n if (parsed && typeof parsed === \"object\") display = JSON.stringify(parsed, null, 2);\n } catch (e) {\n display = message;\n }\n container.appendChild(createTextContent(\"Message\", display));\n return container;\n}\n\n/**\n * Build the details DOM tree for a NEP-413 signature approval.\n *\n * Two presentations, one payload: when the message carries NEAR Intents the intents are broken\n * out one by one, and otherwise the raw message is shown as text. Either way the top-level\n * fields — recipient above all — are rendered, because under NEP-413 the recipient is what\n * tells the user which application their signature is addressed to.\n * @param {object} params The render parameters.\n * @param {Array<{label: string, value: string|undefined}>} params.fields Top-level fields (recipient, callback URL, signer, deadline).\n * @param {string} params.message The raw NEP-413 message, shown when there are no intents to break out.\n * @param {string} [params.intents] JSON string with the intents array, when the message is a NEAR Intents body.\n * @returns {HTMLElement} The details node.\n */\nfunction renderNep413Details(params) {\n ensureBufferPolyfill();\n const box = document.createElement(\"div\");\n box.classList.add(\"box\");\n\n for (const field of params.fields || []) {\n if (field.value === undefined || field.value === null || field.value === \"\") continue;\n box.appendChild(createTextContent(field.label, field.value));\n }\n\n let parsedIntents = null;\n if (params.intents) {\n try {\n const candidate = JSON.parse(params.intents);\n if (Array.isArray(candidate) && candidate.length > 0) parsedIntents = candidate;\n } catch (e) {\n parsedIntents = null;\n }\n }\n\n // No intents to break out (or they were unreadable) — show the message itself. Falling back\n // to the raw text keeps every NEP-413 message approvable, not just the ones we model.\n if (!parsedIntents) {\n box.appendChild(messageContent(params.message || \"\"));\n return box;\n }\n\n const intentsContainer = document.createElement(\"div\");\n intentsContainer.classList.add(\"actions-container\");\n const intentsLabel = document.createElement(\"div\");\n intentsLabel.classList.add(\"label\");\n intentsLabel.textContent = \"Intents\";\n intentsContainer.appendChild(intentsLabel);\n\n for (const intent of parsedIntents) {\n intentsContainer.appendChild(handleIntent(intent));\n }\n\n box.appendChild(intentsContainer);\n return box;\n}\n\nvar __auth0FormHelpers = {\n ensureBufferPolyfill: ensureBufferPolyfill,\n base58Encode: base58Encode,\n yoctoToNear: yoctoToNear,\n formatNearAmount: formatNearAmount,\n decodeFunctionCallArgs: decodeFunctionCallArgs,\n formatPublicKey: formatPublicKey,\n getActionType: getActionType,\n createTextContent: createTextContent,\n createDescription: createDescription,\n createAccordion: createAccordion,\n createAccountContent: createAccountContent,\n deployContractContent: deployContractContent,\n functionCallContent: functionCallContent,\n transferContent: transferContent,\n stakeContent: stakeContent,\n addKeyContent: addKeyContent,\n deleteKeyContent: deleteKeyContent,\n deleteAccountContent: deleteAccountContent,\n signedDelegateContent: signedDelegateContent,\n deployGlobalContractContent: deployGlobalContractContent,\n useGlobalContractContent: useGlobalContractContent,\n handleNearAction: handleNearAction,\n renderDetails: renderDetails,\n intentTokensContent: intentTokensContent,\n transferIntentContent: transferIntentContent,\n unknownIntentContent: unknownIntentContent,\n handleIntent: handleIntent,\n messageContent: messageContent,\n renderNep413Details: renderNep413Details,\n};\n\nif (typeof module !== \"undefined\" && module.exports) {\n module.exports = __auth0FormHelpers;\n}\n\n// --- END __auth0FormHelpers ---\n\n\nreturn (\n/**\n * Custom field that renders two app avatars (left = our wallet, right = requesting app)\n * separated by a small circle with a transfer/icon glyph in the middle.\n *\n * Params (configured per-form in _form_base.json):\n * - leftImageUrl: URL for the left avatar (our wallet/identity logo)\n * - rightImageUrl: URL for the right avatar (the requesting app's logo, e.g. {{ fields.imageUrl }})\n * - iconUrl: URL for the center circle icon\n *\n * If `rightImageUrl` is empty/missing (e.g. the client has no logo_uri configured), the\n * right avatar is hidden so we don't render a broken-image placeholder.\n */\nfunction AuthorizeAppImage(context) {\n return {\n /** Invoked once when the field is created */\n init() {\n const layout = document.createElement(\"div\");\n layout.classList.add(\"layout\");\n\n const { leftImageUrl, rightImageUrl, iconUrl } = context.custom.getParams();\n\n const leftAvatar = document.createElement(\"img\");\n leftAvatar.classList.add(\"avatar\");\n leftAvatar.setAttribute(\"alt\", \"\");\n leftAvatar.setAttribute(\"src\", leftImageUrl);\n\n layout.appendChild(leftAvatar);\n\n const trimmedRight = (rightImageUrl || \"\").trim();\n if (trimmedRight) {\n const circle = document.createElement(\"div\");\n circle.classList.add(\"circle\");\n\n const icon = document.createElement(\"img\");\n icon.classList.add(\"icon\");\n icon.setAttribute(\"alt\", \"\");\n icon.setAttribute(\"src\", iconUrl);\n circle.appendChild(icon);\n\n const rightAvatar = document.createElement(\"img\");\n rightAvatar.classList.add(\"avatar\");\n rightAvatar.setAttribute(\"alt\", \"\");\n rightAvatar.setAttribute(\"src\", trimmedRight);\n\n layout.appendChild(circle);\n layout.appendChild(rightAvatar);\n }\n\n return layout;\n },\n\n /** Returns a list of URLs that the SDK guarantees that will loaded before init() is invoked */\n getScripts() {\n return [];\n },\n\n /** Invoked when field has to be blocked */\n block() {},\n\n /** Invoked when field has to be unblocked */\n unblock() {},\n\n /** Invoked when the SDK needs to get the value (possibly several times) */\n getValue() {},\n };\n}\n\n);\n})()\n", "css": ".avatar {\n width: 4.5rem;\n height: 4.5rem;\n border-radius: 12px;\n object-fit: cover;\n}\n\n.layout {\n position: relative;\n display: flex;\n flex-direction: row;\n gap: 1.5rem;\n align-items: center;\n justify-content: center;\n}\n\n.circle {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n width: 2.5rem;\n height: 2.5rem;\n border-radius: 999px;\n background-color: #f6f6f6;\n z-index: 1;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.icon {\n width: 1.2rem;\n height: 1.2rem;\n}\n", "params": { "iconUrl": "https://peersyst-public-production.s3.eu-west-1.amazonaws.com/ab6afba7-91a2-4c53-b2a3-90e4f8c3492a.png", @@ -49,7 +49,7 @@ "sensitive": false, "config": { "schema": {}, - "code": "(function () {\n// --- BEGIN __auth0FormHelpers (auto-inlined from shared/helpers/index.js) ---\n/**\n * Shared helpers for Auth0 form custom components.\n *\n * Runs in two environments:\n * - Auth0 form runtime: build.js inlines this file into each component's `config.code`,\n * exposing `__auth0FormHelpers` as a top-level variable in the same scope as the component.\n * - Node (tests / playground via require): consumed as a CommonJS module via module.exports.\n *\n * Keep this file free of `require` and ES imports so it can be concatenated as plain text.\n */\n\nfunction ensureBufferPolyfill() {\n if (typeof Buffer !== \"undefined\") return;\n if (typeof globalThis === \"undefined\") return;\n globalThis.Buffer = {\n from: function (data) {\n if (Array.isArray(data)) return new Uint8Array(data);\n if (typeof data === \"string\") return new TextEncoder().encode(data);\n return data;\n },\n };\n}\n\nconst BASE58_ALPHABET = \"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz\";\n\nfunction base58Encode(bytes) {\n let result = \"\";\n let num = BigInt(0);\n for (const byte of bytes) {\n num = (num << BigInt(8)) + BigInt(byte);\n }\n while (num > BigInt(0)) {\n const remainder = num % BigInt(58);\n num = num / BigInt(58);\n result = BASE58_ALPHABET[Number(remainder)] + result;\n }\n for (const byte of bytes) {\n if (byte === 0) result = BASE58_ALPHABET[0] + result;\n else break;\n }\n return result;\n}\n\n/**\n * Format a yocto-NEAR BigInt string as NEAR with up to 8 decimals.\n * \"1000000000000000000000000\" -> \"1\"\n * \"1500000000000000000000000\" -> \"1.5\"\n * \"1\" -> \"0.00000000\" trimmed -> \"0.0\"\n */\nfunction yoctoToNear(bigIntStr) {\n const trimmed = String(bigIntStr).replace(/^0+/, \"\") || \"0\";\n const length = trimmed.length;\n\n if (length <= 24) {\n const zerosNeeded = 24 - length;\n const withZeros = \"0\".repeat(zerosNeeded) + trimmed;\n const decimals = withZeros.slice(0, 8).replace(/0+$/, \"\") || \"0\";\n return `0.${decimals}`;\n }\n\n const splitPos = length - 24;\n const integerPart = trimmed.slice(0, splitPos);\n const decimalPart = trimmed.slice(splitPos, splitPos + 8);\n const trimmedDecimals = decimalPart.replace(/0+$/, \"\");\n if (trimmedDecimals === \"\") return integerPart;\n return `${integerPart}.${trimmedDecimals}`;\n}\n\nfunction formatPublicKey(publicKey) {\n if (publicKey && publicKey.secp256k1Key !== undefined) {\n return `secp256k1:${base58Encode(publicKey.secp256k1Key.data)}`;\n }\n if (publicKey && publicKey.ed25519Key !== undefined) {\n return `ed25519:${base58Encode(publicKey.ed25519Key.data)}`;\n }\n return \"\";\n}\n\n/**\n * Return the canonical action type discriminator (the single non-undefined key of the action).\n * Useful for dispatch and for tests that don't need DOM rendering.\n */\nfunction getActionType(action) {\n if (!action || typeof action !== \"object\") return null;\n for (const key of Object.keys(action)) {\n if (action[key] !== undefined) return key;\n }\n return null;\n}\n\n// --- DOM helpers (require document/window — jsdom in tests, real DOM in browser) ---\n\nfunction createTextContent(label, value, link = false) {\n const textContent = document.createElement(\"div\");\n textContent.classList.add(\"text-content\");\n\n const labelElement = document.createElement(\"div\");\n labelElement.classList.add(\"label\");\n labelElement.textContent = label;\n\n const valueElement = document.createElement(\"div\");\n valueElement.classList.add(\"value\");\n valueElement.textContent = value;\n\n if (link) {\n valueElement.classList.add(\"link\");\n valueElement.setAttribute(\"href\", link);\n valueElement.setAttribute(\"target\", \"_blank\");\n }\n\n textContent.appendChild(labelElement);\n textContent.appendChild(valueElement);\n return textContent;\n}\n\nfunction createDescription(text) {\n const node = document.createElement(\"p\");\n node.classList.add(\"action-description\");\n node.textContent = text;\n return node;\n}\n\nfunction createAccordion(label, content, showWarning = false) {\n const accordion = document.createElement(\"div\");\n accordion.classList.add(\"accordion\");\n\n const header = document.createElement(\"div\");\n header.classList.add(\"accordion-header\");\n\n const headerContent = document.createElement(\"div\");\n headerContent.classList.add(\"accordion-header-content\");\n\n const leftContent = document.createElement(\"div\");\n leftContent.classList.add(\"accordion-left-content\");\n\n if (showWarning) {\n const warningIcon = document.createElement(\"span\");\n warningIcon.classList.add(\"warning-icon\");\n warningIcon.innerHTML = \"⚠️\";\n leftContent.appendChild(warningIcon);\n }\n\n const labelElement = document.createElement(\"span\");\n labelElement.classList.add(\"accordion-header-label\");\n labelElement.textContent = label;\n leftContent.appendChild(labelElement);\n\n const expandIcon = document.createElement(\"span\");\n expandIcon.classList.add(\"expand-icon\");\n expandIcon.innerHTML = \"+\";\n\n headerContent.appendChild(leftContent);\n headerContent.appendChild(expandIcon);\n header.appendChild(headerContent);\n\n const contentElement = document.createElement(\"div\");\n contentElement.classList.add(\"accordion-content\");\n contentElement.appendChild(content);\n\n accordion.appendChild(header);\n accordion.appendChild(contentElement);\n\n header.addEventListener(\"click\", function () {\n contentElement.classList.toggle(\"open\");\n expandIcon.innerHTML = contentElement.classList.contains(\"open\") ? \"−\" : \"+\";\n });\n\n return accordion;\n}\n\n// --- Action content factories ---\n\nfunction createAccountContent() {\n return createDescription(\"By approving this request, a new account will be created.\");\n}\n\nfunction deployContractContent(action) {\n ensureBufferPolyfill();\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the compiled smart contract will be deployed.\");\n const codeLen = action.deployContract && action.deployContract.code ? `${action.deployContract.code.length} bytes` : null;\n container.appendChild(text);\n container.appendChild(createTextContent(\"Code\", codeLen));\n return container;\n}\n\n/**\n * Decode functionCall `args` (a byte array / Uint8Array) to a readable string. NEAR contract\n * args are almost always UTF-8 JSON, so decode the bytes and pretty-print when they parse as\n * JSON; otherwise fall back to the raw decoded text. TextDecoder is used directly because it\n * exists in the browser and the Auth0 form runtime (unlike Buffer, which the runtime lacks).\n */\nfunction decodeFunctionCallArgs(args) {\n if (args == null) return \"\";\n let text;\n try {\n if (typeof TextDecoder !== \"undefined\") {\n text = new TextDecoder().decode(Uint8Array.from(args));\n } else if (typeof Buffer !== \"undefined\" && typeof Buffer.from === \"function\") {\n text = Buffer.from(args).toString(\"utf8\");\n } else {\n text = String.fromCharCode.apply(null, Array.from(args));\n }\n } catch (e) {\n return \"\";\n }\n try {\n return JSON.stringify(JSON.parse(text), null, 2);\n } catch (e) {\n return text;\n }\n}\n\n/**\n * Format a yocto-NEAR amount for display. In transaction mode (showYoctoConversion) amounts at or\n * above 0.00000000001 NEAR are shown as NEAR; smaller amounts and delegate-action mode stay in\n * yoctoNEAR so no precision is hidden.\n */\nfunction formatNearAmount(value, options) {\n const showYoctoConversion = !!(options && options.showYoctoConversion);\n if (showYoctoConversion && value != null && BigInt(value) >= BigInt(\"10000000000000000\")) {\n return `${yoctoToNear(value.toString())} NEAR`;\n }\n return `${value != null ? value.toString() : \"0\"} yoctoNEAR`;\n}\n\nfunction functionCallContent(action, options) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following function will be called.\");\n\n const fc = action.functionCall || {};\n container.appendChild(text);\n container.appendChild(createTextContent(\"Method Name\", fc.methodName));\n container.appendChild(createTextContent(\"Args\", decodeFunctionCallArgs(fc.args)));\n container.appendChild(createTextContent(\"Gas\", fc.gas != null ? fc.gas.toString() : \"\"));\n container.appendChild(createTextContent(\"Deposit\", formatNearAmount(fc.deposit, options)));\n return container;\n}\n\nfunction transferContent(action, options) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following amount will be transferred to the receiver.\");\n\n const deposit = action.transfer ? action.transfer.deposit : undefined;\n container.appendChild(text);\n container.appendChild(createTextContent(\"Deposit\", formatNearAmount(deposit, options)));\n return container;\n}\n\nfunction stakeContent(action, options) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following amount will be staked to the public key.\");\n const stake = action.stake || {};\n container.appendChild(text);\n container.appendChild(createTextContent(\"Stake\", formatNearAmount(stake.stake, options)));\n container.appendChild(createTextContent(\"Public Key\", formatPublicKey(stake.publicKey)));\n return container;\n}\n\nfunction addKeyContent(action) {\n const container = document.createElement(\"div\");\n const ak = action.addKey || {};\n const accessKey = ak.accessKey || {};\n const permission = accessKey.permission || {};\n\n if (permission.fullAccess !== undefined) {\n const warning = document.createElement(\"div\");\n warning.classList.add(\"warning-callout\");\n warning.textContent = \"This key will have full access to your account. Only approve if you trust the recipient.\";\n container.appendChild(warning);\n }\n\n container.appendChild(createDescription(\"By approving this request, the following access key will be added to your account.\"));\n container.appendChild(createTextContent(\"Public Key\", formatPublicKey(ak.publicKey)));\n container.appendChild(createTextContent(\"Nonce\", accessKey.nonce != null ? accessKey.nonce.toString() : \"\"));\n\n if (permission.fullAccess === undefined) {\n container.appendChild(createTextContent(\"Permission\", JSON.stringify(permission.functionCall, null, 2)));\n }\n return container;\n}\n\nfunction deleteKeyContent(action) {\n const container = document.createElement(\"div\");\n const dk = action.deleteKey || {};\n\n const warning = document.createElement(\"div\");\n warning.classList.add(\"warning-callout\");\n warning.textContent = \"Once deleted, the key cannot be recovered.\";\n\n container.appendChild(warning);\n container.appendChild(\n createDescription(\"This action will delete an access key from your account. Only approve if you trust the recipient.\"),\n );\n container.appendChild(createTextContent(\"Public Key\", formatPublicKey(dk.publicKey)));\n return container;\n}\n\nfunction deleteAccountContent(action) {\n const container = document.createElement(\"div\");\n\n const warning = document.createElement(\"div\");\n warning.classList.add(\"warning-callout\");\n warning.textContent = \"Once deleted, the account cannot be recovered.\";\n\n container.appendChild(warning);\n container.appendChild(createDescription(\"By approving this request, the account will be deleted.\"));\n container.appendChild(createTextContent(\"Beneficiary ID\", action.deleteAccount && action.deleteAccount.beneficiaryId));\n return container;\n}\n\nfunction signedDelegateContent(action) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following delegate action will be executed.\");\n\n const sd = action.signedDelegate || {};\n let serialized;\n try {\n serialized = JSON.stringify(\n sd.delegateAction,\n function (_, value) {\n return typeof value === \"bigint\" ? value.toString() : value;\n },\n 2,\n );\n } catch (e) {\n serialized = \"\";\n }\n container.appendChild(text);\n container.appendChild(createTextContent(\"Delegate Action\", serialized));\n return container;\n}\n\nfunction deployGlobalContractContent(action) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the compiled smart contract will be deployed.\");\n const codeLen =\n action.deployGlobalContract && action.deployGlobalContract.code ? `${action.deployGlobalContract.code.length} bytes` : null;\n container.appendChild(text);\n container.appendChild(createTextContent(\"Code\", codeLen));\n return container;\n}\n\nfunction useGlobalContractContent(action) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following global contract will be used.\");\n let identifier;\n try {\n identifier = JSON.stringify(action.useGlobalContract && action.useGlobalContract.contractIdentifier);\n } catch (e) {\n identifier = \"\";\n }\n container.appendChild(text);\n container.appendChild(createTextContent(\"Contract Identifier\", identifier));\n return container;\n}\n\nconst ACTION_DISPATCH = {\n createAccount: {\n label: \"CreateAccount\",\n warn: false,\n render: function () {\n return createAccountContent();\n },\n },\n deployContract: {\n label: \"DeployContract\",\n warn: true,\n render: function (a) {\n return deployContractContent(a);\n },\n },\n functionCall: {\n label: \"FunctionCall\",\n warn: false,\n render: function (a, opts) {\n return functionCallContent(a, opts);\n },\n },\n transfer: {\n label: \"Transfer\",\n warn: false,\n render: function (a, opts) {\n return transferContent(a, opts);\n },\n },\n stake: {\n label: \"Stake\",\n warn: false,\n render: function (a, opts) {\n return stakeContent(a, opts);\n },\n },\n addKey: {\n label: \"AddKey\",\n warn: false,\n render: function (a) {\n return addKeyContent(a);\n },\n warnsWhen: function (a) {\n return !!(\n a.addKey &&\n a.addKey.accessKey &&\n a.addKey.accessKey.permission &&\n a.addKey.accessKey.permission.fullAccess !== undefined\n );\n },\n },\n deleteKey: {\n label: \"DeleteKey\",\n warn: true,\n render: function (a) {\n return deleteKeyContent(a);\n },\n },\n deleteAccount: {\n label: \"DeleteAccount\",\n warn: true,\n render: function (a) {\n return deleteAccountContent(a);\n },\n },\n signedDelegate: {\n label: \"SignedDelegate\",\n warn: false,\n render: function (a) {\n return signedDelegateContent(a);\n },\n },\n deployGlobalContract: {\n label: \"DeployGlobalContract\",\n warn: true,\n render: function (a) {\n return deployGlobalContractContent(a);\n },\n },\n useGlobalContract: {\n label: \"UseGlobalContract\",\n warn: false,\n render: function (a) {\n return useGlobalContractContent(a);\n },\n },\n};\n\nfunction handleNearAction(action, options) {\n const actionKey = getActionType(action);\n const entry = actionKey ? ACTION_DISPATCH[actionKey] : null;\n if (!entry) {\n const unknown = document.createElement(\"div\");\n unknown.textContent = `Unknown action: ${actionKey || \"(empty)\"}`;\n return createAccordion(\"Unknown\", unknown, true);\n }\n const showWarning = entry.warnsWhen ? entry.warnsWhen(action) : !!entry.warn;\n return createAccordion(entry.label, entry.render(action, options), showWarning);\n}\n\n/**\n * Build the full details DOM tree for a form custom field.\n *\n * @param {object} params\n * @param {Array<{label: string, value: string|undefined}>} params.fields - top-level fields like Signer/Receiver/MaxBlockHeight.\n * @param {string} params.actions - JSON string with the actions array (as Auth0 form fields deliver them).\n * @param {{ showYoctoConversion?: boolean }} [params.options]\n */\nfunction renderDetails(params) {\n ensureBufferPolyfill();\n const box = document.createElement(\"div\");\n box.classList.add(\"box\");\n\n for (const field of params.fields || []) {\n if (field.value === undefined || field.value === null || field.value === \"\") continue;\n box.appendChild(createTextContent(field.label, field.value));\n }\n\n const actionsContainer = document.createElement(\"div\");\n actionsContainer.classList.add(\"actions-container\");\n const actionsLabel = document.createElement(\"div\");\n actionsLabel.classList.add(\"label\");\n actionsLabel.textContent = \"Actions\";\n actionsContainer.appendChild(actionsLabel);\n\n let parsedActions = [];\n try {\n parsedActions = JSON.parse(params.actions || \"[]\");\n } catch (e) {\n const errorNode = document.createElement(\"div\");\n errorNode.classList.add(\"warning-callout\");\n errorNode.textContent = \"Failed to parse actions payload.\";\n actionsContainer.appendChild(errorNode);\n box.appendChild(actionsContainer);\n return box;\n }\n\n for (const action of parsedActions) {\n actionsContainer.appendChild(handleNearAction(action, params.options));\n }\n\n box.appendChild(actionsContainer);\n return box;\n}\n\nvar __auth0FormHelpers = {\n ensureBufferPolyfill: ensureBufferPolyfill,\n base58Encode: base58Encode,\n yoctoToNear: yoctoToNear,\n formatNearAmount: formatNearAmount,\n decodeFunctionCallArgs: decodeFunctionCallArgs,\n formatPublicKey: formatPublicKey,\n getActionType: getActionType,\n createTextContent: createTextContent,\n createDescription: createDescription,\n createAccordion: createAccordion,\n createAccountContent: createAccountContent,\n deployContractContent: deployContractContent,\n functionCallContent: functionCallContent,\n transferContent: transferContent,\n stakeContent: stakeContent,\n addKeyContent: addKeyContent,\n deleteKeyContent: deleteKeyContent,\n deleteAccountContent: deleteAccountContent,\n signedDelegateContent: signedDelegateContent,\n deployGlobalContractContent: deployGlobalContractContent,\n useGlobalContractContent: useGlobalContractContent,\n handleNearAction: handleNearAction,\n renderDetails: renderDetails,\n};\n\nif (typeof module !== \"undefined\" && module.exports) {\n module.exports = __auth0FormHelpers;\n}\n\n// --- END __auth0FormHelpers ---\n\n\nreturn (\n/**\n * Custom field that renders the delegate action details (sender, receiver, max block height, actions).\n *\n * Depends on `__auth0FormHelpers`, which is inlined at build time by build.js\n * (or shimmed by the playground via helpers-shim.js).\n */\nfunction AuthorizeAppDelegateActionDetails(context) {\n return {\n init: function () {\n const params = context.custom.getParams();\n return __auth0FormHelpers.renderDetails({\n fields: [\n { label: \"Sender ID\", value: params.senderId },\n { label: \"Receiver ID\", value: params.receiverId },\n { label: \"Max Block Height\", value: params.maxBlockHeight },\n ],\n actions: params.actions,\n options: { showYoctoConversion: false },\n });\n },\n getScripts: function () { return []; },\n block: function () {},\n unblock: function () {},\n getValue: function () {},\n };\n}\n\n);\n})()\n", + "code": "(function () {\n// --- BEGIN __auth0FormHelpers (auto-inlined from shared/helpers/index.js) ---\n/**\n * Shared helpers for Auth0 form custom components.\n *\n * Runs in two environments:\n * - Auth0 form runtime: build.js inlines this file into each component's `config.code`,\n * exposing `__auth0FormHelpers` as a top-level variable in the same scope as the component.\n * - Node (tests / playground via require): consumed as a CommonJS module via module.exports.\n *\n * Keep this file free of `require` and ES imports so it can be concatenated as plain text.\n */\n\nfunction ensureBufferPolyfill() {\n if (typeof Buffer !== \"undefined\") return;\n if (typeof globalThis === \"undefined\") return;\n globalThis.Buffer = {\n from: function (data) {\n if (Array.isArray(data)) return new Uint8Array(data);\n if (typeof data === \"string\") return new TextEncoder().encode(data);\n return data;\n },\n };\n}\n\nconst BASE58_ALPHABET = \"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz\";\n\nfunction base58Encode(bytes) {\n let result = \"\";\n let num = BigInt(0);\n for (const byte of bytes) {\n num = (num << BigInt(8)) + BigInt(byte);\n }\n while (num > BigInt(0)) {\n const remainder = num % BigInt(58);\n num = num / BigInt(58);\n result = BASE58_ALPHABET[Number(remainder)] + result;\n }\n for (const byte of bytes) {\n if (byte === 0) result = BASE58_ALPHABET[0] + result;\n else break;\n }\n return result;\n}\n\n/**\n * Format a yocto-NEAR BigInt string as NEAR with up to 8 decimals.\n * \"1000000000000000000000000\" -> \"1\"\n * \"1500000000000000000000000\" -> \"1.5\"\n * \"1\" -> \"0.00000000\" trimmed -> \"0.0\"\n */\nfunction yoctoToNear(bigIntStr) {\n const trimmed = String(bigIntStr).replace(/^0+/, \"\") || \"0\";\n const length = trimmed.length;\n\n if (length <= 24) {\n const zerosNeeded = 24 - length;\n const withZeros = \"0\".repeat(zerosNeeded) + trimmed;\n const decimals = withZeros.slice(0, 8).replace(/0+$/, \"\") || \"0\";\n return `0.${decimals}`;\n }\n\n const splitPos = length - 24;\n const integerPart = trimmed.slice(0, splitPos);\n const decimalPart = trimmed.slice(splitPos, splitPos + 8);\n const trimmedDecimals = decimalPart.replace(/0+$/, \"\");\n if (trimmedDecimals === \"\") return integerPart;\n return `${integerPart}.${trimmedDecimals}`;\n}\n\nfunction formatPublicKey(publicKey) {\n if (publicKey && publicKey.secp256k1Key !== undefined) {\n return `secp256k1:${base58Encode(publicKey.secp256k1Key.data)}`;\n }\n if (publicKey && publicKey.ed25519Key !== undefined) {\n return `ed25519:${base58Encode(publicKey.ed25519Key.data)}`;\n }\n return \"\";\n}\n\n/**\n * Return the canonical action type discriminator (the single non-undefined key of the action).\n * Useful for dispatch and for tests that don't need DOM rendering.\n */\nfunction getActionType(action) {\n if (!action || typeof action !== \"object\") return null;\n for (const key of Object.keys(action)) {\n if (action[key] !== undefined) return key;\n }\n return null;\n}\n\n// --- DOM helpers (require document/window — jsdom in tests, real DOM in browser) ---\n\nfunction createTextContent(label, value, link = false) {\n const textContent = document.createElement(\"div\");\n textContent.classList.add(\"text-content\");\n\n const labelElement = document.createElement(\"div\");\n labelElement.classList.add(\"label\");\n labelElement.textContent = label;\n\n const valueElement = document.createElement(\"div\");\n valueElement.classList.add(\"value\");\n valueElement.textContent = value;\n\n if (link) {\n valueElement.classList.add(\"link\");\n valueElement.setAttribute(\"href\", link);\n valueElement.setAttribute(\"target\", \"_blank\");\n }\n\n textContent.appendChild(labelElement);\n textContent.appendChild(valueElement);\n return textContent;\n}\n\nfunction createDescription(text) {\n const node = document.createElement(\"p\");\n node.classList.add(\"action-description\");\n node.textContent = text;\n return node;\n}\n\nfunction createAccordion(label, content, showWarning = false) {\n const accordion = document.createElement(\"div\");\n accordion.classList.add(\"accordion\");\n\n const header = document.createElement(\"div\");\n header.classList.add(\"accordion-header\");\n\n const headerContent = document.createElement(\"div\");\n headerContent.classList.add(\"accordion-header-content\");\n\n const leftContent = document.createElement(\"div\");\n leftContent.classList.add(\"accordion-left-content\");\n\n if (showWarning) {\n const warningIcon = document.createElement(\"span\");\n warningIcon.classList.add(\"warning-icon\");\n warningIcon.innerHTML = \"⚠️\";\n leftContent.appendChild(warningIcon);\n }\n\n const labelElement = document.createElement(\"span\");\n labelElement.classList.add(\"accordion-header-label\");\n labelElement.textContent = label;\n leftContent.appendChild(labelElement);\n\n const expandIcon = document.createElement(\"span\");\n expandIcon.classList.add(\"expand-icon\");\n expandIcon.innerHTML = \"+\";\n\n headerContent.appendChild(leftContent);\n headerContent.appendChild(expandIcon);\n header.appendChild(headerContent);\n\n const contentElement = document.createElement(\"div\");\n contentElement.classList.add(\"accordion-content\");\n contentElement.appendChild(content);\n\n accordion.appendChild(header);\n accordion.appendChild(contentElement);\n\n header.addEventListener(\"click\", function () {\n contentElement.classList.toggle(\"open\");\n expandIcon.innerHTML = contentElement.classList.contains(\"open\") ? \"−\" : \"+\";\n });\n\n return accordion;\n}\n\n// --- Action content factories ---\n\nfunction createAccountContent() {\n return createDescription(\"By approving this request, a new account will be created.\");\n}\n\nfunction deployContractContent(action) {\n ensureBufferPolyfill();\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the compiled smart contract will be deployed.\");\n const codeLen = action.deployContract && action.deployContract.code ? `${action.deployContract.code.length} bytes` : null;\n container.appendChild(text);\n container.appendChild(createTextContent(\"Code\", codeLen));\n return container;\n}\n\n/**\n * Decode functionCall `args` (a byte array / Uint8Array) to a readable string. NEAR contract\n * args are almost always UTF-8 JSON, so decode the bytes and pretty-print when they parse as\n * JSON; otherwise fall back to the raw decoded text. TextDecoder is used directly because it\n * exists in the browser and the Auth0 form runtime (unlike Buffer, which the runtime lacks).\n */\nfunction decodeFunctionCallArgs(args) {\n if (args == null) return \"\";\n let text;\n try {\n if (typeof TextDecoder !== \"undefined\") {\n text = new TextDecoder().decode(Uint8Array.from(args));\n } else if (typeof Buffer !== \"undefined\" && typeof Buffer.from === \"function\") {\n text = Buffer.from(args).toString(\"utf8\");\n } else {\n text = String.fromCharCode.apply(null, Array.from(args));\n }\n } catch (e) {\n return \"\";\n }\n try {\n return JSON.stringify(JSON.parse(text), null, 2);\n } catch (e) {\n return text;\n }\n}\n\n/**\n * Format a yocto-NEAR amount for display. In transaction mode (showYoctoConversion) amounts at or\n * above 0.00000000001 NEAR are shown as NEAR; smaller amounts and delegate-action mode stay in\n * yoctoNEAR so no precision is hidden.\n */\nfunction formatNearAmount(value, options) {\n const showYoctoConversion = !!(options && options.showYoctoConversion);\n if (showYoctoConversion && value != null && BigInt(value) >= BigInt(\"10000000000000000\")) {\n return `${yoctoToNear(value.toString())} NEAR`;\n }\n return `${value != null ? value.toString() : \"0\"} yoctoNEAR`;\n}\n\nfunction functionCallContent(action, options) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following function will be called.\");\n\n const fc = action.functionCall || {};\n container.appendChild(text);\n container.appendChild(createTextContent(\"Method Name\", fc.methodName));\n container.appendChild(createTextContent(\"Args\", decodeFunctionCallArgs(fc.args)));\n container.appendChild(createTextContent(\"Gas\", fc.gas != null ? fc.gas.toString() : \"\"));\n container.appendChild(createTextContent(\"Deposit\", formatNearAmount(fc.deposit, options)));\n return container;\n}\n\nfunction transferContent(action, options) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following amount will be transferred to the receiver.\");\n\n const deposit = action.transfer ? action.transfer.deposit : undefined;\n container.appendChild(text);\n container.appendChild(createTextContent(\"Deposit\", formatNearAmount(deposit, options)));\n return container;\n}\n\nfunction stakeContent(action, options) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following amount will be staked to the public key.\");\n const stake = action.stake || {};\n container.appendChild(text);\n container.appendChild(createTextContent(\"Stake\", formatNearAmount(stake.stake, options)));\n container.appendChild(createTextContent(\"Public Key\", formatPublicKey(stake.publicKey)));\n return container;\n}\n\nfunction addKeyContent(action) {\n const container = document.createElement(\"div\");\n const ak = action.addKey || {};\n const accessKey = ak.accessKey || {};\n const permission = accessKey.permission || {};\n\n if (permission.fullAccess !== undefined) {\n const warning = document.createElement(\"div\");\n warning.classList.add(\"warning-callout\");\n warning.textContent = \"This key will have full access to your account. Only approve if you trust the recipient.\";\n container.appendChild(warning);\n }\n\n container.appendChild(createDescription(\"By approving this request, the following access key will be added to your account.\"));\n container.appendChild(createTextContent(\"Public Key\", formatPublicKey(ak.publicKey)));\n container.appendChild(createTextContent(\"Nonce\", accessKey.nonce != null ? accessKey.nonce.toString() : \"\"));\n\n if (permission.fullAccess === undefined) {\n container.appendChild(createTextContent(\"Permission\", JSON.stringify(permission.functionCall, null, 2)));\n }\n return container;\n}\n\nfunction deleteKeyContent(action) {\n const container = document.createElement(\"div\");\n const dk = action.deleteKey || {};\n\n const warning = document.createElement(\"div\");\n warning.classList.add(\"warning-callout\");\n warning.textContent = \"Once deleted, the key cannot be recovered.\";\n\n container.appendChild(warning);\n container.appendChild(\n createDescription(\"This action will delete an access key from your account. Only approve if you trust the recipient.\"),\n );\n container.appendChild(createTextContent(\"Public Key\", formatPublicKey(dk.publicKey)));\n return container;\n}\n\nfunction deleteAccountContent(action) {\n const container = document.createElement(\"div\");\n\n const warning = document.createElement(\"div\");\n warning.classList.add(\"warning-callout\");\n warning.textContent = \"Once deleted, the account cannot be recovered.\";\n\n container.appendChild(warning);\n container.appendChild(createDescription(\"By approving this request, the account will be deleted.\"));\n container.appendChild(createTextContent(\"Beneficiary ID\", action.deleteAccount && action.deleteAccount.beneficiaryId));\n return container;\n}\n\nfunction signedDelegateContent(action) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following delegate action will be executed.\");\n\n const sd = action.signedDelegate || {};\n let serialized;\n try {\n serialized = JSON.stringify(\n sd.delegateAction,\n function (_, value) {\n return typeof value === \"bigint\" ? value.toString() : value;\n },\n 2,\n );\n } catch (e) {\n serialized = \"\";\n }\n container.appendChild(text);\n container.appendChild(createTextContent(\"Delegate Action\", serialized));\n return container;\n}\n\nfunction deployGlobalContractContent(action) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the compiled smart contract will be deployed.\");\n const codeLen =\n action.deployGlobalContract && action.deployGlobalContract.code ? `${action.deployGlobalContract.code.length} bytes` : null;\n container.appendChild(text);\n container.appendChild(createTextContent(\"Code\", codeLen));\n return container;\n}\n\nfunction useGlobalContractContent(action) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following global contract will be used.\");\n let identifier;\n try {\n identifier = JSON.stringify(action.useGlobalContract && action.useGlobalContract.contractIdentifier);\n } catch (e) {\n identifier = \"\";\n }\n container.appendChild(text);\n container.appendChild(createTextContent(\"Contract Identifier\", identifier));\n return container;\n}\n\nconst ACTION_DISPATCH = {\n createAccount: {\n label: \"CreateAccount\",\n warn: false,\n render: function () {\n return createAccountContent();\n },\n },\n deployContract: {\n label: \"DeployContract\",\n warn: true,\n render: function (a) {\n return deployContractContent(a);\n },\n },\n functionCall: {\n label: \"FunctionCall\",\n warn: false,\n render: function (a, opts) {\n return functionCallContent(a, opts);\n },\n },\n transfer: {\n label: \"Transfer\",\n warn: false,\n render: function (a, opts) {\n return transferContent(a, opts);\n },\n },\n stake: {\n label: \"Stake\",\n warn: false,\n render: function (a, opts) {\n return stakeContent(a, opts);\n },\n },\n addKey: {\n label: \"AddKey\",\n warn: false,\n render: function (a) {\n return addKeyContent(a);\n },\n warnsWhen: function (a) {\n return !!(\n a.addKey &&\n a.addKey.accessKey &&\n a.addKey.accessKey.permission &&\n a.addKey.accessKey.permission.fullAccess !== undefined\n );\n },\n },\n deleteKey: {\n label: \"DeleteKey\",\n warn: true,\n render: function (a) {\n return deleteKeyContent(a);\n },\n },\n deleteAccount: {\n label: \"DeleteAccount\",\n warn: true,\n render: function (a) {\n return deleteAccountContent(a);\n },\n },\n signedDelegate: {\n label: \"SignedDelegate\",\n warn: false,\n render: function (a) {\n return signedDelegateContent(a);\n },\n },\n deployGlobalContract: {\n label: \"DeployGlobalContract\",\n warn: true,\n render: function (a) {\n return deployGlobalContractContent(a);\n },\n },\n useGlobalContract: {\n label: \"UseGlobalContract\",\n warn: false,\n render: function (a) {\n return useGlobalContractContent(a);\n },\n },\n};\n\nfunction handleNearAction(action, options) {\n const actionKey = getActionType(action);\n const entry = actionKey ? ACTION_DISPATCH[actionKey] : null;\n if (!entry) {\n const unknown = document.createElement(\"div\");\n unknown.textContent = `Unknown action: ${actionKey || \"(empty)\"}`;\n return createAccordion(\"Unknown\", unknown, true);\n }\n const showWarning = entry.warnsWhen ? entry.warnsWhen(action) : !!entry.warn;\n return createAccordion(entry.label, entry.render(action, options), showWarning);\n}\n\n/**\n * Build the full details DOM tree for a form custom field.\n *\n * @param {object} params\n * @param {Array<{label: string, value: string|undefined}>} params.fields - top-level fields like Signer/Receiver/MaxBlockHeight.\n * @param {string} params.actions - JSON string with the actions array (as Auth0 form fields deliver them).\n * @param {{ showYoctoConversion?: boolean }} [params.options]\n */\nfunction renderDetails(params) {\n ensureBufferPolyfill();\n const box = document.createElement(\"div\");\n box.classList.add(\"box\");\n\n for (const field of params.fields || []) {\n if (field.value === undefined || field.value === null || field.value === \"\") continue;\n box.appendChild(createTextContent(field.label, field.value));\n }\n\n const actionsContainer = document.createElement(\"div\");\n actionsContainer.classList.add(\"actions-container\");\n const actionsLabel = document.createElement(\"div\");\n actionsLabel.classList.add(\"label\");\n actionsLabel.textContent = \"Actions\";\n actionsContainer.appendChild(actionsLabel);\n\n let parsedActions = [];\n try {\n parsedActions = JSON.parse(params.actions || \"[]\");\n } catch (e) {\n const errorNode = document.createElement(\"div\");\n errorNode.classList.add(\"warning-callout\");\n errorNode.textContent = \"Failed to parse actions payload.\";\n actionsContainer.appendChild(errorNode);\n box.appendChild(actionsContainer);\n return box;\n }\n\n for (const action of parsedActions) {\n actionsContainer.appendChild(handleNearAction(action, params.options));\n }\n\n box.appendChild(actionsContainer);\n return box;\n}\n\n// --- NEP-413 intent content factories ---\n\n/**\n * Render the token map of a transfer intent (`{ \"\": \"\" }`).\n *\n * Amounts stay in the token's smallest unit: the form has no token metadata, so converting\n * would mean guessing decimals — and a wrong guess here understates what the user is\n * approving. Showing the raw amount alongside its token id is honest and unambiguous.\n */\nfunction intentTokensContent(tokens) {\n const container = document.createElement(\"div\");\n if (!tokens || typeof tokens !== \"object\") return container;\n for (const tokenId of Object.keys(tokens)) {\n container.appendChild(createTextContent(tokenId, String(tokens[tokenId])));\n }\n return container;\n}\n\nfunction transferIntentContent(intent) {\n const container = document.createElement(\"div\");\n container.appendChild(createDescription(\"By approving this request, the following tokens will be transferred.\"));\n container.appendChild(createTextContent(\"Receiver ID\", intent.receiver_id));\n\n const tokensLabel = document.createElement(\"div\");\n tokensLabel.classList.add(\"label\");\n tokensLabel.textContent = \"Tokens\";\n container.appendChild(tokensLabel);\n container.appendChild(intentTokensContent(intent.tokens));\n return container;\n}\n\n/**\n * Fallback for intent kinds this form does not model explicitly. It is deliberately shown\n * with a warning: the user is approving something the UI cannot describe in plain terms.\n */\nfunction unknownIntentContent(intent) {\n const container = document.createElement(\"div\");\n container.appendChild(createDescription(\"This request contains an intent type this app does not recognize. Review it carefully.\"));\n let serialized;\n try {\n serialized = JSON.stringify(intent, null, 2);\n } catch (e) {\n serialized = \"\";\n }\n container.appendChild(createTextContent(\"Intent\", serialized));\n return container;\n}\n\nconst INTENT_DISPATCH = {\n transfer: {\n label: \"Transfer\",\n warn: false,\n render: function (i) {\n return transferIntentContent(i);\n },\n },\n};\n\nfunction handleIntent(intent) {\n const kind = intent && typeof intent === \"object\" ? intent.intent : null;\n const entry = kind ? INTENT_DISPATCH[kind] : null;\n if (!entry) {\n return createAccordion(kind ? `Unknown: ${kind}` : \"Unknown\", unknownIntentContent(intent), true);\n }\n return createAccordion(entry.label, entry.render(intent), !!entry.warn);\n}\n\n/**\n * Render the message body of a NEP-413 request verbatim.\n *\n * This is the default view, and the one that matters most: NEP-413 messages are arbitrary\n * strings meant to be read by a human (\"Sign in to example.com\"), so showing the text exactly\n * as it will be signed is the honest presentation. Pretty-printing is applied only when the\n * message is JSON, purely for legibility.\n */\nfunction messageContent(message) {\n const container = document.createElement(\"div\");\n let display = message;\n try {\n const parsed = JSON.parse(message);\n if (parsed && typeof parsed === \"object\") display = JSON.stringify(parsed, null, 2);\n } catch (e) {\n display = message;\n }\n container.appendChild(createTextContent(\"Message\", display));\n return container;\n}\n\n/**\n * Build the details DOM tree for a NEP-413 signature approval.\n *\n * Two presentations, one payload: when the message carries NEAR Intents the intents are broken\n * out one by one, and otherwise the raw message is shown as text. Either way the top-level\n * fields — recipient above all — are rendered, because under NEP-413 the recipient is what\n * tells the user which application their signature is addressed to.\n * @param {object} params The render parameters.\n * @param {Array<{label: string, value: string|undefined}>} params.fields Top-level fields (recipient, callback URL, signer, deadline).\n * @param {string} params.message The raw NEP-413 message, shown when there are no intents to break out.\n * @param {string} [params.intents] JSON string with the intents array, when the message is a NEAR Intents body.\n * @returns {HTMLElement} The details node.\n */\nfunction renderNep413Details(params) {\n ensureBufferPolyfill();\n const box = document.createElement(\"div\");\n box.classList.add(\"box\");\n\n for (const field of params.fields || []) {\n if (field.value === undefined || field.value === null || field.value === \"\") continue;\n box.appendChild(createTextContent(field.label, field.value));\n }\n\n let parsedIntents = null;\n if (params.intents) {\n try {\n const candidate = JSON.parse(params.intents);\n if (Array.isArray(candidate) && candidate.length > 0) parsedIntents = candidate;\n } catch (e) {\n parsedIntents = null;\n }\n }\n\n // No intents to break out (or they were unreadable) — show the message itself. Falling back\n // to the raw text keeps every NEP-413 message approvable, not just the ones we model.\n if (!parsedIntents) {\n box.appendChild(messageContent(params.message || \"\"));\n return box;\n }\n\n const intentsContainer = document.createElement(\"div\");\n intentsContainer.classList.add(\"actions-container\");\n const intentsLabel = document.createElement(\"div\");\n intentsLabel.classList.add(\"label\");\n intentsLabel.textContent = \"Intents\";\n intentsContainer.appendChild(intentsLabel);\n\n for (const intent of parsedIntents) {\n intentsContainer.appendChild(handleIntent(intent));\n }\n\n box.appendChild(intentsContainer);\n return box;\n}\n\nvar __auth0FormHelpers = {\n ensureBufferPolyfill: ensureBufferPolyfill,\n base58Encode: base58Encode,\n yoctoToNear: yoctoToNear,\n formatNearAmount: formatNearAmount,\n decodeFunctionCallArgs: decodeFunctionCallArgs,\n formatPublicKey: formatPublicKey,\n getActionType: getActionType,\n createTextContent: createTextContent,\n createDescription: createDescription,\n createAccordion: createAccordion,\n createAccountContent: createAccountContent,\n deployContractContent: deployContractContent,\n functionCallContent: functionCallContent,\n transferContent: transferContent,\n stakeContent: stakeContent,\n addKeyContent: addKeyContent,\n deleteKeyContent: deleteKeyContent,\n deleteAccountContent: deleteAccountContent,\n signedDelegateContent: signedDelegateContent,\n deployGlobalContractContent: deployGlobalContractContent,\n useGlobalContractContent: useGlobalContractContent,\n handleNearAction: handleNearAction,\n renderDetails: renderDetails,\n intentTokensContent: intentTokensContent,\n transferIntentContent: transferIntentContent,\n unknownIntentContent: unknownIntentContent,\n handleIntent: handleIntent,\n messageContent: messageContent,\n renderNep413Details: renderNep413Details,\n};\n\nif (typeof module !== \"undefined\" && module.exports) {\n module.exports = __auth0FormHelpers;\n}\n\n// --- END __auth0FormHelpers ---\n\n\nreturn (\n/**\n * Custom field that renders the delegate action details (sender, receiver, max block height, actions).\n *\n * Depends on `__auth0FormHelpers`, which is inlined at build time by build.js\n * (or shimmed by the playground via helpers-shim.js).\n */\nfunction AuthorizeAppDelegateActionDetails(context) {\n return {\n init: function () {\n const params = context.custom.getParams();\n return __auth0FormHelpers.renderDetails({\n fields: [\n { label: \"Sender ID\", value: params.senderId },\n { label: \"Receiver ID\", value: params.receiverId },\n { label: \"Max Block Height\", value: params.maxBlockHeight },\n ],\n actions: params.actions,\n options: { showYoctoConversion: false },\n });\n },\n getScripts: function () { return []; },\n block: function () {},\n unblock: function () {},\n getValue: function () {},\n };\n}\n\n);\n})()\n", "css": ".box {\n padding: 1.25rem;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 1.25rem;\n width: 100%;\n\n border-radius: 1rem;\n border: 1px solid #e5e5e5;\n}\n\n.text-content {\n display: flex;\n flex-direction: column;\n align-items: left;\n justify-content: left;\n width: 100%;\n}\n\n.label {\n font-size: 0.75rem;\n color: #999999;\n font-weight: 500;\n}\n\n.value {\n color: #3f4246;\n font-size: 0.875rem;\n font-weight: 600;\n}\n\n.json-content {\n display: flex;\n padding: 0.5rem;\n flex-direction: column;\n align-items: left;\n justify-content: left;\n width: 100%;\n border-radius: 12px;\n background-color: #fafafa;\n\n color: #3f4246;\n font-size: 0.875rem;\n font-weight: 500;\n}\n\n.accordion {\n width: 100%;\n border: 1px solid #e5e5e5;\n border-radius: 6px;\n overflow: hidden;\n}\n\n.accordion-header {\n background: #fff;\n padding: 12px 16px;\n cursor: pointer;\n user-select: none;\n}\n\n.accordion-header-content {\n display: flex;\n align-items: center;\n justify-content: space-between;\n width: 100%;\n}\n\n.accordion-left-content {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.accordion-header-label {\n font-size: 0.75rem;\n color: black;\n font-weight: 500;\n}\n\n.warning-icon {\n font-size: 1rem;\n color: #ff4444;\n}\n\n.expand-icon {\n font-size: 1.2rem;\n color: #666;\n font-weight: bold;\n line-height: 1;\n transition: transform 0.2s ease;\n user-select: none;\n min-width: 20px;\n text-align: center;\n}\n\n.accordion-content {\n max-height: 0;\n overflow: hidden;\n font-size: 0.75rem;\n background-color: #fafafa;\n color: #0a0a0a;\n font-weight: 400;\n transition:\n max-height 0.3s ease,\n padding 0.3s ease;\n padding: 0 1rem;\n}\n\n.accordion-content.open {\n padding: 12px 16px;\n max-height: 500px;\n}\n\n.actions-container {\n display: flex;\n flex-direction: column;\n align-items: left;\n justify-content: left;\n width: 100%;\n gap: 0.25rem;\n}\n\n.accordion-content .label {\n font-size: 0.75rem;\n}\n\n.accordion-content .value {\n font-size: 0.75rem;\n white-space: pre-wrap;\n word-break: break-word;\n}\n\n.accordion-content > div {\n display: flex;\n flex-direction: column;\n gap: 0.75rem;\n}\n\n.action-description {\n font-size: 0.75rem;\n color: #3f4246;\n margin: 0;\n}\n\n.warning-callout {\n background-color: #ffebee;\n padding: 16px;\n border-radius: 4px;\n margin: 0;\n color: #c62828;\n}\n", "params": { "actions": "{{ fields.actions }}", @@ -67,7 +67,7 @@ "sensitive": false, "config": { "schema": {}, - "code": "(function () {\n// --- BEGIN __auth0FormHelpers (auto-inlined from shared/helpers/index.js) ---\n/**\n * Shared helpers for Auth0 form custom components.\n *\n * Runs in two environments:\n * - Auth0 form runtime: build.js inlines this file into each component's `config.code`,\n * exposing `__auth0FormHelpers` as a top-level variable in the same scope as the component.\n * - Node (tests / playground via require): consumed as a CommonJS module via module.exports.\n *\n * Keep this file free of `require` and ES imports so it can be concatenated as plain text.\n */\n\nfunction ensureBufferPolyfill() {\n if (typeof Buffer !== \"undefined\") return;\n if (typeof globalThis === \"undefined\") return;\n globalThis.Buffer = {\n from: function (data) {\n if (Array.isArray(data)) return new Uint8Array(data);\n if (typeof data === \"string\") return new TextEncoder().encode(data);\n return data;\n },\n };\n}\n\nconst BASE58_ALPHABET = \"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz\";\n\nfunction base58Encode(bytes) {\n let result = \"\";\n let num = BigInt(0);\n for (const byte of bytes) {\n num = (num << BigInt(8)) + BigInt(byte);\n }\n while (num > BigInt(0)) {\n const remainder = num % BigInt(58);\n num = num / BigInt(58);\n result = BASE58_ALPHABET[Number(remainder)] + result;\n }\n for (const byte of bytes) {\n if (byte === 0) result = BASE58_ALPHABET[0] + result;\n else break;\n }\n return result;\n}\n\n/**\n * Format a yocto-NEAR BigInt string as NEAR with up to 8 decimals.\n * \"1000000000000000000000000\" -> \"1\"\n * \"1500000000000000000000000\" -> \"1.5\"\n * \"1\" -> \"0.00000000\" trimmed -> \"0.0\"\n */\nfunction yoctoToNear(bigIntStr) {\n const trimmed = String(bigIntStr).replace(/^0+/, \"\") || \"0\";\n const length = trimmed.length;\n\n if (length <= 24) {\n const zerosNeeded = 24 - length;\n const withZeros = \"0\".repeat(zerosNeeded) + trimmed;\n const decimals = withZeros.slice(0, 8).replace(/0+$/, \"\") || \"0\";\n return `0.${decimals}`;\n }\n\n const splitPos = length - 24;\n const integerPart = trimmed.slice(0, splitPos);\n const decimalPart = trimmed.slice(splitPos, splitPos + 8);\n const trimmedDecimals = decimalPart.replace(/0+$/, \"\");\n if (trimmedDecimals === \"\") return integerPart;\n return `${integerPart}.${trimmedDecimals}`;\n}\n\nfunction formatPublicKey(publicKey) {\n if (publicKey && publicKey.secp256k1Key !== undefined) {\n return `secp256k1:${base58Encode(publicKey.secp256k1Key.data)}`;\n }\n if (publicKey && publicKey.ed25519Key !== undefined) {\n return `ed25519:${base58Encode(publicKey.ed25519Key.data)}`;\n }\n return \"\";\n}\n\n/**\n * Return the canonical action type discriminator (the single non-undefined key of the action).\n * Useful for dispatch and for tests that don't need DOM rendering.\n */\nfunction getActionType(action) {\n if (!action || typeof action !== \"object\") return null;\n for (const key of Object.keys(action)) {\n if (action[key] !== undefined) return key;\n }\n return null;\n}\n\n// --- DOM helpers (require document/window — jsdom in tests, real DOM in browser) ---\n\nfunction createTextContent(label, value, link = false) {\n const textContent = document.createElement(\"div\");\n textContent.classList.add(\"text-content\");\n\n const labelElement = document.createElement(\"div\");\n labelElement.classList.add(\"label\");\n labelElement.textContent = label;\n\n const valueElement = document.createElement(\"div\");\n valueElement.classList.add(\"value\");\n valueElement.textContent = value;\n\n if (link) {\n valueElement.classList.add(\"link\");\n valueElement.setAttribute(\"href\", link);\n valueElement.setAttribute(\"target\", \"_blank\");\n }\n\n textContent.appendChild(labelElement);\n textContent.appendChild(valueElement);\n return textContent;\n}\n\nfunction createDescription(text) {\n const node = document.createElement(\"p\");\n node.classList.add(\"action-description\");\n node.textContent = text;\n return node;\n}\n\nfunction createAccordion(label, content, showWarning = false) {\n const accordion = document.createElement(\"div\");\n accordion.classList.add(\"accordion\");\n\n const header = document.createElement(\"div\");\n header.classList.add(\"accordion-header\");\n\n const headerContent = document.createElement(\"div\");\n headerContent.classList.add(\"accordion-header-content\");\n\n const leftContent = document.createElement(\"div\");\n leftContent.classList.add(\"accordion-left-content\");\n\n if (showWarning) {\n const warningIcon = document.createElement(\"span\");\n warningIcon.classList.add(\"warning-icon\");\n warningIcon.innerHTML = \"⚠️\";\n leftContent.appendChild(warningIcon);\n }\n\n const labelElement = document.createElement(\"span\");\n labelElement.classList.add(\"accordion-header-label\");\n labelElement.textContent = label;\n leftContent.appendChild(labelElement);\n\n const expandIcon = document.createElement(\"span\");\n expandIcon.classList.add(\"expand-icon\");\n expandIcon.innerHTML = \"+\";\n\n headerContent.appendChild(leftContent);\n headerContent.appendChild(expandIcon);\n header.appendChild(headerContent);\n\n const contentElement = document.createElement(\"div\");\n contentElement.classList.add(\"accordion-content\");\n contentElement.appendChild(content);\n\n accordion.appendChild(header);\n accordion.appendChild(contentElement);\n\n header.addEventListener(\"click\", function () {\n contentElement.classList.toggle(\"open\");\n expandIcon.innerHTML = contentElement.classList.contains(\"open\") ? \"−\" : \"+\";\n });\n\n return accordion;\n}\n\n// --- Action content factories ---\n\nfunction createAccountContent() {\n return createDescription(\"By approving this request, a new account will be created.\");\n}\n\nfunction deployContractContent(action) {\n ensureBufferPolyfill();\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the compiled smart contract will be deployed.\");\n const codeLen = action.deployContract && action.deployContract.code ? `${action.deployContract.code.length} bytes` : null;\n container.appendChild(text);\n container.appendChild(createTextContent(\"Code\", codeLen));\n return container;\n}\n\n/**\n * Decode functionCall `args` (a byte array / Uint8Array) to a readable string. NEAR contract\n * args are almost always UTF-8 JSON, so decode the bytes and pretty-print when they parse as\n * JSON; otherwise fall back to the raw decoded text. TextDecoder is used directly because it\n * exists in the browser and the Auth0 form runtime (unlike Buffer, which the runtime lacks).\n */\nfunction decodeFunctionCallArgs(args) {\n if (args == null) return \"\";\n let text;\n try {\n if (typeof TextDecoder !== \"undefined\") {\n text = new TextDecoder().decode(Uint8Array.from(args));\n } else if (typeof Buffer !== \"undefined\" && typeof Buffer.from === \"function\") {\n text = Buffer.from(args).toString(\"utf8\");\n } else {\n text = String.fromCharCode.apply(null, Array.from(args));\n }\n } catch (e) {\n return \"\";\n }\n try {\n return JSON.stringify(JSON.parse(text), null, 2);\n } catch (e) {\n return text;\n }\n}\n\n/**\n * Format a yocto-NEAR amount for display. In transaction mode (showYoctoConversion) amounts at or\n * above 0.00000000001 NEAR are shown as NEAR; smaller amounts and delegate-action mode stay in\n * yoctoNEAR so no precision is hidden.\n */\nfunction formatNearAmount(value, options) {\n const showYoctoConversion = !!(options && options.showYoctoConversion);\n if (showYoctoConversion && value != null && BigInt(value) >= BigInt(\"10000000000000000\")) {\n return `${yoctoToNear(value.toString())} NEAR`;\n }\n return `${value != null ? value.toString() : \"0\"} yoctoNEAR`;\n}\n\nfunction functionCallContent(action, options) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following function will be called.\");\n\n const fc = action.functionCall || {};\n container.appendChild(text);\n container.appendChild(createTextContent(\"Method Name\", fc.methodName));\n container.appendChild(createTextContent(\"Args\", decodeFunctionCallArgs(fc.args)));\n container.appendChild(createTextContent(\"Gas\", fc.gas != null ? fc.gas.toString() : \"\"));\n container.appendChild(createTextContent(\"Deposit\", formatNearAmount(fc.deposit, options)));\n return container;\n}\n\nfunction transferContent(action, options) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following amount will be transferred to the receiver.\");\n\n const deposit = action.transfer ? action.transfer.deposit : undefined;\n container.appendChild(text);\n container.appendChild(createTextContent(\"Deposit\", formatNearAmount(deposit, options)));\n return container;\n}\n\nfunction stakeContent(action, options) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following amount will be staked to the public key.\");\n const stake = action.stake || {};\n container.appendChild(text);\n container.appendChild(createTextContent(\"Stake\", formatNearAmount(stake.stake, options)));\n container.appendChild(createTextContent(\"Public Key\", formatPublicKey(stake.publicKey)));\n return container;\n}\n\nfunction addKeyContent(action) {\n const container = document.createElement(\"div\");\n const ak = action.addKey || {};\n const accessKey = ak.accessKey || {};\n const permission = accessKey.permission || {};\n\n if (permission.fullAccess !== undefined) {\n const warning = document.createElement(\"div\");\n warning.classList.add(\"warning-callout\");\n warning.textContent = \"This key will have full access to your account. Only approve if you trust the recipient.\";\n container.appendChild(warning);\n }\n\n container.appendChild(createDescription(\"By approving this request, the following access key will be added to your account.\"));\n container.appendChild(createTextContent(\"Public Key\", formatPublicKey(ak.publicKey)));\n container.appendChild(createTextContent(\"Nonce\", accessKey.nonce != null ? accessKey.nonce.toString() : \"\"));\n\n if (permission.fullAccess === undefined) {\n container.appendChild(createTextContent(\"Permission\", JSON.stringify(permission.functionCall, null, 2)));\n }\n return container;\n}\n\nfunction deleteKeyContent(action) {\n const container = document.createElement(\"div\");\n const dk = action.deleteKey || {};\n\n const warning = document.createElement(\"div\");\n warning.classList.add(\"warning-callout\");\n warning.textContent = \"Once deleted, the key cannot be recovered.\";\n\n container.appendChild(warning);\n container.appendChild(\n createDescription(\"This action will delete an access key from your account. Only approve if you trust the recipient.\"),\n );\n container.appendChild(createTextContent(\"Public Key\", formatPublicKey(dk.publicKey)));\n return container;\n}\n\nfunction deleteAccountContent(action) {\n const container = document.createElement(\"div\");\n\n const warning = document.createElement(\"div\");\n warning.classList.add(\"warning-callout\");\n warning.textContent = \"Once deleted, the account cannot be recovered.\";\n\n container.appendChild(warning);\n container.appendChild(createDescription(\"By approving this request, the account will be deleted.\"));\n container.appendChild(createTextContent(\"Beneficiary ID\", action.deleteAccount && action.deleteAccount.beneficiaryId));\n return container;\n}\n\nfunction signedDelegateContent(action) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following delegate action will be executed.\");\n\n const sd = action.signedDelegate || {};\n let serialized;\n try {\n serialized = JSON.stringify(\n sd.delegateAction,\n function (_, value) {\n return typeof value === \"bigint\" ? value.toString() : value;\n },\n 2,\n );\n } catch (e) {\n serialized = \"\";\n }\n container.appendChild(text);\n container.appendChild(createTextContent(\"Delegate Action\", serialized));\n return container;\n}\n\nfunction deployGlobalContractContent(action) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the compiled smart contract will be deployed.\");\n const codeLen =\n action.deployGlobalContract && action.deployGlobalContract.code ? `${action.deployGlobalContract.code.length} bytes` : null;\n container.appendChild(text);\n container.appendChild(createTextContent(\"Code\", codeLen));\n return container;\n}\n\nfunction useGlobalContractContent(action) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following global contract will be used.\");\n let identifier;\n try {\n identifier = JSON.stringify(action.useGlobalContract && action.useGlobalContract.contractIdentifier);\n } catch (e) {\n identifier = \"\";\n }\n container.appendChild(text);\n container.appendChild(createTextContent(\"Contract Identifier\", identifier));\n return container;\n}\n\nconst ACTION_DISPATCH = {\n createAccount: {\n label: \"CreateAccount\",\n warn: false,\n render: function () {\n return createAccountContent();\n },\n },\n deployContract: {\n label: \"DeployContract\",\n warn: true,\n render: function (a) {\n return deployContractContent(a);\n },\n },\n functionCall: {\n label: \"FunctionCall\",\n warn: false,\n render: function (a, opts) {\n return functionCallContent(a, opts);\n },\n },\n transfer: {\n label: \"Transfer\",\n warn: false,\n render: function (a, opts) {\n return transferContent(a, opts);\n },\n },\n stake: {\n label: \"Stake\",\n warn: false,\n render: function (a, opts) {\n return stakeContent(a, opts);\n },\n },\n addKey: {\n label: \"AddKey\",\n warn: false,\n render: function (a) {\n return addKeyContent(a);\n },\n warnsWhen: function (a) {\n return !!(\n a.addKey &&\n a.addKey.accessKey &&\n a.addKey.accessKey.permission &&\n a.addKey.accessKey.permission.fullAccess !== undefined\n );\n },\n },\n deleteKey: {\n label: \"DeleteKey\",\n warn: true,\n render: function (a) {\n return deleteKeyContent(a);\n },\n },\n deleteAccount: {\n label: \"DeleteAccount\",\n warn: true,\n render: function (a) {\n return deleteAccountContent(a);\n },\n },\n signedDelegate: {\n label: \"SignedDelegate\",\n warn: false,\n render: function (a) {\n return signedDelegateContent(a);\n },\n },\n deployGlobalContract: {\n label: \"DeployGlobalContract\",\n warn: true,\n render: function (a) {\n return deployGlobalContractContent(a);\n },\n },\n useGlobalContract: {\n label: \"UseGlobalContract\",\n warn: false,\n render: function (a) {\n return useGlobalContractContent(a);\n },\n },\n};\n\nfunction handleNearAction(action, options) {\n const actionKey = getActionType(action);\n const entry = actionKey ? ACTION_DISPATCH[actionKey] : null;\n if (!entry) {\n const unknown = document.createElement(\"div\");\n unknown.textContent = `Unknown action: ${actionKey || \"(empty)\"}`;\n return createAccordion(\"Unknown\", unknown, true);\n }\n const showWarning = entry.warnsWhen ? entry.warnsWhen(action) : !!entry.warn;\n return createAccordion(entry.label, entry.render(action, options), showWarning);\n}\n\n/**\n * Build the full details DOM tree for a form custom field.\n *\n * @param {object} params\n * @param {Array<{label: string, value: string|undefined}>} params.fields - top-level fields like Signer/Receiver/MaxBlockHeight.\n * @param {string} params.actions - JSON string with the actions array (as Auth0 form fields deliver them).\n * @param {{ showYoctoConversion?: boolean }} [params.options]\n */\nfunction renderDetails(params) {\n ensureBufferPolyfill();\n const box = document.createElement(\"div\");\n box.classList.add(\"box\");\n\n for (const field of params.fields || []) {\n if (field.value === undefined || field.value === null || field.value === \"\") continue;\n box.appendChild(createTextContent(field.label, field.value));\n }\n\n const actionsContainer = document.createElement(\"div\");\n actionsContainer.classList.add(\"actions-container\");\n const actionsLabel = document.createElement(\"div\");\n actionsLabel.classList.add(\"label\");\n actionsLabel.textContent = \"Actions\";\n actionsContainer.appendChild(actionsLabel);\n\n let parsedActions = [];\n try {\n parsedActions = JSON.parse(params.actions || \"[]\");\n } catch (e) {\n const errorNode = document.createElement(\"div\");\n errorNode.classList.add(\"warning-callout\");\n errorNode.textContent = \"Failed to parse actions payload.\";\n actionsContainer.appendChild(errorNode);\n box.appendChild(actionsContainer);\n return box;\n }\n\n for (const action of parsedActions) {\n actionsContainer.appendChild(handleNearAction(action, params.options));\n }\n\n box.appendChild(actionsContainer);\n return box;\n}\n\nvar __auth0FormHelpers = {\n ensureBufferPolyfill: ensureBufferPolyfill,\n base58Encode: base58Encode,\n yoctoToNear: yoctoToNear,\n formatNearAmount: formatNearAmount,\n decodeFunctionCallArgs: decodeFunctionCallArgs,\n formatPublicKey: formatPublicKey,\n getActionType: getActionType,\n createTextContent: createTextContent,\n createDescription: createDescription,\n createAccordion: createAccordion,\n createAccountContent: createAccountContent,\n deployContractContent: deployContractContent,\n functionCallContent: functionCallContent,\n transferContent: transferContent,\n stakeContent: stakeContent,\n addKeyContent: addKeyContent,\n deleteKeyContent: deleteKeyContent,\n deleteAccountContent: deleteAccountContent,\n signedDelegateContent: signedDelegateContent,\n deployGlobalContractContent: deployGlobalContractContent,\n useGlobalContractContent: useGlobalContractContent,\n handleNearAction: handleNearAction,\n renderDetails: renderDetails,\n};\n\nif (typeof module !== \"undefined\" && module.exports) {\n module.exports = __auth0FormHelpers;\n}\n\n// --- END __auth0FormHelpers ---\n\n\nreturn (\n/**\n * Custom field that renders the Approve / Deny buttons for the authorize flow.\n *\n * Auth0 Forms buttons can only navigate — they cannot record a choice. So instead of the\n * native NEXT_BUTTON / PREVIOUS_BUTTON, this field owns both buttons and writes the user's\n * choice into the `decision` hidden field before advancing. The resuming action reads\n * `event.prompt.fields.decision` in onContinuePostLogin and denies access when it is \"denied\".\n *\n * Params (configured per-form in _form_base.json):\n * - approveText: label for the approve button (default \"Approve\")\n * - denyText: label for the deny button (default \"Deny\")\n */\nfunction AuthorizeAppDecision(context) {\n return {\n init: function () {\n const params = context.custom.getParams();\n\n const layout = document.createElement(\"div\");\n layout.classList.add(\"decision-layout\");\n\n const approve = document.createElement(\"button\");\n approve.setAttribute(\"type\", \"button\");\n approve.classList.add(\"decision-button\", \"approve\");\n approve.textContent = params.approveText || \"Approve\";\n approve.addEventListener(\"click\", function () {\n context.form.setHiddenField(\"decision\", \"approved\");\n context.form.goForward();\n });\n\n const deny = document.createElement(\"button\");\n deny.setAttribute(\"type\", \"button\");\n deny.classList.add(\"decision-button\", \"deny\");\n deny.textContent = params.denyText || \"Deny\";\n deny.addEventListener(\"click\", function () {\n context.form.setHiddenField(\"decision\", \"denied\");\n context.form.goForward();\n });\n\n layout.appendChild(approve);\n layout.appendChild(deny);\n return layout;\n },\n getScripts: function () { return []; },\n block: function () {},\n unblock: function () {},\n getValue: function () {},\n };\n}\n\n);\n})()\n", + "code": "(function () {\n// --- BEGIN __auth0FormHelpers (auto-inlined from shared/helpers/index.js) ---\n/**\n * Shared helpers for Auth0 form custom components.\n *\n * Runs in two environments:\n * - Auth0 form runtime: build.js inlines this file into each component's `config.code`,\n * exposing `__auth0FormHelpers` as a top-level variable in the same scope as the component.\n * - Node (tests / playground via require): consumed as a CommonJS module via module.exports.\n *\n * Keep this file free of `require` and ES imports so it can be concatenated as plain text.\n */\n\nfunction ensureBufferPolyfill() {\n if (typeof Buffer !== \"undefined\") return;\n if (typeof globalThis === \"undefined\") return;\n globalThis.Buffer = {\n from: function (data) {\n if (Array.isArray(data)) return new Uint8Array(data);\n if (typeof data === \"string\") return new TextEncoder().encode(data);\n return data;\n },\n };\n}\n\nconst BASE58_ALPHABET = \"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz\";\n\nfunction base58Encode(bytes) {\n let result = \"\";\n let num = BigInt(0);\n for (const byte of bytes) {\n num = (num << BigInt(8)) + BigInt(byte);\n }\n while (num > BigInt(0)) {\n const remainder = num % BigInt(58);\n num = num / BigInt(58);\n result = BASE58_ALPHABET[Number(remainder)] + result;\n }\n for (const byte of bytes) {\n if (byte === 0) result = BASE58_ALPHABET[0] + result;\n else break;\n }\n return result;\n}\n\n/**\n * Format a yocto-NEAR BigInt string as NEAR with up to 8 decimals.\n * \"1000000000000000000000000\" -> \"1\"\n * \"1500000000000000000000000\" -> \"1.5\"\n * \"1\" -> \"0.00000000\" trimmed -> \"0.0\"\n */\nfunction yoctoToNear(bigIntStr) {\n const trimmed = String(bigIntStr).replace(/^0+/, \"\") || \"0\";\n const length = trimmed.length;\n\n if (length <= 24) {\n const zerosNeeded = 24 - length;\n const withZeros = \"0\".repeat(zerosNeeded) + trimmed;\n const decimals = withZeros.slice(0, 8).replace(/0+$/, \"\") || \"0\";\n return `0.${decimals}`;\n }\n\n const splitPos = length - 24;\n const integerPart = trimmed.slice(0, splitPos);\n const decimalPart = trimmed.slice(splitPos, splitPos + 8);\n const trimmedDecimals = decimalPart.replace(/0+$/, \"\");\n if (trimmedDecimals === \"\") return integerPart;\n return `${integerPart}.${trimmedDecimals}`;\n}\n\nfunction formatPublicKey(publicKey) {\n if (publicKey && publicKey.secp256k1Key !== undefined) {\n return `secp256k1:${base58Encode(publicKey.secp256k1Key.data)}`;\n }\n if (publicKey && publicKey.ed25519Key !== undefined) {\n return `ed25519:${base58Encode(publicKey.ed25519Key.data)}`;\n }\n return \"\";\n}\n\n/**\n * Return the canonical action type discriminator (the single non-undefined key of the action).\n * Useful for dispatch and for tests that don't need DOM rendering.\n */\nfunction getActionType(action) {\n if (!action || typeof action !== \"object\") return null;\n for (const key of Object.keys(action)) {\n if (action[key] !== undefined) return key;\n }\n return null;\n}\n\n// --- DOM helpers (require document/window — jsdom in tests, real DOM in browser) ---\n\nfunction createTextContent(label, value, link = false) {\n const textContent = document.createElement(\"div\");\n textContent.classList.add(\"text-content\");\n\n const labelElement = document.createElement(\"div\");\n labelElement.classList.add(\"label\");\n labelElement.textContent = label;\n\n const valueElement = document.createElement(\"div\");\n valueElement.classList.add(\"value\");\n valueElement.textContent = value;\n\n if (link) {\n valueElement.classList.add(\"link\");\n valueElement.setAttribute(\"href\", link);\n valueElement.setAttribute(\"target\", \"_blank\");\n }\n\n textContent.appendChild(labelElement);\n textContent.appendChild(valueElement);\n return textContent;\n}\n\nfunction createDescription(text) {\n const node = document.createElement(\"p\");\n node.classList.add(\"action-description\");\n node.textContent = text;\n return node;\n}\n\nfunction createAccordion(label, content, showWarning = false) {\n const accordion = document.createElement(\"div\");\n accordion.classList.add(\"accordion\");\n\n const header = document.createElement(\"div\");\n header.classList.add(\"accordion-header\");\n\n const headerContent = document.createElement(\"div\");\n headerContent.classList.add(\"accordion-header-content\");\n\n const leftContent = document.createElement(\"div\");\n leftContent.classList.add(\"accordion-left-content\");\n\n if (showWarning) {\n const warningIcon = document.createElement(\"span\");\n warningIcon.classList.add(\"warning-icon\");\n warningIcon.innerHTML = \"⚠️\";\n leftContent.appendChild(warningIcon);\n }\n\n const labelElement = document.createElement(\"span\");\n labelElement.classList.add(\"accordion-header-label\");\n labelElement.textContent = label;\n leftContent.appendChild(labelElement);\n\n const expandIcon = document.createElement(\"span\");\n expandIcon.classList.add(\"expand-icon\");\n expandIcon.innerHTML = \"+\";\n\n headerContent.appendChild(leftContent);\n headerContent.appendChild(expandIcon);\n header.appendChild(headerContent);\n\n const contentElement = document.createElement(\"div\");\n contentElement.classList.add(\"accordion-content\");\n contentElement.appendChild(content);\n\n accordion.appendChild(header);\n accordion.appendChild(contentElement);\n\n header.addEventListener(\"click\", function () {\n contentElement.classList.toggle(\"open\");\n expandIcon.innerHTML = contentElement.classList.contains(\"open\") ? \"−\" : \"+\";\n });\n\n return accordion;\n}\n\n// --- Action content factories ---\n\nfunction createAccountContent() {\n return createDescription(\"By approving this request, a new account will be created.\");\n}\n\nfunction deployContractContent(action) {\n ensureBufferPolyfill();\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the compiled smart contract will be deployed.\");\n const codeLen = action.deployContract && action.deployContract.code ? `${action.deployContract.code.length} bytes` : null;\n container.appendChild(text);\n container.appendChild(createTextContent(\"Code\", codeLen));\n return container;\n}\n\n/**\n * Decode functionCall `args` (a byte array / Uint8Array) to a readable string. NEAR contract\n * args are almost always UTF-8 JSON, so decode the bytes and pretty-print when they parse as\n * JSON; otherwise fall back to the raw decoded text. TextDecoder is used directly because it\n * exists in the browser and the Auth0 form runtime (unlike Buffer, which the runtime lacks).\n */\nfunction decodeFunctionCallArgs(args) {\n if (args == null) return \"\";\n let text;\n try {\n if (typeof TextDecoder !== \"undefined\") {\n text = new TextDecoder().decode(Uint8Array.from(args));\n } else if (typeof Buffer !== \"undefined\" && typeof Buffer.from === \"function\") {\n text = Buffer.from(args).toString(\"utf8\");\n } else {\n text = String.fromCharCode.apply(null, Array.from(args));\n }\n } catch (e) {\n return \"\";\n }\n try {\n return JSON.stringify(JSON.parse(text), null, 2);\n } catch (e) {\n return text;\n }\n}\n\n/**\n * Format a yocto-NEAR amount for display. In transaction mode (showYoctoConversion) amounts at or\n * above 0.00000000001 NEAR are shown as NEAR; smaller amounts and delegate-action mode stay in\n * yoctoNEAR so no precision is hidden.\n */\nfunction formatNearAmount(value, options) {\n const showYoctoConversion = !!(options && options.showYoctoConversion);\n if (showYoctoConversion && value != null && BigInt(value) >= BigInt(\"10000000000000000\")) {\n return `${yoctoToNear(value.toString())} NEAR`;\n }\n return `${value != null ? value.toString() : \"0\"} yoctoNEAR`;\n}\n\nfunction functionCallContent(action, options) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following function will be called.\");\n\n const fc = action.functionCall || {};\n container.appendChild(text);\n container.appendChild(createTextContent(\"Method Name\", fc.methodName));\n container.appendChild(createTextContent(\"Args\", decodeFunctionCallArgs(fc.args)));\n container.appendChild(createTextContent(\"Gas\", fc.gas != null ? fc.gas.toString() : \"\"));\n container.appendChild(createTextContent(\"Deposit\", formatNearAmount(fc.deposit, options)));\n return container;\n}\n\nfunction transferContent(action, options) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following amount will be transferred to the receiver.\");\n\n const deposit = action.transfer ? action.transfer.deposit : undefined;\n container.appendChild(text);\n container.appendChild(createTextContent(\"Deposit\", formatNearAmount(deposit, options)));\n return container;\n}\n\nfunction stakeContent(action, options) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following amount will be staked to the public key.\");\n const stake = action.stake || {};\n container.appendChild(text);\n container.appendChild(createTextContent(\"Stake\", formatNearAmount(stake.stake, options)));\n container.appendChild(createTextContent(\"Public Key\", formatPublicKey(stake.publicKey)));\n return container;\n}\n\nfunction addKeyContent(action) {\n const container = document.createElement(\"div\");\n const ak = action.addKey || {};\n const accessKey = ak.accessKey || {};\n const permission = accessKey.permission || {};\n\n if (permission.fullAccess !== undefined) {\n const warning = document.createElement(\"div\");\n warning.classList.add(\"warning-callout\");\n warning.textContent = \"This key will have full access to your account. Only approve if you trust the recipient.\";\n container.appendChild(warning);\n }\n\n container.appendChild(createDescription(\"By approving this request, the following access key will be added to your account.\"));\n container.appendChild(createTextContent(\"Public Key\", formatPublicKey(ak.publicKey)));\n container.appendChild(createTextContent(\"Nonce\", accessKey.nonce != null ? accessKey.nonce.toString() : \"\"));\n\n if (permission.fullAccess === undefined) {\n container.appendChild(createTextContent(\"Permission\", JSON.stringify(permission.functionCall, null, 2)));\n }\n return container;\n}\n\nfunction deleteKeyContent(action) {\n const container = document.createElement(\"div\");\n const dk = action.deleteKey || {};\n\n const warning = document.createElement(\"div\");\n warning.classList.add(\"warning-callout\");\n warning.textContent = \"Once deleted, the key cannot be recovered.\";\n\n container.appendChild(warning);\n container.appendChild(\n createDescription(\"This action will delete an access key from your account. Only approve if you trust the recipient.\"),\n );\n container.appendChild(createTextContent(\"Public Key\", formatPublicKey(dk.publicKey)));\n return container;\n}\n\nfunction deleteAccountContent(action) {\n const container = document.createElement(\"div\");\n\n const warning = document.createElement(\"div\");\n warning.classList.add(\"warning-callout\");\n warning.textContent = \"Once deleted, the account cannot be recovered.\";\n\n container.appendChild(warning);\n container.appendChild(createDescription(\"By approving this request, the account will be deleted.\"));\n container.appendChild(createTextContent(\"Beneficiary ID\", action.deleteAccount && action.deleteAccount.beneficiaryId));\n return container;\n}\n\nfunction signedDelegateContent(action) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following delegate action will be executed.\");\n\n const sd = action.signedDelegate || {};\n let serialized;\n try {\n serialized = JSON.stringify(\n sd.delegateAction,\n function (_, value) {\n return typeof value === \"bigint\" ? value.toString() : value;\n },\n 2,\n );\n } catch (e) {\n serialized = \"\";\n }\n container.appendChild(text);\n container.appendChild(createTextContent(\"Delegate Action\", serialized));\n return container;\n}\n\nfunction deployGlobalContractContent(action) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the compiled smart contract will be deployed.\");\n const codeLen =\n action.deployGlobalContract && action.deployGlobalContract.code ? `${action.deployGlobalContract.code.length} bytes` : null;\n container.appendChild(text);\n container.appendChild(createTextContent(\"Code\", codeLen));\n return container;\n}\n\nfunction useGlobalContractContent(action) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following global contract will be used.\");\n let identifier;\n try {\n identifier = JSON.stringify(action.useGlobalContract && action.useGlobalContract.contractIdentifier);\n } catch (e) {\n identifier = \"\";\n }\n container.appendChild(text);\n container.appendChild(createTextContent(\"Contract Identifier\", identifier));\n return container;\n}\n\nconst ACTION_DISPATCH = {\n createAccount: {\n label: \"CreateAccount\",\n warn: false,\n render: function () {\n return createAccountContent();\n },\n },\n deployContract: {\n label: \"DeployContract\",\n warn: true,\n render: function (a) {\n return deployContractContent(a);\n },\n },\n functionCall: {\n label: \"FunctionCall\",\n warn: false,\n render: function (a, opts) {\n return functionCallContent(a, opts);\n },\n },\n transfer: {\n label: \"Transfer\",\n warn: false,\n render: function (a, opts) {\n return transferContent(a, opts);\n },\n },\n stake: {\n label: \"Stake\",\n warn: false,\n render: function (a, opts) {\n return stakeContent(a, opts);\n },\n },\n addKey: {\n label: \"AddKey\",\n warn: false,\n render: function (a) {\n return addKeyContent(a);\n },\n warnsWhen: function (a) {\n return !!(\n a.addKey &&\n a.addKey.accessKey &&\n a.addKey.accessKey.permission &&\n a.addKey.accessKey.permission.fullAccess !== undefined\n );\n },\n },\n deleteKey: {\n label: \"DeleteKey\",\n warn: true,\n render: function (a) {\n return deleteKeyContent(a);\n },\n },\n deleteAccount: {\n label: \"DeleteAccount\",\n warn: true,\n render: function (a) {\n return deleteAccountContent(a);\n },\n },\n signedDelegate: {\n label: \"SignedDelegate\",\n warn: false,\n render: function (a) {\n return signedDelegateContent(a);\n },\n },\n deployGlobalContract: {\n label: \"DeployGlobalContract\",\n warn: true,\n render: function (a) {\n return deployGlobalContractContent(a);\n },\n },\n useGlobalContract: {\n label: \"UseGlobalContract\",\n warn: false,\n render: function (a) {\n return useGlobalContractContent(a);\n },\n },\n};\n\nfunction handleNearAction(action, options) {\n const actionKey = getActionType(action);\n const entry = actionKey ? ACTION_DISPATCH[actionKey] : null;\n if (!entry) {\n const unknown = document.createElement(\"div\");\n unknown.textContent = `Unknown action: ${actionKey || \"(empty)\"}`;\n return createAccordion(\"Unknown\", unknown, true);\n }\n const showWarning = entry.warnsWhen ? entry.warnsWhen(action) : !!entry.warn;\n return createAccordion(entry.label, entry.render(action, options), showWarning);\n}\n\n/**\n * Build the full details DOM tree for a form custom field.\n *\n * @param {object} params\n * @param {Array<{label: string, value: string|undefined}>} params.fields - top-level fields like Signer/Receiver/MaxBlockHeight.\n * @param {string} params.actions - JSON string with the actions array (as Auth0 form fields deliver them).\n * @param {{ showYoctoConversion?: boolean }} [params.options]\n */\nfunction renderDetails(params) {\n ensureBufferPolyfill();\n const box = document.createElement(\"div\");\n box.classList.add(\"box\");\n\n for (const field of params.fields || []) {\n if (field.value === undefined || field.value === null || field.value === \"\") continue;\n box.appendChild(createTextContent(field.label, field.value));\n }\n\n const actionsContainer = document.createElement(\"div\");\n actionsContainer.classList.add(\"actions-container\");\n const actionsLabel = document.createElement(\"div\");\n actionsLabel.classList.add(\"label\");\n actionsLabel.textContent = \"Actions\";\n actionsContainer.appendChild(actionsLabel);\n\n let parsedActions = [];\n try {\n parsedActions = JSON.parse(params.actions || \"[]\");\n } catch (e) {\n const errorNode = document.createElement(\"div\");\n errorNode.classList.add(\"warning-callout\");\n errorNode.textContent = \"Failed to parse actions payload.\";\n actionsContainer.appendChild(errorNode);\n box.appendChild(actionsContainer);\n return box;\n }\n\n for (const action of parsedActions) {\n actionsContainer.appendChild(handleNearAction(action, params.options));\n }\n\n box.appendChild(actionsContainer);\n return box;\n}\n\n// --- NEP-413 intent content factories ---\n\n/**\n * Render the token map of a transfer intent (`{ \"\": \"\" }`).\n *\n * Amounts stay in the token's smallest unit: the form has no token metadata, so converting\n * would mean guessing decimals — and a wrong guess here understates what the user is\n * approving. Showing the raw amount alongside its token id is honest and unambiguous.\n */\nfunction intentTokensContent(tokens) {\n const container = document.createElement(\"div\");\n if (!tokens || typeof tokens !== \"object\") return container;\n for (const tokenId of Object.keys(tokens)) {\n container.appendChild(createTextContent(tokenId, String(tokens[tokenId])));\n }\n return container;\n}\n\nfunction transferIntentContent(intent) {\n const container = document.createElement(\"div\");\n container.appendChild(createDescription(\"By approving this request, the following tokens will be transferred.\"));\n container.appendChild(createTextContent(\"Receiver ID\", intent.receiver_id));\n\n const tokensLabel = document.createElement(\"div\");\n tokensLabel.classList.add(\"label\");\n tokensLabel.textContent = \"Tokens\";\n container.appendChild(tokensLabel);\n container.appendChild(intentTokensContent(intent.tokens));\n return container;\n}\n\n/**\n * Fallback for intent kinds this form does not model explicitly. It is deliberately shown\n * with a warning: the user is approving something the UI cannot describe in plain terms.\n */\nfunction unknownIntentContent(intent) {\n const container = document.createElement(\"div\");\n container.appendChild(createDescription(\"This request contains an intent type this app does not recognize. Review it carefully.\"));\n let serialized;\n try {\n serialized = JSON.stringify(intent, null, 2);\n } catch (e) {\n serialized = \"\";\n }\n container.appendChild(createTextContent(\"Intent\", serialized));\n return container;\n}\n\nconst INTENT_DISPATCH = {\n transfer: {\n label: \"Transfer\",\n warn: false,\n render: function (i) {\n return transferIntentContent(i);\n },\n },\n};\n\nfunction handleIntent(intent) {\n const kind = intent && typeof intent === \"object\" ? intent.intent : null;\n const entry = kind ? INTENT_DISPATCH[kind] : null;\n if (!entry) {\n return createAccordion(kind ? `Unknown: ${kind}` : \"Unknown\", unknownIntentContent(intent), true);\n }\n return createAccordion(entry.label, entry.render(intent), !!entry.warn);\n}\n\n/**\n * Render the message body of a NEP-413 request verbatim.\n *\n * This is the default view, and the one that matters most: NEP-413 messages are arbitrary\n * strings meant to be read by a human (\"Sign in to example.com\"), so showing the text exactly\n * as it will be signed is the honest presentation. Pretty-printing is applied only when the\n * message is JSON, purely for legibility.\n */\nfunction messageContent(message) {\n const container = document.createElement(\"div\");\n let display = message;\n try {\n const parsed = JSON.parse(message);\n if (parsed && typeof parsed === \"object\") display = JSON.stringify(parsed, null, 2);\n } catch (e) {\n display = message;\n }\n container.appendChild(createTextContent(\"Message\", display));\n return container;\n}\n\n/**\n * Build the details DOM tree for a NEP-413 signature approval.\n *\n * Two presentations, one payload: when the message carries NEAR Intents the intents are broken\n * out one by one, and otherwise the raw message is shown as text. Either way the top-level\n * fields — recipient above all — are rendered, because under NEP-413 the recipient is what\n * tells the user which application their signature is addressed to.\n * @param {object} params The render parameters.\n * @param {Array<{label: string, value: string|undefined}>} params.fields Top-level fields (recipient, callback URL, signer, deadline).\n * @param {string} params.message The raw NEP-413 message, shown when there are no intents to break out.\n * @param {string} [params.intents] JSON string with the intents array, when the message is a NEAR Intents body.\n * @returns {HTMLElement} The details node.\n */\nfunction renderNep413Details(params) {\n ensureBufferPolyfill();\n const box = document.createElement(\"div\");\n box.classList.add(\"box\");\n\n for (const field of params.fields || []) {\n if (field.value === undefined || field.value === null || field.value === \"\") continue;\n box.appendChild(createTextContent(field.label, field.value));\n }\n\n let parsedIntents = null;\n if (params.intents) {\n try {\n const candidate = JSON.parse(params.intents);\n if (Array.isArray(candidate) && candidate.length > 0) parsedIntents = candidate;\n } catch (e) {\n parsedIntents = null;\n }\n }\n\n // No intents to break out (or they were unreadable) — show the message itself. Falling back\n // to the raw text keeps every NEP-413 message approvable, not just the ones we model.\n if (!parsedIntents) {\n box.appendChild(messageContent(params.message || \"\"));\n return box;\n }\n\n const intentsContainer = document.createElement(\"div\");\n intentsContainer.classList.add(\"actions-container\");\n const intentsLabel = document.createElement(\"div\");\n intentsLabel.classList.add(\"label\");\n intentsLabel.textContent = \"Intents\";\n intentsContainer.appendChild(intentsLabel);\n\n for (const intent of parsedIntents) {\n intentsContainer.appendChild(handleIntent(intent));\n }\n\n box.appendChild(intentsContainer);\n return box;\n}\n\nvar __auth0FormHelpers = {\n ensureBufferPolyfill: ensureBufferPolyfill,\n base58Encode: base58Encode,\n yoctoToNear: yoctoToNear,\n formatNearAmount: formatNearAmount,\n decodeFunctionCallArgs: decodeFunctionCallArgs,\n formatPublicKey: formatPublicKey,\n getActionType: getActionType,\n createTextContent: createTextContent,\n createDescription: createDescription,\n createAccordion: createAccordion,\n createAccountContent: createAccountContent,\n deployContractContent: deployContractContent,\n functionCallContent: functionCallContent,\n transferContent: transferContent,\n stakeContent: stakeContent,\n addKeyContent: addKeyContent,\n deleteKeyContent: deleteKeyContent,\n deleteAccountContent: deleteAccountContent,\n signedDelegateContent: signedDelegateContent,\n deployGlobalContractContent: deployGlobalContractContent,\n useGlobalContractContent: useGlobalContractContent,\n handleNearAction: handleNearAction,\n renderDetails: renderDetails,\n intentTokensContent: intentTokensContent,\n transferIntentContent: transferIntentContent,\n unknownIntentContent: unknownIntentContent,\n handleIntent: handleIntent,\n messageContent: messageContent,\n renderNep413Details: renderNep413Details,\n};\n\nif (typeof module !== \"undefined\" && module.exports) {\n module.exports = __auth0FormHelpers;\n}\n\n// --- END __auth0FormHelpers ---\n\n\nreturn (\n/**\n * Custom field that renders the Approve / Deny buttons for the authorize flow.\n *\n * Auth0 Forms buttons can only navigate — they cannot record a choice. So instead of the\n * native NEXT_BUTTON / PREVIOUS_BUTTON, this field owns both buttons and writes the user's\n * choice into the `decision` hidden field before advancing. The resuming action reads\n * `event.prompt.fields.decision` in onContinuePostLogin and denies access when it is \"denied\".\n *\n * Params (configured per-form in _form_base.json):\n * - approveText: label for the approve button (default \"Approve\")\n * - denyText: label for the deny button (default \"Deny\")\n */\nfunction AuthorizeAppDecision(context) {\n return {\n init: function () {\n const params = context.custom.getParams();\n\n const layout = document.createElement(\"div\");\n layout.classList.add(\"decision-layout\");\n\n const approve = document.createElement(\"button\");\n approve.setAttribute(\"type\", \"button\");\n approve.classList.add(\"decision-button\", \"approve\");\n approve.textContent = params.approveText || \"Approve\";\n approve.addEventListener(\"click\", function () {\n context.form.setHiddenField(\"decision\", \"approved\");\n context.form.goForward();\n });\n\n const deny = document.createElement(\"button\");\n deny.setAttribute(\"type\", \"button\");\n deny.classList.add(\"decision-button\", \"deny\");\n deny.textContent = params.denyText || \"Deny\";\n deny.addEventListener(\"click\", function () {\n context.form.setHiddenField(\"decision\", \"denied\");\n context.form.goForward();\n });\n\n layout.appendChild(approve);\n layout.appendChild(deny);\n return layout;\n },\n getScripts: function () { return []; },\n block: function () {},\n unblock: function () {},\n getValue: function () {},\n };\n}\n\n);\n})()\n", "css": ".decision-layout {\n display: flex;\n flex-direction: column;\n gap: 0.75rem;\n width: 100%;\n}\n\n.decision-button {\n width: 100%;\n padding: 0.75rem 1rem;\n border-radius: 0.75rem;\n font-size: 0.875rem;\n font-weight: 600;\n cursor: pointer;\n border: 1px solid transparent;\n}\n\n.decision-button.approve {\n background-color: #0a0a0a;\n color: #ffffff;\n}\n\n.decision-button.deny {\n background-color: #ffffff;\n color: #3f4246;\n border-color: #e5e5e5;\n}\n", "params": { "approveText": "Approve", diff --git a/packages/auth0/src/forms/nep413/details/index.css b/packages/auth0/src/forms/nep413/details/index.css new file mode 100644 index 00000000..15bf99cd --- /dev/null +++ b/packages/auth0/src/forms/nep413/details/index.css @@ -0,0 +1,153 @@ +.box { + padding: 1.25rem; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 1.25rem; + width: 100%; + + border-radius: 1rem; + border: 1px solid #e5e5e5; +} + +.text-content { + display: flex; + flex-direction: column; + align-items: left; + justify-content: left; + width: 100%; +} + +.label { + font-size: 0.75rem; + color: #999999; + font-weight: 500; +} + +.value { + color: #3f4246; + font-size: 0.875rem; + font-weight: 600; +} + +.json-content { + display: flex; + padding: 0.5rem; + flex-direction: column; + align-items: left; + justify-content: left; + width: 100%; + border-radius: 12px; + background-color: #fafafa; + + color: #3f4246; + font-size: 0.875rem; + font-weight: 500; +} + +.accordion { + width: 100%; + border: 1px solid #e5e5e5; + border-radius: 6px; + overflow: hidden; +} + +.accordion-header { + background: #fff; + padding: 12px 16px; + cursor: pointer; + user-select: none; +} + +.accordion-header-content { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; +} + +.accordion-left-content { + display: flex; + align-items: center; + gap: 8px; +} + +.accordion-header-label { + font-size: 0.75rem; + color: black; + font-weight: 500; +} + +.warning-icon { + font-size: 1rem; + color: #ff4444; +} + +.expand-icon { + font-size: 1.2rem; + color: #666; + font-weight: bold; + line-height: 1; + transition: transform 0.2s ease; + user-select: none; + min-width: 20px; + text-align: center; +} + +.accordion-content { + max-height: 0; + overflow: hidden; + font-size: 0.75rem; + background-color: #fafafa; + color: #0a0a0a; + font-weight: 400; + transition: + max-height 0.3s ease, + padding 0.3s ease; + padding: 0 1rem; +} + +.accordion-content.open { + padding: 12px 16px; + max-height: 500px; +} + +.actions-container { + display: flex; + flex-direction: column; + align-items: left; + justify-content: left; + width: 100%; + gap: 0.25rem; +} + +.accordion-content .label { + font-size: 0.75rem; +} + +.accordion-content .value { + font-size: 0.75rem; + white-space: pre-wrap; + word-break: break-word; +} + +.accordion-content > div { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.action-description { + font-size: 0.75rem; + color: #3f4246; + margin: 0; +} + +.warning-callout { + background-color: #ffebee; + padding: 16px; + border-radius: 4px; + margin: 0; + color: #c62828; +} diff --git a/packages/auth0/src/forms/nep413/details/index.js b/packages/auth0/src/forms/nep413/details/index.js new file mode 100644 index 00000000..f330f0ee --- /dev/null +++ b/packages/auth0/src/forms/nep413/details/index.js @@ -0,0 +1,30 @@ +/** + * Custom field that renders a NEP-413 signature request (recipient, callback URL, message). + * + * When the message carries NEAR Intents the helper breaks them out one by one; otherwise the + * message is shown verbatim, which is what the standard expects of an arbitrary signed string. + * + * Depends on `__auth0FormHelpers`, which is inlined at build time by build.js + * (or shimmed by the playground via helpers-shim.js). + */ +function AuthorizeAppNep413Details(context) { + return { + init: function () { + const params = context.custom.getParams(); + return __auth0FormHelpers.renderNep413Details({ + fields: [ + { label: "Recipient", value: params.recipient }, + { label: "Callback URL", value: params.callbackUrl }, + { label: "Signer ID", value: params.signerId }, + { label: "Deadline", value: params.deadline }, + ], + message: params.message, + intents: params.intents, + }); + }, + getScripts: function () { return []; }, + block: function () {}, + unblock: function () {}, + getValue: function () {}, + }; +} diff --git a/packages/auth0/src/forms/nep413/nep413_form.json b/packages/auth0/src/forms/nep413/nep413_form.json new file mode 100644 index 00000000..fccdb5ab --- /dev/null +++ b/packages/auth0/src/forms/nep413/nep413_form.json @@ -0,0 +1,131 @@ +{ + "version": "4.0.0", + "form": { + "name": "NEP-413 form", + "languages": { + "primary": "en" + }, + "nodes": [ + { + "id": "step_in7K", + "type": "STEP", + "coordinates": { + "x": 463, + "y": -81 + }, + "alias": "New step", + "config": { + "components": [ + { + "id": "custom_4Mqg", + "category": "FIELD", + "type": "CUSTOM", + "required": false, + "sensitive": false, + "config": { + "schema": {}, + "code": "(function () {\n// --- BEGIN __auth0FormHelpers (auto-inlined from shared/helpers/index.js) ---\n/**\n * Shared helpers for Auth0 form custom components.\n *\n * Runs in two environments:\n * - Auth0 form runtime: build.js inlines this file into each component's `config.code`,\n * exposing `__auth0FormHelpers` as a top-level variable in the same scope as the component.\n * - Node (tests / playground via require): consumed as a CommonJS module via module.exports.\n *\n * Keep this file free of `require` and ES imports so it can be concatenated as plain text.\n */\n\nfunction ensureBufferPolyfill() {\n if (typeof Buffer !== \"undefined\") return;\n if (typeof globalThis === \"undefined\") return;\n globalThis.Buffer = {\n from: function (data) {\n if (Array.isArray(data)) return new Uint8Array(data);\n if (typeof data === \"string\") return new TextEncoder().encode(data);\n return data;\n },\n };\n}\n\nconst BASE58_ALPHABET = \"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz\";\n\nfunction base58Encode(bytes) {\n let result = \"\";\n let num = BigInt(0);\n for (const byte of bytes) {\n num = (num << BigInt(8)) + BigInt(byte);\n }\n while (num > BigInt(0)) {\n const remainder = num % BigInt(58);\n num = num / BigInt(58);\n result = BASE58_ALPHABET[Number(remainder)] + result;\n }\n for (const byte of bytes) {\n if (byte === 0) result = BASE58_ALPHABET[0] + result;\n else break;\n }\n return result;\n}\n\n/**\n * Format a yocto-NEAR BigInt string as NEAR with up to 8 decimals.\n * \"1000000000000000000000000\" -> \"1\"\n * \"1500000000000000000000000\" -> \"1.5\"\n * \"1\" -> \"0.00000000\" trimmed -> \"0.0\"\n */\nfunction yoctoToNear(bigIntStr) {\n const trimmed = String(bigIntStr).replace(/^0+/, \"\") || \"0\";\n const length = trimmed.length;\n\n if (length <= 24) {\n const zerosNeeded = 24 - length;\n const withZeros = \"0\".repeat(zerosNeeded) + trimmed;\n const decimals = withZeros.slice(0, 8).replace(/0+$/, \"\") || \"0\";\n return `0.${decimals}`;\n }\n\n const splitPos = length - 24;\n const integerPart = trimmed.slice(0, splitPos);\n const decimalPart = trimmed.slice(splitPos, splitPos + 8);\n const trimmedDecimals = decimalPart.replace(/0+$/, \"\");\n if (trimmedDecimals === \"\") return integerPart;\n return `${integerPart}.${trimmedDecimals}`;\n}\n\nfunction formatPublicKey(publicKey) {\n if (publicKey && publicKey.secp256k1Key !== undefined) {\n return `secp256k1:${base58Encode(publicKey.secp256k1Key.data)}`;\n }\n if (publicKey && publicKey.ed25519Key !== undefined) {\n return `ed25519:${base58Encode(publicKey.ed25519Key.data)}`;\n }\n return \"\";\n}\n\n/**\n * Return the canonical action type discriminator (the single non-undefined key of the action).\n * Useful for dispatch and for tests that don't need DOM rendering.\n */\nfunction getActionType(action) {\n if (!action || typeof action !== \"object\") return null;\n for (const key of Object.keys(action)) {\n if (action[key] !== undefined) return key;\n }\n return null;\n}\n\n// --- DOM helpers (require document/window — jsdom in tests, real DOM in browser) ---\n\nfunction createTextContent(label, value, link = false) {\n const textContent = document.createElement(\"div\");\n textContent.classList.add(\"text-content\");\n\n const labelElement = document.createElement(\"div\");\n labelElement.classList.add(\"label\");\n labelElement.textContent = label;\n\n const valueElement = document.createElement(\"div\");\n valueElement.classList.add(\"value\");\n valueElement.textContent = value;\n\n if (link) {\n valueElement.classList.add(\"link\");\n valueElement.setAttribute(\"href\", link);\n valueElement.setAttribute(\"target\", \"_blank\");\n }\n\n textContent.appendChild(labelElement);\n textContent.appendChild(valueElement);\n return textContent;\n}\n\nfunction createDescription(text) {\n const node = document.createElement(\"p\");\n node.classList.add(\"action-description\");\n node.textContent = text;\n return node;\n}\n\nfunction createAccordion(label, content, showWarning = false) {\n const accordion = document.createElement(\"div\");\n accordion.classList.add(\"accordion\");\n\n const header = document.createElement(\"div\");\n header.classList.add(\"accordion-header\");\n\n const headerContent = document.createElement(\"div\");\n headerContent.classList.add(\"accordion-header-content\");\n\n const leftContent = document.createElement(\"div\");\n leftContent.classList.add(\"accordion-left-content\");\n\n if (showWarning) {\n const warningIcon = document.createElement(\"span\");\n warningIcon.classList.add(\"warning-icon\");\n warningIcon.innerHTML = \"⚠️\";\n leftContent.appendChild(warningIcon);\n }\n\n const labelElement = document.createElement(\"span\");\n labelElement.classList.add(\"accordion-header-label\");\n labelElement.textContent = label;\n leftContent.appendChild(labelElement);\n\n const expandIcon = document.createElement(\"span\");\n expandIcon.classList.add(\"expand-icon\");\n expandIcon.innerHTML = \"+\";\n\n headerContent.appendChild(leftContent);\n headerContent.appendChild(expandIcon);\n header.appendChild(headerContent);\n\n const contentElement = document.createElement(\"div\");\n contentElement.classList.add(\"accordion-content\");\n contentElement.appendChild(content);\n\n accordion.appendChild(header);\n accordion.appendChild(contentElement);\n\n header.addEventListener(\"click\", function () {\n contentElement.classList.toggle(\"open\");\n expandIcon.innerHTML = contentElement.classList.contains(\"open\") ? \"−\" : \"+\";\n });\n\n return accordion;\n}\n\n// --- Action content factories ---\n\nfunction createAccountContent() {\n return createDescription(\"By approving this request, a new account will be created.\");\n}\n\nfunction deployContractContent(action) {\n ensureBufferPolyfill();\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the compiled smart contract will be deployed.\");\n const codeLen = action.deployContract && action.deployContract.code ? `${action.deployContract.code.length} bytes` : null;\n container.appendChild(text);\n container.appendChild(createTextContent(\"Code\", codeLen));\n return container;\n}\n\n/**\n * Decode functionCall `args` (a byte array / Uint8Array) to a readable string. NEAR contract\n * args are almost always UTF-8 JSON, so decode the bytes and pretty-print when they parse as\n * JSON; otherwise fall back to the raw decoded text. TextDecoder is used directly because it\n * exists in the browser and the Auth0 form runtime (unlike Buffer, which the runtime lacks).\n */\nfunction decodeFunctionCallArgs(args) {\n if (args == null) return \"\";\n let text;\n try {\n if (typeof TextDecoder !== \"undefined\") {\n text = new TextDecoder().decode(Uint8Array.from(args));\n } else if (typeof Buffer !== \"undefined\" && typeof Buffer.from === \"function\") {\n text = Buffer.from(args).toString(\"utf8\");\n } else {\n text = String.fromCharCode.apply(null, Array.from(args));\n }\n } catch (e) {\n return \"\";\n }\n try {\n return JSON.stringify(JSON.parse(text), null, 2);\n } catch (e) {\n return text;\n }\n}\n\n/**\n * Format a yocto-NEAR amount for display. In transaction mode (showYoctoConversion) amounts at or\n * above 0.00000000001 NEAR are shown as NEAR; smaller amounts and delegate-action mode stay in\n * yoctoNEAR so no precision is hidden.\n */\nfunction formatNearAmount(value, options) {\n const showYoctoConversion = !!(options && options.showYoctoConversion);\n if (showYoctoConversion && value != null && BigInt(value) >= BigInt(\"10000000000000000\")) {\n return `${yoctoToNear(value.toString())} NEAR`;\n }\n return `${value != null ? value.toString() : \"0\"} yoctoNEAR`;\n}\n\nfunction functionCallContent(action, options) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following function will be called.\");\n\n const fc = action.functionCall || {};\n container.appendChild(text);\n container.appendChild(createTextContent(\"Method Name\", fc.methodName));\n container.appendChild(createTextContent(\"Args\", decodeFunctionCallArgs(fc.args)));\n container.appendChild(createTextContent(\"Gas\", fc.gas != null ? fc.gas.toString() : \"\"));\n container.appendChild(createTextContent(\"Deposit\", formatNearAmount(fc.deposit, options)));\n return container;\n}\n\nfunction transferContent(action, options) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following amount will be transferred to the receiver.\");\n\n const deposit = action.transfer ? action.transfer.deposit : undefined;\n container.appendChild(text);\n container.appendChild(createTextContent(\"Deposit\", formatNearAmount(deposit, options)));\n return container;\n}\n\nfunction stakeContent(action, options) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following amount will be staked to the public key.\");\n const stake = action.stake || {};\n container.appendChild(text);\n container.appendChild(createTextContent(\"Stake\", formatNearAmount(stake.stake, options)));\n container.appendChild(createTextContent(\"Public Key\", formatPublicKey(stake.publicKey)));\n return container;\n}\n\nfunction addKeyContent(action) {\n const container = document.createElement(\"div\");\n const ak = action.addKey || {};\n const accessKey = ak.accessKey || {};\n const permission = accessKey.permission || {};\n\n if (permission.fullAccess !== undefined) {\n const warning = document.createElement(\"div\");\n warning.classList.add(\"warning-callout\");\n warning.textContent = \"This key will have full access to your account. Only approve if you trust the recipient.\";\n container.appendChild(warning);\n }\n\n container.appendChild(createDescription(\"By approving this request, the following access key will be added to your account.\"));\n container.appendChild(createTextContent(\"Public Key\", formatPublicKey(ak.publicKey)));\n container.appendChild(createTextContent(\"Nonce\", accessKey.nonce != null ? accessKey.nonce.toString() : \"\"));\n\n if (permission.fullAccess === undefined) {\n container.appendChild(createTextContent(\"Permission\", JSON.stringify(permission.functionCall, null, 2)));\n }\n return container;\n}\n\nfunction deleteKeyContent(action) {\n const container = document.createElement(\"div\");\n const dk = action.deleteKey || {};\n\n const warning = document.createElement(\"div\");\n warning.classList.add(\"warning-callout\");\n warning.textContent = \"Once deleted, the key cannot be recovered.\";\n\n container.appendChild(warning);\n container.appendChild(\n createDescription(\"This action will delete an access key from your account. Only approve if you trust the recipient.\"),\n );\n container.appendChild(createTextContent(\"Public Key\", formatPublicKey(dk.publicKey)));\n return container;\n}\n\nfunction deleteAccountContent(action) {\n const container = document.createElement(\"div\");\n\n const warning = document.createElement(\"div\");\n warning.classList.add(\"warning-callout\");\n warning.textContent = \"Once deleted, the account cannot be recovered.\";\n\n container.appendChild(warning);\n container.appendChild(createDescription(\"By approving this request, the account will be deleted.\"));\n container.appendChild(createTextContent(\"Beneficiary ID\", action.deleteAccount && action.deleteAccount.beneficiaryId));\n return container;\n}\n\nfunction signedDelegateContent(action) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following delegate action will be executed.\");\n\n const sd = action.signedDelegate || {};\n let serialized;\n try {\n serialized = JSON.stringify(\n sd.delegateAction,\n function (_, value) {\n return typeof value === \"bigint\" ? value.toString() : value;\n },\n 2,\n );\n } catch (e) {\n serialized = \"\";\n }\n container.appendChild(text);\n container.appendChild(createTextContent(\"Delegate Action\", serialized));\n return container;\n}\n\nfunction deployGlobalContractContent(action) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the compiled smart contract will be deployed.\");\n const codeLen =\n action.deployGlobalContract && action.deployGlobalContract.code ? `${action.deployGlobalContract.code.length} bytes` : null;\n container.appendChild(text);\n container.appendChild(createTextContent(\"Code\", codeLen));\n return container;\n}\n\nfunction useGlobalContractContent(action) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following global contract will be used.\");\n let identifier;\n try {\n identifier = JSON.stringify(action.useGlobalContract && action.useGlobalContract.contractIdentifier);\n } catch (e) {\n identifier = \"\";\n }\n container.appendChild(text);\n container.appendChild(createTextContent(\"Contract Identifier\", identifier));\n return container;\n}\n\nconst ACTION_DISPATCH = {\n createAccount: {\n label: \"CreateAccount\",\n warn: false,\n render: function () {\n return createAccountContent();\n },\n },\n deployContract: {\n label: \"DeployContract\",\n warn: true,\n render: function (a) {\n return deployContractContent(a);\n },\n },\n functionCall: {\n label: \"FunctionCall\",\n warn: false,\n render: function (a, opts) {\n return functionCallContent(a, opts);\n },\n },\n transfer: {\n label: \"Transfer\",\n warn: false,\n render: function (a, opts) {\n return transferContent(a, opts);\n },\n },\n stake: {\n label: \"Stake\",\n warn: false,\n render: function (a, opts) {\n return stakeContent(a, opts);\n },\n },\n addKey: {\n label: \"AddKey\",\n warn: false,\n render: function (a) {\n return addKeyContent(a);\n },\n warnsWhen: function (a) {\n return !!(\n a.addKey &&\n a.addKey.accessKey &&\n a.addKey.accessKey.permission &&\n a.addKey.accessKey.permission.fullAccess !== undefined\n );\n },\n },\n deleteKey: {\n label: \"DeleteKey\",\n warn: true,\n render: function (a) {\n return deleteKeyContent(a);\n },\n },\n deleteAccount: {\n label: \"DeleteAccount\",\n warn: true,\n render: function (a) {\n return deleteAccountContent(a);\n },\n },\n signedDelegate: {\n label: \"SignedDelegate\",\n warn: false,\n render: function (a) {\n return signedDelegateContent(a);\n },\n },\n deployGlobalContract: {\n label: \"DeployGlobalContract\",\n warn: true,\n render: function (a) {\n return deployGlobalContractContent(a);\n },\n },\n useGlobalContract: {\n label: \"UseGlobalContract\",\n warn: false,\n render: function (a) {\n return useGlobalContractContent(a);\n },\n },\n};\n\nfunction handleNearAction(action, options) {\n const actionKey = getActionType(action);\n const entry = actionKey ? ACTION_DISPATCH[actionKey] : null;\n if (!entry) {\n const unknown = document.createElement(\"div\");\n unknown.textContent = `Unknown action: ${actionKey || \"(empty)\"}`;\n return createAccordion(\"Unknown\", unknown, true);\n }\n const showWarning = entry.warnsWhen ? entry.warnsWhen(action) : !!entry.warn;\n return createAccordion(entry.label, entry.render(action, options), showWarning);\n}\n\n/**\n * Build the full details DOM tree for a form custom field.\n *\n * @param {object} params\n * @param {Array<{label: string, value: string|undefined}>} params.fields - top-level fields like Signer/Receiver/MaxBlockHeight.\n * @param {string} params.actions - JSON string with the actions array (as Auth0 form fields deliver them).\n * @param {{ showYoctoConversion?: boolean }} [params.options]\n */\nfunction renderDetails(params) {\n ensureBufferPolyfill();\n const box = document.createElement(\"div\");\n box.classList.add(\"box\");\n\n for (const field of params.fields || []) {\n if (field.value === undefined || field.value === null || field.value === \"\") continue;\n box.appendChild(createTextContent(field.label, field.value));\n }\n\n const actionsContainer = document.createElement(\"div\");\n actionsContainer.classList.add(\"actions-container\");\n const actionsLabel = document.createElement(\"div\");\n actionsLabel.classList.add(\"label\");\n actionsLabel.textContent = \"Actions\";\n actionsContainer.appendChild(actionsLabel);\n\n let parsedActions = [];\n try {\n parsedActions = JSON.parse(params.actions || \"[]\");\n } catch (e) {\n const errorNode = document.createElement(\"div\");\n errorNode.classList.add(\"warning-callout\");\n errorNode.textContent = \"Failed to parse actions payload.\";\n actionsContainer.appendChild(errorNode);\n box.appendChild(actionsContainer);\n return box;\n }\n\n for (const action of parsedActions) {\n actionsContainer.appendChild(handleNearAction(action, params.options));\n }\n\n box.appendChild(actionsContainer);\n return box;\n}\n\n// --- NEP-413 intent content factories ---\n\n/**\n * Render the token map of a transfer intent (`{ \"\": \"\" }`).\n *\n * Amounts stay in the token's smallest unit: the form has no token metadata, so converting\n * would mean guessing decimals — and a wrong guess here understates what the user is\n * approving. Showing the raw amount alongside its token id is honest and unambiguous.\n */\nfunction intentTokensContent(tokens) {\n const container = document.createElement(\"div\");\n if (!tokens || typeof tokens !== \"object\") return container;\n for (const tokenId of Object.keys(tokens)) {\n container.appendChild(createTextContent(tokenId, String(tokens[tokenId])));\n }\n return container;\n}\n\nfunction transferIntentContent(intent) {\n const container = document.createElement(\"div\");\n container.appendChild(createDescription(\"By approving this request, the following tokens will be transferred.\"));\n container.appendChild(createTextContent(\"Receiver ID\", intent.receiver_id));\n\n const tokensLabel = document.createElement(\"div\");\n tokensLabel.classList.add(\"label\");\n tokensLabel.textContent = \"Tokens\";\n container.appendChild(tokensLabel);\n container.appendChild(intentTokensContent(intent.tokens));\n return container;\n}\n\n/**\n * Fallback for intent kinds this form does not model explicitly. It is deliberately shown\n * with a warning: the user is approving something the UI cannot describe in plain terms.\n */\nfunction unknownIntentContent(intent) {\n const container = document.createElement(\"div\");\n container.appendChild(createDescription(\"This request contains an intent type this app does not recognize. Review it carefully.\"));\n let serialized;\n try {\n serialized = JSON.stringify(intent, null, 2);\n } catch (e) {\n serialized = \"\";\n }\n container.appendChild(createTextContent(\"Intent\", serialized));\n return container;\n}\n\nconst INTENT_DISPATCH = {\n transfer: {\n label: \"Transfer\",\n warn: false,\n render: function (i) {\n return transferIntentContent(i);\n },\n },\n};\n\nfunction handleIntent(intent) {\n const kind = intent && typeof intent === \"object\" ? intent.intent : null;\n const entry = kind ? INTENT_DISPATCH[kind] : null;\n if (!entry) {\n return createAccordion(kind ? `Unknown: ${kind}` : \"Unknown\", unknownIntentContent(intent), true);\n }\n return createAccordion(entry.label, entry.render(intent), !!entry.warn);\n}\n\n/**\n * Render the message body of a NEP-413 request verbatim.\n *\n * This is the default view, and the one that matters most: NEP-413 messages are arbitrary\n * strings meant to be read by a human (\"Sign in to example.com\"), so showing the text exactly\n * as it will be signed is the honest presentation. Pretty-printing is applied only when the\n * message is JSON, purely for legibility.\n */\nfunction messageContent(message) {\n const container = document.createElement(\"div\");\n let display = message;\n try {\n const parsed = JSON.parse(message);\n if (parsed && typeof parsed === \"object\") display = JSON.stringify(parsed, null, 2);\n } catch (e) {\n display = message;\n }\n container.appendChild(createTextContent(\"Message\", display));\n return container;\n}\n\n/**\n * Build the details DOM tree for a NEP-413 signature approval.\n *\n * Two presentations, one payload: when the message carries NEAR Intents the intents are broken\n * out one by one, and otherwise the raw message is shown as text. Either way the top-level\n * fields — recipient above all — are rendered, because under NEP-413 the recipient is what\n * tells the user which application their signature is addressed to.\n * @param {object} params The render parameters.\n * @param {Array<{label: string, value: string|undefined}>} params.fields Top-level fields (recipient, callback URL, signer, deadline).\n * @param {string} params.message The raw NEP-413 message, shown when there are no intents to break out.\n * @param {string} [params.intents] JSON string with the intents array, when the message is a NEAR Intents body.\n * @returns {HTMLElement} The details node.\n */\nfunction renderNep413Details(params) {\n ensureBufferPolyfill();\n const box = document.createElement(\"div\");\n box.classList.add(\"box\");\n\n for (const field of params.fields || []) {\n if (field.value === undefined || field.value === null || field.value === \"\") continue;\n box.appendChild(createTextContent(field.label, field.value));\n }\n\n let parsedIntents = null;\n if (params.intents) {\n try {\n const candidate = JSON.parse(params.intents);\n if (Array.isArray(candidate) && candidate.length > 0) parsedIntents = candidate;\n } catch (e) {\n parsedIntents = null;\n }\n }\n\n // No intents to break out (or they were unreadable) — show the message itself. Falling back\n // to the raw text keeps every NEP-413 message approvable, not just the ones we model.\n if (!parsedIntents) {\n box.appendChild(messageContent(params.message || \"\"));\n return box;\n }\n\n const intentsContainer = document.createElement(\"div\");\n intentsContainer.classList.add(\"actions-container\");\n const intentsLabel = document.createElement(\"div\");\n intentsLabel.classList.add(\"label\");\n intentsLabel.textContent = \"Intents\";\n intentsContainer.appendChild(intentsLabel);\n\n for (const intent of parsedIntents) {\n intentsContainer.appendChild(handleIntent(intent));\n }\n\n box.appendChild(intentsContainer);\n return box;\n}\n\nvar __auth0FormHelpers = {\n ensureBufferPolyfill: ensureBufferPolyfill,\n base58Encode: base58Encode,\n yoctoToNear: yoctoToNear,\n formatNearAmount: formatNearAmount,\n decodeFunctionCallArgs: decodeFunctionCallArgs,\n formatPublicKey: formatPublicKey,\n getActionType: getActionType,\n createTextContent: createTextContent,\n createDescription: createDescription,\n createAccordion: createAccordion,\n createAccountContent: createAccountContent,\n deployContractContent: deployContractContent,\n functionCallContent: functionCallContent,\n transferContent: transferContent,\n stakeContent: stakeContent,\n addKeyContent: addKeyContent,\n deleteKeyContent: deleteKeyContent,\n deleteAccountContent: deleteAccountContent,\n signedDelegateContent: signedDelegateContent,\n deployGlobalContractContent: deployGlobalContractContent,\n useGlobalContractContent: useGlobalContractContent,\n handleNearAction: handleNearAction,\n renderDetails: renderDetails,\n intentTokensContent: intentTokensContent,\n transferIntentContent: transferIntentContent,\n unknownIntentContent: unknownIntentContent,\n handleIntent: handleIntent,\n messageContent: messageContent,\n renderNep413Details: renderNep413Details,\n};\n\nif (typeof module !== \"undefined\" && module.exports) {\n module.exports = __auth0FormHelpers;\n}\n\n// --- END __auth0FormHelpers ---\n\n\nreturn (\n/**\n * Custom field that renders two app avatars (left = our wallet, right = requesting app)\n * separated by a small circle with a transfer/icon glyph in the middle.\n *\n * Params (configured per-form in _form_base.json):\n * - leftImageUrl: URL for the left avatar (our wallet/identity logo)\n * - rightImageUrl: URL for the right avatar (the requesting app's logo, e.g. {{ fields.imageUrl }})\n * - iconUrl: URL for the center circle icon\n *\n * If `rightImageUrl` is empty/missing (e.g. the client has no logo_uri configured), the\n * right avatar is hidden so we don't render a broken-image placeholder.\n */\nfunction AuthorizeAppImage(context) {\n return {\n /** Invoked once when the field is created */\n init() {\n const layout = document.createElement(\"div\");\n layout.classList.add(\"layout\");\n\n const { leftImageUrl, rightImageUrl, iconUrl } = context.custom.getParams();\n\n const leftAvatar = document.createElement(\"img\");\n leftAvatar.classList.add(\"avatar\");\n leftAvatar.setAttribute(\"alt\", \"\");\n leftAvatar.setAttribute(\"src\", leftImageUrl);\n\n layout.appendChild(leftAvatar);\n\n const trimmedRight = (rightImageUrl || \"\").trim();\n if (trimmedRight) {\n const circle = document.createElement(\"div\");\n circle.classList.add(\"circle\");\n\n const icon = document.createElement(\"img\");\n icon.classList.add(\"icon\");\n icon.setAttribute(\"alt\", \"\");\n icon.setAttribute(\"src\", iconUrl);\n circle.appendChild(icon);\n\n const rightAvatar = document.createElement(\"img\");\n rightAvatar.classList.add(\"avatar\");\n rightAvatar.setAttribute(\"alt\", \"\");\n rightAvatar.setAttribute(\"src\", trimmedRight);\n\n layout.appendChild(circle);\n layout.appendChild(rightAvatar);\n }\n\n return layout;\n },\n\n /** Returns a list of URLs that the SDK guarantees that will loaded before init() is invoked */\n getScripts() {\n return [];\n },\n\n /** Invoked when field has to be blocked */\n block() {},\n\n /** Invoked when field has to be unblocked */\n unblock() {},\n\n /** Invoked when the SDK needs to get the value (possibly several times) */\n getValue() {},\n };\n}\n\n);\n})()\n", + "css": ".avatar {\n width: 4.5rem;\n height: 4.5rem;\n border-radius: 12px;\n object-fit: cover;\n}\n\n.layout {\n position: relative;\n display: flex;\n flex-direction: row;\n gap: 1.5rem;\n align-items: center;\n justify-content: center;\n}\n\n.circle {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n width: 2.5rem;\n height: 2.5rem;\n border-radius: 999px;\n background-color: #f6f6f6;\n z-index: 1;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.icon {\n width: 1.2rem;\n height: 1.2rem;\n}\n", + "params": { + "iconUrl": "https://peersyst-public-production.s3.eu-west-1.amazonaws.com/1b54f479-7990-4834-9b37-95b26e2023fb.png", + "leftImageUrl": "https://peersyst-public-production.s3.eu-west-1.amazonaws.com/db9f38ff-53ea-4f76-a1c0-33c728386e5b.png", + "rightImageUrl": "{{ fields.imageUrl }}" + } + } + }, + { + "id": "rich_text_iN9x", + "category": "BLOCK", + "type": "RICH_TEXT", + "config": { + "content": "

{{ fields.name }} wants you to sign a message

" + } + }, + { + "id": "custom_I420", + "category": "FIELD", + "type": "CUSTOM", + "required": false, + "sensitive": false, + "config": { + "schema": {}, + "code": "(function () {\n// --- BEGIN __auth0FormHelpers (auto-inlined from shared/helpers/index.js) ---\n/**\n * Shared helpers for Auth0 form custom components.\n *\n * Runs in two environments:\n * - Auth0 form runtime: build.js inlines this file into each component's `config.code`,\n * exposing `__auth0FormHelpers` as a top-level variable in the same scope as the component.\n * - Node (tests / playground via require): consumed as a CommonJS module via module.exports.\n *\n * Keep this file free of `require` and ES imports so it can be concatenated as plain text.\n */\n\nfunction ensureBufferPolyfill() {\n if (typeof Buffer !== \"undefined\") return;\n if (typeof globalThis === \"undefined\") return;\n globalThis.Buffer = {\n from: function (data) {\n if (Array.isArray(data)) return new Uint8Array(data);\n if (typeof data === \"string\") return new TextEncoder().encode(data);\n return data;\n },\n };\n}\n\nconst BASE58_ALPHABET = \"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz\";\n\nfunction base58Encode(bytes) {\n let result = \"\";\n let num = BigInt(0);\n for (const byte of bytes) {\n num = (num << BigInt(8)) + BigInt(byte);\n }\n while (num > BigInt(0)) {\n const remainder = num % BigInt(58);\n num = num / BigInt(58);\n result = BASE58_ALPHABET[Number(remainder)] + result;\n }\n for (const byte of bytes) {\n if (byte === 0) result = BASE58_ALPHABET[0] + result;\n else break;\n }\n return result;\n}\n\n/**\n * Format a yocto-NEAR BigInt string as NEAR with up to 8 decimals.\n * \"1000000000000000000000000\" -> \"1\"\n * \"1500000000000000000000000\" -> \"1.5\"\n * \"1\" -> \"0.00000000\" trimmed -> \"0.0\"\n */\nfunction yoctoToNear(bigIntStr) {\n const trimmed = String(bigIntStr).replace(/^0+/, \"\") || \"0\";\n const length = trimmed.length;\n\n if (length <= 24) {\n const zerosNeeded = 24 - length;\n const withZeros = \"0\".repeat(zerosNeeded) + trimmed;\n const decimals = withZeros.slice(0, 8).replace(/0+$/, \"\") || \"0\";\n return `0.${decimals}`;\n }\n\n const splitPos = length - 24;\n const integerPart = trimmed.slice(0, splitPos);\n const decimalPart = trimmed.slice(splitPos, splitPos + 8);\n const trimmedDecimals = decimalPart.replace(/0+$/, \"\");\n if (trimmedDecimals === \"\") return integerPart;\n return `${integerPart}.${trimmedDecimals}`;\n}\n\nfunction formatPublicKey(publicKey) {\n if (publicKey && publicKey.secp256k1Key !== undefined) {\n return `secp256k1:${base58Encode(publicKey.secp256k1Key.data)}`;\n }\n if (publicKey && publicKey.ed25519Key !== undefined) {\n return `ed25519:${base58Encode(publicKey.ed25519Key.data)}`;\n }\n return \"\";\n}\n\n/**\n * Return the canonical action type discriminator (the single non-undefined key of the action).\n * Useful for dispatch and for tests that don't need DOM rendering.\n */\nfunction getActionType(action) {\n if (!action || typeof action !== \"object\") return null;\n for (const key of Object.keys(action)) {\n if (action[key] !== undefined) return key;\n }\n return null;\n}\n\n// --- DOM helpers (require document/window — jsdom in tests, real DOM in browser) ---\n\nfunction createTextContent(label, value, link = false) {\n const textContent = document.createElement(\"div\");\n textContent.classList.add(\"text-content\");\n\n const labelElement = document.createElement(\"div\");\n labelElement.classList.add(\"label\");\n labelElement.textContent = label;\n\n const valueElement = document.createElement(\"div\");\n valueElement.classList.add(\"value\");\n valueElement.textContent = value;\n\n if (link) {\n valueElement.classList.add(\"link\");\n valueElement.setAttribute(\"href\", link);\n valueElement.setAttribute(\"target\", \"_blank\");\n }\n\n textContent.appendChild(labelElement);\n textContent.appendChild(valueElement);\n return textContent;\n}\n\nfunction createDescription(text) {\n const node = document.createElement(\"p\");\n node.classList.add(\"action-description\");\n node.textContent = text;\n return node;\n}\n\nfunction createAccordion(label, content, showWarning = false) {\n const accordion = document.createElement(\"div\");\n accordion.classList.add(\"accordion\");\n\n const header = document.createElement(\"div\");\n header.classList.add(\"accordion-header\");\n\n const headerContent = document.createElement(\"div\");\n headerContent.classList.add(\"accordion-header-content\");\n\n const leftContent = document.createElement(\"div\");\n leftContent.classList.add(\"accordion-left-content\");\n\n if (showWarning) {\n const warningIcon = document.createElement(\"span\");\n warningIcon.classList.add(\"warning-icon\");\n warningIcon.innerHTML = \"⚠️\";\n leftContent.appendChild(warningIcon);\n }\n\n const labelElement = document.createElement(\"span\");\n labelElement.classList.add(\"accordion-header-label\");\n labelElement.textContent = label;\n leftContent.appendChild(labelElement);\n\n const expandIcon = document.createElement(\"span\");\n expandIcon.classList.add(\"expand-icon\");\n expandIcon.innerHTML = \"+\";\n\n headerContent.appendChild(leftContent);\n headerContent.appendChild(expandIcon);\n header.appendChild(headerContent);\n\n const contentElement = document.createElement(\"div\");\n contentElement.classList.add(\"accordion-content\");\n contentElement.appendChild(content);\n\n accordion.appendChild(header);\n accordion.appendChild(contentElement);\n\n header.addEventListener(\"click\", function () {\n contentElement.classList.toggle(\"open\");\n expandIcon.innerHTML = contentElement.classList.contains(\"open\") ? \"−\" : \"+\";\n });\n\n return accordion;\n}\n\n// --- Action content factories ---\n\nfunction createAccountContent() {\n return createDescription(\"By approving this request, a new account will be created.\");\n}\n\nfunction deployContractContent(action) {\n ensureBufferPolyfill();\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the compiled smart contract will be deployed.\");\n const codeLen = action.deployContract && action.deployContract.code ? `${action.deployContract.code.length} bytes` : null;\n container.appendChild(text);\n container.appendChild(createTextContent(\"Code\", codeLen));\n return container;\n}\n\n/**\n * Decode functionCall `args` (a byte array / Uint8Array) to a readable string. NEAR contract\n * args are almost always UTF-8 JSON, so decode the bytes and pretty-print when they parse as\n * JSON; otherwise fall back to the raw decoded text. TextDecoder is used directly because it\n * exists in the browser and the Auth0 form runtime (unlike Buffer, which the runtime lacks).\n */\nfunction decodeFunctionCallArgs(args) {\n if (args == null) return \"\";\n let text;\n try {\n if (typeof TextDecoder !== \"undefined\") {\n text = new TextDecoder().decode(Uint8Array.from(args));\n } else if (typeof Buffer !== \"undefined\" && typeof Buffer.from === \"function\") {\n text = Buffer.from(args).toString(\"utf8\");\n } else {\n text = String.fromCharCode.apply(null, Array.from(args));\n }\n } catch (e) {\n return \"\";\n }\n try {\n return JSON.stringify(JSON.parse(text), null, 2);\n } catch (e) {\n return text;\n }\n}\n\n/**\n * Format a yocto-NEAR amount for display. In transaction mode (showYoctoConversion) amounts at or\n * above 0.00000000001 NEAR are shown as NEAR; smaller amounts and delegate-action mode stay in\n * yoctoNEAR so no precision is hidden.\n */\nfunction formatNearAmount(value, options) {\n const showYoctoConversion = !!(options && options.showYoctoConversion);\n if (showYoctoConversion && value != null && BigInt(value) >= BigInt(\"10000000000000000\")) {\n return `${yoctoToNear(value.toString())} NEAR`;\n }\n return `${value != null ? value.toString() : \"0\"} yoctoNEAR`;\n}\n\nfunction functionCallContent(action, options) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following function will be called.\");\n\n const fc = action.functionCall || {};\n container.appendChild(text);\n container.appendChild(createTextContent(\"Method Name\", fc.methodName));\n container.appendChild(createTextContent(\"Args\", decodeFunctionCallArgs(fc.args)));\n container.appendChild(createTextContent(\"Gas\", fc.gas != null ? fc.gas.toString() : \"\"));\n container.appendChild(createTextContent(\"Deposit\", formatNearAmount(fc.deposit, options)));\n return container;\n}\n\nfunction transferContent(action, options) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following amount will be transferred to the receiver.\");\n\n const deposit = action.transfer ? action.transfer.deposit : undefined;\n container.appendChild(text);\n container.appendChild(createTextContent(\"Deposit\", formatNearAmount(deposit, options)));\n return container;\n}\n\nfunction stakeContent(action, options) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following amount will be staked to the public key.\");\n const stake = action.stake || {};\n container.appendChild(text);\n container.appendChild(createTextContent(\"Stake\", formatNearAmount(stake.stake, options)));\n container.appendChild(createTextContent(\"Public Key\", formatPublicKey(stake.publicKey)));\n return container;\n}\n\nfunction addKeyContent(action) {\n const container = document.createElement(\"div\");\n const ak = action.addKey || {};\n const accessKey = ak.accessKey || {};\n const permission = accessKey.permission || {};\n\n if (permission.fullAccess !== undefined) {\n const warning = document.createElement(\"div\");\n warning.classList.add(\"warning-callout\");\n warning.textContent = \"This key will have full access to your account. Only approve if you trust the recipient.\";\n container.appendChild(warning);\n }\n\n container.appendChild(createDescription(\"By approving this request, the following access key will be added to your account.\"));\n container.appendChild(createTextContent(\"Public Key\", formatPublicKey(ak.publicKey)));\n container.appendChild(createTextContent(\"Nonce\", accessKey.nonce != null ? accessKey.nonce.toString() : \"\"));\n\n if (permission.fullAccess === undefined) {\n container.appendChild(createTextContent(\"Permission\", JSON.stringify(permission.functionCall, null, 2)));\n }\n return container;\n}\n\nfunction deleteKeyContent(action) {\n const container = document.createElement(\"div\");\n const dk = action.deleteKey || {};\n\n const warning = document.createElement(\"div\");\n warning.classList.add(\"warning-callout\");\n warning.textContent = \"Once deleted, the key cannot be recovered.\";\n\n container.appendChild(warning);\n container.appendChild(\n createDescription(\"This action will delete an access key from your account. Only approve if you trust the recipient.\"),\n );\n container.appendChild(createTextContent(\"Public Key\", formatPublicKey(dk.publicKey)));\n return container;\n}\n\nfunction deleteAccountContent(action) {\n const container = document.createElement(\"div\");\n\n const warning = document.createElement(\"div\");\n warning.classList.add(\"warning-callout\");\n warning.textContent = \"Once deleted, the account cannot be recovered.\";\n\n container.appendChild(warning);\n container.appendChild(createDescription(\"By approving this request, the account will be deleted.\"));\n container.appendChild(createTextContent(\"Beneficiary ID\", action.deleteAccount && action.deleteAccount.beneficiaryId));\n return container;\n}\n\nfunction signedDelegateContent(action) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following delegate action will be executed.\");\n\n const sd = action.signedDelegate || {};\n let serialized;\n try {\n serialized = JSON.stringify(\n sd.delegateAction,\n function (_, value) {\n return typeof value === \"bigint\" ? value.toString() : value;\n },\n 2,\n );\n } catch (e) {\n serialized = \"\";\n }\n container.appendChild(text);\n container.appendChild(createTextContent(\"Delegate Action\", serialized));\n return container;\n}\n\nfunction deployGlobalContractContent(action) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the compiled smart contract will be deployed.\");\n const codeLen =\n action.deployGlobalContract && action.deployGlobalContract.code ? `${action.deployGlobalContract.code.length} bytes` : null;\n container.appendChild(text);\n container.appendChild(createTextContent(\"Code\", codeLen));\n return container;\n}\n\nfunction useGlobalContractContent(action) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following global contract will be used.\");\n let identifier;\n try {\n identifier = JSON.stringify(action.useGlobalContract && action.useGlobalContract.contractIdentifier);\n } catch (e) {\n identifier = \"\";\n }\n container.appendChild(text);\n container.appendChild(createTextContent(\"Contract Identifier\", identifier));\n return container;\n}\n\nconst ACTION_DISPATCH = {\n createAccount: {\n label: \"CreateAccount\",\n warn: false,\n render: function () {\n return createAccountContent();\n },\n },\n deployContract: {\n label: \"DeployContract\",\n warn: true,\n render: function (a) {\n return deployContractContent(a);\n },\n },\n functionCall: {\n label: \"FunctionCall\",\n warn: false,\n render: function (a, opts) {\n return functionCallContent(a, opts);\n },\n },\n transfer: {\n label: \"Transfer\",\n warn: false,\n render: function (a, opts) {\n return transferContent(a, opts);\n },\n },\n stake: {\n label: \"Stake\",\n warn: false,\n render: function (a, opts) {\n return stakeContent(a, opts);\n },\n },\n addKey: {\n label: \"AddKey\",\n warn: false,\n render: function (a) {\n return addKeyContent(a);\n },\n warnsWhen: function (a) {\n return !!(\n a.addKey &&\n a.addKey.accessKey &&\n a.addKey.accessKey.permission &&\n a.addKey.accessKey.permission.fullAccess !== undefined\n );\n },\n },\n deleteKey: {\n label: \"DeleteKey\",\n warn: true,\n render: function (a) {\n return deleteKeyContent(a);\n },\n },\n deleteAccount: {\n label: \"DeleteAccount\",\n warn: true,\n render: function (a) {\n return deleteAccountContent(a);\n },\n },\n signedDelegate: {\n label: \"SignedDelegate\",\n warn: false,\n render: function (a) {\n return signedDelegateContent(a);\n },\n },\n deployGlobalContract: {\n label: \"DeployGlobalContract\",\n warn: true,\n render: function (a) {\n return deployGlobalContractContent(a);\n },\n },\n useGlobalContract: {\n label: \"UseGlobalContract\",\n warn: false,\n render: function (a) {\n return useGlobalContractContent(a);\n },\n },\n};\n\nfunction handleNearAction(action, options) {\n const actionKey = getActionType(action);\n const entry = actionKey ? ACTION_DISPATCH[actionKey] : null;\n if (!entry) {\n const unknown = document.createElement(\"div\");\n unknown.textContent = `Unknown action: ${actionKey || \"(empty)\"}`;\n return createAccordion(\"Unknown\", unknown, true);\n }\n const showWarning = entry.warnsWhen ? entry.warnsWhen(action) : !!entry.warn;\n return createAccordion(entry.label, entry.render(action, options), showWarning);\n}\n\n/**\n * Build the full details DOM tree for a form custom field.\n *\n * @param {object} params\n * @param {Array<{label: string, value: string|undefined}>} params.fields - top-level fields like Signer/Receiver/MaxBlockHeight.\n * @param {string} params.actions - JSON string with the actions array (as Auth0 form fields deliver them).\n * @param {{ showYoctoConversion?: boolean }} [params.options]\n */\nfunction renderDetails(params) {\n ensureBufferPolyfill();\n const box = document.createElement(\"div\");\n box.classList.add(\"box\");\n\n for (const field of params.fields || []) {\n if (field.value === undefined || field.value === null || field.value === \"\") continue;\n box.appendChild(createTextContent(field.label, field.value));\n }\n\n const actionsContainer = document.createElement(\"div\");\n actionsContainer.classList.add(\"actions-container\");\n const actionsLabel = document.createElement(\"div\");\n actionsLabel.classList.add(\"label\");\n actionsLabel.textContent = \"Actions\";\n actionsContainer.appendChild(actionsLabel);\n\n let parsedActions = [];\n try {\n parsedActions = JSON.parse(params.actions || \"[]\");\n } catch (e) {\n const errorNode = document.createElement(\"div\");\n errorNode.classList.add(\"warning-callout\");\n errorNode.textContent = \"Failed to parse actions payload.\";\n actionsContainer.appendChild(errorNode);\n box.appendChild(actionsContainer);\n return box;\n }\n\n for (const action of parsedActions) {\n actionsContainer.appendChild(handleNearAction(action, params.options));\n }\n\n box.appendChild(actionsContainer);\n return box;\n}\n\n// --- NEP-413 intent content factories ---\n\n/**\n * Render the token map of a transfer intent (`{ \"\": \"\" }`).\n *\n * Amounts stay in the token's smallest unit: the form has no token metadata, so converting\n * would mean guessing decimals — and a wrong guess here understates what the user is\n * approving. Showing the raw amount alongside its token id is honest and unambiguous.\n */\nfunction intentTokensContent(tokens) {\n const container = document.createElement(\"div\");\n if (!tokens || typeof tokens !== \"object\") return container;\n for (const tokenId of Object.keys(tokens)) {\n container.appendChild(createTextContent(tokenId, String(tokens[tokenId])));\n }\n return container;\n}\n\nfunction transferIntentContent(intent) {\n const container = document.createElement(\"div\");\n container.appendChild(createDescription(\"By approving this request, the following tokens will be transferred.\"));\n container.appendChild(createTextContent(\"Receiver ID\", intent.receiver_id));\n\n const tokensLabel = document.createElement(\"div\");\n tokensLabel.classList.add(\"label\");\n tokensLabel.textContent = \"Tokens\";\n container.appendChild(tokensLabel);\n container.appendChild(intentTokensContent(intent.tokens));\n return container;\n}\n\n/**\n * Fallback for intent kinds this form does not model explicitly. It is deliberately shown\n * with a warning: the user is approving something the UI cannot describe in plain terms.\n */\nfunction unknownIntentContent(intent) {\n const container = document.createElement(\"div\");\n container.appendChild(createDescription(\"This request contains an intent type this app does not recognize. Review it carefully.\"));\n let serialized;\n try {\n serialized = JSON.stringify(intent, null, 2);\n } catch (e) {\n serialized = \"\";\n }\n container.appendChild(createTextContent(\"Intent\", serialized));\n return container;\n}\n\nconst INTENT_DISPATCH = {\n transfer: {\n label: \"Transfer\",\n warn: false,\n render: function (i) {\n return transferIntentContent(i);\n },\n },\n};\n\nfunction handleIntent(intent) {\n const kind = intent && typeof intent === \"object\" ? intent.intent : null;\n const entry = kind ? INTENT_DISPATCH[kind] : null;\n if (!entry) {\n return createAccordion(kind ? `Unknown: ${kind}` : \"Unknown\", unknownIntentContent(intent), true);\n }\n return createAccordion(entry.label, entry.render(intent), !!entry.warn);\n}\n\n/**\n * Render the message body of a NEP-413 request verbatim.\n *\n * This is the default view, and the one that matters most: NEP-413 messages are arbitrary\n * strings meant to be read by a human (\"Sign in to example.com\"), so showing the text exactly\n * as it will be signed is the honest presentation. Pretty-printing is applied only when the\n * message is JSON, purely for legibility.\n */\nfunction messageContent(message) {\n const container = document.createElement(\"div\");\n let display = message;\n try {\n const parsed = JSON.parse(message);\n if (parsed && typeof parsed === \"object\") display = JSON.stringify(parsed, null, 2);\n } catch (e) {\n display = message;\n }\n container.appendChild(createTextContent(\"Message\", display));\n return container;\n}\n\n/**\n * Build the details DOM tree for a NEP-413 signature approval.\n *\n * Two presentations, one payload: when the message carries NEAR Intents the intents are broken\n * out one by one, and otherwise the raw message is shown as text. Either way the top-level\n * fields — recipient above all — are rendered, because under NEP-413 the recipient is what\n * tells the user which application their signature is addressed to.\n * @param {object} params The render parameters.\n * @param {Array<{label: string, value: string|undefined}>} params.fields Top-level fields (recipient, callback URL, signer, deadline).\n * @param {string} params.message The raw NEP-413 message, shown when there are no intents to break out.\n * @param {string} [params.intents] JSON string with the intents array, when the message is a NEAR Intents body.\n * @returns {HTMLElement} The details node.\n */\nfunction renderNep413Details(params) {\n ensureBufferPolyfill();\n const box = document.createElement(\"div\");\n box.classList.add(\"box\");\n\n for (const field of params.fields || []) {\n if (field.value === undefined || field.value === null || field.value === \"\") continue;\n box.appendChild(createTextContent(field.label, field.value));\n }\n\n let parsedIntents = null;\n if (params.intents) {\n try {\n const candidate = JSON.parse(params.intents);\n if (Array.isArray(candidate) && candidate.length > 0) parsedIntents = candidate;\n } catch (e) {\n parsedIntents = null;\n }\n }\n\n // No intents to break out (or they were unreadable) — show the message itself. Falling back\n // to the raw text keeps every NEP-413 message approvable, not just the ones we model.\n if (!parsedIntents) {\n box.appendChild(messageContent(params.message || \"\"));\n return box;\n }\n\n const intentsContainer = document.createElement(\"div\");\n intentsContainer.classList.add(\"actions-container\");\n const intentsLabel = document.createElement(\"div\");\n intentsLabel.classList.add(\"label\");\n intentsLabel.textContent = \"Intents\";\n intentsContainer.appendChild(intentsLabel);\n\n for (const intent of parsedIntents) {\n intentsContainer.appendChild(handleIntent(intent));\n }\n\n box.appendChild(intentsContainer);\n return box;\n}\n\nvar __auth0FormHelpers = {\n ensureBufferPolyfill: ensureBufferPolyfill,\n base58Encode: base58Encode,\n yoctoToNear: yoctoToNear,\n formatNearAmount: formatNearAmount,\n decodeFunctionCallArgs: decodeFunctionCallArgs,\n formatPublicKey: formatPublicKey,\n getActionType: getActionType,\n createTextContent: createTextContent,\n createDescription: createDescription,\n createAccordion: createAccordion,\n createAccountContent: createAccountContent,\n deployContractContent: deployContractContent,\n functionCallContent: functionCallContent,\n transferContent: transferContent,\n stakeContent: stakeContent,\n addKeyContent: addKeyContent,\n deleteKeyContent: deleteKeyContent,\n deleteAccountContent: deleteAccountContent,\n signedDelegateContent: signedDelegateContent,\n deployGlobalContractContent: deployGlobalContractContent,\n useGlobalContractContent: useGlobalContractContent,\n handleNearAction: handleNearAction,\n renderDetails: renderDetails,\n intentTokensContent: intentTokensContent,\n transferIntentContent: transferIntentContent,\n unknownIntentContent: unknownIntentContent,\n handleIntent: handleIntent,\n messageContent: messageContent,\n renderNep413Details: renderNep413Details,\n};\n\nif (typeof module !== \"undefined\" && module.exports) {\n module.exports = __auth0FormHelpers;\n}\n\n// --- END __auth0FormHelpers ---\n\n\nreturn (\n/**\n * Custom field that renders a NEP-413 signature request (recipient, callback URL, message).\n *\n * When the message carries NEAR Intents the helper breaks them out one by one; otherwise the\n * message is shown verbatim, which is what the standard expects of an arbitrary signed string.\n *\n * Depends on `__auth0FormHelpers`, which is inlined at build time by build.js\n * (or shimmed by the playground via helpers-shim.js).\n */\nfunction AuthorizeAppNep413Details(context) {\n return {\n init: function () {\n const params = context.custom.getParams();\n return __auth0FormHelpers.renderNep413Details({\n fields: [\n { label: \"Recipient\", value: params.recipient },\n { label: \"Callback URL\", value: params.callbackUrl },\n { label: \"Signer ID\", value: params.signerId },\n { label: \"Deadline\", value: params.deadline },\n ],\n message: params.message,\n intents: params.intents,\n });\n },\n getScripts: function () { return []; },\n block: function () {},\n unblock: function () {},\n getValue: function () {},\n };\n}\n\n);\n})()\n", + "css": ".box {\n padding: 1.25rem;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 1.25rem;\n width: 100%;\n\n border-radius: 1rem;\n border: 1px solid #e5e5e5;\n}\n\n.text-content {\n display: flex;\n flex-direction: column;\n align-items: left;\n justify-content: left;\n width: 100%;\n}\n\n.label {\n font-size: 0.75rem;\n color: #999999;\n font-weight: 500;\n}\n\n.value {\n color: #3f4246;\n font-size: 0.875rem;\n font-weight: 600;\n}\n\n.json-content {\n display: flex;\n padding: 0.5rem;\n flex-direction: column;\n align-items: left;\n justify-content: left;\n width: 100%;\n border-radius: 12px;\n background-color: #fafafa;\n\n color: #3f4246;\n font-size: 0.875rem;\n font-weight: 500;\n}\n\n.accordion {\n width: 100%;\n border: 1px solid #e5e5e5;\n border-radius: 6px;\n overflow: hidden;\n}\n\n.accordion-header {\n background: #fff;\n padding: 12px 16px;\n cursor: pointer;\n user-select: none;\n}\n\n.accordion-header-content {\n display: flex;\n align-items: center;\n justify-content: space-between;\n width: 100%;\n}\n\n.accordion-left-content {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.accordion-header-label {\n font-size: 0.75rem;\n color: black;\n font-weight: 500;\n}\n\n.warning-icon {\n font-size: 1rem;\n color: #ff4444;\n}\n\n.expand-icon {\n font-size: 1.2rem;\n color: #666;\n font-weight: bold;\n line-height: 1;\n transition: transform 0.2s ease;\n user-select: none;\n min-width: 20px;\n text-align: center;\n}\n\n.accordion-content {\n max-height: 0;\n overflow: hidden;\n font-size: 0.75rem;\n background-color: #fafafa;\n color: #0a0a0a;\n font-weight: 400;\n transition:\n max-height 0.3s ease,\n padding 0.3s ease;\n padding: 0 1rem;\n}\n\n.accordion-content.open {\n padding: 12px 16px;\n max-height: 500px;\n}\n\n.actions-container {\n display: flex;\n flex-direction: column;\n align-items: left;\n justify-content: left;\n width: 100%;\n gap: 0.25rem;\n}\n\n.accordion-content .label {\n font-size: 0.75rem;\n}\n\n.accordion-content .value {\n font-size: 0.75rem;\n white-space: pre-wrap;\n word-break: break-word;\n}\n\n.accordion-content > div {\n display: flex;\n flex-direction: column;\n gap: 0.75rem;\n}\n\n.action-description {\n font-size: 0.75rem;\n color: #3f4246;\n margin: 0;\n}\n\n.warning-callout {\n background-color: #ffebee;\n padding: 16px;\n border-radius: 4px;\n margin: 0;\n color: #c62828;\n}\n", + "params": { + "message": "{{ fields.message }}", + "intents": "{{ fields.intents }}", + "recipient": "{{ fields.recipient }}", + "callbackUrl": "{{ fields.callbackUrl }}", + "signerId": "{{ fields.signerId }}", + "deadline": "{{ fields.deadline }}" + } + } + }, + { + "id": "custom_decision_intent", + "category": "FIELD", + "type": "CUSTOM", + "required": false, + "sensitive": false, + "config": { + "schema": {}, + "code": "(function () {\n// --- BEGIN __auth0FormHelpers (auto-inlined from shared/helpers/index.js) ---\n/**\n * Shared helpers for Auth0 form custom components.\n *\n * Runs in two environments:\n * - Auth0 form runtime: build.js inlines this file into each component's `config.code`,\n * exposing `__auth0FormHelpers` as a top-level variable in the same scope as the component.\n * - Node (tests / playground via require): consumed as a CommonJS module via module.exports.\n *\n * Keep this file free of `require` and ES imports so it can be concatenated as plain text.\n */\n\nfunction ensureBufferPolyfill() {\n if (typeof Buffer !== \"undefined\") return;\n if (typeof globalThis === \"undefined\") return;\n globalThis.Buffer = {\n from: function (data) {\n if (Array.isArray(data)) return new Uint8Array(data);\n if (typeof data === \"string\") return new TextEncoder().encode(data);\n return data;\n },\n };\n}\n\nconst BASE58_ALPHABET = \"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz\";\n\nfunction base58Encode(bytes) {\n let result = \"\";\n let num = BigInt(0);\n for (const byte of bytes) {\n num = (num << BigInt(8)) + BigInt(byte);\n }\n while (num > BigInt(0)) {\n const remainder = num % BigInt(58);\n num = num / BigInt(58);\n result = BASE58_ALPHABET[Number(remainder)] + result;\n }\n for (const byte of bytes) {\n if (byte === 0) result = BASE58_ALPHABET[0] + result;\n else break;\n }\n return result;\n}\n\n/**\n * Format a yocto-NEAR BigInt string as NEAR with up to 8 decimals.\n * \"1000000000000000000000000\" -> \"1\"\n * \"1500000000000000000000000\" -> \"1.5\"\n * \"1\" -> \"0.00000000\" trimmed -> \"0.0\"\n */\nfunction yoctoToNear(bigIntStr) {\n const trimmed = String(bigIntStr).replace(/^0+/, \"\") || \"0\";\n const length = trimmed.length;\n\n if (length <= 24) {\n const zerosNeeded = 24 - length;\n const withZeros = \"0\".repeat(zerosNeeded) + trimmed;\n const decimals = withZeros.slice(0, 8).replace(/0+$/, \"\") || \"0\";\n return `0.${decimals}`;\n }\n\n const splitPos = length - 24;\n const integerPart = trimmed.slice(0, splitPos);\n const decimalPart = trimmed.slice(splitPos, splitPos + 8);\n const trimmedDecimals = decimalPart.replace(/0+$/, \"\");\n if (trimmedDecimals === \"\") return integerPart;\n return `${integerPart}.${trimmedDecimals}`;\n}\n\nfunction formatPublicKey(publicKey) {\n if (publicKey && publicKey.secp256k1Key !== undefined) {\n return `secp256k1:${base58Encode(publicKey.secp256k1Key.data)}`;\n }\n if (publicKey && publicKey.ed25519Key !== undefined) {\n return `ed25519:${base58Encode(publicKey.ed25519Key.data)}`;\n }\n return \"\";\n}\n\n/**\n * Return the canonical action type discriminator (the single non-undefined key of the action).\n * Useful for dispatch and for tests that don't need DOM rendering.\n */\nfunction getActionType(action) {\n if (!action || typeof action !== \"object\") return null;\n for (const key of Object.keys(action)) {\n if (action[key] !== undefined) return key;\n }\n return null;\n}\n\n// --- DOM helpers (require document/window — jsdom in tests, real DOM in browser) ---\n\nfunction createTextContent(label, value, link = false) {\n const textContent = document.createElement(\"div\");\n textContent.classList.add(\"text-content\");\n\n const labelElement = document.createElement(\"div\");\n labelElement.classList.add(\"label\");\n labelElement.textContent = label;\n\n const valueElement = document.createElement(\"div\");\n valueElement.classList.add(\"value\");\n valueElement.textContent = value;\n\n if (link) {\n valueElement.classList.add(\"link\");\n valueElement.setAttribute(\"href\", link);\n valueElement.setAttribute(\"target\", \"_blank\");\n }\n\n textContent.appendChild(labelElement);\n textContent.appendChild(valueElement);\n return textContent;\n}\n\nfunction createDescription(text) {\n const node = document.createElement(\"p\");\n node.classList.add(\"action-description\");\n node.textContent = text;\n return node;\n}\n\nfunction createAccordion(label, content, showWarning = false) {\n const accordion = document.createElement(\"div\");\n accordion.classList.add(\"accordion\");\n\n const header = document.createElement(\"div\");\n header.classList.add(\"accordion-header\");\n\n const headerContent = document.createElement(\"div\");\n headerContent.classList.add(\"accordion-header-content\");\n\n const leftContent = document.createElement(\"div\");\n leftContent.classList.add(\"accordion-left-content\");\n\n if (showWarning) {\n const warningIcon = document.createElement(\"span\");\n warningIcon.classList.add(\"warning-icon\");\n warningIcon.innerHTML = \"⚠️\";\n leftContent.appendChild(warningIcon);\n }\n\n const labelElement = document.createElement(\"span\");\n labelElement.classList.add(\"accordion-header-label\");\n labelElement.textContent = label;\n leftContent.appendChild(labelElement);\n\n const expandIcon = document.createElement(\"span\");\n expandIcon.classList.add(\"expand-icon\");\n expandIcon.innerHTML = \"+\";\n\n headerContent.appendChild(leftContent);\n headerContent.appendChild(expandIcon);\n header.appendChild(headerContent);\n\n const contentElement = document.createElement(\"div\");\n contentElement.classList.add(\"accordion-content\");\n contentElement.appendChild(content);\n\n accordion.appendChild(header);\n accordion.appendChild(contentElement);\n\n header.addEventListener(\"click\", function () {\n contentElement.classList.toggle(\"open\");\n expandIcon.innerHTML = contentElement.classList.contains(\"open\") ? \"−\" : \"+\";\n });\n\n return accordion;\n}\n\n// --- Action content factories ---\n\nfunction createAccountContent() {\n return createDescription(\"By approving this request, a new account will be created.\");\n}\n\nfunction deployContractContent(action) {\n ensureBufferPolyfill();\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the compiled smart contract will be deployed.\");\n const codeLen = action.deployContract && action.deployContract.code ? `${action.deployContract.code.length} bytes` : null;\n container.appendChild(text);\n container.appendChild(createTextContent(\"Code\", codeLen));\n return container;\n}\n\n/**\n * Decode functionCall `args` (a byte array / Uint8Array) to a readable string. NEAR contract\n * args are almost always UTF-8 JSON, so decode the bytes and pretty-print when they parse as\n * JSON; otherwise fall back to the raw decoded text. TextDecoder is used directly because it\n * exists in the browser and the Auth0 form runtime (unlike Buffer, which the runtime lacks).\n */\nfunction decodeFunctionCallArgs(args) {\n if (args == null) return \"\";\n let text;\n try {\n if (typeof TextDecoder !== \"undefined\") {\n text = new TextDecoder().decode(Uint8Array.from(args));\n } else if (typeof Buffer !== \"undefined\" && typeof Buffer.from === \"function\") {\n text = Buffer.from(args).toString(\"utf8\");\n } else {\n text = String.fromCharCode.apply(null, Array.from(args));\n }\n } catch (e) {\n return \"\";\n }\n try {\n return JSON.stringify(JSON.parse(text), null, 2);\n } catch (e) {\n return text;\n }\n}\n\n/**\n * Format a yocto-NEAR amount for display. In transaction mode (showYoctoConversion) amounts at or\n * above 0.00000000001 NEAR are shown as NEAR; smaller amounts and delegate-action mode stay in\n * yoctoNEAR so no precision is hidden.\n */\nfunction formatNearAmount(value, options) {\n const showYoctoConversion = !!(options && options.showYoctoConversion);\n if (showYoctoConversion && value != null && BigInt(value) >= BigInt(\"10000000000000000\")) {\n return `${yoctoToNear(value.toString())} NEAR`;\n }\n return `${value != null ? value.toString() : \"0\"} yoctoNEAR`;\n}\n\nfunction functionCallContent(action, options) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following function will be called.\");\n\n const fc = action.functionCall || {};\n container.appendChild(text);\n container.appendChild(createTextContent(\"Method Name\", fc.methodName));\n container.appendChild(createTextContent(\"Args\", decodeFunctionCallArgs(fc.args)));\n container.appendChild(createTextContent(\"Gas\", fc.gas != null ? fc.gas.toString() : \"\"));\n container.appendChild(createTextContent(\"Deposit\", formatNearAmount(fc.deposit, options)));\n return container;\n}\n\nfunction transferContent(action, options) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following amount will be transferred to the receiver.\");\n\n const deposit = action.transfer ? action.transfer.deposit : undefined;\n container.appendChild(text);\n container.appendChild(createTextContent(\"Deposit\", formatNearAmount(deposit, options)));\n return container;\n}\n\nfunction stakeContent(action, options) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following amount will be staked to the public key.\");\n const stake = action.stake || {};\n container.appendChild(text);\n container.appendChild(createTextContent(\"Stake\", formatNearAmount(stake.stake, options)));\n container.appendChild(createTextContent(\"Public Key\", formatPublicKey(stake.publicKey)));\n return container;\n}\n\nfunction addKeyContent(action) {\n const container = document.createElement(\"div\");\n const ak = action.addKey || {};\n const accessKey = ak.accessKey || {};\n const permission = accessKey.permission || {};\n\n if (permission.fullAccess !== undefined) {\n const warning = document.createElement(\"div\");\n warning.classList.add(\"warning-callout\");\n warning.textContent = \"This key will have full access to your account. Only approve if you trust the recipient.\";\n container.appendChild(warning);\n }\n\n container.appendChild(createDescription(\"By approving this request, the following access key will be added to your account.\"));\n container.appendChild(createTextContent(\"Public Key\", formatPublicKey(ak.publicKey)));\n container.appendChild(createTextContent(\"Nonce\", accessKey.nonce != null ? accessKey.nonce.toString() : \"\"));\n\n if (permission.fullAccess === undefined) {\n container.appendChild(createTextContent(\"Permission\", JSON.stringify(permission.functionCall, null, 2)));\n }\n return container;\n}\n\nfunction deleteKeyContent(action) {\n const container = document.createElement(\"div\");\n const dk = action.deleteKey || {};\n\n const warning = document.createElement(\"div\");\n warning.classList.add(\"warning-callout\");\n warning.textContent = \"Once deleted, the key cannot be recovered.\";\n\n container.appendChild(warning);\n container.appendChild(\n createDescription(\"This action will delete an access key from your account. Only approve if you trust the recipient.\"),\n );\n container.appendChild(createTextContent(\"Public Key\", formatPublicKey(dk.publicKey)));\n return container;\n}\n\nfunction deleteAccountContent(action) {\n const container = document.createElement(\"div\");\n\n const warning = document.createElement(\"div\");\n warning.classList.add(\"warning-callout\");\n warning.textContent = \"Once deleted, the account cannot be recovered.\";\n\n container.appendChild(warning);\n container.appendChild(createDescription(\"By approving this request, the account will be deleted.\"));\n container.appendChild(createTextContent(\"Beneficiary ID\", action.deleteAccount && action.deleteAccount.beneficiaryId));\n return container;\n}\n\nfunction signedDelegateContent(action) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following delegate action will be executed.\");\n\n const sd = action.signedDelegate || {};\n let serialized;\n try {\n serialized = JSON.stringify(\n sd.delegateAction,\n function (_, value) {\n return typeof value === \"bigint\" ? value.toString() : value;\n },\n 2,\n );\n } catch (e) {\n serialized = \"\";\n }\n container.appendChild(text);\n container.appendChild(createTextContent(\"Delegate Action\", serialized));\n return container;\n}\n\nfunction deployGlobalContractContent(action) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the compiled smart contract will be deployed.\");\n const codeLen =\n action.deployGlobalContract && action.deployGlobalContract.code ? `${action.deployGlobalContract.code.length} bytes` : null;\n container.appendChild(text);\n container.appendChild(createTextContent(\"Code\", codeLen));\n return container;\n}\n\nfunction useGlobalContractContent(action) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following global contract will be used.\");\n let identifier;\n try {\n identifier = JSON.stringify(action.useGlobalContract && action.useGlobalContract.contractIdentifier);\n } catch (e) {\n identifier = \"\";\n }\n container.appendChild(text);\n container.appendChild(createTextContent(\"Contract Identifier\", identifier));\n return container;\n}\n\nconst ACTION_DISPATCH = {\n createAccount: {\n label: \"CreateAccount\",\n warn: false,\n render: function () {\n return createAccountContent();\n },\n },\n deployContract: {\n label: \"DeployContract\",\n warn: true,\n render: function (a) {\n return deployContractContent(a);\n },\n },\n functionCall: {\n label: \"FunctionCall\",\n warn: false,\n render: function (a, opts) {\n return functionCallContent(a, opts);\n },\n },\n transfer: {\n label: \"Transfer\",\n warn: false,\n render: function (a, opts) {\n return transferContent(a, opts);\n },\n },\n stake: {\n label: \"Stake\",\n warn: false,\n render: function (a, opts) {\n return stakeContent(a, opts);\n },\n },\n addKey: {\n label: \"AddKey\",\n warn: false,\n render: function (a) {\n return addKeyContent(a);\n },\n warnsWhen: function (a) {\n return !!(\n a.addKey &&\n a.addKey.accessKey &&\n a.addKey.accessKey.permission &&\n a.addKey.accessKey.permission.fullAccess !== undefined\n );\n },\n },\n deleteKey: {\n label: \"DeleteKey\",\n warn: true,\n render: function (a) {\n return deleteKeyContent(a);\n },\n },\n deleteAccount: {\n label: \"DeleteAccount\",\n warn: true,\n render: function (a) {\n return deleteAccountContent(a);\n },\n },\n signedDelegate: {\n label: \"SignedDelegate\",\n warn: false,\n render: function (a) {\n return signedDelegateContent(a);\n },\n },\n deployGlobalContract: {\n label: \"DeployGlobalContract\",\n warn: true,\n render: function (a) {\n return deployGlobalContractContent(a);\n },\n },\n useGlobalContract: {\n label: \"UseGlobalContract\",\n warn: false,\n render: function (a) {\n return useGlobalContractContent(a);\n },\n },\n};\n\nfunction handleNearAction(action, options) {\n const actionKey = getActionType(action);\n const entry = actionKey ? ACTION_DISPATCH[actionKey] : null;\n if (!entry) {\n const unknown = document.createElement(\"div\");\n unknown.textContent = `Unknown action: ${actionKey || \"(empty)\"}`;\n return createAccordion(\"Unknown\", unknown, true);\n }\n const showWarning = entry.warnsWhen ? entry.warnsWhen(action) : !!entry.warn;\n return createAccordion(entry.label, entry.render(action, options), showWarning);\n}\n\n/**\n * Build the full details DOM tree for a form custom field.\n *\n * @param {object} params\n * @param {Array<{label: string, value: string|undefined}>} params.fields - top-level fields like Signer/Receiver/MaxBlockHeight.\n * @param {string} params.actions - JSON string with the actions array (as Auth0 form fields deliver them).\n * @param {{ showYoctoConversion?: boolean }} [params.options]\n */\nfunction renderDetails(params) {\n ensureBufferPolyfill();\n const box = document.createElement(\"div\");\n box.classList.add(\"box\");\n\n for (const field of params.fields || []) {\n if (field.value === undefined || field.value === null || field.value === \"\") continue;\n box.appendChild(createTextContent(field.label, field.value));\n }\n\n const actionsContainer = document.createElement(\"div\");\n actionsContainer.classList.add(\"actions-container\");\n const actionsLabel = document.createElement(\"div\");\n actionsLabel.classList.add(\"label\");\n actionsLabel.textContent = \"Actions\";\n actionsContainer.appendChild(actionsLabel);\n\n let parsedActions = [];\n try {\n parsedActions = JSON.parse(params.actions || \"[]\");\n } catch (e) {\n const errorNode = document.createElement(\"div\");\n errorNode.classList.add(\"warning-callout\");\n errorNode.textContent = \"Failed to parse actions payload.\";\n actionsContainer.appendChild(errorNode);\n box.appendChild(actionsContainer);\n return box;\n }\n\n for (const action of parsedActions) {\n actionsContainer.appendChild(handleNearAction(action, params.options));\n }\n\n box.appendChild(actionsContainer);\n return box;\n}\n\n// --- NEP-413 intent content factories ---\n\n/**\n * Render the token map of a transfer intent (`{ \"\": \"\" }`).\n *\n * Amounts stay in the token's smallest unit: the form has no token metadata, so converting\n * would mean guessing decimals — and a wrong guess here understates what the user is\n * approving. Showing the raw amount alongside its token id is honest and unambiguous.\n */\nfunction intentTokensContent(tokens) {\n const container = document.createElement(\"div\");\n if (!tokens || typeof tokens !== \"object\") return container;\n for (const tokenId of Object.keys(tokens)) {\n container.appendChild(createTextContent(tokenId, String(tokens[tokenId])));\n }\n return container;\n}\n\nfunction transferIntentContent(intent) {\n const container = document.createElement(\"div\");\n container.appendChild(createDescription(\"By approving this request, the following tokens will be transferred.\"));\n container.appendChild(createTextContent(\"Receiver ID\", intent.receiver_id));\n\n const tokensLabel = document.createElement(\"div\");\n tokensLabel.classList.add(\"label\");\n tokensLabel.textContent = \"Tokens\";\n container.appendChild(tokensLabel);\n container.appendChild(intentTokensContent(intent.tokens));\n return container;\n}\n\n/**\n * Fallback for intent kinds this form does not model explicitly. It is deliberately shown\n * with a warning: the user is approving something the UI cannot describe in plain terms.\n */\nfunction unknownIntentContent(intent) {\n const container = document.createElement(\"div\");\n container.appendChild(createDescription(\"This request contains an intent type this app does not recognize. Review it carefully.\"));\n let serialized;\n try {\n serialized = JSON.stringify(intent, null, 2);\n } catch (e) {\n serialized = \"\";\n }\n container.appendChild(createTextContent(\"Intent\", serialized));\n return container;\n}\n\nconst INTENT_DISPATCH = {\n transfer: {\n label: \"Transfer\",\n warn: false,\n render: function (i) {\n return transferIntentContent(i);\n },\n },\n};\n\nfunction handleIntent(intent) {\n const kind = intent && typeof intent === \"object\" ? intent.intent : null;\n const entry = kind ? INTENT_DISPATCH[kind] : null;\n if (!entry) {\n return createAccordion(kind ? `Unknown: ${kind}` : \"Unknown\", unknownIntentContent(intent), true);\n }\n return createAccordion(entry.label, entry.render(intent), !!entry.warn);\n}\n\n/**\n * Render the message body of a NEP-413 request verbatim.\n *\n * This is the default view, and the one that matters most: NEP-413 messages are arbitrary\n * strings meant to be read by a human (\"Sign in to example.com\"), so showing the text exactly\n * as it will be signed is the honest presentation. Pretty-printing is applied only when the\n * message is JSON, purely for legibility.\n */\nfunction messageContent(message) {\n const container = document.createElement(\"div\");\n let display = message;\n try {\n const parsed = JSON.parse(message);\n if (parsed && typeof parsed === \"object\") display = JSON.stringify(parsed, null, 2);\n } catch (e) {\n display = message;\n }\n container.appendChild(createTextContent(\"Message\", display));\n return container;\n}\n\n/**\n * Build the details DOM tree for a NEP-413 signature approval.\n *\n * Two presentations, one payload: when the message carries NEAR Intents the intents are broken\n * out one by one, and otherwise the raw message is shown as text. Either way the top-level\n * fields — recipient above all — are rendered, because under NEP-413 the recipient is what\n * tells the user which application their signature is addressed to.\n * @param {object} params The render parameters.\n * @param {Array<{label: string, value: string|undefined}>} params.fields Top-level fields (recipient, callback URL, signer, deadline).\n * @param {string} params.message The raw NEP-413 message, shown when there are no intents to break out.\n * @param {string} [params.intents] JSON string with the intents array, when the message is a NEAR Intents body.\n * @returns {HTMLElement} The details node.\n */\nfunction renderNep413Details(params) {\n ensureBufferPolyfill();\n const box = document.createElement(\"div\");\n box.classList.add(\"box\");\n\n for (const field of params.fields || []) {\n if (field.value === undefined || field.value === null || field.value === \"\") continue;\n box.appendChild(createTextContent(field.label, field.value));\n }\n\n let parsedIntents = null;\n if (params.intents) {\n try {\n const candidate = JSON.parse(params.intents);\n if (Array.isArray(candidate) && candidate.length > 0) parsedIntents = candidate;\n } catch (e) {\n parsedIntents = null;\n }\n }\n\n // No intents to break out (or they were unreadable) — show the message itself. Falling back\n // to the raw text keeps every NEP-413 message approvable, not just the ones we model.\n if (!parsedIntents) {\n box.appendChild(messageContent(params.message || \"\"));\n return box;\n }\n\n const intentsContainer = document.createElement(\"div\");\n intentsContainer.classList.add(\"actions-container\");\n const intentsLabel = document.createElement(\"div\");\n intentsLabel.classList.add(\"label\");\n intentsLabel.textContent = \"Intents\";\n intentsContainer.appendChild(intentsLabel);\n\n for (const intent of parsedIntents) {\n intentsContainer.appendChild(handleIntent(intent));\n }\n\n box.appendChild(intentsContainer);\n return box;\n}\n\nvar __auth0FormHelpers = {\n ensureBufferPolyfill: ensureBufferPolyfill,\n base58Encode: base58Encode,\n yoctoToNear: yoctoToNear,\n formatNearAmount: formatNearAmount,\n decodeFunctionCallArgs: decodeFunctionCallArgs,\n formatPublicKey: formatPublicKey,\n getActionType: getActionType,\n createTextContent: createTextContent,\n createDescription: createDescription,\n createAccordion: createAccordion,\n createAccountContent: createAccountContent,\n deployContractContent: deployContractContent,\n functionCallContent: functionCallContent,\n transferContent: transferContent,\n stakeContent: stakeContent,\n addKeyContent: addKeyContent,\n deleteKeyContent: deleteKeyContent,\n deleteAccountContent: deleteAccountContent,\n signedDelegateContent: signedDelegateContent,\n deployGlobalContractContent: deployGlobalContractContent,\n useGlobalContractContent: useGlobalContractContent,\n handleNearAction: handleNearAction,\n renderDetails: renderDetails,\n intentTokensContent: intentTokensContent,\n transferIntentContent: transferIntentContent,\n unknownIntentContent: unknownIntentContent,\n handleIntent: handleIntent,\n messageContent: messageContent,\n renderNep413Details: renderNep413Details,\n};\n\nif (typeof module !== \"undefined\" && module.exports) {\n module.exports = __auth0FormHelpers;\n}\n\n// --- END __auth0FormHelpers ---\n\n\nreturn (\n/**\n * Custom field that renders the Approve / Deny buttons for the authorize flow.\n *\n * Auth0 Forms buttons can only navigate — they cannot record a choice. So instead of the\n * native NEXT_BUTTON / PREVIOUS_BUTTON, this field owns both buttons and writes the user's\n * choice into the `decision` hidden field before advancing. The resuming action reads\n * `event.prompt.fields.decision` in onContinuePostLogin and denies access when it is \"denied\".\n *\n * Params (configured per-form in _form_base.json):\n * - approveText: label for the approve button (default \"Approve\")\n * - denyText: label for the deny button (default \"Deny\")\n */\nfunction AuthorizeAppDecision(context) {\n return {\n init: function () {\n const params = context.custom.getParams();\n\n const layout = document.createElement(\"div\");\n layout.classList.add(\"decision-layout\");\n\n const approve = document.createElement(\"button\");\n approve.setAttribute(\"type\", \"button\");\n approve.classList.add(\"decision-button\", \"approve\");\n approve.textContent = params.approveText || \"Approve\";\n approve.addEventListener(\"click\", function () {\n context.form.setHiddenField(\"decision\", \"approved\");\n context.form.goForward();\n });\n\n const deny = document.createElement(\"button\");\n deny.setAttribute(\"type\", \"button\");\n deny.classList.add(\"decision-button\", \"deny\");\n deny.textContent = params.denyText || \"Deny\";\n deny.addEventListener(\"click\", function () {\n context.form.setHiddenField(\"decision\", \"denied\");\n context.form.goForward();\n });\n\n layout.appendChild(approve);\n layout.appendChild(deny);\n return layout;\n },\n getScripts: function () { return []; },\n block: function () {},\n unblock: function () {},\n getValue: function () {},\n };\n}\n\n);\n})()\n", + "css": ".decision-layout {\n display: flex;\n flex-direction: column;\n gap: 0.75rem;\n width: 100%;\n}\n\n.decision-button {\n width: 100%;\n padding: 0.75rem 1rem;\n border-radius: 0.75rem;\n font-size: 0.875rem;\n font-weight: 600;\n cursor: pointer;\n border: 1px solid transparent;\n}\n\n.decision-button.approve {\n background-color: #0a0a0a;\n color: #ffffff;\n}\n\n.decision-button.deny {\n background-color: #ffffff;\n color: #3f4246;\n border-color: #e5e5e5;\n}\n", + "params": { + "approveText": "Approve", + "denyText": "Deny" + } + } + } + ], + "next_node": "$ending" + } + } + ], + "start": { + "hidden_fields": [ + { + "key": "message" + }, + { + "key": "intents" + }, + { + "key": "recipient" + }, + { + "key": "callbackUrl" + }, + { + "key": "signerId" + }, + { + "key": "deadline" + }, + { + "key": "name" + }, + { + "key": "imageUrl" + }, + { + "key": "decision" + } + ], + "next_node": "step_in7K", + "coordinates": { + "x": 132, + "y": -102 + } + }, + "ending": { + "resume_flow": true, + "coordinates": { + "x": 1060, + "y": -118 + } + } + }, + "flows": {}, + "connections": {} +} diff --git a/packages/auth0/src/forms/nep413/nep413_form_base.json b/packages/auth0/src/forms/nep413/nep413_form_base.json new file mode 100644 index 00000000..473e911c --- /dev/null +++ b/packages/auth0/src/forms/nep413/nep413_form_base.json @@ -0,0 +1,134 @@ +{ + "version": "4.0.0", + "form": { + "name": "NEP-413 form", + "languages": { + "primary": "en" + }, + "nodes": [ + { + "id": "step_in7K", + "type": "STEP", + "coordinates": { + "x": 463, + "y": -81 + }, + "alias": "New step", + "config": { + "components": [ + { + "$source": "shared/image", + "id": "custom_4Mqg", + "category": "FIELD", + "type": "CUSTOM", + "required": false, + "sensitive": false, + "config": { + "schema": {}, + "code": "", + "css": "", + "params": { + "iconUrl": "https://peersyst-public-production.s3.eu-west-1.amazonaws.com/1b54f479-7990-4834-9b37-95b26e2023fb.png", + "leftImageUrl": "https://peersyst-public-production.s3.eu-west-1.amazonaws.com/db9f38ff-53ea-4f76-a1c0-33c728386e5b.png", + "rightImageUrl": "{{ fields.imageUrl }}" + } + } + }, + { + "id": "rich_text_iN9x", + "category": "BLOCK", + "type": "RICH_TEXT", + "config": { + "content": "

{{ fields.name }} wants you to sign a message

" + } + }, + { + "$source": "details", + "id": "custom_I420", + "category": "FIELD", + "type": "CUSTOM", + "required": false, + "sensitive": false, + "config": { + "schema": {}, + "code": "", + "css": "", + "params": { + "message": "{{ fields.message }}", + "intents": "{{ fields.intents }}", + "recipient": "{{ fields.recipient }}", + "callbackUrl": "{{ fields.callbackUrl }}", + "signerId": "{{ fields.signerId }}", + "deadline": "{{ fields.deadline }}" + } + } + }, + { + "$source": "shared/decision", + "id": "custom_decision_intent", + "category": "FIELD", + "type": "CUSTOM", + "required": false, + "sensitive": false, + "config": { + "schema": {}, + "code": "", + "css": "", + "params": { + "approveText": "Approve", + "denyText": "Deny" + } + } + } + ], + "next_node": "$ending" + } + } + ], + "start": { + "hidden_fields": [ + { + "key": "message" + }, + { + "key": "intents" + }, + { + "key": "recipient" + }, + { + "key": "callbackUrl" + }, + { + "key": "signerId" + }, + { + "key": "deadline" + }, + { + "key": "name" + }, + { + "key": "imageUrl" + }, + { + "key": "decision" + } + ], + "next_node": "step_in7K", + "coordinates": { + "x": 132, + "y": -102 + } + }, + "ending": { + "resume_flow": true, + "coordinates": { + "x": 1060, + "y": -118 + } + } + }, + "flows": {}, + "connections": {} +} diff --git a/packages/auth0/src/forms/shared/helpers/index.js b/packages/auth0/src/forms/shared/helpers/index.js index 17c3e93d..1e4cf541 100644 --- a/packages/auth0/src/forms/shared/helpers/index.js +++ b/packages/auth0/src/forms/shared/helpers/index.js @@ -500,6 +500,149 @@ function renderDetails(params) { return box; } +// --- NEP-413 intent content factories --- + +/** + * Render the token map of a transfer intent (`{ "": "" }`). + * + * Amounts stay in the token's smallest unit: the form has no token metadata, so converting + * would mean guessing decimals — and a wrong guess here understates what the user is + * approving. Showing the raw amount alongside its token id is honest and unambiguous. + */ +function intentTokensContent(tokens) { + const container = document.createElement("div"); + if (!tokens || typeof tokens !== "object") return container; + for (const tokenId of Object.keys(tokens)) { + container.appendChild(createTextContent(tokenId, String(tokens[tokenId]))); + } + return container; +} + +function transferIntentContent(intent) { + const container = document.createElement("div"); + container.appendChild(createDescription("By approving this request, the following tokens will be transferred.")); + container.appendChild(createTextContent("Receiver ID", intent.receiver_id)); + + const tokensLabel = document.createElement("div"); + tokensLabel.classList.add("label"); + tokensLabel.textContent = "Tokens"; + container.appendChild(tokensLabel); + container.appendChild(intentTokensContent(intent.tokens)); + return container; +} + +/** + * Fallback for intent kinds this form does not model explicitly. It is deliberately shown + * with a warning: the user is approving something the UI cannot describe in plain terms. + */ +function unknownIntentContent(intent) { + const container = document.createElement("div"); + container.appendChild(createDescription("This request contains an intent type this app does not recognize. Review it carefully.")); + let serialized; + try { + serialized = JSON.stringify(intent, null, 2); + } catch (e) { + serialized = ""; + } + container.appendChild(createTextContent("Intent", serialized)); + return container; +} + +const INTENT_DISPATCH = { + transfer: { + label: "Transfer", + warn: false, + render: function (i) { + return transferIntentContent(i); + }, + }, +}; + +function handleIntent(intent) { + const kind = intent && typeof intent === "object" ? intent.intent : null; + const entry = kind ? INTENT_DISPATCH[kind] : null; + if (!entry) { + return createAccordion(kind ? `Unknown: ${kind}` : "Unknown", unknownIntentContent(intent), true); + } + return createAccordion(entry.label, entry.render(intent), !!entry.warn); +} + +/** + * Render the message body of a NEP-413 request verbatim. + * + * This is the default view, and the one that matters most: NEP-413 messages are arbitrary + * strings meant to be read by a human ("Sign in to example.com"), so showing the text exactly + * as it will be signed is the honest presentation. Pretty-printing is applied only when the + * message is JSON, purely for legibility. + */ +function messageContent(message) { + const container = document.createElement("div"); + let display = message; + try { + const parsed = JSON.parse(message); + if (parsed && typeof parsed === "object") display = JSON.stringify(parsed, null, 2); + } catch (e) { + display = message; + } + container.appendChild(createTextContent("Message", display)); + return container; +} + +/** + * Build the details DOM tree for a NEP-413 signature approval. + * + * Two presentations, one payload: when the message carries NEAR Intents the intents are broken + * out one by one, and otherwise the raw message is shown as text. Either way the top-level + * fields — recipient above all — are rendered, because under NEP-413 the recipient is what + * tells the user which application their signature is addressed to. + * @param {object} params The render parameters. + * @param {Array<{label: string, value: string|undefined}>} params.fields Top-level fields (recipient, callback URL, signer, deadline). + * @param {string} params.message The raw NEP-413 message, shown when there are no intents to break out. + * @param {string} [params.intents] JSON string with the intents array, when the message is a NEAR Intents body. + * @returns {HTMLElement} The details node. + */ +function renderNep413Details(params) { + ensureBufferPolyfill(); + const box = document.createElement("div"); + box.classList.add("box"); + + for (const field of params.fields || []) { + if (field.value === undefined || field.value === null || field.value === "") continue; + box.appendChild(createTextContent(field.label, field.value)); + } + + let parsedIntents = null; + if (params.intents) { + try { + const candidate = JSON.parse(params.intents); + if (Array.isArray(candidate) && candidate.length > 0) parsedIntents = candidate; + } catch (e) { + parsedIntents = null; + } + } + + // No intents to break out (or they were unreadable) — show the message itself. Falling back + // to the raw text keeps every NEP-413 message approvable, not just the ones we model. + if (!parsedIntents) { + box.appendChild(messageContent(params.message || "")); + return box; + } + + const intentsContainer = document.createElement("div"); + intentsContainer.classList.add("actions-container"); + const intentsLabel = document.createElement("div"); + intentsLabel.classList.add("label"); + intentsLabel.textContent = "Intents"; + intentsContainer.appendChild(intentsLabel); + + for (const intent of parsedIntents) { + intentsContainer.appendChild(handleIntent(intent)); + } + + box.appendChild(intentsContainer); + return box; +} + var __auth0FormHelpers = { ensureBufferPolyfill: ensureBufferPolyfill, base58Encode: base58Encode, @@ -524,6 +667,12 @@ var __auth0FormHelpers = { useGlobalContractContent: useGlobalContractContent, handleNearAction: handleNearAction, renderDetails: renderDetails, + intentTokensContent: intentTokensContent, + transferIntentContent: transferIntentContent, + unknownIntentContent: unknownIntentContent, + handleIntent: handleIntent, + messageContent: messageContent, + renderNep413Details: renderNep413Details, }; if (typeof module !== "undefined" && module.exports) { diff --git a/packages/auth0/src/forms/transaction/transaction_form.json b/packages/auth0/src/forms/transaction/transaction_form.json index c131a5bb..79b0b1f2 100644 --- a/packages/auth0/src/forms/transaction/transaction_form.json +++ b/packages/auth0/src/forms/transaction/transaction_form.json @@ -24,7 +24,7 @@ "sensitive": false, "config": { "schema": {}, - "code": "(function () {\n// --- BEGIN __auth0FormHelpers (auto-inlined from shared/helpers/index.js) ---\n/**\n * Shared helpers for Auth0 form custom components.\n *\n * Runs in two environments:\n * - Auth0 form runtime: build.js inlines this file into each component's `config.code`,\n * exposing `__auth0FormHelpers` as a top-level variable in the same scope as the component.\n * - Node (tests / playground via require): consumed as a CommonJS module via module.exports.\n *\n * Keep this file free of `require` and ES imports so it can be concatenated as plain text.\n */\n\nfunction ensureBufferPolyfill() {\n if (typeof Buffer !== \"undefined\") return;\n if (typeof globalThis === \"undefined\") return;\n globalThis.Buffer = {\n from: function (data) {\n if (Array.isArray(data)) return new Uint8Array(data);\n if (typeof data === \"string\") return new TextEncoder().encode(data);\n return data;\n },\n };\n}\n\nconst BASE58_ALPHABET = \"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz\";\n\nfunction base58Encode(bytes) {\n let result = \"\";\n let num = BigInt(0);\n for (const byte of bytes) {\n num = (num << BigInt(8)) + BigInt(byte);\n }\n while (num > BigInt(0)) {\n const remainder = num % BigInt(58);\n num = num / BigInt(58);\n result = BASE58_ALPHABET[Number(remainder)] + result;\n }\n for (const byte of bytes) {\n if (byte === 0) result = BASE58_ALPHABET[0] + result;\n else break;\n }\n return result;\n}\n\n/**\n * Format a yocto-NEAR BigInt string as NEAR with up to 8 decimals.\n * \"1000000000000000000000000\" -> \"1\"\n * \"1500000000000000000000000\" -> \"1.5\"\n * \"1\" -> \"0.00000000\" trimmed -> \"0.0\"\n */\nfunction yoctoToNear(bigIntStr) {\n const trimmed = String(bigIntStr).replace(/^0+/, \"\") || \"0\";\n const length = trimmed.length;\n\n if (length <= 24) {\n const zerosNeeded = 24 - length;\n const withZeros = \"0\".repeat(zerosNeeded) + trimmed;\n const decimals = withZeros.slice(0, 8).replace(/0+$/, \"\") || \"0\";\n return `0.${decimals}`;\n }\n\n const splitPos = length - 24;\n const integerPart = trimmed.slice(0, splitPos);\n const decimalPart = trimmed.slice(splitPos, splitPos + 8);\n const trimmedDecimals = decimalPart.replace(/0+$/, \"\");\n if (trimmedDecimals === \"\") return integerPart;\n return `${integerPart}.${trimmedDecimals}`;\n}\n\nfunction formatPublicKey(publicKey) {\n if (publicKey && publicKey.secp256k1Key !== undefined) {\n return `secp256k1:${base58Encode(publicKey.secp256k1Key.data)}`;\n }\n if (publicKey && publicKey.ed25519Key !== undefined) {\n return `ed25519:${base58Encode(publicKey.ed25519Key.data)}`;\n }\n return \"\";\n}\n\n/**\n * Return the canonical action type discriminator (the single non-undefined key of the action).\n * Useful for dispatch and for tests that don't need DOM rendering.\n */\nfunction getActionType(action) {\n if (!action || typeof action !== \"object\") return null;\n for (const key of Object.keys(action)) {\n if (action[key] !== undefined) return key;\n }\n return null;\n}\n\n// --- DOM helpers (require document/window — jsdom in tests, real DOM in browser) ---\n\nfunction createTextContent(label, value, link = false) {\n const textContent = document.createElement(\"div\");\n textContent.classList.add(\"text-content\");\n\n const labelElement = document.createElement(\"div\");\n labelElement.classList.add(\"label\");\n labelElement.textContent = label;\n\n const valueElement = document.createElement(\"div\");\n valueElement.classList.add(\"value\");\n valueElement.textContent = value;\n\n if (link) {\n valueElement.classList.add(\"link\");\n valueElement.setAttribute(\"href\", link);\n valueElement.setAttribute(\"target\", \"_blank\");\n }\n\n textContent.appendChild(labelElement);\n textContent.appendChild(valueElement);\n return textContent;\n}\n\nfunction createDescription(text) {\n const node = document.createElement(\"p\");\n node.classList.add(\"action-description\");\n node.textContent = text;\n return node;\n}\n\nfunction createAccordion(label, content, showWarning = false) {\n const accordion = document.createElement(\"div\");\n accordion.classList.add(\"accordion\");\n\n const header = document.createElement(\"div\");\n header.classList.add(\"accordion-header\");\n\n const headerContent = document.createElement(\"div\");\n headerContent.classList.add(\"accordion-header-content\");\n\n const leftContent = document.createElement(\"div\");\n leftContent.classList.add(\"accordion-left-content\");\n\n if (showWarning) {\n const warningIcon = document.createElement(\"span\");\n warningIcon.classList.add(\"warning-icon\");\n warningIcon.innerHTML = \"⚠️\";\n leftContent.appendChild(warningIcon);\n }\n\n const labelElement = document.createElement(\"span\");\n labelElement.classList.add(\"accordion-header-label\");\n labelElement.textContent = label;\n leftContent.appendChild(labelElement);\n\n const expandIcon = document.createElement(\"span\");\n expandIcon.classList.add(\"expand-icon\");\n expandIcon.innerHTML = \"+\";\n\n headerContent.appendChild(leftContent);\n headerContent.appendChild(expandIcon);\n header.appendChild(headerContent);\n\n const contentElement = document.createElement(\"div\");\n contentElement.classList.add(\"accordion-content\");\n contentElement.appendChild(content);\n\n accordion.appendChild(header);\n accordion.appendChild(contentElement);\n\n header.addEventListener(\"click\", function () {\n contentElement.classList.toggle(\"open\");\n expandIcon.innerHTML = contentElement.classList.contains(\"open\") ? \"−\" : \"+\";\n });\n\n return accordion;\n}\n\n// --- Action content factories ---\n\nfunction createAccountContent() {\n return createDescription(\"By approving this request, a new account will be created.\");\n}\n\nfunction deployContractContent(action) {\n ensureBufferPolyfill();\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the compiled smart contract will be deployed.\");\n const codeLen = action.deployContract && action.deployContract.code ? `${action.deployContract.code.length} bytes` : null;\n container.appendChild(text);\n container.appendChild(createTextContent(\"Code\", codeLen));\n return container;\n}\n\n/**\n * Decode functionCall `args` (a byte array / Uint8Array) to a readable string. NEAR contract\n * args are almost always UTF-8 JSON, so decode the bytes and pretty-print when they parse as\n * JSON; otherwise fall back to the raw decoded text. TextDecoder is used directly because it\n * exists in the browser and the Auth0 form runtime (unlike Buffer, which the runtime lacks).\n */\nfunction decodeFunctionCallArgs(args) {\n if (args == null) return \"\";\n let text;\n try {\n if (typeof TextDecoder !== \"undefined\") {\n text = new TextDecoder().decode(Uint8Array.from(args));\n } else if (typeof Buffer !== \"undefined\" && typeof Buffer.from === \"function\") {\n text = Buffer.from(args).toString(\"utf8\");\n } else {\n text = String.fromCharCode.apply(null, Array.from(args));\n }\n } catch (e) {\n return \"\";\n }\n try {\n return JSON.stringify(JSON.parse(text), null, 2);\n } catch (e) {\n return text;\n }\n}\n\n/**\n * Format a yocto-NEAR amount for display. In transaction mode (showYoctoConversion) amounts at or\n * above 0.00000000001 NEAR are shown as NEAR; smaller amounts and delegate-action mode stay in\n * yoctoNEAR so no precision is hidden.\n */\nfunction formatNearAmount(value, options) {\n const showYoctoConversion = !!(options && options.showYoctoConversion);\n if (showYoctoConversion && value != null && BigInt(value) >= BigInt(\"10000000000000000\")) {\n return `${yoctoToNear(value.toString())} NEAR`;\n }\n return `${value != null ? value.toString() : \"0\"} yoctoNEAR`;\n}\n\nfunction functionCallContent(action, options) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following function will be called.\");\n\n const fc = action.functionCall || {};\n container.appendChild(text);\n container.appendChild(createTextContent(\"Method Name\", fc.methodName));\n container.appendChild(createTextContent(\"Args\", decodeFunctionCallArgs(fc.args)));\n container.appendChild(createTextContent(\"Gas\", fc.gas != null ? fc.gas.toString() : \"\"));\n container.appendChild(createTextContent(\"Deposit\", formatNearAmount(fc.deposit, options)));\n return container;\n}\n\nfunction transferContent(action, options) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following amount will be transferred to the receiver.\");\n\n const deposit = action.transfer ? action.transfer.deposit : undefined;\n container.appendChild(text);\n container.appendChild(createTextContent(\"Deposit\", formatNearAmount(deposit, options)));\n return container;\n}\n\nfunction stakeContent(action, options) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following amount will be staked to the public key.\");\n const stake = action.stake || {};\n container.appendChild(text);\n container.appendChild(createTextContent(\"Stake\", formatNearAmount(stake.stake, options)));\n container.appendChild(createTextContent(\"Public Key\", formatPublicKey(stake.publicKey)));\n return container;\n}\n\nfunction addKeyContent(action) {\n const container = document.createElement(\"div\");\n const ak = action.addKey || {};\n const accessKey = ak.accessKey || {};\n const permission = accessKey.permission || {};\n\n if (permission.fullAccess !== undefined) {\n const warning = document.createElement(\"div\");\n warning.classList.add(\"warning-callout\");\n warning.textContent = \"This key will have full access to your account. Only approve if you trust the recipient.\";\n container.appendChild(warning);\n }\n\n container.appendChild(createDescription(\"By approving this request, the following access key will be added to your account.\"));\n container.appendChild(createTextContent(\"Public Key\", formatPublicKey(ak.publicKey)));\n container.appendChild(createTextContent(\"Nonce\", accessKey.nonce != null ? accessKey.nonce.toString() : \"\"));\n\n if (permission.fullAccess === undefined) {\n container.appendChild(createTextContent(\"Permission\", JSON.stringify(permission.functionCall, null, 2)));\n }\n return container;\n}\n\nfunction deleteKeyContent(action) {\n const container = document.createElement(\"div\");\n const dk = action.deleteKey || {};\n\n const warning = document.createElement(\"div\");\n warning.classList.add(\"warning-callout\");\n warning.textContent = \"Once deleted, the key cannot be recovered.\";\n\n container.appendChild(warning);\n container.appendChild(\n createDescription(\"This action will delete an access key from your account. Only approve if you trust the recipient.\"),\n );\n container.appendChild(createTextContent(\"Public Key\", formatPublicKey(dk.publicKey)));\n return container;\n}\n\nfunction deleteAccountContent(action) {\n const container = document.createElement(\"div\");\n\n const warning = document.createElement(\"div\");\n warning.classList.add(\"warning-callout\");\n warning.textContent = \"Once deleted, the account cannot be recovered.\";\n\n container.appendChild(warning);\n container.appendChild(createDescription(\"By approving this request, the account will be deleted.\"));\n container.appendChild(createTextContent(\"Beneficiary ID\", action.deleteAccount && action.deleteAccount.beneficiaryId));\n return container;\n}\n\nfunction signedDelegateContent(action) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following delegate action will be executed.\");\n\n const sd = action.signedDelegate || {};\n let serialized;\n try {\n serialized = JSON.stringify(\n sd.delegateAction,\n function (_, value) {\n return typeof value === \"bigint\" ? value.toString() : value;\n },\n 2,\n );\n } catch (e) {\n serialized = \"\";\n }\n container.appendChild(text);\n container.appendChild(createTextContent(\"Delegate Action\", serialized));\n return container;\n}\n\nfunction deployGlobalContractContent(action) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the compiled smart contract will be deployed.\");\n const codeLen =\n action.deployGlobalContract && action.deployGlobalContract.code ? `${action.deployGlobalContract.code.length} bytes` : null;\n container.appendChild(text);\n container.appendChild(createTextContent(\"Code\", codeLen));\n return container;\n}\n\nfunction useGlobalContractContent(action) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following global contract will be used.\");\n let identifier;\n try {\n identifier = JSON.stringify(action.useGlobalContract && action.useGlobalContract.contractIdentifier);\n } catch (e) {\n identifier = \"\";\n }\n container.appendChild(text);\n container.appendChild(createTextContent(\"Contract Identifier\", identifier));\n return container;\n}\n\nconst ACTION_DISPATCH = {\n createAccount: {\n label: \"CreateAccount\",\n warn: false,\n render: function () {\n return createAccountContent();\n },\n },\n deployContract: {\n label: \"DeployContract\",\n warn: true,\n render: function (a) {\n return deployContractContent(a);\n },\n },\n functionCall: {\n label: \"FunctionCall\",\n warn: false,\n render: function (a, opts) {\n return functionCallContent(a, opts);\n },\n },\n transfer: {\n label: \"Transfer\",\n warn: false,\n render: function (a, opts) {\n return transferContent(a, opts);\n },\n },\n stake: {\n label: \"Stake\",\n warn: false,\n render: function (a, opts) {\n return stakeContent(a, opts);\n },\n },\n addKey: {\n label: \"AddKey\",\n warn: false,\n render: function (a) {\n return addKeyContent(a);\n },\n warnsWhen: function (a) {\n return !!(\n a.addKey &&\n a.addKey.accessKey &&\n a.addKey.accessKey.permission &&\n a.addKey.accessKey.permission.fullAccess !== undefined\n );\n },\n },\n deleteKey: {\n label: \"DeleteKey\",\n warn: true,\n render: function (a) {\n return deleteKeyContent(a);\n },\n },\n deleteAccount: {\n label: \"DeleteAccount\",\n warn: true,\n render: function (a) {\n return deleteAccountContent(a);\n },\n },\n signedDelegate: {\n label: \"SignedDelegate\",\n warn: false,\n render: function (a) {\n return signedDelegateContent(a);\n },\n },\n deployGlobalContract: {\n label: \"DeployGlobalContract\",\n warn: true,\n render: function (a) {\n return deployGlobalContractContent(a);\n },\n },\n useGlobalContract: {\n label: \"UseGlobalContract\",\n warn: false,\n render: function (a) {\n return useGlobalContractContent(a);\n },\n },\n};\n\nfunction handleNearAction(action, options) {\n const actionKey = getActionType(action);\n const entry = actionKey ? ACTION_DISPATCH[actionKey] : null;\n if (!entry) {\n const unknown = document.createElement(\"div\");\n unknown.textContent = `Unknown action: ${actionKey || \"(empty)\"}`;\n return createAccordion(\"Unknown\", unknown, true);\n }\n const showWarning = entry.warnsWhen ? entry.warnsWhen(action) : !!entry.warn;\n return createAccordion(entry.label, entry.render(action, options), showWarning);\n}\n\n/**\n * Build the full details DOM tree for a form custom field.\n *\n * @param {object} params\n * @param {Array<{label: string, value: string|undefined}>} params.fields - top-level fields like Signer/Receiver/MaxBlockHeight.\n * @param {string} params.actions - JSON string with the actions array (as Auth0 form fields deliver them).\n * @param {{ showYoctoConversion?: boolean }} [params.options]\n */\nfunction renderDetails(params) {\n ensureBufferPolyfill();\n const box = document.createElement(\"div\");\n box.classList.add(\"box\");\n\n for (const field of params.fields || []) {\n if (field.value === undefined || field.value === null || field.value === \"\") continue;\n box.appendChild(createTextContent(field.label, field.value));\n }\n\n const actionsContainer = document.createElement(\"div\");\n actionsContainer.classList.add(\"actions-container\");\n const actionsLabel = document.createElement(\"div\");\n actionsLabel.classList.add(\"label\");\n actionsLabel.textContent = \"Actions\";\n actionsContainer.appendChild(actionsLabel);\n\n let parsedActions = [];\n try {\n parsedActions = JSON.parse(params.actions || \"[]\");\n } catch (e) {\n const errorNode = document.createElement(\"div\");\n errorNode.classList.add(\"warning-callout\");\n errorNode.textContent = \"Failed to parse actions payload.\";\n actionsContainer.appendChild(errorNode);\n box.appendChild(actionsContainer);\n return box;\n }\n\n for (const action of parsedActions) {\n actionsContainer.appendChild(handleNearAction(action, params.options));\n }\n\n box.appendChild(actionsContainer);\n return box;\n}\n\nvar __auth0FormHelpers = {\n ensureBufferPolyfill: ensureBufferPolyfill,\n base58Encode: base58Encode,\n yoctoToNear: yoctoToNear,\n formatNearAmount: formatNearAmount,\n decodeFunctionCallArgs: decodeFunctionCallArgs,\n formatPublicKey: formatPublicKey,\n getActionType: getActionType,\n createTextContent: createTextContent,\n createDescription: createDescription,\n createAccordion: createAccordion,\n createAccountContent: createAccountContent,\n deployContractContent: deployContractContent,\n functionCallContent: functionCallContent,\n transferContent: transferContent,\n stakeContent: stakeContent,\n addKeyContent: addKeyContent,\n deleteKeyContent: deleteKeyContent,\n deleteAccountContent: deleteAccountContent,\n signedDelegateContent: signedDelegateContent,\n deployGlobalContractContent: deployGlobalContractContent,\n useGlobalContractContent: useGlobalContractContent,\n handleNearAction: handleNearAction,\n renderDetails: renderDetails,\n};\n\nif (typeof module !== \"undefined\" && module.exports) {\n module.exports = __auth0FormHelpers;\n}\n\n// --- END __auth0FormHelpers ---\n\n\nreturn (\n/**\n * Custom field that renders two app avatars (left = our wallet, right = requesting app)\n * separated by a small circle with a transfer/icon glyph in the middle.\n *\n * Params (configured per-form in _form_base.json):\n * - leftImageUrl: URL for the left avatar (our wallet/identity logo)\n * - rightImageUrl: URL for the right avatar (the requesting app's logo, e.g. {{ fields.imageUrl }})\n * - iconUrl: URL for the center circle icon\n *\n * If `rightImageUrl` is empty/missing (e.g. the client has no logo_uri configured), the\n * right avatar is hidden so we don't render a broken-image placeholder.\n */\nfunction AuthorizeAppImage(context) {\n return {\n /** Invoked once when the field is created */\n init() {\n const layout = document.createElement(\"div\");\n layout.classList.add(\"layout\");\n\n const { leftImageUrl, rightImageUrl, iconUrl } = context.custom.getParams();\n\n const leftAvatar = document.createElement(\"img\");\n leftAvatar.classList.add(\"avatar\");\n leftAvatar.setAttribute(\"alt\", \"\");\n leftAvatar.setAttribute(\"src\", leftImageUrl);\n\n layout.appendChild(leftAvatar);\n\n const trimmedRight = (rightImageUrl || \"\").trim();\n if (trimmedRight) {\n const circle = document.createElement(\"div\");\n circle.classList.add(\"circle\");\n\n const icon = document.createElement(\"img\");\n icon.classList.add(\"icon\");\n icon.setAttribute(\"alt\", \"\");\n icon.setAttribute(\"src\", iconUrl);\n circle.appendChild(icon);\n\n const rightAvatar = document.createElement(\"img\");\n rightAvatar.classList.add(\"avatar\");\n rightAvatar.setAttribute(\"alt\", \"\");\n rightAvatar.setAttribute(\"src\", trimmedRight);\n\n layout.appendChild(circle);\n layout.appendChild(rightAvatar);\n }\n\n return layout;\n },\n\n /** Returns a list of URLs that the SDK guarantees that will loaded before init() is invoked */\n getScripts() {\n return [];\n },\n\n /** Invoked when field has to be blocked */\n block() {},\n\n /** Invoked when field has to be unblocked */\n unblock() {},\n\n /** Invoked when the SDK needs to get the value (possibly several times) */\n getValue() {},\n };\n}\n\n);\n})()\n", + "code": "(function () {\n// --- BEGIN __auth0FormHelpers (auto-inlined from shared/helpers/index.js) ---\n/**\n * Shared helpers for Auth0 form custom components.\n *\n * Runs in two environments:\n * - Auth0 form runtime: build.js inlines this file into each component's `config.code`,\n * exposing `__auth0FormHelpers` as a top-level variable in the same scope as the component.\n * - Node (tests / playground via require): consumed as a CommonJS module via module.exports.\n *\n * Keep this file free of `require` and ES imports so it can be concatenated as plain text.\n */\n\nfunction ensureBufferPolyfill() {\n if (typeof Buffer !== \"undefined\") return;\n if (typeof globalThis === \"undefined\") return;\n globalThis.Buffer = {\n from: function (data) {\n if (Array.isArray(data)) return new Uint8Array(data);\n if (typeof data === \"string\") return new TextEncoder().encode(data);\n return data;\n },\n };\n}\n\nconst BASE58_ALPHABET = \"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz\";\n\nfunction base58Encode(bytes) {\n let result = \"\";\n let num = BigInt(0);\n for (const byte of bytes) {\n num = (num << BigInt(8)) + BigInt(byte);\n }\n while (num > BigInt(0)) {\n const remainder = num % BigInt(58);\n num = num / BigInt(58);\n result = BASE58_ALPHABET[Number(remainder)] + result;\n }\n for (const byte of bytes) {\n if (byte === 0) result = BASE58_ALPHABET[0] + result;\n else break;\n }\n return result;\n}\n\n/**\n * Format a yocto-NEAR BigInt string as NEAR with up to 8 decimals.\n * \"1000000000000000000000000\" -> \"1\"\n * \"1500000000000000000000000\" -> \"1.5\"\n * \"1\" -> \"0.00000000\" trimmed -> \"0.0\"\n */\nfunction yoctoToNear(bigIntStr) {\n const trimmed = String(bigIntStr).replace(/^0+/, \"\") || \"0\";\n const length = trimmed.length;\n\n if (length <= 24) {\n const zerosNeeded = 24 - length;\n const withZeros = \"0\".repeat(zerosNeeded) + trimmed;\n const decimals = withZeros.slice(0, 8).replace(/0+$/, \"\") || \"0\";\n return `0.${decimals}`;\n }\n\n const splitPos = length - 24;\n const integerPart = trimmed.slice(0, splitPos);\n const decimalPart = trimmed.slice(splitPos, splitPos + 8);\n const trimmedDecimals = decimalPart.replace(/0+$/, \"\");\n if (trimmedDecimals === \"\") return integerPart;\n return `${integerPart}.${trimmedDecimals}`;\n}\n\nfunction formatPublicKey(publicKey) {\n if (publicKey && publicKey.secp256k1Key !== undefined) {\n return `secp256k1:${base58Encode(publicKey.secp256k1Key.data)}`;\n }\n if (publicKey && publicKey.ed25519Key !== undefined) {\n return `ed25519:${base58Encode(publicKey.ed25519Key.data)}`;\n }\n return \"\";\n}\n\n/**\n * Return the canonical action type discriminator (the single non-undefined key of the action).\n * Useful for dispatch and for tests that don't need DOM rendering.\n */\nfunction getActionType(action) {\n if (!action || typeof action !== \"object\") return null;\n for (const key of Object.keys(action)) {\n if (action[key] !== undefined) return key;\n }\n return null;\n}\n\n// --- DOM helpers (require document/window — jsdom in tests, real DOM in browser) ---\n\nfunction createTextContent(label, value, link = false) {\n const textContent = document.createElement(\"div\");\n textContent.classList.add(\"text-content\");\n\n const labelElement = document.createElement(\"div\");\n labelElement.classList.add(\"label\");\n labelElement.textContent = label;\n\n const valueElement = document.createElement(\"div\");\n valueElement.classList.add(\"value\");\n valueElement.textContent = value;\n\n if (link) {\n valueElement.classList.add(\"link\");\n valueElement.setAttribute(\"href\", link);\n valueElement.setAttribute(\"target\", \"_blank\");\n }\n\n textContent.appendChild(labelElement);\n textContent.appendChild(valueElement);\n return textContent;\n}\n\nfunction createDescription(text) {\n const node = document.createElement(\"p\");\n node.classList.add(\"action-description\");\n node.textContent = text;\n return node;\n}\n\nfunction createAccordion(label, content, showWarning = false) {\n const accordion = document.createElement(\"div\");\n accordion.classList.add(\"accordion\");\n\n const header = document.createElement(\"div\");\n header.classList.add(\"accordion-header\");\n\n const headerContent = document.createElement(\"div\");\n headerContent.classList.add(\"accordion-header-content\");\n\n const leftContent = document.createElement(\"div\");\n leftContent.classList.add(\"accordion-left-content\");\n\n if (showWarning) {\n const warningIcon = document.createElement(\"span\");\n warningIcon.classList.add(\"warning-icon\");\n warningIcon.innerHTML = \"⚠️\";\n leftContent.appendChild(warningIcon);\n }\n\n const labelElement = document.createElement(\"span\");\n labelElement.classList.add(\"accordion-header-label\");\n labelElement.textContent = label;\n leftContent.appendChild(labelElement);\n\n const expandIcon = document.createElement(\"span\");\n expandIcon.classList.add(\"expand-icon\");\n expandIcon.innerHTML = \"+\";\n\n headerContent.appendChild(leftContent);\n headerContent.appendChild(expandIcon);\n header.appendChild(headerContent);\n\n const contentElement = document.createElement(\"div\");\n contentElement.classList.add(\"accordion-content\");\n contentElement.appendChild(content);\n\n accordion.appendChild(header);\n accordion.appendChild(contentElement);\n\n header.addEventListener(\"click\", function () {\n contentElement.classList.toggle(\"open\");\n expandIcon.innerHTML = contentElement.classList.contains(\"open\") ? \"−\" : \"+\";\n });\n\n return accordion;\n}\n\n// --- Action content factories ---\n\nfunction createAccountContent() {\n return createDescription(\"By approving this request, a new account will be created.\");\n}\n\nfunction deployContractContent(action) {\n ensureBufferPolyfill();\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the compiled smart contract will be deployed.\");\n const codeLen = action.deployContract && action.deployContract.code ? `${action.deployContract.code.length} bytes` : null;\n container.appendChild(text);\n container.appendChild(createTextContent(\"Code\", codeLen));\n return container;\n}\n\n/**\n * Decode functionCall `args` (a byte array / Uint8Array) to a readable string. NEAR contract\n * args are almost always UTF-8 JSON, so decode the bytes and pretty-print when they parse as\n * JSON; otherwise fall back to the raw decoded text. TextDecoder is used directly because it\n * exists in the browser and the Auth0 form runtime (unlike Buffer, which the runtime lacks).\n */\nfunction decodeFunctionCallArgs(args) {\n if (args == null) return \"\";\n let text;\n try {\n if (typeof TextDecoder !== \"undefined\") {\n text = new TextDecoder().decode(Uint8Array.from(args));\n } else if (typeof Buffer !== \"undefined\" && typeof Buffer.from === \"function\") {\n text = Buffer.from(args).toString(\"utf8\");\n } else {\n text = String.fromCharCode.apply(null, Array.from(args));\n }\n } catch (e) {\n return \"\";\n }\n try {\n return JSON.stringify(JSON.parse(text), null, 2);\n } catch (e) {\n return text;\n }\n}\n\n/**\n * Format a yocto-NEAR amount for display. In transaction mode (showYoctoConversion) amounts at or\n * above 0.00000000001 NEAR are shown as NEAR; smaller amounts and delegate-action mode stay in\n * yoctoNEAR so no precision is hidden.\n */\nfunction formatNearAmount(value, options) {\n const showYoctoConversion = !!(options && options.showYoctoConversion);\n if (showYoctoConversion && value != null && BigInt(value) >= BigInt(\"10000000000000000\")) {\n return `${yoctoToNear(value.toString())} NEAR`;\n }\n return `${value != null ? value.toString() : \"0\"} yoctoNEAR`;\n}\n\nfunction functionCallContent(action, options) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following function will be called.\");\n\n const fc = action.functionCall || {};\n container.appendChild(text);\n container.appendChild(createTextContent(\"Method Name\", fc.methodName));\n container.appendChild(createTextContent(\"Args\", decodeFunctionCallArgs(fc.args)));\n container.appendChild(createTextContent(\"Gas\", fc.gas != null ? fc.gas.toString() : \"\"));\n container.appendChild(createTextContent(\"Deposit\", formatNearAmount(fc.deposit, options)));\n return container;\n}\n\nfunction transferContent(action, options) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following amount will be transferred to the receiver.\");\n\n const deposit = action.transfer ? action.transfer.deposit : undefined;\n container.appendChild(text);\n container.appendChild(createTextContent(\"Deposit\", formatNearAmount(deposit, options)));\n return container;\n}\n\nfunction stakeContent(action, options) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following amount will be staked to the public key.\");\n const stake = action.stake || {};\n container.appendChild(text);\n container.appendChild(createTextContent(\"Stake\", formatNearAmount(stake.stake, options)));\n container.appendChild(createTextContent(\"Public Key\", formatPublicKey(stake.publicKey)));\n return container;\n}\n\nfunction addKeyContent(action) {\n const container = document.createElement(\"div\");\n const ak = action.addKey || {};\n const accessKey = ak.accessKey || {};\n const permission = accessKey.permission || {};\n\n if (permission.fullAccess !== undefined) {\n const warning = document.createElement(\"div\");\n warning.classList.add(\"warning-callout\");\n warning.textContent = \"This key will have full access to your account. Only approve if you trust the recipient.\";\n container.appendChild(warning);\n }\n\n container.appendChild(createDescription(\"By approving this request, the following access key will be added to your account.\"));\n container.appendChild(createTextContent(\"Public Key\", formatPublicKey(ak.publicKey)));\n container.appendChild(createTextContent(\"Nonce\", accessKey.nonce != null ? accessKey.nonce.toString() : \"\"));\n\n if (permission.fullAccess === undefined) {\n container.appendChild(createTextContent(\"Permission\", JSON.stringify(permission.functionCall, null, 2)));\n }\n return container;\n}\n\nfunction deleteKeyContent(action) {\n const container = document.createElement(\"div\");\n const dk = action.deleteKey || {};\n\n const warning = document.createElement(\"div\");\n warning.classList.add(\"warning-callout\");\n warning.textContent = \"Once deleted, the key cannot be recovered.\";\n\n container.appendChild(warning);\n container.appendChild(\n createDescription(\"This action will delete an access key from your account. Only approve if you trust the recipient.\"),\n );\n container.appendChild(createTextContent(\"Public Key\", formatPublicKey(dk.publicKey)));\n return container;\n}\n\nfunction deleteAccountContent(action) {\n const container = document.createElement(\"div\");\n\n const warning = document.createElement(\"div\");\n warning.classList.add(\"warning-callout\");\n warning.textContent = \"Once deleted, the account cannot be recovered.\";\n\n container.appendChild(warning);\n container.appendChild(createDescription(\"By approving this request, the account will be deleted.\"));\n container.appendChild(createTextContent(\"Beneficiary ID\", action.deleteAccount && action.deleteAccount.beneficiaryId));\n return container;\n}\n\nfunction signedDelegateContent(action) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following delegate action will be executed.\");\n\n const sd = action.signedDelegate || {};\n let serialized;\n try {\n serialized = JSON.stringify(\n sd.delegateAction,\n function (_, value) {\n return typeof value === \"bigint\" ? value.toString() : value;\n },\n 2,\n );\n } catch (e) {\n serialized = \"\";\n }\n container.appendChild(text);\n container.appendChild(createTextContent(\"Delegate Action\", serialized));\n return container;\n}\n\nfunction deployGlobalContractContent(action) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the compiled smart contract will be deployed.\");\n const codeLen =\n action.deployGlobalContract && action.deployGlobalContract.code ? `${action.deployGlobalContract.code.length} bytes` : null;\n container.appendChild(text);\n container.appendChild(createTextContent(\"Code\", codeLen));\n return container;\n}\n\nfunction useGlobalContractContent(action) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following global contract will be used.\");\n let identifier;\n try {\n identifier = JSON.stringify(action.useGlobalContract && action.useGlobalContract.contractIdentifier);\n } catch (e) {\n identifier = \"\";\n }\n container.appendChild(text);\n container.appendChild(createTextContent(\"Contract Identifier\", identifier));\n return container;\n}\n\nconst ACTION_DISPATCH = {\n createAccount: {\n label: \"CreateAccount\",\n warn: false,\n render: function () {\n return createAccountContent();\n },\n },\n deployContract: {\n label: \"DeployContract\",\n warn: true,\n render: function (a) {\n return deployContractContent(a);\n },\n },\n functionCall: {\n label: \"FunctionCall\",\n warn: false,\n render: function (a, opts) {\n return functionCallContent(a, opts);\n },\n },\n transfer: {\n label: \"Transfer\",\n warn: false,\n render: function (a, opts) {\n return transferContent(a, opts);\n },\n },\n stake: {\n label: \"Stake\",\n warn: false,\n render: function (a, opts) {\n return stakeContent(a, opts);\n },\n },\n addKey: {\n label: \"AddKey\",\n warn: false,\n render: function (a) {\n return addKeyContent(a);\n },\n warnsWhen: function (a) {\n return !!(\n a.addKey &&\n a.addKey.accessKey &&\n a.addKey.accessKey.permission &&\n a.addKey.accessKey.permission.fullAccess !== undefined\n );\n },\n },\n deleteKey: {\n label: \"DeleteKey\",\n warn: true,\n render: function (a) {\n return deleteKeyContent(a);\n },\n },\n deleteAccount: {\n label: \"DeleteAccount\",\n warn: true,\n render: function (a) {\n return deleteAccountContent(a);\n },\n },\n signedDelegate: {\n label: \"SignedDelegate\",\n warn: false,\n render: function (a) {\n return signedDelegateContent(a);\n },\n },\n deployGlobalContract: {\n label: \"DeployGlobalContract\",\n warn: true,\n render: function (a) {\n return deployGlobalContractContent(a);\n },\n },\n useGlobalContract: {\n label: \"UseGlobalContract\",\n warn: false,\n render: function (a) {\n return useGlobalContractContent(a);\n },\n },\n};\n\nfunction handleNearAction(action, options) {\n const actionKey = getActionType(action);\n const entry = actionKey ? ACTION_DISPATCH[actionKey] : null;\n if (!entry) {\n const unknown = document.createElement(\"div\");\n unknown.textContent = `Unknown action: ${actionKey || \"(empty)\"}`;\n return createAccordion(\"Unknown\", unknown, true);\n }\n const showWarning = entry.warnsWhen ? entry.warnsWhen(action) : !!entry.warn;\n return createAccordion(entry.label, entry.render(action, options), showWarning);\n}\n\n/**\n * Build the full details DOM tree for a form custom field.\n *\n * @param {object} params\n * @param {Array<{label: string, value: string|undefined}>} params.fields - top-level fields like Signer/Receiver/MaxBlockHeight.\n * @param {string} params.actions - JSON string with the actions array (as Auth0 form fields deliver them).\n * @param {{ showYoctoConversion?: boolean }} [params.options]\n */\nfunction renderDetails(params) {\n ensureBufferPolyfill();\n const box = document.createElement(\"div\");\n box.classList.add(\"box\");\n\n for (const field of params.fields || []) {\n if (field.value === undefined || field.value === null || field.value === \"\") continue;\n box.appendChild(createTextContent(field.label, field.value));\n }\n\n const actionsContainer = document.createElement(\"div\");\n actionsContainer.classList.add(\"actions-container\");\n const actionsLabel = document.createElement(\"div\");\n actionsLabel.classList.add(\"label\");\n actionsLabel.textContent = \"Actions\";\n actionsContainer.appendChild(actionsLabel);\n\n let parsedActions = [];\n try {\n parsedActions = JSON.parse(params.actions || \"[]\");\n } catch (e) {\n const errorNode = document.createElement(\"div\");\n errorNode.classList.add(\"warning-callout\");\n errorNode.textContent = \"Failed to parse actions payload.\";\n actionsContainer.appendChild(errorNode);\n box.appendChild(actionsContainer);\n return box;\n }\n\n for (const action of parsedActions) {\n actionsContainer.appendChild(handleNearAction(action, params.options));\n }\n\n box.appendChild(actionsContainer);\n return box;\n}\n\n// --- NEP-413 intent content factories ---\n\n/**\n * Render the token map of a transfer intent (`{ \"\": \"\" }`).\n *\n * Amounts stay in the token's smallest unit: the form has no token metadata, so converting\n * would mean guessing decimals — and a wrong guess here understates what the user is\n * approving. Showing the raw amount alongside its token id is honest and unambiguous.\n */\nfunction intentTokensContent(tokens) {\n const container = document.createElement(\"div\");\n if (!tokens || typeof tokens !== \"object\") return container;\n for (const tokenId of Object.keys(tokens)) {\n container.appendChild(createTextContent(tokenId, String(tokens[tokenId])));\n }\n return container;\n}\n\nfunction transferIntentContent(intent) {\n const container = document.createElement(\"div\");\n container.appendChild(createDescription(\"By approving this request, the following tokens will be transferred.\"));\n container.appendChild(createTextContent(\"Receiver ID\", intent.receiver_id));\n\n const tokensLabel = document.createElement(\"div\");\n tokensLabel.classList.add(\"label\");\n tokensLabel.textContent = \"Tokens\";\n container.appendChild(tokensLabel);\n container.appendChild(intentTokensContent(intent.tokens));\n return container;\n}\n\n/**\n * Fallback for intent kinds this form does not model explicitly. It is deliberately shown\n * with a warning: the user is approving something the UI cannot describe in plain terms.\n */\nfunction unknownIntentContent(intent) {\n const container = document.createElement(\"div\");\n container.appendChild(createDescription(\"This request contains an intent type this app does not recognize. Review it carefully.\"));\n let serialized;\n try {\n serialized = JSON.stringify(intent, null, 2);\n } catch (e) {\n serialized = \"\";\n }\n container.appendChild(createTextContent(\"Intent\", serialized));\n return container;\n}\n\nconst INTENT_DISPATCH = {\n transfer: {\n label: \"Transfer\",\n warn: false,\n render: function (i) {\n return transferIntentContent(i);\n },\n },\n};\n\nfunction handleIntent(intent) {\n const kind = intent && typeof intent === \"object\" ? intent.intent : null;\n const entry = kind ? INTENT_DISPATCH[kind] : null;\n if (!entry) {\n return createAccordion(kind ? `Unknown: ${kind}` : \"Unknown\", unknownIntentContent(intent), true);\n }\n return createAccordion(entry.label, entry.render(intent), !!entry.warn);\n}\n\n/**\n * Render the message body of a NEP-413 request verbatim.\n *\n * This is the default view, and the one that matters most: NEP-413 messages are arbitrary\n * strings meant to be read by a human (\"Sign in to example.com\"), so showing the text exactly\n * as it will be signed is the honest presentation. Pretty-printing is applied only when the\n * message is JSON, purely for legibility.\n */\nfunction messageContent(message) {\n const container = document.createElement(\"div\");\n let display = message;\n try {\n const parsed = JSON.parse(message);\n if (parsed && typeof parsed === \"object\") display = JSON.stringify(parsed, null, 2);\n } catch (e) {\n display = message;\n }\n container.appendChild(createTextContent(\"Message\", display));\n return container;\n}\n\n/**\n * Build the details DOM tree for a NEP-413 signature approval.\n *\n * Two presentations, one payload: when the message carries NEAR Intents the intents are broken\n * out one by one, and otherwise the raw message is shown as text. Either way the top-level\n * fields — recipient above all — are rendered, because under NEP-413 the recipient is what\n * tells the user which application their signature is addressed to.\n * @param {object} params The render parameters.\n * @param {Array<{label: string, value: string|undefined}>} params.fields Top-level fields (recipient, callback URL, signer, deadline).\n * @param {string} params.message The raw NEP-413 message, shown when there are no intents to break out.\n * @param {string} [params.intents] JSON string with the intents array, when the message is a NEAR Intents body.\n * @returns {HTMLElement} The details node.\n */\nfunction renderNep413Details(params) {\n ensureBufferPolyfill();\n const box = document.createElement(\"div\");\n box.classList.add(\"box\");\n\n for (const field of params.fields || []) {\n if (field.value === undefined || field.value === null || field.value === \"\") continue;\n box.appendChild(createTextContent(field.label, field.value));\n }\n\n let parsedIntents = null;\n if (params.intents) {\n try {\n const candidate = JSON.parse(params.intents);\n if (Array.isArray(candidate) && candidate.length > 0) parsedIntents = candidate;\n } catch (e) {\n parsedIntents = null;\n }\n }\n\n // No intents to break out (or they were unreadable) — show the message itself. Falling back\n // to the raw text keeps every NEP-413 message approvable, not just the ones we model.\n if (!parsedIntents) {\n box.appendChild(messageContent(params.message || \"\"));\n return box;\n }\n\n const intentsContainer = document.createElement(\"div\");\n intentsContainer.classList.add(\"actions-container\");\n const intentsLabel = document.createElement(\"div\");\n intentsLabel.classList.add(\"label\");\n intentsLabel.textContent = \"Intents\";\n intentsContainer.appendChild(intentsLabel);\n\n for (const intent of parsedIntents) {\n intentsContainer.appendChild(handleIntent(intent));\n }\n\n box.appendChild(intentsContainer);\n return box;\n}\n\nvar __auth0FormHelpers = {\n ensureBufferPolyfill: ensureBufferPolyfill,\n base58Encode: base58Encode,\n yoctoToNear: yoctoToNear,\n formatNearAmount: formatNearAmount,\n decodeFunctionCallArgs: decodeFunctionCallArgs,\n formatPublicKey: formatPublicKey,\n getActionType: getActionType,\n createTextContent: createTextContent,\n createDescription: createDescription,\n createAccordion: createAccordion,\n createAccountContent: createAccountContent,\n deployContractContent: deployContractContent,\n functionCallContent: functionCallContent,\n transferContent: transferContent,\n stakeContent: stakeContent,\n addKeyContent: addKeyContent,\n deleteKeyContent: deleteKeyContent,\n deleteAccountContent: deleteAccountContent,\n signedDelegateContent: signedDelegateContent,\n deployGlobalContractContent: deployGlobalContractContent,\n useGlobalContractContent: useGlobalContractContent,\n handleNearAction: handleNearAction,\n renderDetails: renderDetails,\n intentTokensContent: intentTokensContent,\n transferIntentContent: transferIntentContent,\n unknownIntentContent: unknownIntentContent,\n handleIntent: handleIntent,\n messageContent: messageContent,\n renderNep413Details: renderNep413Details,\n};\n\nif (typeof module !== \"undefined\" && module.exports) {\n module.exports = __auth0FormHelpers;\n}\n\n// --- END __auth0FormHelpers ---\n\n\nreturn (\n/**\n * Custom field that renders two app avatars (left = our wallet, right = requesting app)\n * separated by a small circle with a transfer/icon glyph in the middle.\n *\n * Params (configured per-form in _form_base.json):\n * - leftImageUrl: URL for the left avatar (our wallet/identity logo)\n * - rightImageUrl: URL for the right avatar (the requesting app's logo, e.g. {{ fields.imageUrl }})\n * - iconUrl: URL for the center circle icon\n *\n * If `rightImageUrl` is empty/missing (e.g. the client has no logo_uri configured), the\n * right avatar is hidden so we don't render a broken-image placeholder.\n */\nfunction AuthorizeAppImage(context) {\n return {\n /** Invoked once when the field is created */\n init() {\n const layout = document.createElement(\"div\");\n layout.classList.add(\"layout\");\n\n const { leftImageUrl, rightImageUrl, iconUrl } = context.custom.getParams();\n\n const leftAvatar = document.createElement(\"img\");\n leftAvatar.classList.add(\"avatar\");\n leftAvatar.setAttribute(\"alt\", \"\");\n leftAvatar.setAttribute(\"src\", leftImageUrl);\n\n layout.appendChild(leftAvatar);\n\n const trimmedRight = (rightImageUrl || \"\").trim();\n if (trimmedRight) {\n const circle = document.createElement(\"div\");\n circle.classList.add(\"circle\");\n\n const icon = document.createElement(\"img\");\n icon.classList.add(\"icon\");\n icon.setAttribute(\"alt\", \"\");\n icon.setAttribute(\"src\", iconUrl);\n circle.appendChild(icon);\n\n const rightAvatar = document.createElement(\"img\");\n rightAvatar.classList.add(\"avatar\");\n rightAvatar.setAttribute(\"alt\", \"\");\n rightAvatar.setAttribute(\"src\", trimmedRight);\n\n layout.appendChild(circle);\n layout.appendChild(rightAvatar);\n }\n\n return layout;\n },\n\n /** Returns a list of URLs that the SDK guarantees that will loaded before init() is invoked */\n getScripts() {\n return [];\n },\n\n /** Invoked when field has to be blocked */\n block() {},\n\n /** Invoked when field has to be unblocked */\n unblock() {},\n\n /** Invoked when the SDK needs to get the value (possibly several times) */\n getValue() {},\n };\n}\n\n);\n})()\n", "css": ".avatar {\n width: 4.5rem;\n height: 4.5rem;\n border-radius: 12px;\n object-fit: cover;\n}\n\n.layout {\n position: relative;\n display: flex;\n flex-direction: row;\n gap: 1.5rem;\n align-items: center;\n justify-content: center;\n}\n\n.circle {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n width: 2.5rem;\n height: 2.5rem;\n border-radius: 999px;\n background-color: #f6f6f6;\n z-index: 1;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.icon {\n width: 1.2rem;\n height: 1.2rem;\n}\n", "params": { "iconUrl": "https://peersyst-public-production.s3.eu-west-1.amazonaws.com/1b54f479-7990-4834-9b37-95b26e2023fb.png", @@ -49,7 +49,7 @@ "sensitive": false, "config": { "schema": {}, - "code": "(function () {\n// --- BEGIN __auth0FormHelpers (auto-inlined from shared/helpers/index.js) ---\n/**\n * Shared helpers for Auth0 form custom components.\n *\n * Runs in two environments:\n * - Auth0 form runtime: build.js inlines this file into each component's `config.code`,\n * exposing `__auth0FormHelpers` as a top-level variable in the same scope as the component.\n * - Node (tests / playground via require): consumed as a CommonJS module via module.exports.\n *\n * Keep this file free of `require` and ES imports so it can be concatenated as plain text.\n */\n\nfunction ensureBufferPolyfill() {\n if (typeof Buffer !== \"undefined\") return;\n if (typeof globalThis === \"undefined\") return;\n globalThis.Buffer = {\n from: function (data) {\n if (Array.isArray(data)) return new Uint8Array(data);\n if (typeof data === \"string\") return new TextEncoder().encode(data);\n return data;\n },\n };\n}\n\nconst BASE58_ALPHABET = \"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz\";\n\nfunction base58Encode(bytes) {\n let result = \"\";\n let num = BigInt(0);\n for (const byte of bytes) {\n num = (num << BigInt(8)) + BigInt(byte);\n }\n while (num > BigInt(0)) {\n const remainder = num % BigInt(58);\n num = num / BigInt(58);\n result = BASE58_ALPHABET[Number(remainder)] + result;\n }\n for (const byte of bytes) {\n if (byte === 0) result = BASE58_ALPHABET[0] + result;\n else break;\n }\n return result;\n}\n\n/**\n * Format a yocto-NEAR BigInt string as NEAR with up to 8 decimals.\n * \"1000000000000000000000000\" -> \"1\"\n * \"1500000000000000000000000\" -> \"1.5\"\n * \"1\" -> \"0.00000000\" trimmed -> \"0.0\"\n */\nfunction yoctoToNear(bigIntStr) {\n const trimmed = String(bigIntStr).replace(/^0+/, \"\") || \"0\";\n const length = trimmed.length;\n\n if (length <= 24) {\n const zerosNeeded = 24 - length;\n const withZeros = \"0\".repeat(zerosNeeded) + trimmed;\n const decimals = withZeros.slice(0, 8).replace(/0+$/, \"\") || \"0\";\n return `0.${decimals}`;\n }\n\n const splitPos = length - 24;\n const integerPart = trimmed.slice(0, splitPos);\n const decimalPart = trimmed.slice(splitPos, splitPos + 8);\n const trimmedDecimals = decimalPart.replace(/0+$/, \"\");\n if (trimmedDecimals === \"\") return integerPart;\n return `${integerPart}.${trimmedDecimals}`;\n}\n\nfunction formatPublicKey(publicKey) {\n if (publicKey && publicKey.secp256k1Key !== undefined) {\n return `secp256k1:${base58Encode(publicKey.secp256k1Key.data)}`;\n }\n if (publicKey && publicKey.ed25519Key !== undefined) {\n return `ed25519:${base58Encode(publicKey.ed25519Key.data)}`;\n }\n return \"\";\n}\n\n/**\n * Return the canonical action type discriminator (the single non-undefined key of the action).\n * Useful for dispatch and for tests that don't need DOM rendering.\n */\nfunction getActionType(action) {\n if (!action || typeof action !== \"object\") return null;\n for (const key of Object.keys(action)) {\n if (action[key] !== undefined) return key;\n }\n return null;\n}\n\n// --- DOM helpers (require document/window — jsdom in tests, real DOM in browser) ---\n\nfunction createTextContent(label, value, link = false) {\n const textContent = document.createElement(\"div\");\n textContent.classList.add(\"text-content\");\n\n const labelElement = document.createElement(\"div\");\n labelElement.classList.add(\"label\");\n labelElement.textContent = label;\n\n const valueElement = document.createElement(\"div\");\n valueElement.classList.add(\"value\");\n valueElement.textContent = value;\n\n if (link) {\n valueElement.classList.add(\"link\");\n valueElement.setAttribute(\"href\", link);\n valueElement.setAttribute(\"target\", \"_blank\");\n }\n\n textContent.appendChild(labelElement);\n textContent.appendChild(valueElement);\n return textContent;\n}\n\nfunction createDescription(text) {\n const node = document.createElement(\"p\");\n node.classList.add(\"action-description\");\n node.textContent = text;\n return node;\n}\n\nfunction createAccordion(label, content, showWarning = false) {\n const accordion = document.createElement(\"div\");\n accordion.classList.add(\"accordion\");\n\n const header = document.createElement(\"div\");\n header.classList.add(\"accordion-header\");\n\n const headerContent = document.createElement(\"div\");\n headerContent.classList.add(\"accordion-header-content\");\n\n const leftContent = document.createElement(\"div\");\n leftContent.classList.add(\"accordion-left-content\");\n\n if (showWarning) {\n const warningIcon = document.createElement(\"span\");\n warningIcon.classList.add(\"warning-icon\");\n warningIcon.innerHTML = \"⚠️\";\n leftContent.appendChild(warningIcon);\n }\n\n const labelElement = document.createElement(\"span\");\n labelElement.classList.add(\"accordion-header-label\");\n labelElement.textContent = label;\n leftContent.appendChild(labelElement);\n\n const expandIcon = document.createElement(\"span\");\n expandIcon.classList.add(\"expand-icon\");\n expandIcon.innerHTML = \"+\";\n\n headerContent.appendChild(leftContent);\n headerContent.appendChild(expandIcon);\n header.appendChild(headerContent);\n\n const contentElement = document.createElement(\"div\");\n contentElement.classList.add(\"accordion-content\");\n contentElement.appendChild(content);\n\n accordion.appendChild(header);\n accordion.appendChild(contentElement);\n\n header.addEventListener(\"click\", function () {\n contentElement.classList.toggle(\"open\");\n expandIcon.innerHTML = contentElement.classList.contains(\"open\") ? \"−\" : \"+\";\n });\n\n return accordion;\n}\n\n// --- Action content factories ---\n\nfunction createAccountContent() {\n return createDescription(\"By approving this request, a new account will be created.\");\n}\n\nfunction deployContractContent(action) {\n ensureBufferPolyfill();\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the compiled smart contract will be deployed.\");\n const codeLen = action.deployContract && action.deployContract.code ? `${action.deployContract.code.length} bytes` : null;\n container.appendChild(text);\n container.appendChild(createTextContent(\"Code\", codeLen));\n return container;\n}\n\n/**\n * Decode functionCall `args` (a byte array / Uint8Array) to a readable string. NEAR contract\n * args are almost always UTF-8 JSON, so decode the bytes and pretty-print when they parse as\n * JSON; otherwise fall back to the raw decoded text. TextDecoder is used directly because it\n * exists in the browser and the Auth0 form runtime (unlike Buffer, which the runtime lacks).\n */\nfunction decodeFunctionCallArgs(args) {\n if (args == null) return \"\";\n let text;\n try {\n if (typeof TextDecoder !== \"undefined\") {\n text = new TextDecoder().decode(Uint8Array.from(args));\n } else if (typeof Buffer !== \"undefined\" && typeof Buffer.from === \"function\") {\n text = Buffer.from(args).toString(\"utf8\");\n } else {\n text = String.fromCharCode.apply(null, Array.from(args));\n }\n } catch (e) {\n return \"\";\n }\n try {\n return JSON.stringify(JSON.parse(text), null, 2);\n } catch (e) {\n return text;\n }\n}\n\n/**\n * Format a yocto-NEAR amount for display. In transaction mode (showYoctoConversion) amounts at or\n * above 0.00000000001 NEAR are shown as NEAR; smaller amounts and delegate-action mode stay in\n * yoctoNEAR so no precision is hidden.\n */\nfunction formatNearAmount(value, options) {\n const showYoctoConversion = !!(options && options.showYoctoConversion);\n if (showYoctoConversion && value != null && BigInt(value) >= BigInt(\"10000000000000000\")) {\n return `${yoctoToNear(value.toString())} NEAR`;\n }\n return `${value != null ? value.toString() : \"0\"} yoctoNEAR`;\n}\n\nfunction functionCallContent(action, options) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following function will be called.\");\n\n const fc = action.functionCall || {};\n container.appendChild(text);\n container.appendChild(createTextContent(\"Method Name\", fc.methodName));\n container.appendChild(createTextContent(\"Args\", decodeFunctionCallArgs(fc.args)));\n container.appendChild(createTextContent(\"Gas\", fc.gas != null ? fc.gas.toString() : \"\"));\n container.appendChild(createTextContent(\"Deposit\", formatNearAmount(fc.deposit, options)));\n return container;\n}\n\nfunction transferContent(action, options) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following amount will be transferred to the receiver.\");\n\n const deposit = action.transfer ? action.transfer.deposit : undefined;\n container.appendChild(text);\n container.appendChild(createTextContent(\"Deposit\", formatNearAmount(deposit, options)));\n return container;\n}\n\nfunction stakeContent(action, options) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following amount will be staked to the public key.\");\n const stake = action.stake || {};\n container.appendChild(text);\n container.appendChild(createTextContent(\"Stake\", formatNearAmount(stake.stake, options)));\n container.appendChild(createTextContent(\"Public Key\", formatPublicKey(stake.publicKey)));\n return container;\n}\n\nfunction addKeyContent(action) {\n const container = document.createElement(\"div\");\n const ak = action.addKey || {};\n const accessKey = ak.accessKey || {};\n const permission = accessKey.permission || {};\n\n if (permission.fullAccess !== undefined) {\n const warning = document.createElement(\"div\");\n warning.classList.add(\"warning-callout\");\n warning.textContent = \"This key will have full access to your account. Only approve if you trust the recipient.\";\n container.appendChild(warning);\n }\n\n container.appendChild(createDescription(\"By approving this request, the following access key will be added to your account.\"));\n container.appendChild(createTextContent(\"Public Key\", formatPublicKey(ak.publicKey)));\n container.appendChild(createTextContent(\"Nonce\", accessKey.nonce != null ? accessKey.nonce.toString() : \"\"));\n\n if (permission.fullAccess === undefined) {\n container.appendChild(createTextContent(\"Permission\", JSON.stringify(permission.functionCall, null, 2)));\n }\n return container;\n}\n\nfunction deleteKeyContent(action) {\n const container = document.createElement(\"div\");\n const dk = action.deleteKey || {};\n\n const warning = document.createElement(\"div\");\n warning.classList.add(\"warning-callout\");\n warning.textContent = \"Once deleted, the key cannot be recovered.\";\n\n container.appendChild(warning);\n container.appendChild(\n createDescription(\"This action will delete an access key from your account. Only approve if you trust the recipient.\"),\n );\n container.appendChild(createTextContent(\"Public Key\", formatPublicKey(dk.publicKey)));\n return container;\n}\n\nfunction deleteAccountContent(action) {\n const container = document.createElement(\"div\");\n\n const warning = document.createElement(\"div\");\n warning.classList.add(\"warning-callout\");\n warning.textContent = \"Once deleted, the account cannot be recovered.\";\n\n container.appendChild(warning);\n container.appendChild(createDescription(\"By approving this request, the account will be deleted.\"));\n container.appendChild(createTextContent(\"Beneficiary ID\", action.deleteAccount && action.deleteAccount.beneficiaryId));\n return container;\n}\n\nfunction signedDelegateContent(action) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following delegate action will be executed.\");\n\n const sd = action.signedDelegate || {};\n let serialized;\n try {\n serialized = JSON.stringify(\n sd.delegateAction,\n function (_, value) {\n return typeof value === \"bigint\" ? value.toString() : value;\n },\n 2,\n );\n } catch (e) {\n serialized = \"\";\n }\n container.appendChild(text);\n container.appendChild(createTextContent(\"Delegate Action\", serialized));\n return container;\n}\n\nfunction deployGlobalContractContent(action) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the compiled smart contract will be deployed.\");\n const codeLen =\n action.deployGlobalContract && action.deployGlobalContract.code ? `${action.deployGlobalContract.code.length} bytes` : null;\n container.appendChild(text);\n container.appendChild(createTextContent(\"Code\", codeLen));\n return container;\n}\n\nfunction useGlobalContractContent(action) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following global contract will be used.\");\n let identifier;\n try {\n identifier = JSON.stringify(action.useGlobalContract && action.useGlobalContract.contractIdentifier);\n } catch (e) {\n identifier = \"\";\n }\n container.appendChild(text);\n container.appendChild(createTextContent(\"Contract Identifier\", identifier));\n return container;\n}\n\nconst ACTION_DISPATCH = {\n createAccount: {\n label: \"CreateAccount\",\n warn: false,\n render: function () {\n return createAccountContent();\n },\n },\n deployContract: {\n label: \"DeployContract\",\n warn: true,\n render: function (a) {\n return deployContractContent(a);\n },\n },\n functionCall: {\n label: \"FunctionCall\",\n warn: false,\n render: function (a, opts) {\n return functionCallContent(a, opts);\n },\n },\n transfer: {\n label: \"Transfer\",\n warn: false,\n render: function (a, opts) {\n return transferContent(a, opts);\n },\n },\n stake: {\n label: \"Stake\",\n warn: false,\n render: function (a, opts) {\n return stakeContent(a, opts);\n },\n },\n addKey: {\n label: \"AddKey\",\n warn: false,\n render: function (a) {\n return addKeyContent(a);\n },\n warnsWhen: function (a) {\n return !!(\n a.addKey &&\n a.addKey.accessKey &&\n a.addKey.accessKey.permission &&\n a.addKey.accessKey.permission.fullAccess !== undefined\n );\n },\n },\n deleteKey: {\n label: \"DeleteKey\",\n warn: true,\n render: function (a) {\n return deleteKeyContent(a);\n },\n },\n deleteAccount: {\n label: \"DeleteAccount\",\n warn: true,\n render: function (a) {\n return deleteAccountContent(a);\n },\n },\n signedDelegate: {\n label: \"SignedDelegate\",\n warn: false,\n render: function (a) {\n return signedDelegateContent(a);\n },\n },\n deployGlobalContract: {\n label: \"DeployGlobalContract\",\n warn: true,\n render: function (a) {\n return deployGlobalContractContent(a);\n },\n },\n useGlobalContract: {\n label: \"UseGlobalContract\",\n warn: false,\n render: function (a) {\n return useGlobalContractContent(a);\n },\n },\n};\n\nfunction handleNearAction(action, options) {\n const actionKey = getActionType(action);\n const entry = actionKey ? ACTION_DISPATCH[actionKey] : null;\n if (!entry) {\n const unknown = document.createElement(\"div\");\n unknown.textContent = `Unknown action: ${actionKey || \"(empty)\"}`;\n return createAccordion(\"Unknown\", unknown, true);\n }\n const showWarning = entry.warnsWhen ? entry.warnsWhen(action) : !!entry.warn;\n return createAccordion(entry.label, entry.render(action, options), showWarning);\n}\n\n/**\n * Build the full details DOM tree for a form custom field.\n *\n * @param {object} params\n * @param {Array<{label: string, value: string|undefined}>} params.fields - top-level fields like Signer/Receiver/MaxBlockHeight.\n * @param {string} params.actions - JSON string with the actions array (as Auth0 form fields deliver them).\n * @param {{ showYoctoConversion?: boolean }} [params.options]\n */\nfunction renderDetails(params) {\n ensureBufferPolyfill();\n const box = document.createElement(\"div\");\n box.classList.add(\"box\");\n\n for (const field of params.fields || []) {\n if (field.value === undefined || field.value === null || field.value === \"\") continue;\n box.appendChild(createTextContent(field.label, field.value));\n }\n\n const actionsContainer = document.createElement(\"div\");\n actionsContainer.classList.add(\"actions-container\");\n const actionsLabel = document.createElement(\"div\");\n actionsLabel.classList.add(\"label\");\n actionsLabel.textContent = \"Actions\";\n actionsContainer.appendChild(actionsLabel);\n\n let parsedActions = [];\n try {\n parsedActions = JSON.parse(params.actions || \"[]\");\n } catch (e) {\n const errorNode = document.createElement(\"div\");\n errorNode.classList.add(\"warning-callout\");\n errorNode.textContent = \"Failed to parse actions payload.\";\n actionsContainer.appendChild(errorNode);\n box.appendChild(actionsContainer);\n return box;\n }\n\n for (const action of parsedActions) {\n actionsContainer.appendChild(handleNearAction(action, params.options));\n }\n\n box.appendChild(actionsContainer);\n return box;\n}\n\nvar __auth0FormHelpers = {\n ensureBufferPolyfill: ensureBufferPolyfill,\n base58Encode: base58Encode,\n yoctoToNear: yoctoToNear,\n formatNearAmount: formatNearAmount,\n decodeFunctionCallArgs: decodeFunctionCallArgs,\n formatPublicKey: formatPublicKey,\n getActionType: getActionType,\n createTextContent: createTextContent,\n createDescription: createDescription,\n createAccordion: createAccordion,\n createAccountContent: createAccountContent,\n deployContractContent: deployContractContent,\n functionCallContent: functionCallContent,\n transferContent: transferContent,\n stakeContent: stakeContent,\n addKeyContent: addKeyContent,\n deleteKeyContent: deleteKeyContent,\n deleteAccountContent: deleteAccountContent,\n signedDelegateContent: signedDelegateContent,\n deployGlobalContractContent: deployGlobalContractContent,\n useGlobalContractContent: useGlobalContractContent,\n handleNearAction: handleNearAction,\n renderDetails: renderDetails,\n};\n\nif (typeof module !== \"undefined\" && module.exports) {\n module.exports = __auth0FormHelpers;\n}\n\n// --- END __auth0FormHelpers ---\n\n\nreturn (\n/**\n * Custom field that renders the transaction details (signer, receiver, actions).\n *\n * Depends on `__auth0FormHelpers`, which is inlined at build time by build.js\n * (or shimmed by the playground via helpers-shim.js).\n */\nfunction AuthorizeAppTransactionDetails(context) {\n return {\n init: function () {\n const params = context.custom.getParams();\n return __auth0FormHelpers.renderDetails({\n fields: [\n { label: \"Signer ID\", value: params.signerId },\n { label: \"Receiver ID\", value: params.receiverId },\n ],\n actions: params.actions,\n options: { showYoctoConversion: true },\n });\n },\n getScripts: function () { return []; },\n block: function () {},\n unblock: function () {},\n getValue: function () {},\n };\n}\n\n);\n})()\n", + "code": "(function () {\n// --- BEGIN __auth0FormHelpers (auto-inlined from shared/helpers/index.js) ---\n/**\n * Shared helpers for Auth0 form custom components.\n *\n * Runs in two environments:\n * - Auth0 form runtime: build.js inlines this file into each component's `config.code`,\n * exposing `__auth0FormHelpers` as a top-level variable in the same scope as the component.\n * - Node (tests / playground via require): consumed as a CommonJS module via module.exports.\n *\n * Keep this file free of `require` and ES imports so it can be concatenated as plain text.\n */\n\nfunction ensureBufferPolyfill() {\n if (typeof Buffer !== \"undefined\") return;\n if (typeof globalThis === \"undefined\") return;\n globalThis.Buffer = {\n from: function (data) {\n if (Array.isArray(data)) return new Uint8Array(data);\n if (typeof data === \"string\") return new TextEncoder().encode(data);\n return data;\n },\n };\n}\n\nconst BASE58_ALPHABET = \"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz\";\n\nfunction base58Encode(bytes) {\n let result = \"\";\n let num = BigInt(0);\n for (const byte of bytes) {\n num = (num << BigInt(8)) + BigInt(byte);\n }\n while (num > BigInt(0)) {\n const remainder = num % BigInt(58);\n num = num / BigInt(58);\n result = BASE58_ALPHABET[Number(remainder)] + result;\n }\n for (const byte of bytes) {\n if (byte === 0) result = BASE58_ALPHABET[0] + result;\n else break;\n }\n return result;\n}\n\n/**\n * Format a yocto-NEAR BigInt string as NEAR with up to 8 decimals.\n * \"1000000000000000000000000\" -> \"1\"\n * \"1500000000000000000000000\" -> \"1.5\"\n * \"1\" -> \"0.00000000\" trimmed -> \"0.0\"\n */\nfunction yoctoToNear(bigIntStr) {\n const trimmed = String(bigIntStr).replace(/^0+/, \"\") || \"0\";\n const length = trimmed.length;\n\n if (length <= 24) {\n const zerosNeeded = 24 - length;\n const withZeros = \"0\".repeat(zerosNeeded) + trimmed;\n const decimals = withZeros.slice(0, 8).replace(/0+$/, \"\") || \"0\";\n return `0.${decimals}`;\n }\n\n const splitPos = length - 24;\n const integerPart = trimmed.slice(0, splitPos);\n const decimalPart = trimmed.slice(splitPos, splitPos + 8);\n const trimmedDecimals = decimalPart.replace(/0+$/, \"\");\n if (trimmedDecimals === \"\") return integerPart;\n return `${integerPart}.${trimmedDecimals}`;\n}\n\nfunction formatPublicKey(publicKey) {\n if (publicKey && publicKey.secp256k1Key !== undefined) {\n return `secp256k1:${base58Encode(publicKey.secp256k1Key.data)}`;\n }\n if (publicKey && publicKey.ed25519Key !== undefined) {\n return `ed25519:${base58Encode(publicKey.ed25519Key.data)}`;\n }\n return \"\";\n}\n\n/**\n * Return the canonical action type discriminator (the single non-undefined key of the action).\n * Useful for dispatch and for tests that don't need DOM rendering.\n */\nfunction getActionType(action) {\n if (!action || typeof action !== \"object\") return null;\n for (const key of Object.keys(action)) {\n if (action[key] !== undefined) return key;\n }\n return null;\n}\n\n// --- DOM helpers (require document/window — jsdom in tests, real DOM in browser) ---\n\nfunction createTextContent(label, value, link = false) {\n const textContent = document.createElement(\"div\");\n textContent.classList.add(\"text-content\");\n\n const labelElement = document.createElement(\"div\");\n labelElement.classList.add(\"label\");\n labelElement.textContent = label;\n\n const valueElement = document.createElement(\"div\");\n valueElement.classList.add(\"value\");\n valueElement.textContent = value;\n\n if (link) {\n valueElement.classList.add(\"link\");\n valueElement.setAttribute(\"href\", link);\n valueElement.setAttribute(\"target\", \"_blank\");\n }\n\n textContent.appendChild(labelElement);\n textContent.appendChild(valueElement);\n return textContent;\n}\n\nfunction createDescription(text) {\n const node = document.createElement(\"p\");\n node.classList.add(\"action-description\");\n node.textContent = text;\n return node;\n}\n\nfunction createAccordion(label, content, showWarning = false) {\n const accordion = document.createElement(\"div\");\n accordion.classList.add(\"accordion\");\n\n const header = document.createElement(\"div\");\n header.classList.add(\"accordion-header\");\n\n const headerContent = document.createElement(\"div\");\n headerContent.classList.add(\"accordion-header-content\");\n\n const leftContent = document.createElement(\"div\");\n leftContent.classList.add(\"accordion-left-content\");\n\n if (showWarning) {\n const warningIcon = document.createElement(\"span\");\n warningIcon.classList.add(\"warning-icon\");\n warningIcon.innerHTML = \"⚠️\";\n leftContent.appendChild(warningIcon);\n }\n\n const labelElement = document.createElement(\"span\");\n labelElement.classList.add(\"accordion-header-label\");\n labelElement.textContent = label;\n leftContent.appendChild(labelElement);\n\n const expandIcon = document.createElement(\"span\");\n expandIcon.classList.add(\"expand-icon\");\n expandIcon.innerHTML = \"+\";\n\n headerContent.appendChild(leftContent);\n headerContent.appendChild(expandIcon);\n header.appendChild(headerContent);\n\n const contentElement = document.createElement(\"div\");\n contentElement.classList.add(\"accordion-content\");\n contentElement.appendChild(content);\n\n accordion.appendChild(header);\n accordion.appendChild(contentElement);\n\n header.addEventListener(\"click\", function () {\n contentElement.classList.toggle(\"open\");\n expandIcon.innerHTML = contentElement.classList.contains(\"open\") ? \"−\" : \"+\";\n });\n\n return accordion;\n}\n\n// --- Action content factories ---\n\nfunction createAccountContent() {\n return createDescription(\"By approving this request, a new account will be created.\");\n}\n\nfunction deployContractContent(action) {\n ensureBufferPolyfill();\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the compiled smart contract will be deployed.\");\n const codeLen = action.deployContract && action.deployContract.code ? `${action.deployContract.code.length} bytes` : null;\n container.appendChild(text);\n container.appendChild(createTextContent(\"Code\", codeLen));\n return container;\n}\n\n/**\n * Decode functionCall `args` (a byte array / Uint8Array) to a readable string. NEAR contract\n * args are almost always UTF-8 JSON, so decode the bytes and pretty-print when they parse as\n * JSON; otherwise fall back to the raw decoded text. TextDecoder is used directly because it\n * exists in the browser and the Auth0 form runtime (unlike Buffer, which the runtime lacks).\n */\nfunction decodeFunctionCallArgs(args) {\n if (args == null) return \"\";\n let text;\n try {\n if (typeof TextDecoder !== \"undefined\") {\n text = new TextDecoder().decode(Uint8Array.from(args));\n } else if (typeof Buffer !== \"undefined\" && typeof Buffer.from === \"function\") {\n text = Buffer.from(args).toString(\"utf8\");\n } else {\n text = String.fromCharCode.apply(null, Array.from(args));\n }\n } catch (e) {\n return \"\";\n }\n try {\n return JSON.stringify(JSON.parse(text), null, 2);\n } catch (e) {\n return text;\n }\n}\n\n/**\n * Format a yocto-NEAR amount for display. In transaction mode (showYoctoConversion) amounts at or\n * above 0.00000000001 NEAR are shown as NEAR; smaller amounts and delegate-action mode stay in\n * yoctoNEAR so no precision is hidden.\n */\nfunction formatNearAmount(value, options) {\n const showYoctoConversion = !!(options && options.showYoctoConversion);\n if (showYoctoConversion && value != null && BigInt(value) >= BigInt(\"10000000000000000\")) {\n return `${yoctoToNear(value.toString())} NEAR`;\n }\n return `${value != null ? value.toString() : \"0\"} yoctoNEAR`;\n}\n\nfunction functionCallContent(action, options) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following function will be called.\");\n\n const fc = action.functionCall || {};\n container.appendChild(text);\n container.appendChild(createTextContent(\"Method Name\", fc.methodName));\n container.appendChild(createTextContent(\"Args\", decodeFunctionCallArgs(fc.args)));\n container.appendChild(createTextContent(\"Gas\", fc.gas != null ? fc.gas.toString() : \"\"));\n container.appendChild(createTextContent(\"Deposit\", formatNearAmount(fc.deposit, options)));\n return container;\n}\n\nfunction transferContent(action, options) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following amount will be transferred to the receiver.\");\n\n const deposit = action.transfer ? action.transfer.deposit : undefined;\n container.appendChild(text);\n container.appendChild(createTextContent(\"Deposit\", formatNearAmount(deposit, options)));\n return container;\n}\n\nfunction stakeContent(action, options) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following amount will be staked to the public key.\");\n const stake = action.stake || {};\n container.appendChild(text);\n container.appendChild(createTextContent(\"Stake\", formatNearAmount(stake.stake, options)));\n container.appendChild(createTextContent(\"Public Key\", formatPublicKey(stake.publicKey)));\n return container;\n}\n\nfunction addKeyContent(action) {\n const container = document.createElement(\"div\");\n const ak = action.addKey || {};\n const accessKey = ak.accessKey || {};\n const permission = accessKey.permission || {};\n\n if (permission.fullAccess !== undefined) {\n const warning = document.createElement(\"div\");\n warning.classList.add(\"warning-callout\");\n warning.textContent = \"This key will have full access to your account. Only approve if you trust the recipient.\";\n container.appendChild(warning);\n }\n\n container.appendChild(createDescription(\"By approving this request, the following access key will be added to your account.\"));\n container.appendChild(createTextContent(\"Public Key\", formatPublicKey(ak.publicKey)));\n container.appendChild(createTextContent(\"Nonce\", accessKey.nonce != null ? accessKey.nonce.toString() : \"\"));\n\n if (permission.fullAccess === undefined) {\n container.appendChild(createTextContent(\"Permission\", JSON.stringify(permission.functionCall, null, 2)));\n }\n return container;\n}\n\nfunction deleteKeyContent(action) {\n const container = document.createElement(\"div\");\n const dk = action.deleteKey || {};\n\n const warning = document.createElement(\"div\");\n warning.classList.add(\"warning-callout\");\n warning.textContent = \"Once deleted, the key cannot be recovered.\";\n\n container.appendChild(warning);\n container.appendChild(\n createDescription(\"This action will delete an access key from your account. Only approve if you trust the recipient.\"),\n );\n container.appendChild(createTextContent(\"Public Key\", formatPublicKey(dk.publicKey)));\n return container;\n}\n\nfunction deleteAccountContent(action) {\n const container = document.createElement(\"div\");\n\n const warning = document.createElement(\"div\");\n warning.classList.add(\"warning-callout\");\n warning.textContent = \"Once deleted, the account cannot be recovered.\";\n\n container.appendChild(warning);\n container.appendChild(createDescription(\"By approving this request, the account will be deleted.\"));\n container.appendChild(createTextContent(\"Beneficiary ID\", action.deleteAccount && action.deleteAccount.beneficiaryId));\n return container;\n}\n\nfunction signedDelegateContent(action) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following delegate action will be executed.\");\n\n const sd = action.signedDelegate || {};\n let serialized;\n try {\n serialized = JSON.stringify(\n sd.delegateAction,\n function (_, value) {\n return typeof value === \"bigint\" ? value.toString() : value;\n },\n 2,\n );\n } catch (e) {\n serialized = \"\";\n }\n container.appendChild(text);\n container.appendChild(createTextContent(\"Delegate Action\", serialized));\n return container;\n}\n\nfunction deployGlobalContractContent(action) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the compiled smart contract will be deployed.\");\n const codeLen =\n action.deployGlobalContract && action.deployGlobalContract.code ? `${action.deployGlobalContract.code.length} bytes` : null;\n container.appendChild(text);\n container.appendChild(createTextContent(\"Code\", codeLen));\n return container;\n}\n\nfunction useGlobalContractContent(action) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following global contract will be used.\");\n let identifier;\n try {\n identifier = JSON.stringify(action.useGlobalContract && action.useGlobalContract.contractIdentifier);\n } catch (e) {\n identifier = \"\";\n }\n container.appendChild(text);\n container.appendChild(createTextContent(\"Contract Identifier\", identifier));\n return container;\n}\n\nconst ACTION_DISPATCH = {\n createAccount: {\n label: \"CreateAccount\",\n warn: false,\n render: function () {\n return createAccountContent();\n },\n },\n deployContract: {\n label: \"DeployContract\",\n warn: true,\n render: function (a) {\n return deployContractContent(a);\n },\n },\n functionCall: {\n label: \"FunctionCall\",\n warn: false,\n render: function (a, opts) {\n return functionCallContent(a, opts);\n },\n },\n transfer: {\n label: \"Transfer\",\n warn: false,\n render: function (a, opts) {\n return transferContent(a, opts);\n },\n },\n stake: {\n label: \"Stake\",\n warn: false,\n render: function (a, opts) {\n return stakeContent(a, opts);\n },\n },\n addKey: {\n label: \"AddKey\",\n warn: false,\n render: function (a) {\n return addKeyContent(a);\n },\n warnsWhen: function (a) {\n return !!(\n a.addKey &&\n a.addKey.accessKey &&\n a.addKey.accessKey.permission &&\n a.addKey.accessKey.permission.fullAccess !== undefined\n );\n },\n },\n deleteKey: {\n label: \"DeleteKey\",\n warn: true,\n render: function (a) {\n return deleteKeyContent(a);\n },\n },\n deleteAccount: {\n label: \"DeleteAccount\",\n warn: true,\n render: function (a) {\n return deleteAccountContent(a);\n },\n },\n signedDelegate: {\n label: \"SignedDelegate\",\n warn: false,\n render: function (a) {\n return signedDelegateContent(a);\n },\n },\n deployGlobalContract: {\n label: \"DeployGlobalContract\",\n warn: true,\n render: function (a) {\n return deployGlobalContractContent(a);\n },\n },\n useGlobalContract: {\n label: \"UseGlobalContract\",\n warn: false,\n render: function (a) {\n return useGlobalContractContent(a);\n },\n },\n};\n\nfunction handleNearAction(action, options) {\n const actionKey = getActionType(action);\n const entry = actionKey ? ACTION_DISPATCH[actionKey] : null;\n if (!entry) {\n const unknown = document.createElement(\"div\");\n unknown.textContent = `Unknown action: ${actionKey || \"(empty)\"}`;\n return createAccordion(\"Unknown\", unknown, true);\n }\n const showWarning = entry.warnsWhen ? entry.warnsWhen(action) : !!entry.warn;\n return createAccordion(entry.label, entry.render(action, options), showWarning);\n}\n\n/**\n * Build the full details DOM tree for a form custom field.\n *\n * @param {object} params\n * @param {Array<{label: string, value: string|undefined}>} params.fields - top-level fields like Signer/Receiver/MaxBlockHeight.\n * @param {string} params.actions - JSON string with the actions array (as Auth0 form fields deliver them).\n * @param {{ showYoctoConversion?: boolean }} [params.options]\n */\nfunction renderDetails(params) {\n ensureBufferPolyfill();\n const box = document.createElement(\"div\");\n box.classList.add(\"box\");\n\n for (const field of params.fields || []) {\n if (field.value === undefined || field.value === null || field.value === \"\") continue;\n box.appendChild(createTextContent(field.label, field.value));\n }\n\n const actionsContainer = document.createElement(\"div\");\n actionsContainer.classList.add(\"actions-container\");\n const actionsLabel = document.createElement(\"div\");\n actionsLabel.classList.add(\"label\");\n actionsLabel.textContent = \"Actions\";\n actionsContainer.appendChild(actionsLabel);\n\n let parsedActions = [];\n try {\n parsedActions = JSON.parse(params.actions || \"[]\");\n } catch (e) {\n const errorNode = document.createElement(\"div\");\n errorNode.classList.add(\"warning-callout\");\n errorNode.textContent = \"Failed to parse actions payload.\";\n actionsContainer.appendChild(errorNode);\n box.appendChild(actionsContainer);\n return box;\n }\n\n for (const action of parsedActions) {\n actionsContainer.appendChild(handleNearAction(action, params.options));\n }\n\n box.appendChild(actionsContainer);\n return box;\n}\n\n// --- NEP-413 intent content factories ---\n\n/**\n * Render the token map of a transfer intent (`{ \"\": \"\" }`).\n *\n * Amounts stay in the token's smallest unit: the form has no token metadata, so converting\n * would mean guessing decimals — and a wrong guess here understates what the user is\n * approving. Showing the raw amount alongside its token id is honest and unambiguous.\n */\nfunction intentTokensContent(tokens) {\n const container = document.createElement(\"div\");\n if (!tokens || typeof tokens !== \"object\") return container;\n for (const tokenId of Object.keys(tokens)) {\n container.appendChild(createTextContent(tokenId, String(tokens[tokenId])));\n }\n return container;\n}\n\nfunction transferIntentContent(intent) {\n const container = document.createElement(\"div\");\n container.appendChild(createDescription(\"By approving this request, the following tokens will be transferred.\"));\n container.appendChild(createTextContent(\"Receiver ID\", intent.receiver_id));\n\n const tokensLabel = document.createElement(\"div\");\n tokensLabel.classList.add(\"label\");\n tokensLabel.textContent = \"Tokens\";\n container.appendChild(tokensLabel);\n container.appendChild(intentTokensContent(intent.tokens));\n return container;\n}\n\n/**\n * Fallback for intent kinds this form does not model explicitly. It is deliberately shown\n * with a warning: the user is approving something the UI cannot describe in plain terms.\n */\nfunction unknownIntentContent(intent) {\n const container = document.createElement(\"div\");\n container.appendChild(createDescription(\"This request contains an intent type this app does not recognize. Review it carefully.\"));\n let serialized;\n try {\n serialized = JSON.stringify(intent, null, 2);\n } catch (e) {\n serialized = \"\";\n }\n container.appendChild(createTextContent(\"Intent\", serialized));\n return container;\n}\n\nconst INTENT_DISPATCH = {\n transfer: {\n label: \"Transfer\",\n warn: false,\n render: function (i) {\n return transferIntentContent(i);\n },\n },\n};\n\nfunction handleIntent(intent) {\n const kind = intent && typeof intent === \"object\" ? intent.intent : null;\n const entry = kind ? INTENT_DISPATCH[kind] : null;\n if (!entry) {\n return createAccordion(kind ? `Unknown: ${kind}` : \"Unknown\", unknownIntentContent(intent), true);\n }\n return createAccordion(entry.label, entry.render(intent), !!entry.warn);\n}\n\n/**\n * Render the message body of a NEP-413 request verbatim.\n *\n * This is the default view, and the one that matters most: NEP-413 messages are arbitrary\n * strings meant to be read by a human (\"Sign in to example.com\"), so showing the text exactly\n * as it will be signed is the honest presentation. Pretty-printing is applied only when the\n * message is JSON, purely for legibility.\n */\nfunction messageContent(message) {\n const container = document.createElement(\"div\");\n let display = message;\n try {\n const parsed = JSON.parse(message);\n if (parsed && typeof parsed === \"object\") display = JSON.stringify(parsed, null, 2);\n } catch (e) {\n display = message;\n }\n container.appendChild(createTextContent(\"Message\", display));\n return container;\n}\n\n/**\n * Build the details DOM tree for a NEP-413 signature approval.\n *\n * Two presentations, one payload: when the message carries NEAR Intents the intents are broken\n * out one by one, and otherwise the raw message is shown as text. Either way the top-level\n * fields — recipient above all — are rendered, because under NEP-413 the recipient is what\n * tells the user which application their signature is addressed to.\n * @param {object} params The render parameters.\n * @param {Array<{label: string, value: string|undefined}>} params.fields Top-level fields (recipient, callback URL, signer, deadline).\n * @param {string} params.message The raw NEP-413 message, shown when there are no intents to break out.\n * @param {string} [params.intents] JSON string with the intents array, when the message is a NEAR Intents body.\n * @returns {HTMLElement} The details node.\n */\nfunction renderNep413Details(params) {\n ensureBufferPolyfill();\n const box = document.createElement(\"div\");\n box.classList.add(\"box\");\n\n for (const field of params.fields || []) {\n if (field.value === undefined || field.value === null || field.value === \"\") continue;\n box.appendChild(createTextContent(field.label, field.value));\n }\n\n let parsedIntents = null;\n if (params.intents) {\n try {\n const candidate = JSON.parse(params.intents);\n if (Array.isArray(candidate) && candidate.length > 0) parsedIntents = candidate;\n } catch (e) {\n parsedIntents = null;\n }\n }\n\n // No intents to break out (or they were unreadable) — show the message itself. Falling back\n // to the raw text keeps every NEP-413 message approvable, not just the ones we model.\n if (!parsedIntents) {\n box.appendChild(messageContent(params.message || \"\"));\n return box;\n }\n\n const intentsContainer = document.createElement(\"div\");\n intentsContainer.classList.add(\"actions-container\");\n const intentsLabel = document.createElement(\"div\");\n intentsLabel.classList.add(\"label\");\n intentsLabel.textContent = \"Intents\";\n intentsContainer.appendChild(intentsLabel);\n\n for (const intent of parsedIntents) {\n intentsContainer.appendChild(handleIntent(intent));\n }\n\n box.appendChild(intentsContainer);\n return box;\n}\n\nvar __auth0FormHelpers = {\n ensureBufferPolyfill: ensureBufferPolyfill,\n base58Encode: base58Encode,\n yoctoToNear: yoctoToNear,\n formatNearAmount: formatNearAmount,\n decodeFunctionCallArgs: decodeFunctionCallArgs,\n formatPublicKey: formatPublicKey,\n getActionType: getActionType,\n createTextContent: createTextContent,\n createDescription: createDescription,\n createAccordion: createAccordion,\n createAccountContent: createAccountContent,\n deployContractContent: deployContractContent,\n functionCallContent: functionCallContent,\n transferContent: transferContent,\n stakeContent: stakeContent,\n addKeyContent: addKeyContent,\n deleteKeyContent: deleteKeyContent,\n deleteAccountContent: deleteAccountContent,\n signedDelegateContent: signedDelegateContent,\n deployGlobalContractContent: deployGlobalContractContent,\n useGlobalContractContent: useGlobalContractContent,\n handleNearAction: handleNearAction,\n renderDetails: renderDetails,\n intentTokensContent: intentTokensContent,\n transferIntentContent: transferIntentContent,\n unknownIntentContent: unknownIntentContent,\n handleIntent: handleIntent,\n messageContent: messageContent,\n renderNep413Details: renderNep413Details,\n};\n\nif (typeof module !== \"undefined\" && module.exports) {\n module.exports = __auth0FormHelpers;\n}\n\n// --- END __auth0FormHelpers ---\n\n\nreturn (\n/**\n * Custom field that renders the transaction details (signer, receiver, actions).\n *\n * Depends on `__auth0FormHelpers`, which is inlined at build time by build.js\n * (or shimmed by the playground via helpers-shim.js).\n */\nfunction AuthorizeAppTransactionDetails(context) {\n return {\n init: function () {\n const params = context.custom.getParams();\n return __auth0FormHelpers.renderDetails({\n fields: [\n { label: \"Signer ID\", value: params.signerId },\n { label: \"Receiver ID\", value: params.receiverId },\n ],\n actions: params.actions,\n options: { showYoctoConversion: true },\n });\n },\n getScripts: function () { return []; },\n block: function () {},\n unblock: function () {},\n getValue: function () {},\n };\n}\n\n);\n})()\n", "css": ".box {\n padding: 1.25rem;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 1.25rem;\n width: 100%;\n\n border-radius: 1rem;\n border: 1px solid #e5e5e5;\n}\n\n.text-content {\n display: flex;\n flex-direction: column;\n align-items: left;\n justify-content: left;\n width: 100%;\n}\n\n.label {\n font-size: 0.75rem;\n color: #999999;\n font-weight: 500;\n}\n\n.value {\n color: #3f4246;\n font-size: 0.875rem;\n font-weight: 600;\n}\n\n.json-content {\n display: flex;\n padding: 0.5rem;\n flex-direction: column;\n align-items: left;\n justify-content: left;\n width: 100%;\n border-radius: 12px;\n background-color: #fafafa;\n\n color: #3f4246;\n font-size: 0.875rem;\n font-weight: 500;\n}\n\n.accordion {\n width: 100%;\n border: 1px solid #e5e5e5;\n border-radius: 6px;\n overflow: hidden;\n}\n\n.accordion-header {\n background: #fff;\n padding: 12px 16px;\n cursor: pointer;\n user-select: none;\n}\n\n.accordion-header-content {\n display: flex;\n align-items: center;\n justify-content: space-between;\n width: 100%;\n}\n\n.accordion-left-content {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.accordion-header-label {\n font-size: 0.75rem;\n color: black;\n font-weight: 500;\n}\n\n.warning-icon {\n font-size: 1rem;\n color: #ff4444;\n}\n\n.expand-icon {\n font-size: 1.2rem;\n color: #666;\n font-weight: bold;\n line-height: 1;\n transition: transform 0.2s ease;\n user-select: none;\n min-width: 20px;\n text-align: center;\n}\n\n.accordion-content {\n max-height: 0;\n overflow: hidden;\n font-size: 0.75rem;\n background-color: #fafafa;\n color: #0a0a0a;\n font-weight: 400;\n transition:\n max-height 0.3s ease,\n padding 0.3s ease;\n padding: 0 1rem;\n}\n\n.accordion-content.open {\n padding: 12px 16px;\n max-height: 500px;\n}\n\n.actions-container {\n display: flex;\n flex-direction: column;\n align-items: left;\n justify-content: left;\n width: 100%;\n gap: 0.25rem;\n}\n\n.accordion-content .label {\n font-size: 0.75rem;\n}\n\n.accordion-content .value {\n font-size: 0.75rem;\n white-space: pre-wrap;\n word-break: break-word;\n}\n\n.accordion-content > div {\n display: flex;\n flex-direction: column;\n gap: 0.75rem;\n}\n\n.action-description {\n font-size: 0.75rem;\n color: #3f4246;\n margin: 0;\n}\n\n.warning-callout {\n background-color: #ffebee;\n padding: 16px;\n border-radius: 4px;\n margin: 0;\n color: #c62828;\n}\n", "params": { "actions": "{{ fields.actions }}", @@ -66,7 +66,7 @@ "sensitive": false, "config": { "schema": {}, - "code": "(function () {\n// --- BEGIN __auth0FormHelpers (auto-inlined from shared/helpers/index.js) ---\n/**\n * Shared helpers for Auth0 form custom components.\n *\n * Runs in two environments:\n * - Auth0 form runtime: build.js inlines this file into each component's `config.code`,\n * exposing `__auth0FormHelpers` as a top-level variable in the same scope as the component.\n * - Node (tests / playground via require): consumed as a CommonJS module via module.exports.\n *\n * Keep this file free of `require` and ES imports so it can be concatenated as plain text.\n */\n\nfunction ensureBufferPolyfill() {\n if (typeof Buffer !== \"undefined\") return;\n if (typeof globalThis === \"undefined\") return;\n globalThis.Buffer = {\n from: function (data) {\n if (Array.isArray(data)) return new Uint8Array(data);\n if (typeof data === \"string\") return new TextEncoder().encode(data);\n return data;\n },\n };\n}\n\nconst BASE58_ALPHABET = \"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz\";\n\nfunction base58Encode(bytes) {\n let result = \"\";\n let num = BigInt(0);\n for (const byte of bytes) {\n num = (num << BigInt(8)) + BigInt(byte);\n }\n while (num > BigInt(0)) {\n const remainder = num % BigInt(58);\n num = num / BigInt(58);\n result = BASE58_ALPHABET[Number(remainder)] + result;\n }\n for (const byte of bytes) {\n if (byte === 0) result = BASE58_ALPHABET[0] + result;\n else break;\n }\n return result;\n}\n\n/**\n * Format a yocto-NEAR BigInt string as NEAR with up to 8 decimals.\n * \"1000000000000000000000000\" -> \"1\"\n * \"1500000000000000000000000\" -> \"1.5\"\n * \"1\" -> \"0.00000000\" trimmed -> \"0.0\"\n */\nfunction yoctoToNear(bigIntStr) {\n const trimmed = String(bigIntStr).replace(/^0+/, \"\") || \"0\";\n const length = trimmed.length;\n\n if (length <= 24) {\n const zerosNeeded = 24 - length;\n const withZeros = \"0\".repeat(zerosNeeded) + trimmed;\n const decimals = withZeros.slice(0, 8).replace(/0+$/, \"\") || \"0\";\n return `0.${decimals}`;\n }\n\n const splitPos = length - 24;\n const integerPart = trimmed.slice(0, splitPos);\n const decimalPart = trimmed.slice(splitPos, splitPos + 8);\n const trimmedDecimals = decimalPart.replace(/0+$/, \"\");\n if (trimmedDecimals === \"\") return integerPart;\n return `${integerPart}.${trimmedDecimals}`;\n}\n\nfunction formatPublicKey(publicKey) {\n if (publicKey && publicKey.secp256k1Key !== undefined) {\n return `secp256k1:${base58Encode(publicKey.secp256k1Key.data)}`;\n }\n if (publicKey && publicKey.ed25519Key !== undefined) {\n return `ed25519:${base58Encode(publicKey.ed25519Key.data)}`;\n }\n return \"\";\n}\n\n/**\n * Return the canonical action type discriminator (the single non-undefined key of the action).\n * Useful for dispatch and for tests that don't need DOM rendering.\n */\nfunction getActionType(action) {\n if (!action || typeof action !== \"object\") return null;\n for (const key of Object.keys(action)) {\n if (action[key] !== undefined) return key;\n }\n return null;\n}\n\n// --- DOM helpers (require document/window — jsdom in tests, real DOM in browser) ---\n\nfunction createTextContent(label, value, link = false) {\n const textContent = document.createElement(\"div\");\n textContent.classList.add(\"text-content\");\n\n const labelElement = document.createElement(\"div\");\n labelElement.classList.add(\"label\");\n labelElement.textContent = label;\n\n const valueElement = document.createElement(\"div\");\n valueElement.classList.add(\"value\");\n valueElement.textContent = value;\n\n if (link) {\n valueElement.classList.add(\"link\");\n valueElement.setAttribute(\"href\", link);\n valueElement.setAttribute(\"target\", \"_blank\");\n }\n\n textContent.appendChild(labelElement);\n textContent.appendChild(valueElement);\n return textContent;\n}\n\nfunction createDescription(text) {\n const node = document.createElement(\"p\");\n node.classList.add(\"action-description\");\n node.textContent = text;\n return node;\n}\n\nfunction createAccordion(label, content, showWarning = false) {\n const accordion = document.createElement(\"div\");\n accordion.classList.add(\"accordion\");\n\n const header = document.createElement(\"div\");\n header.classList.add(\"accordion-header\");\n\n const headerContent = document.createElement(\"div\");\n headerContent.classList.add(\"accordion-header-content\");\n\n const leftContent = document.createElement(\"div\");\n leftContent.classList.add(\"accordion-left-content\");\n\n if (showWarning) {\n const warningIcon = document.createElement(\"span\");\n warningIcon.classList.add(\"warning-icon\");\n warningIcon.innerHTML = \"⚠️\";\n leftContent.appendChild(warningIcon);\n }\n\n const labelElement = document.createElement(\"span\");\n labelElement.classList.add(\"accordion-header-label\");\n labelElement.textContent = label;\n leftContent.appendChild(labelElement);\n\n const expandIcon = document.createElement(\"span\");\n expandIcon.classList.add(\"expand-icon\");\n expandIcon.innerHTML = \"+\";\n\n headerContent.appendChild(leftContent);\n headerContent.appendChild(expandIcon);\n header.appendChild(headerContent);\n\n const contentElement = document.createElement(\"div\");\n contentElement.classList.add(\"accordion-content\");\n contentElement.appendChild(content);\n\n accordion.appendChild(header);\n accordion.appendChild(contentElement);\n\n header.addEventListener(\"click\", function () {\n contentElement.classList.toggle(\"open\");\n expandIcon.innerHTML = contentElement.classList.contains(\"open\") ? \"−\" : \"+\";\n });\n\n return accordion;\n}\n\n// --- Action content factories ---\n\nfunction createAccountContent() {\n return createDescription(\"By approving this request, a new account will be created.\");\n}\n\nfunction deployContractContent(action) {\n ensureBufferPolyfill();\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the compiled smart contract will be deployed.\");\n const codeLen = action.deployContract && action.deployContract.code ? `${action.deployContract.code.length} bytes` : null;\n container.appendChild(text);\n container.appendChild(createTextContent(\"Code\", codeLen));\n return container;\n}\n\n/**\n * Decode functionCall `args` (a byte array / Uint8Array) to a readable string. NEAR contract\n * args are almost always UTF-8 JSON, so decode the bytes and pretty-print when they parse as\n * JSON; otherwise fall back to the raw decoded text. TextDecoder is used directly because it\n * exists in the browser and the Auth0 form runtime (unlike Buffer, which the runtime lacks).\n */\nfunction decodeFunctionCallArgs(args) {\n if (args == null) return \"\";\n let text;\n try {\n if (typeof TextDecoder !== \"undefined\") {\n text = new TextDecoder().decode(Uint8Array.from(args));\n } else if (typeof Buffer !== \"undefined\" && typeof Buffer.from === \"function\") {\n text = Buffer.from(args).toString(\"utf8\");\n } else {\n text = String.fromCharCode.apply(null, Array.from(args));\n }\n } catch (e) {\n return \"\";\n }\n try {\n return JSON.stringify(JSON.parse(text), null, 2);\n } catch (e) {\n return text;\n }\n}\n\n/**\n * Format a yocto-NEAR amount for display. In transaction mode (showYoctoConversion) amounts at or\n * above 0.00000000001 NEAR are shown as NEAR; smaller amounts and delegate-action mode stay in\n * yoctoNEAR so no precision is hidden.\n */\nfunction formatNearAmount(value, options) {\n const showYoctoConversion = !!(options && options.showYoctoConversion);\n if (showYoctoConversion && value != null && BigInt(value) >= BigInt(\"10000000000000000\")) {\n return `${yoctoToNear(value.toString())} NEAR`;\n }\n return `${value != null ? value.toString() : \"0\"} yoctoNEAR`;\n}\n\nfunction functionCallContent(action, options) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following function will be called.\");\n\n const fc = action.functionCall || {};\n container.appendChild(text);\n container.appendChild(createTextContent(\"Method Name\", fc.methodName));\n container.appendChild(createTextContent(\"Args\", decodeFunctionCallArgs(fc.args)));\n container.appendChild(createTextContent(\"Gas\", fc.gas != null ? fc.gas.toString() : \"\"));\n container.appendChild(createTextContent(\"Deposit\", formatNearAmount(fc.deposit, options)));\n return container;\n}\n\nfunction transferContent(action, options) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following amount will be transferred to the receiver.\");\n\n const deposit = action.transfer ? action.transfer.deposit : undefined;\n container.appendChild(text);\n container.appendChild(createTextContent(\"Deposit\", formatNearAmount(deposit, options)));\n return container;\n}\n\nfunction stakeContent(action, options) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following amount will be staked to the public key.\");\n const stake = action.stake || {};\n container.appendChild(text);\n container.appendChild(createTextContent(\"Stake\", formatNearAmount(stake.stake, options)));\n container.appendChild(createTextContent(\"Public Key\", formatPublicKey(stake.publicKey)));\n return container;\n}\n\nfunction addKeyContent(action) {\n const container = document.createElement(\"div\");\n const ak = action.addKey || {};\n const accessKey = ak.accessKey || {};\n const permission = accessKey.permission || {};\n\n if (permission.fullAccess !== undefined) {\n const warning = document.createElement(\"div\");\n warning.classList.add(\"warning-callout\");\n warning.textContent = \"This key will have full access to your account. Only approve if you trust the recipient.\";\n container.appendChild(warning);\n }\n\n container.appendChild(createDescription(\"By approving this request, the following access key will be added to your account.\"));\n container.appendChild(createTextContent(\"Public Key\", formatPublicKey(ak.publicKey)));\n container.appendChild(createTextContent(\"Nonce\", accessKey.nonce != null ? accessKey.nonce.toString() : \"\"));\n\n if (permission.fullAccess === undefined) {\n container.appendChild(createTextContent(\"Permission\", JSON.stringify(permission.functionCall, null, 2)));\n }\n return container;\n}\n\nfunction deleteKeyContent(action) {\n const container = document.createElement(\"div\");\n const dk = action.deleteKey || {};\n\n const warning = document.createElement(\"div\");\n warning.classList.add(\"warning-callout\");\n warning.textContent = \"Once deleted, the key cannot be recovered.\";\n\n container.appendChild(warning);\n container.appendChild(\n createDescription(\"This action will delete an access key from your account. Only approve if you trust the recipient.\"),\n );\n container.appendChild(createTextContent(\"Public Key\", formatPublicKey(dk.publicKey)));\n return container;\n}\n\nfunction deleteAccountContent(action) {\n const container = document.createElement(\"div\");\n\n const warning = document.createElement(\"div\");\n warning.classList.add(\"warning-callout\");\n warning.textContent = \"Once deleted, the account cannot be recovered.\";\n\n container.appendChild(warning);\n container.appendChild(createDescription(\"By approving this request, the account will be deleted.\"));\n container.appendChild(createTextContent(\"Beneficiary ID\", action.deleteAccount && action.deleteAccount.beneficiaryId));\n return container;\n}\n\nfunction signedDelegateContent(action) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following delegate action will be executed.\");\n\n const sd = action.signedDelegate || {};\n let serialized;\n try {\n serialized = JSON.stringify(\n sd.delegateAction,\n function (_, value) {\n return typeof value === \"bigint\" ? value.toString() : value;\n },\n 2,\n );\n } catch (e) {\n serialized = \"\";\n }\n container.appendChild(text);\n container.appendChild(createTextContent(\"Delegate Action\", serialized));\n return container;\n}\n\nfunction deployGlobalContractContent(action) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the compiled smart contract will be deployed.\");\n const codeLen =\n action.deployGlobalContract && action.deployGlobalContract.code ? `${action.deployGlobalContract.code.length} bytes` : null;\n container.appendChild(text);\n container.appendChild(createTextContent(\"Code\", codeLen));\n return container;\n}\n\nfunction useGlobalContractContent(action) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following global contract will be used.\");\n let identifier;\n try {\n identifier = JSON.stringify(action.useGlobalContract && action.useGlobalContract.contractIdentifier);\n } catch (e) {\n identifier = \"\";\n }\n container.appendChild(text);\n container.appendChild(createTextContent(\"Contract Identifier\", identifier));\n return container;\n}\n\nconst ACTION_DISPATCH = {\n createAccount: {\n label: \"CreateAccount\",\n warn: false,\n render: function () {\n return createAccountContent();\n },\n },\n deployContract: {\n label: \"DeployContract\",\n warn: true,\n render: function (a) {\n return deployContractContent(a);\n },\n },\n functionCall: {\n label: \"FunctionCall\",\n warn: false,\n render: function (a, opts) {\n return functionCallContent(a, opts);\n },\n },\n transfer: {\n label: \"Transfer\",\n warn: false,\n render: function (a, opts) {\n return transferContent(a, opts);\n },\n },\n stake: {\n label: \"Stake\",\n warn: false,\n render: function (a, opts) {\n return stakeContent(a, opts);\n },\n },\n addKey: {\n label: \"AddKey\",\n warn: false,\n render: function (a) {\n return addKeyContent(a);\n },\n warnsWhen: function (a) {\n return !!(\n a.addKey &&\n a.addKey.accessKey &&\n a.addKey.accessKey.permission &&\n a.addKey.accessKey.permission.fullAccess !== undefined\n );\n },\n },\n deleteKey: {\n label: \"DeleteKey\",\n warn: true,\n render: function (a) {\n return deleteKeyContent(a);\n },\n },\n deleteAccount: {\n label: \"DeleteAccount\",\n warn: true,\n render: function (a) {\n return deleteAccountContent(a);\n },\n },\n signedDelegate: {\n label: \"SignedDelegate\",\n warn: false,\n render: function (a) {\n return signedDelegateContent(a);\n },\n },\n deployGlobalContract: {\n label: \"DeployGlobalContract\",\n warn: true,\n render: function (a) {\n return deployGlobalContractContent(a);\n },\n },\n useGlobalContract: {\n label: \"UseGlobalContract\",\n warn: false,\n render: function (a) {\n return useGlobalContractContent(a);\n },\n },\n};\n\nfunction handleNearAction(action, options) {\n const actionKey = getActionType(action);\n const entry = actionKey ? ACTION_DISPATCH[actionKey] : null;\n if (!entry) {\n const unknown = document.createElement(\"div\");\n unknown.textContent = `Unknown action: ${actionKey || \"(empty)\"}`;\n return createAccordion(\"Unknown\", unknown, true);\n }\n const showWarning = entry.warnsWhen ? entry.warnsWhen(action) : !!entry.warn;\n return createAccordion(entry.label, entry.render(action, options), showWarning);\n}\n\n/**\n * Build the full details DOM tree for a form custom field.\n *\n * @param {object} params\n * @param {Array<{label: string, value: string|undefined}>} params.fields - top-level fields like Signer/Receiver/MaxBlockHeight.\n * @param {string} params.actions - JSON string with the actions array (as Auth0 form fields deliver them).\n * @param {{ showYoctoConversion?: boolean }} [params.options]\n */\nfunction renderDetails(params) {\n ensureBufferPolyfill();\n const box = document.createElement(\"div\");\n box.classList.add(\"box\");\n\n for (const field of params.fields || []) {\n if (field.value === undefined || field.value === null || field.value === \"\") continue;\n box.appendChild(createTextContent(field.label, field.value));\n }\n\n const actionsContainer = document.createElement(\"div\");\n actionsContainer.classList.add(\"actions-container\");\n const actionsLabel = document.createElement(\"div\");\n actionsLabel.classList.add(\"label\");\n actionsLabel.textContent = \"Actions\";\n actionsContainer.appendChild(actionsLabel);\n\n let parsedActions = [];\n try {\n parsedActions = JSON.parse(params.actions || \"[]\");\n } catch (e) {\n const errorNode = document.createElement(\"div\");\n errorNode.classList.add(\"warning-callout\");\n errorNode.textContent = \"Failed to parse actions payload.\";\n actionsContainer.appendChild(errorNode);\n box.appendChild(actionsContainer);\n return box;\n }\n\n for (const action of parsedActions) {\n actionsContainer.appendChild(handleNearAction(action, params.options));\n }\n\n box.appendChild(actionsContainer);\n return box;\n}\n\nvar __auth0FormHelpers = {\n ensureBufferPolyfill: ensureBufferPolyfill,\n base58Encode: base58Encode,\n yoctoToNear: yoctoToNear,\n formatNearAmount: formatNearAmount,\n decodeFunctionCallArgs: decodeFunctionCallArgs,\n formatPublicKey: formatPublicKey,\n getActionType: getActionType,\n createTextContent: createTextContent,\n createDescription: createDescription,\n createAccordion: createAccordion,\n createAccountContent: createAccountContent,\n deployContractContent: deployContractContent,\n functionCallContent: functionCallContent,\n transferContent: transferContent,\n stakeContent: stakeContent,\n addKeyContent: addKeyContent,\n deleteKeyContent: deleteKeyContent,\n deleteAccountContent: deleteAccountContent,\n signedDelegateContent: signedDelegateContent,\n deployGlobalContractContent: deployGlobalContractContent,\n useGlobalContractContent: useGlobalContractContent,\n handleNearAction: handleNearAction,\n renderDetails: renderDetails,\n};\n\nif (typeof module !== \"undefined\" && module.exports) {\n module.exports = __auth0FormHelpers;\n}\n\n// --- END __auth0FormHelpers ---\n\n\nreturn (\n/**\n * Custom field that renders the Approve / Deny buttons for the authorize flow.\n *\n * Auth0 Forms buttons can only navigate — they cannot record a choice. So instead of the\n * native NEXT_BUTTON / PREVIOUS_BUTTON, this field owns both buttons and writes the user's\n * choice into the `decision` hidden field before advancing. The resuming action reads\n * `event.prompt.fields.decision` in onContinuePostLogin and denies access when it is \"denied\".\n *\n * Params (configured per-form in _form_base.json):\n * - approveText: label for the approve button (default \"Approve\")\n * - denyText: label for the deny button (default \"Deny\")\n */\nfunction AuthorizeAppDecision(context) {\n return {\n init: function () {\n const params = context.custom.getParams();\n\n const layout = document.createElement(\"div\");\n layout.classList.add(\"decision-layout\");\n\n const approve = document.createElement(\"button\");\n approve.setAttribute(\"type\", \"button\");\n approve.classList.add(\"decision-button\", \"approve\");\n approve.textContent = params.approveText || \"Approve\";\n approve.addEventListener(\"click\", function () {\n context.form.setHiddenField(\"decision\", \"approved\");\n context.form.goForward();\n });\n\n const deny = document.createElement(\"button\");\n deny.setAttribute(\"type\", \"button\");\n deny.classList.add(\"decision-button\", \"deny\");\n deny.textContent = params.denyText || \"Deny\";\n deny.addEventListener(\"click\", function () {\n context.form.setHiddenField(\"decision\", \"denied\");\n context.form.goForward();\n });\n\n layout.appendChild(approve);\n layout.appendChild(deny);\n return layout;\n },\n getScripts: function () { return []; },\n block: function () {},\n unblock: function () {},\n getValue: function () {},\n };\n}\n\n);\n})()\n", + "code": "(function () {\n// --- BEGIN __auth0FormHelpers (auto-inlined from shared/helpers/index.js) ---\n/**\n * Shared helpers for Auth0 form custom components.\n *\n * Runs in two environments:\n * - Auth0 form runtime: build.js inlines this file into each component's `config.code`,\n * exposing `__auth0FormHelpers` as a top-level variable in the same scope as the component.\n * - Node (tests / playground via require): consumed as a CommonJS module via module.exports.\n *\n * Keep this file free of `require` and ES imports so it can be concatenated as plain text.\n */\n\nfunction ensureBufferPolyfill() {\n if (typeof Buffer !== \"undefined\") return;\n if (typeof globalThis === \"undefined\") return;\n globalThis.Buffer = {\n from: function (data) {\n if (Array.isArray(data)) return new Uint8Array(data);\n if (typeof data === \"string\") return new TextEncoder().encode(data);\n return data;\n },\n };\n}\n\nconst BASE58_ALPHABET = \"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz\";\n\nfunction base58Encode(bytes) {\n let result = \"\";\n let num = BigInt(0);\n for (const byte of bytes) {\n num = (num << BigInt(8)) + BigInt(byte);\n }\n while (num > BigInt(0)) {\n const remainder = num % BigInt(58);\n num = num / BigInt(58);\n result = BASE58_ALPHABET[Number(remainder)] + result;\n }\n for (const byte of bytes) {\n if (byte === 0) result = BASE58_ALPHABET[0] + result;\n else break;\n }\n return result;\n}\n\n/**\n * Format a yocto-NEAR BigInt string as NEAR with up to 8 decimals.\n * \"1000000000000000000000000\" -> \"1\"\n * \"1500000000000000000000000\" -> \"1.5\"\n * \"1\" -> \"0.00000000\" trimmed -> \"0.0\"\n */\nfunction yoctoToNear(bigIntStr) {\n const trimmed = String(bigIntStr).replace(/^0+/, \"\") || \"0\";\n const length = trimmed.length;\n\n if (length <= 24) {\n const zerosNeeded = 24 - length;\n const withZeros = \"0\".repeat(zerosNeeded) + trimmed;\n const decimals = withZeros.slice(0, 8).replace(/0+$/, \"\") || \"0\";\n return `0.${decimals}`;\n }\n\n const splitPos = length - 24;\n const integerPart = trimmed.slice(0, splitPos);\n const decimalPart = trimmed.slice(splitPos, splitPos + 8);\n const trimmedDecimals = decimalPart.replace(/0+$/, \"\");\n if (trimmedDecimals === \"\") return integerPart;\n return `${integerPart}.${trimmedDecimals}`;\n}\n\nfunction formatPublicKey(publicKey) {\n if (publicKey && publicKey.secp256k1Key !== undefined) {\n return `secp256k1:${base58Encode(publicKey.secp256k1Key.data)}`;\n }\n if (publicKey && publicKey.ed25519Key !== undefined) {\n return `ed25519:${base58Encode(publicKey.ed25519Key.data)}`;\n }\n return \"\";\n}\n\n/**\n * Return the canonical action type discriminator (the single non-undefined key of the action).\n * Useful for dispatch and for tests that don't need DOM rendering.\n */\nfunction getActionType(action) {\n if (!action || typeof action !== \"object\") return null;\n for (const key of Object.keys(action)) {\n if (action[key] !== undefined) return key;\n }\n return null;\n}\n\n// --- DOM helpers (require document/window — jsdom in tests, real DOM in browser) ---\n\nfunction createTextContent(label, value, link = false) {\n const textContent = document.createElement(\"div\");\n textContent.classList.add(\"text-content\");\n\n const labelElement = document.createElement(\"div\");\n labelElement.classList.add(\"label\");\n labelElement.textContent = label;\n\n const valueElement = document.createElement(\"div\");\n valueElement.classList.add(\"value\");\n valueElement.textContent = value;\n\n if (link) {\n valueElement.classList.add(\"link\");\n valueElement.setAttribute(\"href\", link);\n valueElement.setAttribute(\"target\", \"_blank\");\n }\n\n textContent.appendChild(labelElement);\n textContent.appendChild(valueElement);\n return textContent;\n}\n\nfunction createDescription(text) {\n const node = document.createElement(\"p\");\n node.classList.add(\"action-description\");\n node.textContent = text;\n return node;\n}\n\nfunction createAccordion(label, content, showWarning = false) {\n const accordion = document.createElement(\"div\");\n accordion.classList.add(\"accordion\");\n\n const header = document.createElement(\"div\");\n header.classList.add(\"accordion-header\");\n\n const headerContent = document.createElement(\"div\");\n headerContent.classList.add(\"accordion-header-content\");\n\n const leftContent = document.createElement(\"div\");\n leftContent.classList.add(\"accordion-left-content\");\n\n if (showWarning) {\n const warningIcon = document.createElement(\"span\");\n warningIcon.classList.add(\"warning-icon\");\n warningIcon.innerHTML = \"⚠️\";\n leftContent.appendChild(warningIcon);\n }\n\n const labelElement = document.createElement(\"span\");\n labelElement.classList.add(\"accordion-header-label\");\n labelElement.textContent = label;\n leftContent.appendChild(labelElement);\n\n const expandIcon = document.createElement(\"span\");\n expandIcon.classList.add(\"expand-icon\");\n expandIcon.innerHTML = \"+\";\n\n headerContent.appendChild(leftContent);\n headerContent.appendChild(expandIcon);\n header.appendChild(headerContent);\n\n const contentElement = document.createElement(\"div\");\n contentElement.classList.add(\"accordion-content\");\n contentElement.appendChild(content);\n\n accordion.appendChild(header);\n accordion.appendChild(contentElement);\n\n header.addEventListener(\"click\", function () {\n contentElement.classList.toggle(\"open\");\n expandIcon.innerHTML = contentElement.classList.contains(\"open\") ? \"−\" : \"+\";\n });\n\n return accordion;\n}\n\n// --- Action content factories ---\n\nfunction createAccountContent() {\n return createDescription(\"By approving this request, a new account will be created.\");\n}\n\nfunction deployContractContent(action) {\n ensureBufferPolyfill();\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the compiled smart contract will be deployed.\");\n const codeLen = action.deployContract && action.deployContract.code ? `${action.deployContract.code.length} bytes` : null;\n container.appendChild(text);\n container.appendChild(createTextContent(\"Code\", codeLen));\n return container;\n}\n\n/**\n * Decode functionCall `args` (a byte array / Uint8Array) to a readable string. NEAR contract\n * args are almost always UTF-8 JSON, so decode the bytes and pretty-print when they parse as\n * JSON; otherwise fall back to the raw decoded text. TextDecoder is used directly because it\n * exists in the browser and the Auth0 form runtime (unlike Buffer, which the runtime lacks).\n */\nfunction decodeFunctionCallArgs(args) {\n if (args == null) return \"\";\n let text;\n try {\n if (typeof TextDecoder !== \"undefined\") {\n text = new TextDecoder().decode(Uint8Array.from(args));\n } else if (typeof Buffer !== \"undefined\" && typeof Buffer.from === \"function\") {\n text = Buffer.from(args).toString(\"utf8\");\n } else {\n text = String.fromCharCode.apply(null, Array.from(args));\n }\n } catch (e) {\n return \"\";\n }\n try {\n return JSON.stringify(JSON.parse(text), null, 2);\n } catch (e) {\n return text;\n }\n}\n\n/**\n * Format a yocto-NEAR amount for display. In transaction mode (showYoctoConversion) amounts at or\n * above 0.00000000001 NEAR are shown as NEAR; smaller amounts and delegate-action mode stay in\n * yoctoNEAR so no precision is hidden.\n */\nfunction formatNearAmount(value, options) {\n const showYoctoConversion = !!(options && options.showYoctoConversion);\n if (showYoctoConversion && value != null && BigInt(value) >= BigInt(\"10000000000000000\")) {\n return `${yoctoToNear(value.toString())} NEAR`;\n }\n return `${value != null ? value.toString() : \"0\"} yoctoNEAR`;\n}\n\nfunction functionCallContent(action, options) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following function will be called.\");\n\n const fc = action.functionCall || {};\n container.appendChild(text);\n container.appendChild(createTextContent(\"Method Name\", fc.methodName));\n container.appendChild(createTextContent(\"Args\", decodeFunctionCallArgs(fc.args)));\n container.appendChild(createTextContent(\"Gas\", fc.gas != null ? fc.gas.toString() : \"\"));\n container.appendChild(createTextContent(\"Deposit\", formatNearAmount(fc.deposit, options)));\n return container;\n}\n\nfunction transferContent(action, options) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following amount will be transferred to the receiver.\");\n\n const deposit = action.transfer ? action.transfer.deposit : undefined;\n container.appendChild(text);\n container.appendChild(createTextContent(\"Deposit\", formatNearAmount(deposit, options)));\n return container;\n}\n\nfunction stakeContent(action, options) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following amount will be staked to the public key.\");\n const stake = action.stake || {};\n container.appendChild(text);\n container.appendChild(createTextContent(\"Stake\", formatNearAmount(stake.stake, options)));\n container.appendChild(createTextContent(\"Public Key\", formatPublicKey(stake.publicKey)));\n return container;\n}\n\nfunction addKeyContent(action) {\n const container = document.createElement(\"div\");\n const ak = action.addKey || {};\n const accessKey = ak.accessKey || {};\n const permission = accessKey.permission || {};\n\n if (permission.fullAccess !== undefined) {\n const warning = document.createElement(\"div\");\n warning.classList.add(\"warning-callout\");\n warning.textContent = \"This key will have full access to your account. Only approve if you trust the recipient.\";\n container.appendChild(warning);\n }\n\n container.appendChild(createDescription(\"By approving this request, the following access key will be added to your account.\"));\n container.appendChild(createTextContent(\"Public Key\", formatPublicKey(ak.publicKey)));\n container.appendChild(createTextContent(\"Nonce\", accessKey.nonce != null ? accessKey.nonce.toString() : \"\"));\n\n if (permission.fullAccess === undefined) {\n container.appendChild(createTextContent(\"Permission\", JSON.stringify(permission.functionCall, null, 2)));\n }\n return container;\n}\n\nfunction deleteKeyContent(action) {\n const container = document.createElement(\"div\");\n const dk = action.deleteKey || {};\n\n const warning = document.createElement(\"div\");\n warning.classList.add(\"warning-callout\");\n warning.textContent = \"Once deleted, the key cannot be recovered.\";\n\n container.appendChild(warning);\n container.appendChild(\n createDescription(\"This action will delete an access key from your account. Only approve if you trust the recipient.\"),\n );\n container.appendChild(createTextContent(\"Public Key\", formatPublicKey(dk.publicKey)));\n return container;\n}\n\nfunction deleteAccountContent(action) {\n const container = document.createElement(\"div\");\n\n const warning = document.createElement(\"div\");\n warning.classList.add(\"warning-callout\");\n warning.textContent = \"Once deleted, the account cannot be recovered.\";\n\n container.appendChild(warning);\n container.appendChild(createDescription(\"By approving this request, the account will be deleted.\"));\n container.appendChild(createTextContent(\"Beneficiary ID\", action.deleteAccount && action.deleteAccount.beneficiaryId));\n return container;\n}\n\nfunction signedDelegateContent(action) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following delegate action will be executed.\");\n\n const sd = action.signedDelegate || {};\n let serialized;\n try {\n serialized = JSON.stringify(\n sd.delegateAction,\n function (_, value) {\n return typeof value === \"bigint\" ? value.toString() : value;\n },\n 2,\n );\n } catch (e) {\n serialized = \"\";\n }\n container.appendChild(text);\n container.appendChild(createTextContent(\"Delegate Action\", serialized));\n return container;\n}\n\nfunction deployGlobalContractContent(action) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the compiled smart contract will be deployed.\");\n const codeLen =\n action.deployGlobalContract && action.deployGlobalContract.code ? `${action.deployGlobalContract.code.length} bytes` : null;\n container.appendChild(text);\n container.appendChild(createTextContent(\"Code\", codeLen));\n return container;\n}\n\nfunction useGlobalContractContent(action) {\n const container = document.createElement(\"div\");\n const text = createDescription(\"By approving this request, the following global contract will be used.\");\n let identifier;\n try {\n identifier = JSON.stringify(action.useGlobalContract && action.useGlobalContract.contractIdentifier);\n } catch (e) {\n identifier = \"\";\n }\n container.appendChild(text);\n container.appendChild(createTextContent(\"Contract Identifier\", identifier));\n return container;\n}\n\nconst ACTION_DISPATCH = {\n createAccount: {\n label: \"CreateAccount\",\n warn: false,\n render: function () {\n return createAccountContent();\n },\n },\n deployContract: {\n label: \"DeployContract\",\n warn: true,\n render: function (a) {\n return deployContractContent(a);\n },\n },\n functionCall: {\n label: \"FunctionCall\",\n warn: false,\n render: function (a, opts) {\n return functionCallContent(a, opts);\n },\n },\n transfer: {\n label: \"Transfer\",\n warn: false,\n render: function (a, opts) {\n return transferContent(a, opts);\n },\n },\n stake: {\n label: \"Stake\",\n warn: false,\n render: function (a, opts) {\n return stakeContent(a, opts);\n },\n },\n addKey: {\n label: \"AddKey\",\n warn: false,\n render: function (a) {\n return addKeyContent(a);\n },\n warnsWhen: function (a) {\n return !!(\n a.addKey &&\n a.addKey.accessKey &&\n a.addKey.accessKey.permission &&\n a.addKey.accessKey.permission.fullAccess !== undefined\n );\n },\n },\n deleteKey: {\n label: \"DeleteKey\",\n warn: true,\n render: function (a) {\n return deleteKeyContent(a);\n },\n },\n deleteAccount: {\n label: \"DeleteAccount\",\n warn: true,\n render: function (a) {\n return deleteAccountContent(a);\n },\n },\n signedDelegate: {\n label: \"SignedDelegate\",\n warn: false,\n render: function (a) {\n return signedDelegateContent(a);\n },\n },\n deployGlobalContract: {\n label: \"DeployGlobalContract\",\n warn: true,\n render: function (a) {\n return deployGlobalContractContent(a);\n },\n },\n useGlobalContract: {\n label: \"UseGlobalContract\",\n warn: false,\n render: function (a) {\n return useGlobalContractContent(a);\n },\n },\n};\n\nfunction handleNearAction(action, options) {\n const actionKey = getActionType(action);\n const entry = actionKey ? ACTION_DISPATCH[actionKey] : null;\n if (!entry) {\n const unknown = document.createElement(\"div\");\n unknown.textContent = `Unknown action: ${actionKey || \"(empty)\"}`;\n return createAccordion(\"Unknown\", unknown, true);\n }\n const showWarning = entry.warnsWhen ? entry.warnsWhen(action) : !!entry.warn;\n return createAccordion(entry.label, entry.render(action, options), showWarning);\n}\n\n/**\n * Build the full details DOM tree for a form custom field.\n *\n * @param {object} params\n * @param {Array<{label: string, value: string|undefined}>} params.fields - top-level fields like Signer/Receiver/MaxBlockHeight.\n * @param {string} params.actions - JSON string with the actions array (as Auth0 form fields deliver them).\n * @param {{ showYoctoConversion?: boolean }} [params.options]\n */\nfunction renderDetails(params) {\n ensureBufferPolyfill();\n const box = document.createElement(\"div\");\n box.classList.add(\"box\");\n\n for (const field of params.fields || []) {\n if (field.value === undefined || field.value === null || field.value === \"\") continue;\n box.appendChild(createTextContent(field.label, field.value));\n }\n\n const actionsContainer = document.createElement(\"div\");\n actionsContainer.classList.add(\"actions-container\");\n const actionsLabel = document.createElement(\"div\");\n actionsLabel.classList.add(\"label\");\n actionsLabel.textContent = \"Actions\";\n actionsContainer.appendChild(actionsLabel);\n\n let parsedActions = [];\n try {\n parsedActions = JSON.parse(params.actions || \"[]\");\n } catch (e) {\n const errorNode = document.createElement(\"div\");\n errorNode.classList.add(\"warning-callout\");\n errorNode.textContent = \"Failed to parse actions payload.\";\n actionsContainer.appendChild(errorNode);\n box.appendChild(actionsContainer);\n return box;\n }\n\n for (const action of parsedActions) {\n actionsContainer.appendChild(handleNearAction(action, params.options));\n }\n\n box.appendChild(actionsContainer);\n return box;\n}\n\n// --- NEP-413 intent content factories ---\n\n/**\n * Render the token map of a transfer intent (`{ \"\": \"\" }`).\n *\n * Amounts stay in the token's smallest unit: the form has no token metadata, so converting\n * would mean guessing decimals — and a wrong guess here understates what the user is\n * approving. Showing the raw amount alongside its token id is honest and unambiguous.\n */\nfunction intentTokensContent(tokens) {\n const container = document.createElement(\"div\");\n if (!tokens || typeof tokens !== \"object\") return container;\n for (const tokenId of Object.keys(tokens)) {\n container.appendChild(createTextContent(tokenId, String(tokens[tokenId])));\n }\n return container;\n}\n\nfunction transferIntentContent(intent) {\n const container = document.createElement(\"div\");\n container.appendChild(createDescription(\"By approving this request, the following tokens will be transferred.\"));\n container.appendChild(createTextContent(\"Receiver ID\", intent.receiver_id));\n\n const tokensLabel = document.createElement(\"div\");\n tokensLabel.classList.add(\"label\");\n tokensLabel.textContent = \"Tokens\";\n container.appendChild(tokensLabel);\n container.appendChild(intentTokensContent(intent.tokens));\n return container;\n}\n\n/**\n * Fallback for intent kinds this form does not model explicitly. It is deliberately shown\n * with a warning: the user is approving something the UI cannot describe in plain terms.\n */\nfunction unknownIntentContent(intent) {\n const container = document.createElement(\"div\");\n container.appendChild(createDescription(\"This request contains an intent type this app does not recognize. Review it carefully.\"));\n let serialized;\n try {\n serialized = JSON.stringify(intent, null, 2);\n } catch (e) {\n serialized = \"\";\n }\n container.appendChild(createTextContent(\"Intent\", serialized));\n return container;\n}\n\nconst INTENT_DISPATCH = {\n transfer: {\n label: \"Transfer\",\n warn: false,\n render: function (i) {\n return transferIntentContent(i);\n },\n },\n};\n\nfunction handleIntent(intent) {\n const kind = intent && typeof intent === \"object\" ? intent.intent : null;\n const entry = kind ? INTENT_DISPATCH[kind] : null;\n if (!entry) {\n return createAccordion(kind ? `Unknown: ${kind}` : \"Unknown\", unknownIntentContent(intent), true);\n }\n return createAccordion(entry.label, entry.render(intent), !!entry.warn);\n}\n\n/**\n * Render the message body of a NEP-413 request verbatim.\n *\n * This is the default view, and the one that matters most: NEP-413 messages are arbitrary\n * strings meant to be read by a human (\"Sign in to example.com\"), so showing the text exactly\n * as it will be signed is the honest presentation. Pretty-printing is applied only when the\n * message is JSON, purely for legibility.\n */\nfunction messageContent(message) {\n const container = document.createElement(\"div\");\n let display = message;\n try {\n const parsed = JSON.parse(message);\n if (parsed && typeof parsed === \"object\") display = JSON.stringify(parsed, null, 2);\n } catch (e) {\n display = message;\n }\n container.appendChild(createTextContent(\"Message\", display));\n return container;\n}\n\n/**\n * Build the details DOM tree for a NEP-413 signature approval.\n *\n * Two presentations, one payload: when the message carries NEAR Intents the intents are broken\n * out one by one, and otherwise the raw message is shown as text. Either way the top-level\n * fields — recipient above all — are rendered, because under NEP-413 the recipient is what\n * tells the user which application their signature is addressed to.\n * @param {object} params The render parameters.\n * @param {Array<{label: string, value: string|undefined}>} params.fields Top-level fields (recipient, callback URL, signer, deadline).\n * @param {string} params.message The raw NEP-413 message, shown when there are no intents to break out.\n * @param {string} [params.intents] JSON string with the intents array, when the message is a NEAR Intents body.\n * @returns {HTMLElement} The details node.\n */\nfunction renderNep413Details(params) {\n ensureBufferPolyfill();\n const box = document.createElement(\"div\");\n box.classList.add(\"box\");\n\n for (const field of params.fields || []) {\n if (field.value === undefined || field.value === null || field.value === \"\") continue;\n box.appendChild(createTextContent(field.label, field.value));\n }\n\n let parsedIntents = null;\n if (params.intents) {\n try {\n const candidate = JSON.parse(params.intents);\n if (Array.isArray(candidate) && candidate.length > 0) parsedIntents = candidate;\n } catch (e) {\n parsedIntents = null;\n }\n }\n\n // No intents to break out (or they were unreadable) — show the message itself. Falling back\n // to the raw text keeps every NEP-413 message approvable, not just the ones we model.\n if (!parsedIntents) {\n box.appendChild(messageContent(params.message || \"\"));\n return box;\n }\n\n const intentsContainer = document.createElement(\"div\");\n intentsContainer.classList.add(\"actions-container\");\n const intentsLabel = document.createElement(\"div\");\n intentsLabel.classList.add(\"label\");\n intentsLabel.textContent = \"Intents\";\n intentsContainer.appendChild(intentsLabel);\n\n for (const intent of parsedIntents) {\n intentsContainer.appendChild(handleIntent(intent));\n }\n\n box.appendChild(intentsContainer);\n return box;\n}\n\nvar __auth0FormHelpers = {\n ensureBufferPolyfill: ensureBufferPolyfill,\n base58Encode: base58Encode,\n yoctoToNear: yoctoToNear,\n formatNearAmount: formatNearAmount,\n decodeFunctionCallArgs: decodeFunctionCallArgs,\n formatPublicKey: formatPublicKey,\n getActionType: getActionType,\n createTextContent: createTextContent,\n createDescription: createDescription,\n createAccordion: createAccordion,\n createAccountContent: createAccountContent,\n deployContractContent: deployContractContent,\n functionCallContent: functionCallContent,\n transferContent: transferContent,\n stakeContent: stakeContent,\n addKeyContent: addKeyContent,\n deleteKeyContent: deleteKeyContent,\n deleteAccountContent: deleteAccountContent,\n signedDelegateContent: signedDelegateContent,\n deployGlobalContractContent: deployGlobalContractContent,\n useGlobalContractContent: useGlobalContractContent,\n handleNearAction: handleNearAction,\n renderDetails: renderDetails,\n intentTokensContent: intentTokensContent,\n transferIntentContent: transferIntentContent,\n unknownIntentContent: unknownIntentContent,\n handleIntent: handleIntent,\n messageContent: messageContent,\n renderNep413Details: renderNep413Details,\n};\n\nif (typeof module !== \"undefined\" && module.exports) {\n module.exports = __auth0FormHelpers;\n}\n\n// --- END __auth0FormHelpers ---\n\n\nreturn (\n/**\n * Custom field that renders the Approve / Deny buttons for the authorize flow.\n *\n * Auth0 Forms buttons can only navigate — they cannot record a choice. So instead of the\n * native NEXT_BUTTON / PREVIOUS_BUTTON, this field owns both buttons and writes the user's\n * choice into the `decision` hidden field before advancing. The resuming action reads\n * `event.prompt.fields.decision` in onContinuePostLogin and denies access when it is \"denied\".\n *\n * Params (configured per-form in _form_base.json):\n * - approveText: label for the approve button (default \"Approve\")\n * - denyText: label for the deny button (default \"Deny\")\n */\nfunction AuthorizeAppDecision(context) {\n return {\n init: function () {\n const params = context.custom.getParams();\n\n const layout = document.createElement(\"div\");\n layout.classList.add(\"decision-layout\");\n\n const approve = document.createElement(\"button\");\n approve.setAttribute(\"type\", \"button\");\n approve.classList.add(\"decision-button\", \"approve\");\n approve.textContent = params.approveText || \"Approve\";\n approve.addEventListener(\"click\", function () {\n context.form.setHiddenField(\"decision\", \"approved\");\n context.form.goForward();\n });\n\n const deny = document.createElement(\"button\");\n deny.setAttribute(\"type\", \"button\");\n deny.classList.add(\"decision-button\", \"deny\");\n deny.textContent = params.denyText || \"Deny\";\n deny.addEventListener(\"click\", function () {\n context.form.setHiddenField(\"decision\", \"denied\");\n context.form.goForward();\n });\n\n layout.appendChild(approve);\n layout.appendChild(deny);\n return layout;\n },\n getScripts: function () { return []; },\n block: function () {},\n unblock: function () {},\n getValue: function () {},\n };\n}\n\n);\n})()\n", "css": ".decision-layout {\n display: flex;\n flex-direction: column;\n gap: 0.75rem;\n width: 100%;\n}\n\n.decision-button {\n width: 100%;\n padding: 0.75rem 1rem;\n border-radius: 0.75rem;\n font-size: 0.875rem;\n font-weight: 600;\n cursor: pointer;\n border: 1px solid transparent;\n}\n\n.decision-button.approve {\n background-color: #0a0a0a;\n color: #ffffff;\n}\n\n.decision-button.deny {\n background-color: #ffffff;\n color: #3f4246;\n border-color: #e5e5e5;\n}\n", "params": { "approveText": "Approve", diff --git a/packages/auth0/test/fixtures/builders.js b/packages/auth0/test/fixtures/builders.js index 312762d7..18ad0142 100644 --- a/packages/auth0/test/fixtures/builders.js +++ b/packages/auth0/test/fixtures/builders.js @@ -25,6 +25,7 @@ const { GlobalContractIdentifier, } = require("@near-js/transactions"); const { PublicKey } = require("near-api-js").utils; +const { serialize: borshSerialize } = require("borsh"); // jest-environment-jsdom (jest 29) ships an older jsdom without TextEncoder. Fall back to util. const SafeTextEncoder = typeof TextEncoder !== "undefined" ? TextEncoder : require("util").TextEncoder; @@ -280,6 +281,66 @@ const DELEGATE_ACTION_TYPES = ALL_ACTION_TYPES.filter( (a) => !["signedDelegate", "deployGlobalContract", "useGlobalContract"].includes(a.name.split(" ")[0]), ); +// ----- NEP-413 intent payloads ----- + +// Domain-separation tag from NEP-413 (2^31 + 413). +const NEP413_PREFIX_TAG = Math.pow(2, 31) + 413; + +const INTENTS_RECIPIENT = "intents.near"; + +/** + * Borsh schema for the NEP-413 payload, transcribed from the NEP rather than imported from + * the action under test. Duplicating it deliberately: if someone edits the action's schema, + * these fixtures keep encoding per the spec and the round-trip test fails — which is the + * point. Importing the action's own schema would make the test circular. + * + * https://github.com/near/NEPs/blob/master/neps/nep-0413.md#input-interface + */ +const NEP413_SCHEMA = { + struct: { + tag: "u32", + message: "string", + nonce: { array: { type: "u8", len: 32 } }, + recipient: "string", + callbackUrl: { option: "string" }, + }, +}; + +const SAMPLE_NONCE = Uint8Array.from(Array.from({ length: 32 }, (_, i) => (i * 5) % 256)); + +/** + * Build the JSON message body NEAR Intents expects inside a NEP-413 payload. + */ +function buildIntentMessage({ + signerId = "trader.near", + deadline = "2026-01-01T00:00:00.000Z", + intents = [{ intent: "transfer", receiver_id: "deposit.near", tokens: { "nep141:usdc.near": "1000000" } }], +} = {}) { + return { signer_id: signerId, deadline, intents }; +} + +/** + * Encode a NEP-413 payload the way the client SDK puts it on the authorize query string. + * + * @returns {{csv: string, bytes: Uint8Array, payload: object, message: object}} + */ +function buildNep413Payload({ + message, + tag = NEP413_PREFIX_TAG, + recipient = INTENTS_RECIPIENT, + nonce = SAMPLE_NONCE, + callbackUrl = null, + rawMessage, +} = {}) { + const messageObject = message === undefined ? buildIntentMessage() : message; + const messageString = rawMessage !== undefined ? rawMessage : JSON.stringify(messageObject); + + const payload = { tag, message: messageString, nonce, recipient, callbackUrl }; + const bytes = borshSerialize(NEP413_SCHEMA, payload); + + return { csv: toCsv(bytes), bytes, payload, message: messageObject }; +} + module.exports = { DELEGATE_ACTION_PREFIX, ZERO_BLOCK_HASH, @@ -293,4 +354,10 @@ module.exports = { toCsv, ALL_ACTION_TYPES, DELEGATE_ACTION_TYPES, + NEP413_PREFIX_TAG, + INTENTS_RECIPIENT, + NEP413_SCHEMA, + SAMPLE_NONCE, + buildIntentMessage, + buildNep413Payload, }; diff --git a/packages/auth0/test/nep413-decoding.spec.js b/packages/auth0/test/nep413-decoding.spec.js new file mode 100644 index 00000000..acf42284 --- /dev/null +++ b/packages/auth0/test/nep413-decoding.spec.js @@ -0,0 +1,163 @@ +/** + * @jest-environment node + * + * Tests for `decodeNep413Payload` — the validation gate for NEP-413 payloads. + * + * The guard contract never inspects the bytes it signs; it only checks that they equal the + * `fatxn` claim. So every guarantee about *what* the user approved is established here. + * + * Two checks are load-bearing, and only two: + * + * - the domain tag, without which the bytes could double as a NEAR transaction; + * - a non-empty message, without which the approval screen has nothing to show. + * + * The recipient is deliberately *not* restricted. Under NEP-413 it names the application a + * message is addressed to, and the protection is that the user sees it on the approval screen — + * the same way NEAR wallets behave. Restricting it here would also be the wrong granularity: a + * recipient belongs to an application, not to a tenant that hosts many of them. + */ +const { decodeNep413Payload, extractIntents, stringifyIntents, NEP413_PREFIX_TAG } = require("../src/actions/authorize-app.action.js"); +const { buildNep413Payload, buildIntentMessage, buildTransaction, toCsv, INTENTS_RECIPIENT } = require("./fixtures/builders.js"); + +describe("decodeNep413Payload — any valid message", () => { + test("decodes a plain-text sign-in challenge", () => { + const { csv } = buildNep413Payload({ rawMessage: "Sign in to example.com", recipient: "example.com" }); + const { payload, message } = decodeNep413Payload(csv); + + expect(payload.tag).toBe(NEP413_PREFIX_TAG); + expect(payload.message).toBe("Sign in to example.com"); + expect(payload.recipient).toBe("example.com"); + // Not JSON, so there is no structured message — and that is fine. + expect(message).toBeNull(); + }); + + test("decodes a NEAR Intents message and exposes its body", () => { + const { csv, message: original } = buildNep413Payload(); + const { payload, message } = decodeNep413Payload(csv); + + expect(payload.recipient).toBe(INTENTS_RECIPIENT); + expect(message).toEqual(original); + expect(extractIntents(message)).toHaveLength(1); + }); + + test("accepts any recipient", () => { + for (const recipient of ["intents.near", "app.example.com", "alice.near", "some-dapp"]) { + const { csv } = buildNep413Payload({ rawMessage: "hello", recipient }); + expect(decodeNep413Payload(csv).payload.recipient).toBe(recipient); + } + }); + + test("accepts JSON that is not an intents body", () => { + const { csv } = buildNep413Payload({ rawMessage: JSON.stringify({ purpose: "login", session: "abc" }) }); + const { message } = decodeNep413Payload(csv); + + expect(message).toEqual({ purpose: "login", session: "abc" }); + expect(extractIntents(message)).toBeNull(); + }); + + test("carries the callback url through when present", () => { + const { csv } = buildNep413Payload({ rawMessage: "hello", callbackUrl: "https://example.com/cb" }); + expect(decodeNep413Payload(csv).payload.callbackUrl).toBe("https://example.com/cb"); + }); + + test("round-trips the exact byte string it was given", () => { + const { csv, bytes } = buildNep413Payload(); + // The action puts the query value straight into `fatxn`, so what gets approved must be + // byte-identical to what was decoded for display. + expect(csv.split(",").map(Number)).toEqual(Array.from(bytes)); + }); + + test("accepts multiple intents in a single message", () => { + const message = buildIntentMessage({ + intents: [ + { intent: "transfer", receiver_id: "a.near", tokens: { "nep141:usdc.near": "1" } }, + { intent: "transfer", receiver_id: "b.near", tokens: { "nep141:usdt.near": "2" } }, + ], + }); + const { csv } = buildNep413Payload({ message }); + expect(extractIntents(decodeNep413Payload(csv).message)).toHaveLength(2); + }); +}); + +describe("decodeNep413Payload — domain separation", () => { + test("rejects a payload whose tag is not the NEP-413 prefix", () => { + const { csv } = buildNep413Payload({ tag: 1 }); + expect(() => decodeNep413Payload(csv)).toThrow(/missing the NEP-413 domain tag/); + }); + + test("rejects the delegate-action prefix reused as a tag", () => { + const { csv } = buildNep413Payload({ tag: Math.pow(2, 30) + 366 }); + expect(() => decodeNep413Payload(csv)).toThrow(/missing the NEP-413 domain tag/); + }); + + test("rejects transaction bytes submitted as a message", () => { + // A real NEAR transaction must never decode into an approvable message, whether it + // fails at borsh or at the tag check. + const { csv } = buildTransaction(); + expect(() => decodeNep413Payload(csv)).toThrow(); + }); +}); + +describe("decodeNep413Payload — message must be showable", () => { + test("rejects an empty message", () => { + const { csv } = buildNep413Payload({ rawMessage: "" }); + expect(() => decodeNep413Payload(csv)).toThrow(/message is empty/); + }); + + test("accepts a message with no intents array", () => { + // Previously rejected. A message need not be a NEAR Intents body to be worth signing. + const { csv } = buildNep413Payload({ rawMessage: JSON.stringify({ signer_id: "trader.near" }) }); + expect(() => decodeNep413Payload(csv)).not.toThrow(); + }); + + test("accepts a message whose intents array is empty", () => { + const { csv } = buildNep413Payload({ message: buildIntentMessage({ intents: [] }) }); + const { message } = decodeNep413Payload(csv); + expect(extractIntents(message)).toBeNull(); + }); +}); + +describe("decodeNep413Payload — malformed input", () => { + test("rejects bytes that are not a NEP-413 payload", () => { + expect(() => decodeNep413Payload(toCsv(Uint8Array.from([1, 2, 3, 4])))).toThrow(/not a valid NEP-413 message/); + }); + + test("rejects an empty payload", () => { + expect(() => decodeNep413Payload("")).toThrow(/not a valid NEP-413 message/); + }); + + test("rejects a truncated payload", () => { + const { bytes } = buildNep413Payload(); + expect(() => decodeNep413Payload(toCsv(bytes.slice(0, Math.floor(bytes.length / 2))))).toThrow( + /not a valid NEP-413 message/, + ); + }); +}); + +describe("extractIntents", () => { + test("returns the intents array of a NEAR Intents body", () => { + const message = buildIntentMessage(); + expect(extractIntents(message)).toEqual(message.intents); + }); + + test("returns null for anything that is not one", () => { + for (const value of [null, {}, { intents: [] }, { intents: "transfer" }, { intents: {} }]) { + expect(extractIntents(value)).toBeNull(); + } + }); +}); + +describe("stringifyIntents", () => { + test("pretty-prints the intents for the approval screen", () => { + const serialized = stringifyIntents(buildIntentMessage().intents); + expect(JSON.parse(serialized)).toEqual(buildIntentMessage().intents); + expect(serialized).toContain("\n"); + }); + + test("renders bigint amounts instead of throwing on them", () => { + // JSON.stringify throws on BigInt by default, and token amounts are a plausible place + // for one to arrive. + const serialized = stringifyIntents([{ intent: "transfer", tokens: { "nep141:usdc.near": BigInt("1000000") } }]); + expect(JSON.parse(serialized).tokens ?? JSON.parse(serialized)[0].tokens).toEqual({ "nep141:usdc.near": "1000000" }); + }); +}); diff --git a/packages/auth0/test/nep413-handlers.spec.js b/packages/auth0/test/nep413-handlers.spec.js new file mode 100644 index 00000000..a20fd761 --- /dev/null +++ b/packages/auth0/test/nep413-handlers.spec.js @@ -0,0 +1,176 @@ +/** + * @jest-environment node + * + * Tests for the intent branch of `onExecutePostLogin`: form dispatch, the `fields` shape the + * approval screen receives, the `fatxn` claim, denial on an undecodable payload, and the + * single-payload rule. + */ +const { onExecutePostLogin } = require("../src/actions/authorize-app.action.js"); +const { buildNep413Payload, buildIntentMessage, buildTransaction, buildDelegateAction } = require("./fixtures/builders.js"); + +const ONCHAIN_AUDIENCE = "https://onchain.example"; + +function makeApi() { + const calls = { deny: [], removedScopes: [], customClaims: {}, render: null }; + const api = { + access: { + deny: (msg) => { + calls.deny.push(msg); + }, + }, + accessToken: { + removeScope: (s) => { + calls.removedScopes.push(s); + }, + setCustomClaim: (k, v) => { + calls.customClaims[k] = v; + }, + }, + prompt: { + render: (modalId, opts) => { + calls.render = { modalId, opts }; + }, + }, + }; + return { api, calls }; +} + +function makeEvent({ query = {}, audience = ONCHAIN_AUDIENCE } = {}) { + return { + secrets: { + ONCHAIN_AUDIENCE, + TRANSACTION_FORM: "modal_tx", + DELEGATE_ACTION_FORM: "modal_delegate", + NEP413_FORM: "modal_nep413", + }, + request: { query }, + resource_server: audience == null ? undefined : { identifier: audience }, + client: { name: "Test App", metadata: { logo_uri: "https://logo.example/x.png" } }, + }; +} + +describe("onExecutePostLogin — intent dispatch", () => { + test("renders the intent form with signer, verifier and deadline", async () => { + const { api, calls } = makeApi(); + const { csv, message } = buildNep413Payload(); + + await onExecutePostLogin(makeEvent({ query: { nep413: csv } }), api); + + expect(calls.deny).toEqual([]); + expect(calls.render.modalId).toBe("modal_nep413"); + expect(calls.render.opts.fields).toMatchObject({ + name: "Test App", + imageUrl: "https://logo.example/x.png", + signerId: message.signer_id, + recipient: "intents.near", + deadline: message.deadline, + }); + }); + + test("hands the intents to the form as a JSON string", async () => { + const { api, calls } = makeApi(); + const { csv, message } = buildNep413Payload(); + + await onExecutePostLogin(makeEvent({ query: { nep413: csv } }), api); + + expect(JSON.parse(calls.render.opts.fields.intents)).toEqual(message.intents); + }); + + test("sets fatxn to the exact bytes received", async () => { + const { api, calls } = makeApi(); + const { csv, bytes } = buildNep413Payload(); + + await onExecutePostLogin(makeEvent({ query: { nep413: csv } }), api); + + // The guard compares fatxn to sign_payload byte for byte; any transformation here + // would make every signature fail on-chain. + expect(calls.customClaims.fatxn).toEqual(Array.from(bytes)); + }); + + test("strips the OIDC profile scopes like the other payload types", async () => { + const { api, calls } = makeApi(); + const { csv } = buildNep413Payload(); + + await onExecutePostLogin(makeEvent({ query: { nep413: csv } }), api); + + expect(calls.removedScopes).toEqual(["profile", "email", "offline_access"]); + }); + + test("passes the recipient through unchanged whatever it is", async () => { + const { api, calls } = makeApi(); + const { csv } = buildNep413Payload({ recipient: "intents.testnet" }); + + await onExecutePostLogin(makeEvent({ query: { nep413: csv } }), api); + + expect(calls.deny).toEqual([]); + expect(calls.render.opts.fields.recipient).toBe("intents.testnet"); + }); +}); + +describe("onExecutePostLogin — intent rejection", () => { + test("denies instead of rendering when the payload cannot be decoded", async () => { + const { api, calls } = makeApi(); + const { csv } = buildNep413Payload({ tag: 7 }); + + await onExecutePostLogin(makeEvent({ query: { nep413: csv } }), api); + + expect(calls.deny).toEqual(["Payload is missing the NEP-413 domain tag"]); + expect(calls.render).toBeNull(); + expect(calls.customClaims.fatxn).toBeUndefined(); + }); + + test("accepts any recipient and shows it to the user", async () => { + const { api, calls } = makeApi(); + const { csv } = buildNep413Payload({ rawMessage: "Sign in to example.com", recipient: "example.com" }); + + await onExecutePostLogin(makeEvent({ query: { nep413: csv } }), api); + + expect(calls.deny).toEqual([]); + expect(calls.render.opts.fields.recipient).toBe("example.com"); + }); + + test("denies a message with nothing to show the user", async () => { + const { api, calls } = makeApi(); + const { csv } = buildNep413Payload({ rawMessage: "" }); + + await onExecutePostLogin(makeEvent({ query: { nep413: csv } }), api); + + expect(calls.deny).toEqual(["NEP-413 message is empty"]); + expect(calls.customClaims.fatxn).toBeUndefined(); + }); + + test("denies a message sent to a non-signing audience", async () => { + const { api, calls } = makeApi(); + const { csv } = buildNep413Payload(); + + await onExecutePostLogin(makeEvent({ query: { nep413: csv }, audience: "https://other.example" }), api); + + expect(calls.deny).toEqual(["Transaction payload only allowed with signing audience"]); + }); +}); + +describe("onExecutePostLogin — single payload rule", () => { + test("denies when an intent arrives alongside a transaction", async () => { + const { api, calls } = makeApi(); + const { csv: intentCsv } = buildNep413Payload(); + const { csv: txCsv } = buildTransaction(); + + await onExecutePostLogin(makeEvent({ query: { nep413: intentCsv, transaction: txCsv } }), api); + + // Otherwise the screen could show one payload while a different one lands in fatxn. + expect(calls.deny).toEqual(["Only one signing payload may be requested at a time"]); + expect(calls.render).toBeNull(); + expect(calls.customClaims.fatxn).toBeUndefined(); + }); + + test("denies when a transaction arrives alongside a delegate action", async () => { + const { api, calls } = makeApi(); + const { csv: txCsv } = buildTransaction(); + const { csv: delegateCsv } = buildDelegateAction(); + + await onExecutePostLogin(makeEvent({ query: { transaction: txCsv, delegateAction: delegateCsv } }), api); + + expect(calls.deny).toEqual(["Only one signing payload may be requested at a time"]); + expect(calls.render).toBeNull(); + }); +}); diff --git a/packages/auth0/test/nep413-helpers.spec.js b/packages/auth0/test/nep413-helpers.spec.js new file mode 100644 index 00000000..7dc314ff --- /dev/null +++ b/packages/auth0/test/nep413-helpers.spec.js @@ -0,0 +1,144 @@ +/** + * @jest-environment jsdom + * + * Tests for `renderNep413Details` — the approval screen for NEP-413 intents. + * + * This is the half of the security model the contract cannot enforce: the user must be able + * to read what they are about to sign. These tests assert the transfer details actually reach + * the DOM, and that anything the form cannot describe is surfaced with a warning rather than + * rendered as if it were understood. + */ +const helpers = require("../src/forms/shared/helpers/index.js"); + +const TRANSFER = { intent: "transfer", receiver_id: "deposit.near", tokens: { "nep141:usdc.near": "1000000" } }; + +function render(intents, fields = []) { + return helpers.renderNep413Details({ + fields, + message: JSON.stringify({ intents }), + intents: JSON.stringify(intents), + }); +} + +describe("renderNep413Details — top-level fields", () => { + test("renders the fields it is given", () => { + const box = render( + [TRANSFER], + [ + { label: "Signer ID", value: "trader.near" }, + { label: "Verifier", value: "intents.near" }, + { label: "Deadline", value: "2026-01-01T00:00:00.000Z" }, + ], + ); + const text = box.textContent; + expect(text).toContain("trader.near"); + expect(text).toContain("intents.near"); + expect(text).toContain("2026-01-01T00:00:00.000Z"); + }); + + test("skips empty fields instead of rendering blank rows", () => { + const box = render([TRANSFER], [{ label: "Deadline", value: "" }]); + expect(box.textContent).not.toContain("Deadline"); + }); + + test("labels the section as Intents", () => { + const box = render([TRANSFER]); + expect(box.querySelector(".actions-container").textContent).toContain("Intents"); + }); +}); + +describe("renderNep413Details — transfer intents", () => { + test("shows the receiver and the token amount", () => { + const box = render([TRANSFER]); + const text = box.textContent; + expect(text).toContain("deposit.near"); + expect(text).toContain("nep141:usdc.near"); + expect(text).toContain("1000000"); + }); + + test("labels the accordion as Transfer without a warning", () => { + const box = render([TRANSFER]); + expect(box.querySelector(".accordion-header-label").textContent).toBe("Transfer"); + expect(box.querySelector(".warning-icon")).toBeNull(); + }); + + test("renders every token in a multi-token transfer", () => { + const box = render([{ ...TRANSFER, tokens: { "nep141:usdc.near": "1", "nep141:usdt.near": "2" } }]); + const text = box.textContent; + expect(text).toContain("nep141:usdc.near"); + expect(text).toContain("nep141:usdt.near"); + }); + + test("renders one accordion per intent", () => { + const box = render([TRANSFER, { ...TRANSFER, receiver_id: "other.near" }]); + expect(box.querySelectorAll(".accordion")).toHaveLength(2); + expect(box.textContent).toContain("other.near"); + }); + + test("survives a transfer with no tokens map", () => { + const box = render([{ intent: "transfer", receiver_id: "deposit.near" }]); + expect(box.textContent).toContain("deposit.near"); + }); +}); + +describe("renderNep413Details — unrecognized intents", () => { + test("flags an unknown intent kind with a warning", () => { + const box = render([{ intent: "token_diff", diff: { "nep141:usdc.near": "-1" } }]); + expect(box.querySelector(".warning-icon")).not.toBeNull(); + expect(box.querySelector(".accordion-header-label").textContent).toBe("Unknown: token_diff"); + }); + + test("still shows the raw payload of an unknown intent", () => { + const box = render([{ intent: "token_diff", diff: { "nep141:usdc.near": "-1" } }]); + expect(box.textContent).toContain("token_diff"); + expect(box.textContent).toContain("nep141:usdc.near"); + }); + + test("flags an intent with no kind at all", () => { + const box = render([{ receiver_id: "deposit.near" }]); + expect(box.querySelector(".accordion-header-label").textContent).toBe("Unknown"); + expect(box.querySelector(".warning-icon")).not.toBeNull(); + }); + + test("warns per intent, leaving known ones unflagged", () => { + const box = render([TRANSFER, { intent: "mystery" }]); + const labels = Array.from(box.querySelectorAll(".accordion-header-label")).map((n) => n.textContent); + expect(labels).toEqual(["Transfer", "Unknown: mystery"]); + expect(box.querySelectorAll(".warning-icon")).toHaveLength(1); + }); +}); + +describe("renderNep413Details — plain messages", () => { + test("shows an arbitrary message verbatim", () => { + const box = helpers.renderNep413Details({ fields: [], message: "Sign in to example.com" }); + expect(box.textContent).toContain("Sign in to example.com"); + expect(box.querySelectorAll(".accordion")).toHaveLength(0); + }); + + test("pretty-prints a JSON message that is not an intents body", () => { + const box = helpers.renderNep413Details({ fields: [], message: JSON.stringify({ purpose: "login" }) }); + expect(box.textContent).toContain("purpose"); + expect(box.textContent).toContain("login"); + }); + + test("falls back to the message when the intents payload is unreadable", () => { + // An unparseable structured view must never hide what is actually being signed. + const box = helpers.renderNep413Details({ fields: [], message: "the real message", intents: "{not json" }); + expect(box.textContent).toContain("the real message"); + }); + + test("falls back to the message when the intents array is empty", () => { + const box = helpers.renderNep413Details({ fields: [], message: "nothing to break out", intents: "[]" }); + expect(box.textContent).toContain("nothing to break out"); + expect(box.querySelectorAll(".accordion")).toHaveLength(0); + }); + + test("shows the recipient even for a plain message", () => { + const box = helpers.renderNep413Details({ + fields: [{ label: "Recipient", value: "example.com" }], + message: "Sign in", + }); + // NEP-413 leans on the user seeing who the message is addressed to. + expect(box.textContent).toContain("example.com"); + }); +}); diff --git a/packages/providers/javascript/package.json b/packages/providers/javascript/package.json index f1da50b2..4c1025c2 100644 --- a/packages/providers/javascript/package.json +++ b/packages/providers/javascript/package.json @@ -22,6 +22,7 @@ "@auth0/auth0-spa-js": "^2.11.3", "@near-js/accounts": "^2.0.2", "@near-js/transactions": "^2.0.2", + "borsh": "^2.0.0", "elliptic": "^6.6.1", "jwt-decode": "^3.1.2", "near-api-js": "^5.1.1", diff --git a/packages/providers/javascript/src/index.ts b/packages/providers/javascript/src/index.ts index 84f64a16..14f87d82 100644 --- a/packages/providers/javascript/src/index.ts +++ b/packages/providers/javascript/src/index.ts @@ -1,3 +1,4 @@ export * from "./provider"; export * from "./types"; +export * from "./nep413"; export * from "@shared/core"; diff --git a/packages/providers/javascript/src/nep413.ts b/packages/providers/javascript/src/nep413.ts new file mode 100644 index 00000000..3f032796 --- /dev/null +++ b/packages/providers/javascript/src/nep413.ts @@ -0,0 +1,136 @@ +import { serialize } from "borsh"; +import type { Schema } from "borsh"; + +/** + * Domain-separation tag mandated by NEP-413 (2^31 + 413). Prepending it guarantees the signed + * bytes can never be reinterpreted as a NEAR transaction, whose borsh encoding begins with the + * signerId length — a small u32. + * + * https://github.com/near/NEPs/blob/master/neps/nep-0413.md#how-to-ensure-the-message-is-not-a-transaction + */ +export const NEP413_PREFIX_TAG = 2147484061; + +/** NEP-413 fixes the nonce at 32 bytes. */ +export const NEP413_NONCE_LENGTH = 32; + +/** + * Borsh schema of the NEP-413 payload. Field order is normative — it must match the schema the + * Auth0 action decodes with, or the bytes committed to `fatxn` will not match what was signed. + * + * https://github.com/near/NEPs/blob/master/neps/nep-0413.md#input-interface + */ +export const NEP413_PAYLOAD_SCHEMA: Schema = { + struct: { + tag: "u32", + message: "string", + nonce: { array: { type: "u8", len: NEP413_NONCE_LENGTH } }, + recipient: "string", + callbackUrl: { option: "string" }, + }, +}; + +export type NEP413Payload = { + /** + * The message to sign. Any string: a human-readable challenge such as "Sign in to + * example.com", or a structured body like the NEAR Intents JSON (`signer_id`, `deadline`, + * `intents`), which the approval screen renders as individual intents. + */ + message: string; + /** + * 32-byte nonce guarding against replay. Generated when omitted. + */ + nonce?: Uint8Array; + /** + * Account the message is addressed to, e.g. `intents.near` or an app's own account. Under + * NEP-413 this is what stops a message from being relayed to a third party, and it is shown + * to the user on the approval screen. + */ + recipient: string; + /** + * Optional URL the signing result is returned to. Part of the signed payload. + */ + callbackUrl?: string; +}; + +/** + * A NEP-413 signature result, in the shape the standard defines for `signMessage`. + * https://github.com/near/NEPs/blob/master/neps/nep-0413.md#output-interface + */ +export type NEP413SignedMessage = { + /** The signing account. */ + accountId: string; + /** Public key as `:`. */ + publicKey: string; + /** Base64-encoded signature over sha256 of the serialized payload. */ + signature: string; + /** + * Echo of the caller's CSRF state, when one was supplied. + * + * NEP-413's `state` never leaves the client: the standard defines it as a value the caller + * generates, holds, and matches when the result comes back. It is deliberately absent from + * the signed payload and from the authorization request — Auth0 mints its own `state` for + * the OAuth exchange and overwrites anything passed alongside it, so routing NEP-413's state + * through there would silently drop it. Hold it caller-side and attach it here. + */ + state?: string; +}; + +/** + * Generate a random 32-byte NEP-413 nonce. + * @returns The nonce. + */ +export function generateNep413Nonce(): Uint8Array { + return globalThis.crypto.getRandomValues(new Uint8Array(NEP413_NONCE_LENGTH)); +} + +/** + * Borsh-serialize a NEP-413 payload into the exact bytes that get signed and committed to the fatxn claim. + * @param payload The payload to encode. + * @returns The serialized payload. + */ +export function serializeNep413Payload(payload: NEP413Payload): Uint8Array { + const nonce = payload.nonce ?? generateNep413Nonce(); + if (nonce.length !== NEP413_NONCE_LENGTH) { + throw new Error(`NEP-413 nonce must be ${NEP413_NONCE_LENGTH} bytes, got ${nonce.length}`); + } + + return serialize(NEP413_PAYLOAD_SCHEMA, { + tag: NEP413_PREFIX_TAG, + message: payload.message, + nonce, + recipient: payload.recipient, + callbackUrl: payload.callbackUrl ?? null, + }); +} + +/** + * Encode a NEP-413 payload as the number array the Auth0 authorize query string carries. + * @param payload The payload to encode. + * @returns The encoded payload. + */ +export function encodeNep413Payload(payload: NEP413Payload): number[] { + return Array.from(serializeNep413Payload(payload)); +} + +/** + * Assemble a NEP-413 SignedMessage from a signature produced through the FastAuth flow, so callers can hand verifiers the exact shape the standard defines. + * @param params The signing account, its public key, the raw signature and an optional state. + * @returns The signed message. + */ +export function buildNep413SignedMessage(params: { + accountId: string; + publicKey: string; + signature: Uint8Array | number[]; + state?: string; +}): NEP413SignedMessage { + const bytes = params.signature instanceof Uint8Array ? params.signature : Uint8Array.from(params.signature); + let binary = ""; + for (const byte of bytes) binary += String.fromCharCode(byte); + + return { + accountId: params.accountId, + publicKey: params.publicKey, + signature: globalThis.btoa(binary), + ...(params.state !== undefined ? { state: params.state } : {}), + }; +} diff --git a/packages/providers/javascript/src/provider.ts b/packages/providers/javascript/src/provider.ts index 4b0f25db..1c2d5a9c 100644 --- a/packages/providers/javascript/src/provider.ts +++ b/packages/providers/javascript/src/provider.ts @@ -10,13 +10,18 @@ import { JavascriptLoginOptions, JavascriptLoginWithRedirectOptions, JavascriptLoginWithPopupOptions, + JavascriptRequestMessageSignatureOptions, + JavascriptRequestMessageSignatureWithRedirectOptions, + JavascriptRequestMessageSignatureWithPopupOptions, } from "./types"; +import { encodeNep413Payload } from "./nep413"; import { FAST_AUTH_AUTH0_DEFAULTS, GetSignatureRequestResponse, IFastAuthProvider, LoginResponse, RequestDelegateActionSignatureResponse, + RequestMessageSignatureResponse, RequestTransactionSignatureResponse, User, } from "@shared/core"; @@ -276,6 +281,59 @@ export class JavascriptProvider implements IFastAuthProvider { return this.getUserId(); } + /** + * Request a NEP-413 message signature with redirect. + * @param requestSignatureOptions The options for the request message signature with redirect. + * @returns The void. + */ + private async requestMessageSignatureWithRedirect( + requestSignatureOptions: JavascriptRequestMessageSignatureWithRedirectOptions, + ): Promise { + const { redirectUri, payload, ...opts } = requestSignatureOptions; + await this.client.loginWithRedirect({ + authorizationParams: { + audience: this.options.signingAudience, + scope: "transaction:sign", + nep413: encodeNep413Payload(payload), + redirect_uri: redirectUri, + }, + ...opts, + }); + } + + /** + * Request a NEP-413 message signature with popup. + * @param requestSignatureOptions The options for the request message signature with popup. + * @returns The void. + */ + private async requestMessageSignatureWithPopup( + requestSignatureOptions: JavascriptRequestMessageSignatureWithPopupOptions, + ): Promise { + const { payload, ...opts } = requestSignatureOptions; + await this.client.loginWithPopup({ + authorizationParams: { + audience: this.options.signingAudience, + scope: "transaction:sign", + nep413: encodeNep413Payload(payload), + }, + ...opts, + }); + } + + /** + * Request a signature over a NEP-413 off-chain message, covering wallet sign-in challenges, app authentication and NEAR Intents alike. The signed bytes are not a NEAR transaction, so nothing is broadcast and no gas is spent; for intents the result is published to the solver relay, which executes it and pays the gas, leaving the signer with no need for an on-chain account. + * @param options The options for the request message signature. + * @returns The user. + */ + async requestMessageSignature(options: JavascriptRequestMessageSignatureOptions): Promise { + if ("redirectUri" in options && options.redirectUri) { + await this.requestMessageSignatureWithRedirect(options as JavascriptRequestMessageSignatureWithRedirectOptions); + } else { + await this.requestMessageSignatureWithPopup(options); + } + return this.getUserId(); + } + /** * Get the signature request. * @returns The signature request. diff --git a/packages/providers/javascript/src/types.ts b/packages/providers/javascript/src/types.ts index b9a511a2..89ef812e 100644 --- a/packages/providers/javascript/src/types.ts +++ b/packages/providers/javascript/src/types.ts @@ -2,6 +2,7 @@ import { Transaction } from "near-api-js/lib/transaction"; import { DelegateAction } from "@near-js/transactions"; import { PopupLoginOptions, RedirectLoginOptions } from "@auth0/auth0-spa-js"; import { FastAuthNetwork } from "@shared/core"; +import { NEP413Payload } from "./nep413"; export type { FastAuthNetwork } from "@shared/core"; @@ -51,3 +52,20 @@ export type JavascriptRequestDelegateActionSignatureWithPopupOptions = Javascrip export type JavascriptRequestDelegateActionSignatureOptions = | JavascriptRequestDelegateActionSignatureWithRedirectOptions | JavascriptRequestDelegateActionSignatureWithPopupOptions; + +export type JavascriptBaseRequestMessageSignatureOptions = JavascriptBaseRequestSignatureOptions & { + /** + * The NEP-413 payload to sign. + */ + payload: NEP413Payload; +}; + +export type JavascriptRequestMessageSignatureWithRedirectOptions = JavascriptBaseRequestMessageSignatureOptions & + Omit; + +export type JavascriptRequestMessageSignatureWithPopupOptions = JavascriptBaseRequestMessageSignatureOptions & + Omit; + +export type JavascriptRequestMessageSignatureOptions = + | JavascriptRequestMessageSignatureWithRedirectOptions + | JavascriptRequestMessageSignatureWithPopupOptions; diff --git a/packages/providers/javascript/test/nep413.spec.ts b/packages/providers/javascript/test/nep413.spec.ts new file mode 100644 index 00000000..2980c7ee --- /dev/null +++ b/packages/providers/javascript/test/nep413.spec.ts @@ -0,0 +1,156 @@ +import { deserialize } from "borsh"; +import { + NEP413_NONCE_LENGTH, + NEP413_PAYLOAD_SCHEMA, + NEP413_PREFIX_TAG, + encodeNep413Payload, + generateNep413Nonce, + serializeNep413Payload, + buildNep413SignedMessage, +} from "../src/nep413"; + +const FIXED_NONCE = Uint8Array.from(Array.from({ length: NEP413_NONCE_LENGTH }, (_, i) => (i * 3) % 256)); + +const INTENT_MESSAGE = JSON.stringify({ + signer_id: "trader.near", + deadline: "2026-01-01T00:00:00.000Z", + intents: [{ intent: "transfer", receiver_id: "deposit.near", tokens: { "nep141:usdc.near": "1000000" } }], +}); + +describe("serializeNep413Payload", () => { + it("round-trips every field through the NEP-413 schema", () => { + const bytes = serializeNep413Payload({ + message: INTENT_MESSAGE, + nonce: FIXED_NONCE, + recipient: "intents.near", + }); + + const decoded = deserialize(NEP413_PAYLOAD_SCHEMA, bytes) as any; + + expect(decoded.tag).toBe(NEP413_PREFIX_TAG); + expect(decoded.message).toBe(INTENT_MESSAGE); + expect(Array.from(decoded.nonce)).toEqual(Array.from(FIXED_NONCE)); + expect(decoded.recipient).toBe("intents.near"); + expect(decoded.callbackUrl).toBeNull(); + }); + + it("always stamps the NEP-413 domain tag", () => { + // Domain separation is what keeps these bytes from being a valid NEAR transaction. + // It is not caller-supplied precisely so it cannot be omitted. + const bytes = serializeNep413Payload({ message: "{}", nonce: FIXED_NONCE, recipient: "intents.near" }); + const decoded = deserialize(NEP413_PAYLOAD_SCHEMA, bytes) as any; + expect(decoded.tag).toBe(NEP413_PREFIX_TAG); + expect(NEP413_PREFIX_TAG).toBe(Math.pow(2, 31) + 413); + }); + + it("includes the callback url when given", () => { + const bytes = serializeNep413Payload({ + message: "{}", + nonce: FIXED_NONCE, + recipient: "intents.near", + callbackUrl: "https://app.example/cb", + }); + const decoded = deserialize(NEP413_PAYLOAD_SCHEMA, bytes) as any; + expect(decoded.callbackUrl).toBe("https://app.example/cb"); + }); + + it("generates a nonce when none is supplied", () => { + const bytes = serializeNep413Payload({ message: "{}", recipient: "intents.near" }); + const decoded = deserialize(NEP413_PAYLOAD_SCHEMA, bytes) as any; + expect(decoded.nonce).toHaveLength(NEP413_NONCE_LENGTH); + }); + + it("is deterministic for a fixed nonce", () => { + const params = { message: INTENT_MESSAGE, nonce: FIXED_NONCE, recipient: "intents.near" }; + expect(Array.from(serializeNep413Payload(params))).toEqual(Array.from(serializeNep413Payload(params))); + }); + + it("produces different bytes for different recipients", () => { + const a = serializeNep413Payload({ message: "{}", nonce: FIXED_NONCE, recipient: "intents.near" }); + const b = serializeNep413Payload({ message: "{}", nonce: FIXED_NONCE, recipient: "intents.testnet" }); + expect(Array.from(a)).not.toEqual(Array.from(b)); + }); + + it("rejects a nonce of the wrong length", () => { + expect(() => serializeNep413Payload({ message: "{}", nonce: new Uint8Array(16), recipient: "intents.near" })).toThrow( + /must be 32 bytes/, + ); + }); +}); + +describe("generateNep413Nonce", () => { + it("returns 32 bytes", () => { + expect(generateNep413Nonce()).toHaveLength(NEP413_NONCE_LENGTH); + }); + + it("does not repeat across calls", () => { + expect(Array.from(generateNep413Nonce())).not.toEqual(Array.from(generateNep413Nonce())); + }); +}); + +describe("encodeNep413Payload", () => { + it("returns the serialized bytes as a plain number array", () => { + const params = { message: INTENT_MESSAGE, nonce: FIXED_NONCE, recipient: "intents.near" }; + const encoded = encodeNep413Payload(params); + + expect(Array.isArray(encoded)).toBe(true); + expect(encoded).toEqual(Array.from(serializeNep413Payload(params))); + expect(encoded.every((value) => Number.isInteger(value) && value >= 0 && value <= 255)).toBe(true); + }); + + it("encodes an arbitrary sign-in challenge, not just intents", () => { + // NEP-413 messages are arbitrary strings; the serializer must not assume JSON. + const encoded = encodeNep413Payload({ + message: "Sign in to example.com", + nonce: FIXED_NONCE, + recipient: "example.com", + }); + expect(encoded.length).toBeGreaterThan(0); + }); +}); + +describe("buildNep413SignedMessage", () => { + const SIGNATURE = Uint8Array.from(Array.from({ length: 64 }, (_, i) => i)); + + it("returns the shape NEP-413 defines for a signed message", () => { + const signed = buildNep413SignedMessage({ + accountId: "trader.near", + publicKey: "ed25519:abc", + signature: SIGNATURE, + }); + + expect(signed).toEqual({ + accountId: "trader.near", + publicKey: "ed25519:abc", + signature: expect.any(String), + }); + }); + + it("base64-encodes the signature", () => { + const signed = buildNep413SignedMessage({ accountId: "a.near", publicKey: "ed25519:k", signature: SIGNATURE }); + // Round-trip through atob to prove the bytes survive intact. + const decoded = Uint8Array.from(globalThis.atob(signed.signature), (c) => c.charCodeAt(0)); + expect(Array.from(decoded)).toEqual(Array.from(SIGNATURE)); + }); + + it("accepts a plain number array as the signature", () => { + const fromArray = buildNep413SignedMessage({ accountId: "a.near", publicKey: "ed25519:k", signature: Array.from(SIGNATURE) }); + const fromBytes = buildNep413SignedMessage({ accountId: "a.near", publicKey: "ed25519:k", signature: SIGNATURE }); + expect(fromArray.signature).toBe(fromBytes.signature); + }); + + it("echoes the state when one was supplied", () => { + const signed = buildNep413SignedMessage({ + accountId: "a.near", + publicKey: "ed25519:k", + signature: SIGNATURE, + state: "csrf-token", + }); + expect(signed.state).toBe("csrf-token"); + }); + + it("omits state entirely when none was supplied", () => { + const signed = buildNep413SignedMessage({ accountId: "a.near", publicKey: "ed25519:k", signature: SIGNATURE }); + expect("state" in signed).toBe(false); + }); +}); diff --git a/packages/providers/javascript/test/provider.spec.ts b/packages/providers/javascript/test/provider.spec.ts index 32845ec9..83d928b5 100644 --- a/packages/providers/javascript/test/provider.spec.ts +++ b/packages/providers/javascript/test/provider.spec.ts @@ -678,4 +678,104 @@ describe("JavascriptProvider", () => { await expect(provider.getSignatureRequest()).rejects.toThrow("JWT decode failed"); }); }); + + describe("requestMessageSignature", () => { + const payload = { + message: JSON.stringify({ + signer_id: "trader.near", + deadline: "2026-01-01T00:00:00.000Z", + intents: [{ intent: "transfer", receiver_id: "deposit.near", tokens: { "nep141:usdc.near": "1000000" } }], + }), + nonce: Uint8Array.from(Array.from({ length: 32 }, (_, i) => i)), + recipient: "intents.near", + }; + + beforeEach(() => { + jest.clearAllMocks(); + mockAuth0Client.getIdTokenClaims.mockResolvedValue({ sub: "test-user-id" }); + }); + + describe("with redirect", () => { + it("should call loginWithRedirect with the encoded payload", async () => { + mockAuth0Client.loginWithRedirect.mockResolvedValue(undefined); + + await provider.requestMessageSignature({ payload, redirectUri: "http://localhost:3000/callback" }); + + const params = mockAuth0Client.loginWithRedirect.mock.calls[0][0].authorizationParams; + expect(params.audience).toBe("auth0.jwt.fast-auth.testnet"); + expect(params.scope).toBe("transaction:sign"); + expect(params.redirect_uri).toBe("http://localhost:3000/callback"); + expect(Array.isArray(params.nep413)).toBe(true); + expect(mockAuth0Client.loginWithPopup).not.toHaveBeenCalled(); + }); + + it("should encode the payload with the NEP-413 domain tag first", async () => { + mockAuth0Client.loginWithRedirect.mockResolvedValue(undefined); + + await provider.requestMessageSignature({ payload, redirectUri: "http://localhost:3000/callback" }); + + // The tag is a little-endian u32 at offset 0 — the first thing the action checks. + const encoded: number[] = mockAuth0Client.loginWithRedirect.mock.calls[0][0].authorizationParams.nep413; + const tag = encoded[0] | (encoded[1] << 8) | (encoded[2] << 16) | (encoded[3] << 24); + expect(tag >>> 0).toBe(Math.pow(2, 31) + 413); + }); + + it("should propagate errors from loginWithRedirect", async () => { + mockAuth0Client.loginWithRedirect.mockRejectedValue(new Error("Login redirect failed")); + + await expect(provider.requestMessageSignature({ payload, redirectUri: "http://localhost:3000/callback" })).rejects.toThrow( + "Login redirect failed", + ); + }); + }); + + describe("with popup", () => { + it("should call loginWithPopup when no redirectUri is provided", async () => { + mockAuth0Client.loginWithPopup.mockResolvedValue(undefined); + + await provider.requestMessageSignature({ payload }); + + const params = mockAuth0Client.loginWithPopup.mock.calls[0][0].authorizationParams; + expect(params.audience).toBe("auth0.jwt.fast-auth.testnet"); + expect(params.scope).toBe("transaction:sign"); + expect(Array.isArray(params.nep413)).toBe(true); + expect(mockAuth0Client.loginWithRedirect).not.toHaveBeenCalled(); + }); + + it("should propagate errors from loginWithPopup", async () => { + mockAuth0Client.loginWithPopup.mockRejectedValue(new Error("Login popup failed")); + + await expect(provider.requestMessageSignature({ payload })).rejects.toThrow("Login popup failed"); + }); + }); + + it("should not forward a caller state to Auth0", async () => { + // auth0-spa-js mints its own `state` for the OAuth exchange and overwrites anything + // passed in authorizationParams, so a NEP-413 state routed through here would be + // dropped without a trace. It is held caller-side and attached to the result instead. + mockAuth0Client.loginWithPopup.mockResolvedValue(undefined); + + await provider.requestMessageSignature({ payload, state: "csrf-token" } as any); + + const params = mockAuth0Client.loginWithPopup.mock.calls[0][0].authorizationParams; + expect(params.state).toBeUndefined(); + }); + + it("should return the user id after signing", async () => { + mockAuth0Client.loginWithPopup.mockResolvedValue(undefined); + + const result = await provider.requestMessageSignature({ payload }); + + expect(result).toEqual({ userId: "test-user-id" }); + }); + + it("should throw when the user is not logged in", async () => { + mockAuth0Client.loginWithPopup.mockResolvedValue(undefined); + mockAuth0Client.getIdTokenClaims.mockResolvedValue(undefined); + + await expect(provider.requestMessageSignature({ payload })).rejects.toThrow( + new JavascriptProviderError(JavascriptProviderErrorCodes.USER_NOT_LOGGED_IN), + ); + }); + }); }); diff --git a/packages/sdks/browser/src/signers/signer.ts b/packages/sdks/browser/src/signers/signer.ts index acc942c7..2a8317c1 100644 --- a/packages/sdks/browser/src/signers/signer.ts +++ b/packages/sdks/browser/src/signers/signer.ts @@ -105,6 +105,18 @@ export class FastAuthSigner

{ return await this.fastAuthProvider.requestDelegateActionSignature(...args); } + /** + * Request a signature over a NEP-413 off-chain message, covering wallet sign-in challenges, app authentication and NEAR Intents alike. The method is optional on the provider interface, so providers that have not implemented the flow raise a clear error instead of failing on an undefined call. + * @param args The arguments to request a message signature. + * @returns The signed message response. + */ + async requestMessageSignature(...args: any[]) { + if (typeof this.fastAuthProvider.requestMessageSignature !== "function") { + throw new Error("The configured FastAuth provider does not support NEP-413 message signatures"); + } + return await this.fastAuthProvider.requestMessageSignature(...args); + } + /** * Get a signature request. * @returns The signature request. @@ -157,6 +169,15 @@ export class FastAuthSigner

{ return await this.connection.provider.sendTransaction(signedTransaction); } + /** + * Derive the NEAR implicit account id for this signer: the hex encoding of its ed25519 public key. The account needs no on-chain creation to receive funds or to authorize NEP-413 messages, which is what lets an app operate without provisioning an account per user. + * @returns The implicit account id. + */ + async getImplicitAccountId(): Promise { + const publicKey = await this.getPublicKey("ed25519"); + return Buffer.from(publicKey.data).toString("hex"); + } + /** * Get the public key of the account. * @param algorithm The algorithm to use. diff --git a/packages/sdks/browser/test/signers/signer.spec.ts b/packages/sdks/browser/test/signers/signer.spec.ts index 9f254a4e..1c55731d 100644 --- a/packages/sdks/browser/test/signers/signer.spec.ts +++ b/packages/sdks/browser/test/signers/signer.spec.ts @@ -59,6 +59,51 @@ describe("FastAuthSigner", () => { }); }); + describe("requestMessageSignature", () => { + it("should delegate to the provider when it supports NEP-413", async () => { + const args = [{ payload: { message: "Sign in", recipient: "example.com" } }]; + (mockProvider as any).requestMessageSignature = jest.fn().mockResolvedValue({ userId: "u" }); + // @ts-ignore testing spread args passthrough + await signer.requestMessageSignature(...(args as any)); + expect((mockProvider as any).requestMessageSignature).toHaveBeenCalledWith(...args); + }); + + it("should raise a clear error when the provider does not implement it", async () => { + // The method is optional on the interface, so an unimplemented provider must fail + // with an explanation rather than "is not a function". + delete (mockProvider as any).requestMessageSignature; + await expect(signer.requestMessageSignature({} as any)).rejects.toThrow(/does not support NEP-413/); + }); + }); + + describe("getImplicitAccountId", () => { + beforeEach(async () => { + mockProvider.getPath.mockResolvedValue("jwt#path/"); + await signer.init(); + }); + + it("should hex-encode the ed25519 public key", async () => { + const data = Uint8Array.from(Array.from({ length: 32 }, (_, i) => i)); + (mockConnection.provider.query as jest.Mock).mockResolvedValue({ + result: Buffer.from(JSON.stringify("pk")), + }); + jest.spyOn(signer, "getPublicKey").mockResolvedValue({ data } as any); + + const accountId = await signer.getImplicitAccountId(); + + // A NEAR implicit account id is exactly the 64-char hex of its public key. + expect(accountId).toBe(Buffer.from(data).toString("hex")); + expect(accountId).toHaveLength(64); + expect(accountId).toMatch(/^[0-9a-f]{64}$/); + }); + + it("should always ask for the ed25519 key", async () => { + const spy = jest.spyOn(signer, "getPublicKey").mockResolvedValue({ data: new Uint8Array(32) } as any); + await signer.getImplicitAccountId(); + expect(spy).toHaveBeenCalledWith("ed25519"); + }); + }); + describe("getPublicKey (viewFunction)", () => { beforeEach(async () => { mockProvider.getPath.mockResolvedValue("jwt#path/"); diff --git a/packages/shared/core/src/index.ts b/packages/shared/core/src/index.ts index 04099fe2..d2701450 100644 --- a/packages/shared/core/src/index.ts +++ b/packages/shared/core/src/index.ts @@ -24,6 +24,7 @@ export type { LoginResponse, RequestTransactionSignatureResponse, RequestDelegateActionSignatureResponse, + RequestMessageSignatureResponse, GetSignatureRequestResponse, IFastAuthProvider, MPCContractAlgorithm, diff --git a/packages/shared/core/src/provider.ts b/packages/shared/core/src/provider.ts index 150a9aa9..3612a672 100644 --- a/packages/shared/core/src/provider.ts +++ b/packages/shared/core/src/provider.ts @@ -20,6 +20,11 @@ export type RequestTransactionSignatureResponse = User; */ export type RequestDelegateActionSignatureResponse = User; +/** + * Response of a NEP-413 message signature request. + */ +export type RequestMessageSignatureResponse = User; + /** * Response returned after a successful signature request */ @@ -31,6 +36,13 @@ export interface IFastAuthProvider { isLoggedIn(): Promise; requestTransactionSignature(...args: any[]): Promise; requestDelegateActionSignature(...args: any[]): Promise; + /** + * Request a signature over a NEP-413 off-chain message: wallet sign-in challenges, app + * authentication, or NEAR Intents authorized without submitting a transaction. Optional: + * providers that have not implemented the flow simply omit it, and callers must check for + * its presence before use. + */ + requestMessageSignature?(...args: any[]): Promise; getSignatureRequest(): Promise; getPath(): Promise; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cd029d8f..8136286e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -718,6 +718,9 @@ importers: '@near-js/transactions': specifier: ^2.0.2 version: 2.2.3(@near-js/crypto@1.4.2)(@near-js/types@0.3.1)(@near-js/utils@1.1.0) + borsh: + specifier: ^2.0.0 + version: 2.0.0 elliptic: specifier: ^6.6.1 version: 6.6.1