Files

26 KiB
Raw Permalink Blame History

Qortal External Auth

Minimal local daemon + shared auth core for Qortal-compatible account import and signing.

Getting Started

Install

npm install

Dev mode

npm run dev

Build + start (production)

npm run build
npm run start

Docker

Build image:

docker build -t qortal-external-auth:latest .

Run container (bind host port 3191, persist data):

docker run --rm -p 3191:3191 \
  -e QORTAL_AUTH_HOST=0.0.0.0 \
  -e QORTAL_AUTH_DATA_DIR=/data \
  -v qortal-auth-data:/data \
  qortal-external-auth:latest

Note: use a named volume (as shown) instead of a host bind-mount to keep wallet files out of your normal filesystem paths. Named volumes live under Docker's internal data directory and are less likely to be accessed accidentally.

If the Qortal node runs on the Docker host, set QORTAL_AUTH_NODE_URL and add the host gateway:

docker run --rm -p 3191:3191 \
  -e QORTAL_AUTH_HOST=0.0.0.0 \
  -e QORTAL_AUTH_DATA_DIR=/data \
  -e QORTAL_AUTH_NODE_URL=http://host.docker.internal:12391 \
  --add-host=host.docker.internal:host-gateway \
  -v qortal-auth-data:/data \
  qortal-external-auth:latest

Docker Compose (multi-repo)

This repo includes a compose.yaml that builds the local auth daemon and pulls/builds the Nextcloud integration from your Gitea repo.

docker compose up --build

Notes:

  • The Compose file uses a named volume for wallet data (qortal-auth-data).
  • Services can reach each other by service name on the default network, e.g. http://qortal-external-auth:3191.
  • If you need a Qortal node container, add it and set QORTAL_AUTH_NODE_URL=http://<service-name>:12391.

Environment

Optional environment variables:

  • QORTAL_AUTH_PORT (default 3191)
  • QORTAL_AUTH_HOST (default 127.0.0.1)
  • QORTAL_AUTH_NODE_URL (default node URL if not provided in requests)
  • QORTAL_AUTH_NODE_API_KEY (optional API key for restricted node endpoints, sent as X-API-KEY)
  • QORTAL_AUTH_NODE_API_KEY_MODE (auto|always|never|paths|retry, default auto)
  • QORTAL_AUTH_NODE_API_KEY_PATHS (comma-separated path patterns when using paths, e.g. /admin/stop,/render/{service}/{name}. Use / to match all node paths.)
  • QORTAL_AUTH_BODY_LIMIT_BYTES (max JSON request body size in bytes; default 262144000 / 250 MiB)
  • QORTAL_AUTH_SAVE_FILE_DIR (base directory for qortalRequest('SAVE_FILE'); default <dataDir>/saved-files)

retry mode attempts a request without the key and retries once on 401/403 with the key, so the key is only sent when required.

Regardless of mode, if the node explicitly responds with a missing X-API-KEY error, the daemon retries once with the configured key.

auto mode uses a built-in list of Qortal endpoints that call Security.checkApiCallAllowed or declare @SecurityRequirement(name = "apiKey") in core. paths supports simple {param} placeholders and {path:.*} wildcards.

  • QORTAL_AUTH_DATA_DIR (default ./data)
  • QORTAL_AUTH_STORAGE_PASSPHRASE (used when encrypted backups are enabled)
  • QORTAL_AUTH_DOCS_HOST (override Swagger server host if needed)

Large inline base64 publishes use JSON request bodies and can exceed this limit. For very large content, prefer uploadType: "file" with filePath so data is streamed to the node in chunks instead of being sent as one large JSON payload.

You can also create a .env file in the repo root (it is gitignored).

API

All endpoints are JSON over HTTP. By default the daemon listens on 127.0.0.1:3191.

API docs (interactive)

Swagger UI (try calls in browser):

http://127.0.0.1:3191/docs

OpenAPI JSON:

http://127.0.0.1:3191/openapi.json

Authentication

Most endpoints require a bearer token:

Authorization: Bearer <TOKEN>

If a permission is in ask mode, include:

X-Qortal-Approval-Token: <TOKEN>

Unauthenticated endpoints:

  • GET / (redirect to docs)
  • GET /health
  • GET /openapi.json
  • GET /docs

Typical flow

  1. POST /apps/register to create an app id/secret.
  2. POST /sessions to create a bearer token.
  3. Import a wallet (/wallet/import-seed or /wallet/import-backup).
  4. Set permissions (/permissions/set) or issue one-time approvals (/permissions/approve).
  5. Unlock a wallet (/wallet/unlock) or pass password per request.
  6. Call signing or QDN endpoints.

Endpoint reference (daemon)

Apps:

POST /apps/register
Body: {"name":"my-app"}
Response: {"appId":"...","appSecret":"..."}

Sessions:

POST /sessions
Body: {"appId":"...","appSecret":"..."}
Response: {"token":"...","expiresAt":<unix ms>}

Wallets:

POST /wallet/import-seed
Body: {"seedPhrase":"...","password":"..."}

POST /wallet/create
Body: {"password":"..."}

POST /wallet/import-backup
Body: {"password":"...","backup":{...}}

POST /wallet/backup/export
Body: {"walletId":"...","password":"...","reEncryptPassword":"..."}

POST /wallet/backup-json
Body: {"walletId":"...","password":"...","newPassword":"..."}

POST /wallet/backup
Body: {"walletId":"...","password":"..."}

GET /wallets
GET /wallet/<WALLET_ID>

POST /wallet/unlock
Body: {"walletId":"...","password":"...","ttlSeconds":1200}

GET /wallet/unlock/status?walletId=<WALLET_ID>

POST /wallet/lock
Body: {"walletId":"..."}

Permissions:

POST /permissions/set
Body: {"walletId":"...","scope":"SIGN_TRANSACTION","mode":"ask|session|persistent","ttlSeconds":3600}

POST /permissions/approve
Body: {"walletId":"...","scope":"SIGN_TRANSACTION"}

GET /permissions?walletId=<WALLET_ID>

Settings:

GET /settings?walletId=<WALLET_ID>
POST /settings
Body: {"scope":"global|app|wallet","walletId":"<WALLET_ID>","settings":{...}}

Transactions and QDN:

POST /tx/sign
Body: {"walletId":"...","unsignedBytes":"<BASE58>","process":false,"password":"...","nodeUrl":"..."}

POST /qdn/publish
Body: {"walletId":"...","data":"<BASE64>","service":"DOCUMENT","registeredName":"name","identifier":"id","password":"..."}

Qortal request router and node proxy:

POST /qortal/request
Body: {"requestType":"GET_NODE_INFO","payload":{...}}

POST /node/proxy
Body: {"method":"GET","path":"/admin/info","query":{...},"body":{...}}

GET /node/proxy?path=/addresses/balance/<ADDRESS>&assetId=0

System endpoints

Health check:

curl -s http://127.0.0.1:3191/health

OpenAPI JSON:

curl -s http://127.0.0.1:3191/openapi.json

Node proxy (generic)

Use /node/proxy to pass through a Qortal node call. This is useful for endpoints not yet exposed via /qortal/request.

curl -s -X POST http://127.0.0.1:3191/node/proxy \
  -H 'Authorization: Bearer <TOKEN>' \
  -H 'Content-Type: application/json' \
  -d '{"method":"GET","path":"/admin/info"}'

GET shortcut for simple read-only requests:

curl -s -X GET 'http://127.0.0.1:3191/node/proxy?path=/addresses/balance/QiNKXRfnX9mTodSed1yRQexhL1HA42RHHo&assetId=0' \
  -H 'Authorization: Bearer <TOKEN>'

Swagger UI authorization (quick)

  1. Register app:
curl -s -X POST http://127.0.0.1:3191/apps/register \
  -H 'Content-Type: application/json' \
  -d '{"name":"my-app"}'
  1. Create session:
curl -s -X POST http://127.0.0.1:3191/sessions \
  -H 'Content-Type: application/json' \
  -d '{"appId":"<APP_ID>","appSecret":"<APP_SECRET>"}'
  1. In Swagger UI (/docs), click Authorize and paste:
Bearer <TOKEN>

End-to-end example (import backup + sign)

Scenario: An external app wants to import a Qortal backup file, unlock it for 20 minutes, and sign a transaction.

  1. Register app + create session (same as above).

  2. Import a backup file:

curl -s -X POST http://127.0.0.1:3191/wallet/import-backup \
  -H 'Authorization: Bearer <TOKEN>' \
  -H 'Content-Type: application/json' \
  -d '{"password":"backup-password","backup":{...}}'
  1. Set permissions for signing (session mode):
curl -s -X POST http://127.0.0.1:3191/permissions/set \
  -H 'Authorization: Bearer <TOKEN>' \
  -H 'Content-Type: application/json' \
  -d '{"walletId":"<WALLET_ID>","scope":"SIGN_TRANSACTION","mode":"session","ttlSeconds":3600}'
  1. Unlock the wallet for 20 minutes:
curl -s -X POST http://127.0.0.1:3191/wallet/unlock \
  -H 'Authorization: Bearer <TOKEN>' \
  -H 'Content-Type: application/json' \
  -d '{"walletId":"<WALLET_ID>","password":"backup-password","ttlSeconds":1200}'
  1. Sign a transaction (wallet already unlocked):
curl -s -X POST http://127.0.0.1:3191/tx/sign \
  -H 'Authorization: Bearer <TOKEN>' \
  -H 'Content-Type: application/json' \
  -d '{"walletId":"<WALLET_ID>","unsignedBytes":"<BASE58>","process":false,"nodeUrl":"http://127.0.0.1:12391"}'

Register app

curl -s -X POST http://127.0.0.1:3191/apps/register \
  -H 'Content-Type: application/json' \
  -d '{"name":"my-app"}'

Create session

curl -s -X POST http://127.0.0.1:3191/sessions \
  -H 'Content-Type: application/json' \
  -d '{"appId":"<APP_ID>","appSecret":"<APP_SECRET>"}'

Import seed phrase (creates and saves Qortal backup file)

curl -s -X POST http://127.0.0.1:3191/wallet/import-seed \
  -H 'Authorization: Bearer <TOKEN>' \
  -H 'Content-Type: application/json' \
  -d '{"seedPhrase":"seed words here","password":"backup-password"}'

Create new wallet (random seed + save backup)

curl -s -X POST http://127.0.0.1:3191/wallet/create \
  -H 'Authorization: Bearer <TOKEN>' \
  -H 'Content-Type: application/json' \
  -d '{"password":"backup-password"}'

Import Qortal backup file

curl -s -X POST http://127.0.0.1:3191/wallet/import-backup \
  -H 'Authorization: Bearer <TOKEN>' \
  -H 'Content-Type: application/json' \
  -d '{"password":"backup-password","backup":{...}}'

Export backup file (password required)

curl -s -X POST http://127.0.0.1:3191/wallet/backup/export \
  -H 'Authorization: Bearer <TOKEN>' \
  -H 'Content-Type: application/json' \
  -d '{"walletId":"<WALLET_ID>","password":"backup-password"}'

Optional compatibility fields for re-encryption on export:

  • reEncryptPassword
  • newPassword

If both are provided, they must match. Alias routes /wallet/backup-json and /wallet/backup use the same behavior.

List wallets

curl -s -X GET http://127.0.0.1:3191/wallets \
  -H 'Authorization: Bearer <TOKEN>'

Get wallet

curl -s -X GET http://127.0.0.1:3191/wallet/<WALLET_ID> \
  -H 'Authorization: Bearer <TOKEN>'

Permissions

Permissions gate sensitive operations. Modes:

  • ask: requires a one-time approval token
  • session: auto-approve until TTL expires
  • persistent: always approved for this app + wallet + scope

Set permission mode

curl -s -X POST http://127.0.0.1:3191/permissions/set \
  -H 'Authorization: Bearer <TOKEN>' \
  -H 'Content-Type: application/json' \
  -d '{"walletId":"<WALLET_ID>","scope":"SIGN_TRANSACTION","mode":"session","ttlSeconds":3600}'

Approve once (for ask mode)

curl -s -X POST http://127.0.0.1:3191/permissions/approve \
  -H 'Authorization: Bearer <TOKEN>' \
  -H 'Content-Type: application/json' \
  -d '{"walletId":"<WALLET_ID>","scope":"SIGN_TRANSACTION"}'

Use the returned approvalToken as header:

-H 'X-Qortal-Approval-Token: <TOKEN>'

List persistent permissions

curl -s -X GET http://127.0.0.1:3191/permissions?walletId=<WALLET_ID> \
  -H 'Authorization: Bearer <TOKEN>'

Settings

Settings are stored in data/settings.json and can be configured at:

  • global defaults
  • per-app
  • per-wallet

These settings control the default auth mode and the default node URL if a request does not supply nodeUrl. When authMode is session, the daemon will autounlock the wallet when a signing request includes a password.

Get settings (resolved)

curl -s -X GET 'http://127.0.0.1:3191/settings?walletId=<WALLET_ID>' \
  -H 'Authorization: Bearer <TOKEN>'

Set global defaults

curl -s -X POST http://127.0.0.1:3191/settings \
  -H 'Authorization: Bearer <TOKEN>' \
  -H 'Content-Type: application/json' \
  -d '{\"scope\":\"global\",\"settings\":{\"authMode\":\"ask\",\"sessionTtlSeconds\":3600,\"approvalTtlSeconds\":300,\"unlockTtlSeconds\":1200,\"nodeUrl\":\"http://127.0.0.1:12391\"}}'

Set app defaults

curl -s -X POST http://127.0.0.1:3191/settings \
  -H 'Authorization: Bearer <TOKEN>' \
  -H 'Content-Type: application/json' \
  -d '{\"scope\":\"app\",\"settings\":{\"authMode\":\"session\"}}'

Set per-wallet defaults

curl -s -X POST http://127.0.0.1:3191/settings \
  -H 'Authorization: Bearer <TOKEN>' \
  -H 'Content-Type: application/json' \
  -d '{\"scope\":\"wallet\",\"walletId\":\"<WALLET_ID>\",\"settings\":{\"authMode\":\"persistent\",\"nodeUrl\":\"http://127.0.0.1:12391\"}}'

Note: If authMode is set to session or persistent, the daemon will autogrant permissions for that scope without requiring approval tokens.

Encrypted backups (optional)

If encryptedBackups is enabled in global settings, wallet backups are encrypted at rest using a storage passphrase. On startup the daemon will prompt for the passphrase if QORTAL_AUTH_STORAGE_PASSPHRASE is not set.

Enable via settings:

curl -s -X POST http://127.0.0.1:3191/settings \
  -H 'Authorization: Bearer <TOKEN>' \
  -H 'Content-Type: application/json' \
  -d '{\"scope\":\"global\",\"settings\":{\"encryptedBackups\":true}}'

Then start the daemon with:

QORTAL_AUTH_STORAGE_PASSPHRASE='your-passphrase' npm run dev

Wallet Unlock

Unlocking decrypts the backup once, then stores an encrypted private key in memory with a TTL. Signing calls can omit password while unlocked.

Unlock

curl -s -X POST http://127.0.0.1:3191/wallet/unlock \
  -H 'Authorization: Bearer <TOKEN>' \
  -H 'Content-Type: application/json' \
  -d '{\"walletId\":\"<WALLET_ID>\",\"password\":\"backup-password\",\"ttlSeconds\":1200}'

Unlock status

curl -s -X GET 'http://127.0.0.1:3191/wallet/unlock/status?walletId=<WALLET_ID>' \
  -H 'Authorization: Bearer <TOKEN>'

Lock

curl -s -X POST http://127.0.0.1:3191/wallet/lock \
  -H 'Authorization: Bearer <TOKEN>' \
  -H 'Content-Type: application/json' \
  -d '{\"walletId\":\"<WALLET_ID>\"}'

Signing

Sign transaction (Base58 unsigned bytes)

If the wallet is unlocked, you can omit password.

curl -s -X POST http://127.0.0.1:3191/tx/sign \
  -H 'Authorization: Bearer <TOKEN>' \
  -H 'Content-Type: application/json' \
  -d '{"walletId":"<WALLET_ID>","password":"backup-password","unsignedBytes":"<BASE58>","process":false,"nodeUrl":"http://127.0.0.1:12391"}'

QDN publish (base64)

This is a minimal base64 publish flow (mirrors Hubs PUBLISH_QDN_RESOURCE). If the wallet is unlocked, you can omit password.

curl -s -X POST http://127.0.0.1:3191/qdn/publish \
  -H 'Authorization: Bearer <TOKEN>' \
  -H 'Content-Type: application/json' \
  -d '{"walletId":"<WALLET_ID>","password":"backup-password","registeredName":"name","service":"DOCUMENT_PRIVATE","identifier":"example","data":"<BASE64>","nodeUrl":"http://127.0.0.1:12391"}'

Qortal request router

A compatibility endpoint that mirrors Hub-style request types.

Request format:

curl -s -X POST http://127.0.0.1:3191/qortal/request \
  -H 'Authorization: Bearer <TOKEN>' \
  -H 'Content-Type: application/json' \
  -d '{"requestType":"GET_NODE_INFO","payload":{}}'

Node and status

  • GET_NODE_INFO
  • GET_NODE_STATUS
  • GET_DAY_SUMMARY

Accounts and names

  • GET_ACCOUNT_DATA (payload: address or walletId)
  • GET_ACCOUNT_NAMES (payload: address or walletId)
  • GET_PRIMARY_NAME (payload: address or walletId)
  • GET_NAME_DATA (payload: name)
  • SEARCH_NAMES (payload: query, limit, offset, reverse)

Balances

  • GET_BALANCE (payload: address or walletId, returns QORT balance)
  • GET_WALLET_BALANCE (payload: coin + address or walletId)

coin supports QORT, an asset name, or an assetId.

Wallet metadata

  • GET_USER_WALLET (payload: walletId)
  • GET_USER_WALLET_INFO (payload: walletId)
  • GET_USER_ACCOUNT (payload: optional walletId)
  • WHICH_UI (payload optional, returns HUB_WEB by default; set QORTAL_AUTH_UI=HUB_ELECTRON to override)

Encryption

  • ENCRYPT_DATA (payload: walletId or address, data64|base64, optional publicKeys[])
  • DECRYPT_DATA (payload: walletId or address, encryptedData|data64|base64, and publicKey when decrypting legacy qortalEncryptedData)
  • ENCRYPT_QORTAL_GROUP_DATA (payload: walletId or address, data64|base64, and either secretKeyObject or groupId, optional isAdmins, optional typeNumber, optional refreshCache)
  • DECRYPT_QORTAL_GROUP_DATA (payload: walletId or address, data64|base64, and either secretKeyObject or groupId, optional isAdmins, optional refreshCache)

If secretKeyObject is omitted and groupId is provided, the daemon mirrors Hub behavior: it fetches group admins, resolves their primary names, finds the latest matching key publish (symmetric-qchat-group-<groupId> or admins-symmetric-qchat-group-<groupId>), decrypts it with the caller wallet key, and uses the resulting key object. Resolved keys are cached for 20 minutes unless refreshCache=true.

Transactions and blocks

  • GET_USER_WALLET_TRANSACTIONS (payload: walletId, limit, offset, reverse, confirmationStatus, txType)
  • SEARCH_TRANSACTIONS (payload: limit, offset, reverse, plus any /transactions/search params)
  • DECODE_TRANSACTION (payload: bytes|unsignedBytes|signedBytes, optional ignoreValidityChecks)
  • FETCH_BLOCK (payload: height or signature, empty = latest)
  • FETCH_BLOCK_RANGE (payload: start, end)

Groups and ATs

  • LIST_GROUPS (payload: limit, offset, reverse)
  • LIST_ATS (payload: limit, offset, reverse)
  • GET_AT (payload: at or atAddress)
  • GET_AT_DATA (payload: at or atAddress)

QDN and hosted data

  • PUBLISH_QDN_RESOURCE (payload: walletId, registeredName, service, plus one of data (base64), stringData/text (plain), or filePath (chunked), optional uploadType, optional encrypt + publicKeys[], identifier, title, description, category, tags, fee)
  • PUBLISH_MULTIPLE_QDN_RESOURCES (payload: walletId, resources[], optional continueOnError, optional top-level encrypt + publicKeys[], each resource can include uploadType/filePath and optional disableEncrypt)
  • GET_QDN_RESOURCE_URL (payload: service, name, optional identifier)
  • LINK_TO_QDN_RESOURCE (alias of GET_QDN_RESOURCE_URL)
  • FETCH_QDN_RESOURCE (payload: service, name, optional identifier, build, attempt, timeoutMs, nodeUrl)
  • GET_QDN_RESOURCE_STATUS (payload: service, name, identifier, optional build)
  • GET_QDN_RESOURCE_PROPERTIES (payload: service, name, identifier, optional build)
  • GET_QDN_RESOURCE_METADATA (payload: service, name, optional identifier)
  • LIST_QDN_RESOURCES (payload: service, name, identifier, limit, offset, reverse)
  • SEARCH_QDN_RESOURCES (payload: query, service, name, identifier, limit, offset, reverse)
  • GET_HOSTED_DATA (payload: limit, offset, query)
  • DELETE_HOSTED_DATA (payload: walletId, hostedData[])
  • SAVE_FILE (payload: filename or relative filePath, plus text or base64|data64|data; writes under QORTAL_AUTH_SAVE_FILE_DIR)

Crosschain

  • GET_CROSSCHAIN_SERVER_INFO (payload: coin)
  • GET_SERVER_CONNECTION_HISTORY (payload: coin)
  • GET_TX_ACTIVITY_SUMMARY (payload: coin)
  • GET_FOREIGN_FEE (payload: coin, type where type is feekb or feerequired)
  • GET_PRICE (payload: blockchain, optional limit, offset, reverse, inverse)
  • GET_ARRR_SYNC_STATUS (payload optional)
  • ADD_FOREIGN_SERVER (payload: coin, serverInfo)
  • REMOVE_FOREIGN_SERVER (payload: coin, serverInfo)
  • SET_CURRENT_FOREIGN_SERVER (payload: coin, serverInfo)
  • UPDATE_FOREIGN_FEE (payload: coin, feeType=feekb|feerequired, fee)
  • SIGN_FOREIGN_FEES (payload: signedFees[])

Lists

  • GET_LIST_ITEMS (payload: listName)
  • ADD_LIST_ITEMS (payload: listName, items[])
  • DELETE_LIST_ITEM (payload: listName, items[])

Chat

  • SEARCH_CHAT_MESSAGES (payload: txGroupId or involving[], plus search params)
  • SEND_CHAT_MESSAGE (build + sign + process; see transaction build list below)

Transaction build + sign + process

These build a raw unsigned tx via the Qortal node, sign it with your wallet, and (by default) process it. All require payload.walletId and a node-format payload.tx unless noted. Set payload.process=false to return signed bytes without processing. If timestamp, reference (or lastReference), fee, or the required *PublicKey field are omitted, the daemon will fill them using the wallet and node.

  • REGISTER_NAME (can auto-build from name + optional description)
  • UPDATE_NAME
  • SELL_NAME
  • CANCEL_SELL_NAME
  • BUY_NAME
  • CREATE_GROUP
  • UPDATE_GROUP
  • ADD_GROUP_ADMIN
  • REMOVE_GROUP_ADMIN
  • BAN_FROM_GROUP
  • CANCEL_GROUP_BAN
  • KICK_FROM_GROUP
  • INVITE_TO_GROUP
  • CANCEL_GROUP_INVITE
  • JOIN_GROUP
  • LEAVE_GROUP
  • CREATE_POLL
  • VOTE_ON_POLL
  • SEND_COIN (PAYMENT)
  • TRANSFER_ASSET
  • CREATE_ASSET_ORDER
  • CANCEL_ASSET_ORDER
  • DEPLOY_AT
  • SEND_CHAT_MESSAGE (includes /chat/compute PoW step)

REGISTER_NAME (auto-build)

curl -s -X POST http://127.0.0.1:3191/qortal/request \
  -H 'Authorization: Bearer <TOKEN>' \
  -H 'Content-Type: application/json' \
  -d '{"requestType":"REGISTER_NAME","payload":{"walletId":"<WALLET_ID>","name":"my-name","description":"Registered Name"}}'

SIGN_TRANSACTION

curl -s -X POST http://127.0.0.1:3191/qortal/request \
  -H 'Authorization: Bearer <TOKEN>' \
  -H 'Content-Type: application/json' \
  -d '{"requestType":"SIGN_TRANSACTION","payload":{"walletId":"<WALLET_ID>","password":"backup-password","unsignedBytes":"<BASE58>","process":false}}'

DECODE_TRANSACTION

curl -s -X POST http://127.0.0.1:3191/qortal/request \
  -H 'Authorization: Bearer <TOKEN>' \
  -H 'Content-Type: application/json' \
  -d '{"requestType":"DECODE_TRANSACTION","payload":{"bytes":"<BASE58_TX_BYTES>"}}'

If a request needs approval and the wallet/app is in ask mode, the daemon returns:

{ "error": "approval_required", "scope": "<SCOPE>", "walletId": "<WALLET_ID>" }

To approve once, request a short-lived approval token and retry with X-Qortal-Approval-Token:

curl -s -X POST http://127.0.0.1:3191/permissions/approve \
  -H 'Authorization: Bearer <TOKEN>' \
  -H 'Content-Type: application/json' \
  -d '{"walletId":"<WALLET_ID>","scope":"SIGN_TRANSACTION"}'

Then retry the original request with:

  -H 'X-Qortal-Approval-Token: <APPROVAL_TOKEN>'

To auto-approve for a scope, set a mode:

curl -s -X POST http://127.0.0.1:3191/permissions/set \
  -H 'Authorization: Bearer <TOKEN>' \
  -H 'Content-Type: application/json' \
  -d '{"walletId":"<WALLET_ID>","scope":"SIGN_TRANSACTION","mode":"persistent"}'

PUBLISH_QDN_RESOURCE

uploadType is optional and auto-detected. filePath is read from the daemon filesystem (mount it if running in Docker). For zip files with filePath, set isZip: true.

  • file if filePath is provided (chunked upload)
  • string if stringData or text is provided
  • zip if uploadType is explicitly zip (base64 zip via data)
  • base64 otherwise (data is base64)
curl -s -X POST http://127.0.0.1:3191/qortal/request \
  -H 'Authorization: Bearer <TOKEN>' \
  -H 'Content-Type: application/json' \
  -d '{"requestType":"PUBLISH_QDN_RESOURCE","payload":{"walletId":"<WALLET_ID>","password":"backup-password","registeredName":"name","service":"DOCUMENT_PRIVATE","identifier":"example","data":"<BASE64>"}}'
curl -s -X POST http://127.0.0.1:3191/qortal/request \
  -H 'Authorization: Bearer <TOKEN>' \
  -H 'Content-Type: application/json' \
  -d '{"requestType":"PUBLISH_QDN_RESOURCE","payload":{"walletId":"<WALLET_ID>","password":"backup-password","registeredName":"name","service":"DOCUMENT_PRIVATE","identifier":"example","uploadType":"file","filePath":"/data/uploads/document.pdf"}}'

PUBLISH_MULTIPLE_QDN_RESOURCES

curl -s -X POST http://127.0.0.1:3191/qortal/request \
  -H 'Authorization: Bearer <TOKEN>' \
  -H 'Content-Type: application/json' \
  -d '{"requestType":"PUBLISH_MULTIPLE_QDN_RESOURCES","payload":{"walletId":"<WALLET_ID>","password":"backup-password","continueOnError":false,"resources":[{"registeredName":"name","service":"DOCUMENT_PRIVATE","identifier":"one","data":"<BASE64>"},{"registeredName":"name","service":"DOCUMENT_PRIVATE","identifier":"two","data":"<BASE64>"}]}}'

GET_USER_ACCOUNT

curl -s -X POST http://127.0.0.1:3191/qortal/request \
  -H 'Authorization: Bearer <TOKEN>' \
  -H 'Content-Type: application/json' \
  -d '{"requestType":"GET_USER_ACCOUNT","payload":{"walletId":"<WALLET_ID>"}}'

WHICH_UI

curl -s -X POST http://127.0.0.1:3191/qortal/request \
  -H 'Authorization: Bearer <TOKEN>' \
  -H 'Content-Type: application/json' \
  -d '{"requestType":"WHICH_UI","payload":{}}'

Response is a string (HUB_WEB or HUB_ELECTRON).

SAVE_FILE

Writes a file on the daemon host filesystem. Relative paths are resolved under QORTAL_AUTH_SAVE_FILE_DIR (default data/saved-files). Absolute paths are only accepted if they are inside that same base directory.

curl -s -X POST http://127.0.0.1:3191/qortal/request \
  -H 'Authorization: Bearer <TOKEN>' \
  -H 'Content-Type: application/json' \
  -d '{"requestType":"SAVE_FILE","payload":{"filename":"example.txt","text":"hello world"}}'

Storage

Data is stored under ./data/ by default:

  • data/state.json (apps + wallet metadata + persistent permissions)
  • data/settings.json (global/app/wallet settings)
  • data/audit.log (JSONL audit trail)
  • data/wallets/qortal_backup_<address>.json (Qortal-compatible backups)

Override with QORTAL_AUTH_DATA_DIR if needed. Use QORTAL_AUTH_NODE_URL to set the default node URL.

Testing

Run the backup parity test

Put a Qortal backup file in TEST-FILES/ and run:

QORTAL_TEST_BACKUP_PASSWORD='Password1234x!' \
QORTAL_TEST_BACKUP_FILE='not-used-test-2.json' \
npm run test

If you omit QORTAL_TEST_BACKUP_FILE, it will use the default file already referenced in the test.