--- url: https://dstorage.pro/docs/guide.md --- # Guide Six guides that gradually introduce new concepts — each one swaps in a more realistic set of storage/chain adapters than the last (except Passkey Encryption, which swaps the encryption adapter instead), while the core `init()` / `store()` / `retrieveByRefId()` call pattern stays the same throughout. Every guide can also be followed hands-on against a ready-to-run starter app instead of building from scratch — clone [`dstorage-starter-template`](https://github.com/dStorageTech/dstorage-starter-template) and swap in each guide's adapters as you go. Working with an AI coding agent? Each guide starts with a ready-made prompt for it — see [Build with AI](/guide/build-with-ai) for those and for pointing your agent at these docs. * **[Mock Adapters](/guide/mock-adapters)** — fully in-memory, zero external dependencies. Your first encrypted upload and retrieval, in a few minutes. * **[Core Concepts](/guide/core-concepts)** — how your data gets encrypted, `storageId` vs `refId`, and how the three adapter slots compose. Conceptual, not hands-on — read this before or after Mock Adapters. * **[Local & Simulator Adapters](/guide/local-simulator-adapters)** — real circuit behavior and real local storage, still with no live wallet or Docker proof server. * **[Managed Payments Service](/guide/managed-payments-service)** — route storage and chain payments through dStorage Pro's `TEST` sandbox service, still on arlocal and the simulator. * **[Midnight Network Adapter](/guide/midnight-network-adapter)** — a real Midnight network, a real wallet, and a live proof server. * **[Passkey Encryption](/guide/passkey-encryption)** — swap in `WebAuthnPrfEncryptionAdapter` to unlock uploads with a platform passkey instead of a password. * **[Managed Payments on Midnight](/guide/managed-payments-midnight)** — a real Midnight network with DUST fees sponsored via dStorage Pro's managed service, so the wallet needs no DUST or NIGHT. --- --- url: https://dstorage.pro/docs/guide/build-with-ai.md description: >- Use these docs with Claude Code, Cursor, Codex or any other AI coding agent — llms.txt, project instructions, and a ready-made prompt for every guide. --- # Build with AI These docs are written to be read by AI coding agents as well as people. The dStorage SDK is new and its API changes between versions, so an agent working from what it remembers will often get it wrong. Point it at these docs instead, and it works from the current API. ## Point your agent at the docs Every page is also published as plain Markdown for agents: * **[`llms.txt`](https://dstorage.pro/docs/llms.txt)** — an index of every page, with links to their Markdown versions. Start here. * **[`llms-full.txt`](https://dstorage.pro/docs/llms-full.txt)** — the whole docs in one file, for agents that work best with everything in context at once. * **A Markdown copy of each page** — the page URL with `.md` instead of `.html`, e.g. [`/docs/guide/mock-adapters.md`](https://dstorage.pro/docs/guide/mock-adapters.md). Some agents' fetch tools can't read the `.md` copies. If yours can't, give it the normal page URL or `llms-full.txt` instead. ## Add the docs to your project's agent instructions Most coding agents read a project instructions file on every run: `AGENTS.md` (Codex, Cursor and others), `CLAUDE.md` (Claude Code) or `.cursor/rules`. Add this to yours, so the agent checks these docs whenever it touches dStorage code, without you having to remind it: ```md ## dStorage SDK This project uses @dstorage-tech/dstorage-sdk. Its API changes between versions, so don't rely on prior knowledge of it. Before writing or changing dStorage code, read the docs: - Docs index: https://dstorage.pro/docs/llms.txt - Full docs in one file: https://dstorage.pro/docs/llms-full.txt Check the SDK version installed in package.json, and flag anything in the docs that doesn't match it. ``` ## Ask about any page The **Copy page for AI** button in the top bar copies the current page as Markdown. Its menu can also open the page in Claude or ChatGPT, ready for you to ask questions about it — handy for the [FAQ](/faq/) and [Features](/features/) pages. ## A prompt for every guide Each [guide](/guide/) starts with a ready-made prompt that has your agent follow it in your own project, ending with a check that it worked. They're collected here in the same order as the guides, each building on the one before: Prefer a working app to start from? Clone [`dstorage-starter-template`](https://github.com/dStorageTech/dstorage-starter-template) and run the prompts against it. ## Tips * **Review what the agent changes**, especially anything touching keys, passwords or tokens. * **Never paste a `ds_*` secret token into a prompt or browser code.** Keep tokens in environment variables; in the browser, use a scoped JWT token (see [Managed Payments](/faq/managed-payments)). * **Steps that need you** — approving wallet prompts, passkey ceremonies, creating a dStorage Pro token — can't be done by the agent. The prompts tell it to stop and ask you at those points. --- --- url: https://dstorage.pro/docs/guide/mock-adapters.md --- # Mock Adapters This guide gets you from zero to your first encrypted upload and retrieval in a few minutes, using dStorage's in-memory Mock adapters — no Midnight wallet, no Docker, no external services. They're fully self-contained and behave like the real storage/chain adapters, just without the network calls. ## Prerequisites * Node.js 22 or later * npm That's it — Mock adapters need no wallet extensions, no proof server, and no tokens. Fast track: clone [`dstorage-starter-template`](https://github.com/dStorageTech/dstorage-starter-template) and wire up this guide's adapters in minutes. ## Install ```sh npm install @dstorage-tech/dstorage-sdk ``` ## Step 1 — Configure the SDK `DStorage` is configured with three kinds of adapters, each with a distinct job: * **`storageAdapters`** — where the encrypted bytes are stored. `MockStorageAdapter` keeps them in memory. * **`chainAdapters`** — where the on-chain reference (the tamper-proof pointer to your data) is written. `MockChainAdapter` simulates this without a real blockchain. * **`encryptionAdapters`** — how the per-upload encryption key is protected. `PasswordEncryptionAdapter` derives it from a password you provide. `storageAdapters` and `chainAdapters` are both arrays — pass a single-entry array unless you want fallback across multiple adapters (see [Adapters](/faq/adapters#how-do-multiple-storage-chain-adapters-and-fallback-work)). ```typescript import { DStorage, MockStorageAdapter, MockChainAdapter, PasswordEncryptionAdapter, } from "@dstorage-tech/dstorage-sdk"; const sdk = new DStorage({ storageAdapters: [new MockStorageAdapter()], chainAdapters: [new MockChainAdapter()], encryptionAdapters: [ new PasswordEncryptionAdapter({ password: "Correct-Horse-Battery!", salt: "myapp:v1", }), ], }); ``` The `salt` scopes key derivation to your application — use an app-specific label like this one, not a per-user secret. ## Step 2 — Init, store, retrieve ```typescript await sdk.init(); const { chainRefId } = await sdk.store( new TextEncoder().encode("hello, dStorage"), ); const { bytes } = await sdk.retrieveByRefId(chainRefId); console.log(new TextDecoder().decode(bytes)); // "hello, dStorage" ``` What happened at each step: 1. **`sdk.init()`** prepares the SDK — with `chainAdapters` configured, this is where the `DataRegistry` contract gets deployed (or joined, if you passed an existing address — more on that in the [Midnight Network Adapter](/guide/midnight-network-adapter) guide). 2. **`sdk.store()`** encrypts your data on the client with a fresh random key before anything leaves the process, uploads the ciphertext via the storage adapter, and writes an encrypted reference on-chain via the chain adapter. It returns a `chainRefId` — your handle for retrieving the data later. 3. **`sdk.retrieveByRefId()`** looks up the reference, downloads the ciphertext, and decrypts it locally using the same password-derived key. The operator — and the storage/chain adapters themselves — never see the plaintext. ## Step 3 — Clean up Call `sdk.destroy()` when you're done with the instance — on logout, on unmount, or whenever the SDK is no longer needed: ```typescript sdk.destroy(); ``` This immediately wipes the in-memory key material (the live encryption adapters and any wallet reference) so it doesn't linger in the JS heap for the lifetime of the process or page. It does **not** delete any stored data or on-chain references — only the local key material. ## Learn More * Browse the [FAQ](/faq/) for answers on encryption, adapters, and troubleshooting. --- --- url: https://dstorage.pro/docs/guide/core-concepts.md --- # Core Concepts The [Mock Adapters](/guide/mock-adapters) guide showed *how* to call the SDK. This page explains *why* it works the way it does — the three ideas worth having in your head before you plug in real adapters. ## How your data gets encrypted Every upload gets its own random encryption key, called the **DEK** (Data Encryption Key). The DEK encrypts your data — and only your data for that one upload. The DEK itself then gets encrypted ("wrapped") under a separate key called the **KEK** (Key Encryption Key), which comes from whichever `encryptionAdapters` you configured (password, mnemonic, keypair, or passkey). The wrapped DEK is stored on-chain in a field called `keyEnvelope`. ``` your data --[encrypted by]--> DEK --[wrapped by]--> KEK (from your adapter) ``` To decrypt later, the SDK unwraps the DEK using one of your registered adapters, then uses the DEK to decrypt the data. If you register multiple encryption adapters (e.g. a password *and* a recovery mnemonic), each one gets an independent copy of the wrapped DEK — any one of them can unlock the data on its own. ## `storageId` vs `refId` Two different identifiers come out of `sdk.store()`, and they point at two different things: * **`storageId`** identifies the encrypted blob on the storage network itself. It's a pointer to *bytes*. * **`refId`** identifies the on-chain record in the `DataRegistry` contract. It's a pointer to a *reference* — which itself contains the (encrypted) `storageId`, plus the `keyEnvelope` needed to decrypt the stored blob. In everyday use you only need `refId`: `retrieveByRefId(refId)` looks up the reference, finds the storage pointer, downloads the ciphertext, and decrypts it — all in one call. `storageId` is a lower-level handle, useful mainly for storage-only mode (no chain adapter) or for retrieving public data directly. ## How adapters compose `DStorage` is configured with three independent adapter slots. Each one answers a different question: | Slot | Answers | Required? | | ---------------------- | ------------------------------------------ | -------------------------------------- | | `storageAdapters` | Where do the encrypted bytes live? | Always — at least one | | `chainAdapters` | Where's the on-chain reference written? | Optional — omit or `[]` for storage-only mode | | `encryptionAdapters` | How is the per-upload encryption key protected? | Optional — omit for public uploads | `storageAdapters` and `chainAdapters` are ordered lists, not single values — reads and writes try each configured adapter in turn and fall back to the next on failure. A single-entry list behaves exactly like the single-adapter config these guides show. See [Adapters](../faq/adapters#how-do-multiple-storage-chain-adapters-and-fallback-work) for the full fallback semantics. Because these are independent, you can mix and match: start with Mock everything for local development, swap in `ArweaveLocalStorageAdapter` once you want realistic storage behavior, then move to real `MidnightChainAdapter` and `ArweaveStorageAdapter` for production — without changing how you call `store()` or `retrieveByRefId()`. The rest of the guides in this section walk through those combinations. ## What data lives on-chain vs off-chain * **Off-chain** (the storage network): the encrypted data payload — the actual bytes. * **On-chain** (the `DataRegistry` contract): the encrypted `storageId`, the encrypted `keyEnvelope`, and a few small metadata fields — also encrypted, and optionally supplied by you via `store()`. Never the raw data, never an unwrapped key. ## Learn More * Browse the [FAQ](/faq/concepts#concepts-features) for more depth on any of these topics, including [encryption & security](/faq/encryption-security#encryption-security). --- --- url: https://dstorage.pro/docs/guide/local-simulator-adapters.md --- # Local & Simulator Adapters The [Mock Adapters](/guide/mock-adapters) guide used fully in-memory adapters. This guide swaps them for `ArweaveLocalStorageAdapter` and `MidnightSimulatorChainAdapter` — adapters that behave like the real thing (real network calls to a local Arweave node, and real execution of the DataRegistry contract) but still need no Midnight proof server, no wallet extension, and no real tokens. This is a good next step once Mock Adapters feels too simplified: `refId` values now use the real circuit-derived format, and ownership checks run exactly as they would against the live chain — so tests written against these adapters exercise realistic behavior. ## Prerequisites * Node.js 22 or later * [arlocal](https://github.com/textury/arlocal) — a local Arweave-compatible node * No Midnight proof server, wallet extension, or DUST tokens needed — `MidnightSimulatorChainAdapter` runs the real `DataRegistry` circuits in-process Fast track: clone [`dstorage-starter-template`](https://github.com/dStorageTech/dstorage-starter-template) and wire up this guide's adapters in minutes. ## Step 1 — Start arlocal ```sh docker run -d --rm -p 1984:1984 textury/arlocal # or, without Docker: npx arlocal ``` No real AR tokens are needed — arlocal auto-funds test wallets. Once you've finished working through this guide, stop arlocal so it doesn't keep running in the background. If you started it with `npx arlocal`, press Ctrl+C in that terminal. If you started it with Docker, run: ```sh docker stop $(docker ps -q --filter ancestor=textury/arlocal) ``` The `--rm` flag used when starting the container cleans it up automatically once stopped. ## Step 2 — Configure the SDK This guide keeps the same three-adapter shape from Mock Adapters, but swaps two of them out for versions that behave like the real thing — only `encryptionAdapters` stays as-is: * **`storageAdapters`** — `ArweaveLocalStorageAdapter.createWithTestWallet()` replaces `MockStorageAdapter`. It generates a fresh, disposable JWK test wallet (not connected to real funds) and funds it with test AR against your running arlocal instance. * **`chainAdapters`** — `MidnightSimulatorChainAdapter` replaces `MockChainAdapter`, running the real `DataRegistry` contract logic in-process instead of simulating it in memory. It's still not talking to the Midnight network — no node, indexer, or proof server involved — just a much more faithful in-process simulator than `MockChainAdapter`'s. * **`encryptionAdapters`** — unchanged. `PasswordEncryptionAdapter` still protects the per-upload encryption key exactly as it did in the Mock Adapters guide. ```typescript import { DStorage, ArweaveLocalStorageAdapter, MidnightSimulatorChainAdapter, PasswordEncryptionAdapter, } from "@dstorage-tech/dstorage-sdk"; const { adapter: storageAdapter } = await ArweaveLocalStorageAdapter.createWithTestWallet({ fundAr: 5, // amount of test AR to fund the generated wallet with }); const sdk = new DStorage({ storageAdapters: [storageAdapter], chainAdapters: [new MidnightSimulatorChainAdapter()], encryptionAdapters: [ new PasswordEncryptionAdapter({ password: "Correct-Horse-Battery!", salt: "myapp:v1", }), ], }); ``` ## Step 3 — Init, store, retrieve The API is identical to the Mock Adapters guide — that's the point, adapters are interchangeable: ```typescript await sdk.init(); const { chainRefId } = await sdk.store( new TextEncoder().encode("hello, dStorage"), ); const { bytes } = await sdk.retrieveByRefId(chainRefId); console.log(new TextDecoder().decode(bytes)); // "hello, dStorage" ``` What's different from Mock under the hood: * **`chainRefId`** is now generated by running the real `DataRegistry` contract logic — a 32-byte hex ID, not the random placeholder ID that `MockChainAdapter` uses. * **Ownership checks** are enforced by the simulator just as they would be by the real contract running on the Midnight network, so tests here catch ownership bugs that Mock's simplified checks would miss. * **Uploads are sent over the network** to your local arlocal instance, instead of just being held in memory like `MockStorageAdapter` did. * **`storageId`** is now a real Arweave transaction ID in your local arlocal instance, instead of a random placeholder — it's not on the real Arweave network, but it can actually be looked up there. ## Learn More * Browse the [FAQ](/faq/adapters#adapters) for the full adapter reference. * Next: [Managed Payments Service](/guide/managed-payments-service) routes this guide's storage and chain payments through dStorage Pro's `TEST` sandbox service. --- --- url: https://dstorage.pro/docs/guide/managed-payments-service.md --- # Managed Payments Service dStorage Pro is a hosted signing/payment service run by the dStorage team. It covers the network fees your app incurs across every supported network — Arweave storage costs and Midnight DUST chain fees alike — billed to your dStorage Pro account instead of drawn from each end user's own wallet, so your users never need a funded wallet on any of them. Access is authorized per-request via a token you configure once. It also offers a `TEST` sandbox, which runs the same payment flow against your account without spending real funds, so you can build and test your integration before going live. This guide takes the [Local & Simulator Adapters](/guide/local-simulator-adapters) script and routes its storage and chain payments through dStorage Pro's `TEST` (sandbox) service. You get the full managed-payment round trip against your real dStorage Pro account, while uploads still go to your local arlocal instance and no real funds are spent. ## Prerequisites Everything from the [Local & Simulator Adapters](/guide/local-simulator-adapters) guide, plus a dStorage Pro token: * Node.js 22 or later * [arlocal](https://github.com/textury/arlocal) running locally — see [Step 1 of the Local & Simulator Adapters guide](/guide/local-simulator-adapters#step-1-—-start-arlocal) * A dStorage Pro account and API token from [portal.dstorage.pro](https://portal.dstorage.pro) Fast track: clone [`dstorage-starter-template`](https://github.com/dStorageTech/dstorage-starter-template) and wire up this guide's adapters in minutes. ## Step 1 — Get a dStorage Pro API token Go to [portal.dstorage.pro](https://portal.dstorage.pro), sign in, and create a token from the dashboard. Copy it — you'll paste it into the adapter config in Step 2. ::: tip Free coupons Free coupons for the dStorage Pro service and its sandbox are regularly shared on our socials — [X](https://x.com/dStorageTech), [Reddit](https://www.reddit.com/r/dStorage), and [LinkedIn](https://www.linkedin.com/company/dstorage-tech) — like [this one](https://x.com/dStorageTech/status/2105119589810217410?s=20). Redeem them from the portal to top up your balance. ::: See the [Managed Payments FAQ](/faq/managed-payments#managed-payments-dstorage-pro) for the different token types and how to scope them. ## Step 2 — Add managed payment to your adapters Starting from the exact Step 2 config of the Local & Simulator Adapters guide, the only change is a single `managedPayment` config object, passed to both `ArweaveLocalStorageAdapter.createWithTestWallet()` and `MidnightSimulatorChainAdapter`: ```typescript import { DStorage, ArweaveLocalStorageAdapter, MidnightSimulatorChainAdapter, PasswordEncryptionAdapter, } from "@dstorage-tech/dstorage-sdk"; const managedPayment = { signingServerUrl: "https://portal.dstorage.pro", authToken: "your_dstorage_pro_token_here", // from portal.dstorage.pro testMode: true, }; const { adapter: storageAdapter } = await ArweaveLocalStorageAdapter.createWithTestWallet({ fundAr: 5, // amount of test AR to fund the generated wallet with managedPayment, }); const sdk = new DStorage({ storageAdapters: [storageAdapter], chainAdapters: [new MidnightSimulatorChainAdapter({ managedPayment })], encryptionAdapters: [ new PasswordEncryptionAdapter({ password: "Correct-Horse-Battery!", salt: "myapp:v1", }), ], }); ``` With `testMode: true`, each upload's payment request is sent to dStorage Pro with the `TEST` network identifier: dStorage Pro records it as a sandbox payment against your account, without spending any real funds. Because the same `managedPayment` object is also passed to `MidnightSimulatorChainAdapter`, the on-chain reference write is paid through dStorage Pro's `TEST` sandbox too. Each `store()` call therefore records two sandbox payments — one for storage and one for the chain — just as a production setup has dStorage Pro cover both the Arweave and the Midnight fees. If you only want to route storage through dStorage Pro, leave `{ managedPayment }` off the chain adapter. Your content never goes to dStorage Pro — not the raw data, and not even the encrypted bytes. The storage payment only sends the transaction metadata and its Merkle root (`data_root`), and the chain payment only sends a hash of the content; the encrypted content is uploaded directly to arlocal afterwards. ## Step 3 — Init, store, retrieve Same call pattern as every other guide in this series: ```typescript await sdk.init(); const { chainRefId } = await sdk.store( new TextEncoder().encode("hello, dStorage"), ); const { bytes } = await sdk.retrieveByRefId(chainRefId); console.log(new TextDecoder().decode(bytes)); // "hello, dStorage" ``` What's different from the Local & Simulator Adapters guide: * **Both payments go through dStorage Pro** — the storage upload and the on-chain reference write are each recorded as a `TEST` payment on your dStorage Pro account, so you can see them in the portal dashboard. * **Storage and chain still run locally** — uploads still land in your local arlocal instance, and the on-chain reference is still written by `MidnightSimulatorChainAdapter`; only their fees now go through dStorage Pro. ## Learn More * Browse the [FAQ](/faq/managed-payments#managed-payments-dstorage-pro) for the full managed-payments reference, including token scoping and security notes. * Next: [Midnight Network Adapter](/guide/midnight-network-adapter) swaps the simulator for a real Midnight network, a real wallet, and a live proof server. --- --- url: https://dstorage.pro/docs/guide/midnight-network-adapter.md --- # Midnight Network Adapter The [Local & Simulator Adapters](/guide/local-simulator-adapters) guide ran real DataRegistry circuits in-process, with no live network. This guide swaps `MidnightSimulatorChainAdapter` for the real `MidnightChainAdapter` — connecting to an actual Midnight network with a real wallet and a real proof server. It's the first guide in this series that talks to live infrastructure. ## Prerequisites * Node.js 22 or later, to run the Vite dev server (the app code itself runs in the browser) * Docker, to run the Midnight proof server * [arlocal](https://github.com/textury/arlocal) — a local Arweave-compatible node, still running from [Step 1 of the Local & Simulator Adapters guide](/guide/local-simulator-adapters#step-1-—-start-arlocal) * [1AM](https://1am.xyz) wallet extension (latest version) — or [Lace](https://www.lace.io), or any other wallet implementing the dApp Connector API — **switched to the Preprod network in its own settings** — this guide runs in the browser and connects to it directly, and a wallet only exposes addresses for the network it's currently set to * A Midnight wallet funded with DUST — for local development, request test tokens from the [Midnight Preprod Faucet](https://faucet.preprod.midnight.network/) Fast track: clone [`dstorage-starter-template`](https://github.com/dStorageTech/dstorage-starter-template), run `npm install && npm run dev`, and open the printed local URL to see this guide running. The same project doubles as a boilerplate — keep building on `index.html` / `src/main.ts` for your own app instead of starting from scratch. ## Step 1 — Start the Midnight proof server The proof server generates the zero-knowledge proofs needed to submit transactions to the Midnight network — it's what lets `MidnightChainAdapter` write on-chain references without revealing the private inputs behind them. ```sh docker run -p 6300:6300 midnightntwrk/proof-server:8.1.0 -- midnight-proof-server -v ``` Once you've finished working through this guide, stop the proof server so it doesn't keep running in the background. If it's running attached to your terminal (as shown above), press Ctrl+C there. If you started it detached, run: ```sh docker stop $(docker ps -q --filter ancestor=midnightntwrk/proof-server:8.1.0) ``` ::: tip Proving without your own proof server This guide runs its own proof server, which `MidnightChainAdapter` uses by default. In the browser you can instead set `prover` to have the wallet's proof server or the wallet itself generate the proofs — see [Where are ZK proofs generated in connector mode?](/faq/adapters#where-are-zk-proofs-generated-in-connector-mode). Use those modes only with a wallet extension you trust. ::: ## Step 2 — Configure the SDK `MidnightChainAdapter` takes a `walletMode`, which depends on where your code runs: * **`"provider"`** (Node.js) — you build, start, and sync a `WalletFacade` yourself and pass it in. Useful for backend services; see the [full adapter reference](/faq/adapters#adapters) if you need this instead. * **`"connector"`** (browser) — delegates wallet/key management to a Midnight-compatible wallet extension (1AM by default, but Lace or any other wallet implementing the dApp Connector API works too). This is what the rest of this guide uses. Connector mode fetches the compiled `DataRegistry` contract's ZK artifacts (`keys/` and `zkir/`) over HTTP from your app's own origin, instead of reading them from disk. Copy them from the installed SDK package into Vite's `public/` directory so they're served alongside your app (`dstorage-starter-template`'s `npm run dev` does this for you automatically — see `scripts/copy-zk-artifacts.mjs`): ```sh cp -r node_modules/@dstorage-tech/dstorage-sdk/dist/contracts/dataregistry/managed/keys public/keys cp -r node_modules/@dstorage-tech/dstorage-sdk/dist/contracts/dataregistry/managed/zkir public/zkir ``` This guide keeps `ArweaveLocalStorageAdapter` for storage, to isolate what changes on the chain side. ```typescript import { DStorage, ArweaveLocalStorageAdapter, MidnightChainAdapter, PasswordEncryptionAdapter, } from "@dstorage-tech/dstorage-sdk/browser"; const { adapter: storageAdapter } = await ArweaveLocalStorageAdapter.createWithTestWallet({ fundAr: 5, }); const chainAdapter = new MidnightChainAdapter({ walletMode: "connector", connectorName: "1am", // matches window.midnight['1am'] by injected key or wallet name — omit to use whichever wallet is found first zkConfigBaseUrl: window.location.origin, // serves the keys/ and zkir/ you copied to public/ network: "preprod", // matches the Preprod Faucet used to fund your wallet proofServerEndpoint: "http://localhost:6300", // required with the default prover }); const sdk = new DStorage({ storageAdapters: [storageAdapter], chainAdapters: [chainAdapter], encryptionAdapters: [ new PasswordEncryptionAdapter({ password: "Correct-Horse-Battery!", salt: "myapp:v1", }), ], }); ``` A few things worth knowing about how connector mode behaves: * **Private state is in-memory only.** The `DataRegistry` contract's off-chain data lives only for the life of the page — a reload starts fresh, so hold onto the `contractAddress` you get back from `init()` if you want to reconnect to the same contract afterwards. * **ZK artifacts come over HTTP.** They're fetched from `zkConfigBaseUrl` instead of read off the filesystem. * **Your app never touches signing keys.** The adapter hands transactions to the connected wallet extension for balancing and signing, then submits the result — your code only ever sees the public coin/encryption keys the circuit needs. ## Step 3 — Init, store, retrieve The same call pattern as every other guide in this series, but now triggered from a browser button click instead of a Node.js script — `index.html` loads `main.ts`, which wires this up to a **Run** button: ```typescript const contractAddress = await sdk.init(); console.log("DataRegistry contract address:", contractAddress); const { chainRefId } = await sdk.store( new TextEncoder().encode("hello, dStorage"), ); const { bytes } = await sdk.retrieveByRefId(chainRefId); console.log(new TextDecoder().decode(bytes)); // "hello, dStorage" ``` What's different now that you're on a live network: * **`sdk.init()`** prompts your wallet extension to connect (if it hasn't already), then deploys or joins the `DataRegistry` contract with a real transaction — this costs DUST and takes real proof-generation + confirmation time. * **`store()`** encrypts your data and uploads it to arlocal as before, but the on-chain reference is now written for real — through the proof server you started in Step 1 — instead of only being simulated. * **Save the `contractAddress`** returned by `init()` — copy it from the log line above the first time you run this, then pass it back in as the `contractAddress` config field on `MidnightChainAdapter` on subsequent runs, so you reconnect to the same contract instead of deploying a new one each time. ## Learn More * Browse the FAQ for more on [deployment & configuration](/faq/deployment-configuration#deployment-configuration) and the [full adapter reference](/faq/adapters#adapters). --- --- url: https://dstorage.pro/docs/guide/passkey-encryption.md --- # Passkey Encryption (WebAuthn PRF) This guide uses the [WebAuthn PRF extension](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API/WebAuthn_extensions) — see MDN's docs if you want the full spec-level detail behind how passkeys generate the key material this guide relies on. Every guide so far encrypted uploads with `PasswordEncryptionAdapter`. This guide continues from the [Midnight Network Adapter](/guide/midnight-network-adapter) guide's browser app and swaps `PasswordEncryptionAdapter` out for `WebAuthnPrfEncryptionAdapter` — uploads unlock with a platform passkey rather than a typed password. Only the encryption adapter changes here; storage and chain setup stay exactly as they were in the Midnight Network Adapter guide. ## Prerequisites Everything from the [Midnight Network Adapter](/guide/midnight-network-adapter) guide, plus: * A browser and authenticator with WebAuthn PRF extension support, or a CTAP2 hardware security key with `hmac-secret` support (e.g. a YubiKey 5). * Served over HTTPS or `localhost` — a WebAuthn requirement, already satisfied by the Vite dev server the other guides use. Fast track: clone [`dstorage-starter-template`](https://github.com/dStorageTech/dstorage-starter-template), run `npm install && npm run dev`, and open the printed local URL. ## Step 1 — Register a passkey `WebAuthnPrfEncryptionAdapter` isn't constructed with `new` — you get an instance back from `register()` (first visit) or `authenticate()` (returning visits), both static methods. Check `isSupported()` first, since PRF isn't available everywhere: ```typescript import { WebAuthnPrfEncryptionAdapter } from "@dstorage-tech/dstorage-sdk/browser"; const supported = await WebAuthnPrfEncryptionAdapter.isSupported(); if (!supported) { throw new Error("WebAuthn PRF isn't supported in this browser"); } const storedCredentialId = localStorage.getItem("passkeyCredentialId"); const passkeyAdapter = storedCredentialId ? await WebAuthnPrfEncryptionAdapter.authenticate([ Uint8Array.from(atob(storedCredentialId), (c) => c.charCodeAt(0)), ]) : await (async () => { const adapter = await WebAuthnPrfEncryptionAdapter.register(); localStorage.setItem( "passkeyCredentialId", btoa(String.fromCharCode(...adapter.credentialId)), ); return adapter; })(); ``` `credentialId` isn't secret — it just tells the browser which passkey to prompt for — but the adapter doesn't persist it for you. Your app owns that responsibility, the same way it owns the `contractAddress` returned by `init()`. For a simpler flow with nothing to persist client-side at all, register with `residentKey: "required"` and later call `authenticate([], { discoverable: true })` — the browser's native picker handles credential selection instead. See the [full adapter reference](/faq/adapters#adapters) for details. ## Step 2 — Configure the SDK Same `ArweaveLocalStorageAdapter` + connector-mode `MidnightChainAdapter` as the Midnight Network Adapter guide. The only change is `encryptionAdapters`: ```typescript import { DStorage, ArweaveLocalStorageAdapter, MidnightChainAdapter, } from "@dstorage-tech/dstorage-sdk/browser"; const { adapter: storageAdapter } = await ArweaveLocalStorageAdapter.createWithTestWallet({ fundAr: 5, }); const chainAdapter = new MidnightChainAdapter({ walletMode: "connector", connectorName: "1am", zkConfigBaseUrl: window.location.origin, network: "preprod", proofServerEndpoint: "http://localhost:6300", }); const sdk = new DStorage({ storageAdapters: [storageAdapter], chainAdapters: [chainAdapter], encryptionAdapters: [passkeyAdapter], }); ``` What's different from `PasswordEncryptionAdapter`: * **No password to type or remember.** The passkey ceremony is what authorizes decryption instead. * **`credentialId` replaces the password/salt pair** as the thing your app needs to hold onto — it's not a secret, just a lookup key for the browser's passkey prompt. * **Same DEK-wrapping model underneath.** `WebAuthnPrfEncryptionAdapter` implements the same interface as every other encryption adapter, so it composes the same way — you could list it alongside a `PasswordEncryptionAdapter` or `MnemonicEncryptionAdapter` in `encryptionAdapters: [...]` as a backup unlock method, exactly as shown in the [full adapter reference](/faq/adapters#adapters). ## Step 3 — Init, store, retrieve The same call pattern as every other guide in this series: ```typescript const contractAddress = await sdk.init(); console.log("DataRegistry contract address:", contractAddress); const { chainRefId } = await sdk.store( new TextEncoder().encode("hello, dStorage"), ); const { bytes } = await sdk.retrieveByRefId(chainRefId); console.log(new TextDecoder().decode(bytes)); // "hello, dStorage" ``` What's different about unlocking: * **Within the same session**, `retrieveByRefId()` needs no extra passkey prompt — it reuses the `passkeyAdapter` instance already unlocked in Step 1. * **In a new session** (page reload, different device), you need `authenticate([credentialId])` again before `retrieveByRefId()` can unwrap the DEK — that's the branch already wired up in Step 1's snippet via `storedCredentialId`. ## Learn More * The [full adapter reference](/faq/adapters#adapters) covers every encryption adapter — Password, Mnemonic, Keypair, and this one — including how to combine several in `encryptionAdapters: [...]` so any one of them can unlock the same upload. * The [Encryption & Security FAQ](/faq/encryption-security#encryption-security) compares their post-quantum security tradeoffs. * Next: [Managed Payments on Midnight](/guide/managed-payments-midnight), the final guide in this series, has dStorage Pro sponsor the Midnight DUST fees so the wallet needs no DUST or NIGHT. --- --- url: https://dstorage.pro/docs/guide/managed-payments-midnight.md --- # Managed Payments on Midnight dStorage Pro is a hosted signing/payment service run by the dStorage team. It can front the Midnight DUST chain fees your app incurs, billed to your dStorage Pro account instead of drawn from each end user's own wallet — access is authorized per-request via a token you configure once. This guide uses the same `ArweaveLocalStorageAdapter` + connector-mode `MidnightChainAdapter` browser app as the [Midnight Network Adapter](/guide/midnight-network-adapter) and [Passkey Encryption](/guide/passkey-encryption) guides, with `PasswordEncryptionAdapter` for encryption, and turns managed payment on for the chain side — so neither you nor your end users need a wallet funded with DUST or NIGHT. If you'd rather try the managed-payment flow without a real network first, see [Managed Payments Service](/guide/managed-payments-service), which runs it against the simulator and dStorage Pro's `TEST` sandbox. ## Prerequisites Everything from the [Midnight Network Adapter](/guide/midnight-network-adapter) guide, except the funded wallet: **you don't need any DUST or NIGHT** — no faucet request, no tokens in your wallet. * Node.js 22 or later, to run the Vite dev server * Docker, to run the Midnight proof server * [arlocal](https://github.com/textury/arlocal) running locally — see [Step 1 of the Local & Simulator Adapters guide](/guide/local-simulator-adapters#step-1-—-start-arlocal) * [1AM](https://1am.xyz) wallet extension (latest version) — or [Lace](https://www.lace.io), or any other wallet implementing the dApp Connector API — switched to the Preprod network — your wallet still connects to expose public keys and submit transactions, but it can be completely empty: no DUST and no NIGHT needed * A dStorage Pro account and API token from [portal.dstorage.pro](https://portal.dstorage.pro) Fast track: clone [`dstorage-starter-template`](https://github.com/dStorageTech/dstorage-starter-template), run `npm install && npm run dev`, and open the printed local URL. ## Step 1 — Get a dStorage Pro API token Go to [portal.dstorage.pro](https://portal.dstorage.pro) and sign in with Google, Lace, MetaMask, or 1AM. Top up your balance — by card, bank transfer, local payment methods, or a coupon — then create a token from the dashboard. Tokens come in two flavors: * **JWT token** — scoped and safe to embed in a browser bundle. Its allowed origin, spend cap, and request cap are baked into the signature, and it's instantly revocable from the portal. This is what the rest of this guide uses. * **`ds_*` secret token** — full account access (manage other tokens, payment history). Server-side only — never ship one in browser JavaScript. Use this instead if you're proxying requests through your own Node.js backend rather than calling dStorage Pro directly from the browser; see the [full adapter reference](/faq/adapters#adapters) for that setup. Create a JWT token in the portal's JWT Tokens tab and copy it — you'll paste it directly into the adapter config in Step 2. ::: tip Free coupons and TEST tokens Free coupons for the dStorage Pro service and its `TEST` sandbox are regularly shared on our socials — [X](https://x.com/dStorageTech), [Reddit](https://www.reddit.com/r/dStorage), and [LinkedIn](https://www.linkedin.com/company/dstorage-tech) — like [this one](https://x.com/dStorageTech/status/2105119589810217410?s=20). Redeem them from the portal to top up your balance. If you've claimed `TEST` tokens, dStorage Pro sponsors your DUST fees for free, so you can follow this guide on Preprod without spending any real funds. ::: ## Step 2 — Add managed payment to the chain adapter Starting from the same `ArweaveLocalStorageAdapter` + connector-mode `MidnightChainAdapter` config as the Midnight Network Adapter guide, the only change is a `managedPayment` config object passed to `MidnightChainAdapter`: ```typescript import { DStorage, ArweaveLocalStorageAdapter, MidnightChainAdapter, PasswordEncryptionAdapter, } from "@dstorage-tech/dstorage-sdk/browser"; const managedPayment = { signingServerUrl: "https://portal.dstorage.pro", authToken: "your_jwt_token_here", // from portal.dstorage.pro }; const { adapter: storageAdapter } = await ArweaveLocalStorageAdapter.createWithTestWallet({ fundAr: 5, }); const chainAdapter = new MidnightChainAdapter({ walletMode: "connector", connectorName: "1am", zkConfigBaseUrl: window.location.origin, network: "preprod", proofServerEndpoint: "http://localhost:6300", managedPayment, }); const sdk = new DStorage({ storageAdapters: [storageAdapter], chainAdapters: [chainAdapter], encryptionAdapters: [ new PasswordEncryptionAdapter({ password: "Correct-Horse-Battery!", salt: "myapp:v1", }), ], }); ``` Adding `managedPayment` to `MidnightChainAdapter` delegates DUST chain fees to dStorage Pro: it balances and signs the on-chain transaction instead of the wallet. The wallet extension is still involved, though — it still connects, still exposes the public keys the ZK circuit needs, and still submits the final transaction. It just never needs a DUST or NIGHT balance to do any of that — a freshly created, unfunded wallet works. Privacy is preserved too: only the already-proven Midnight transaction is sent for balancing and signing. Thanks to the ZK proof, that transaction reveals nothing about the private data it references, so no content or witness data is ever sent to dStorage Pro for the DUST payment. ## Step 3 — Init, store, retrieve Same call pattern as every other guide in this series: ```typescript const contractAddress = await sdk.init(); console.log("DataRegistry contract address:", contractAddress); const { chainRefId } = await sdk.store( new TextEncoder().encode("hello, dStorage"), ); const { bytes } = await sdk.retrieveByRefId(chainRefId); console.log(new TextDecoder().decode(bytes)); // "hello, dStorage" ``` What's different from the Midnight Network Adapter guide: * **No DUST or NIGHT needed in the wallet** — dStorage Pro balances and signs the on-chain reference transaction, covering the chain fee, so you can skip the Preprod faucet entirely. * **Each `store()` records a chain payment on your dStorage Pro account**, so you can see it in the portal dashboard. * **Storage is unchanged** — uploads still land in your local arlocal instance via `ArweaveLocalStorageAdapter`. ## Learn More You've reached the end of the adapter progression — from fully in-memory Mock adapters, through local/simulator adapters, to a real Midnight network with managed DUST payments. From here: * Browse the [FAQ](/faq/managed-payments#managed-payments-dstorage-pro) for the full managed-payments reference, including token scoping and security notes. * The [full adapter reference](/faq/adapters#adapters) covers every adapter combination, including ones not shown in these guides. --- --- url: https://dstorage.pro/docs/faq.md --- # FAQ Answers to the questions most relevant to understanding the underlying technology, the dStorage SDK, and its features — encryption and secrets, wallet and chain integration, deployment, and the managed payments service. Jump to a section below. If your question isn't covered, check the [open issues](https://github.com/dStorageTech/dstorage-docs/issues) on the docs repo — someone may have already asked it — or file a new one if not. * [Concepts & Features](./concepts#concepts-features) * [Developers](./developers#developers) * [Encryption & Security](./encryption-security#encryption-security) * [Deployment & Configuration](./deployment-configuration#deployment-configuration) * [Adapters](./adapters#adapters) * [Managed Payments & dStorage Pro](./managed-payments#managed-payments-dstorage-pro) * [Troubleshooting](./troubleshooting#troubleshooting) --- --- url: https://dstorage.pro/docs/faq/concepts.md --- # Concepts & Features ### What is dStorage? dStorage is a privacy-first SDK that combines client-side encryption, decentralised storage (currently Arweave), and on-chain coordination (Midnight) in a single developer-facing API. It lets dApps store sensitive user data without the operator ever seeing the plaintext. ### Who is dStorage designed for? Teams building privacy-sensitive dApps — health records, identity, finance, enterprise workflows — that need strong client-side confidentiality without running their own plaintext data infrastructure. ### What problem does it solve? Most services store user data on servers they control. Users must trust the operator not to read, sell, or lose their data. dStorage removes that requirement: data is encrypted on the user's device before it ever leaves, stored on a decentralised network, and the on-chain reference gives the owner a verifiable receipt. Operators never see the plaintext. ### What is an on-chain reference and why does it matter? An on-chain reference is a tamper-proof record written to the Midnight blockchain that links an owner's identity to an encrypted storage blob. It is what makes data *retrievable* — you can look it up later with just a `refId`. The reference contains an encrypted storage pointer and a key envelope; without the owner's key neither piece reveals anything meaningful. ### What is the `DataRegistry` contract? `DataRegistry` is dStorage's own Midnight smart contract — you don't write or deploy custom contract logic yourself. The SDK deploys a fresh instance (or joins an existing one if you pass `contractAddress`) the first time you call `init()`. It acts as the on-chain registry of references: every `store()` call adds one entry, and each entry holds an encrypted storage pointer (`storageId`), an encrypted key envelope (`keyEnvelope`), the encryption scheme label, a `writtenAt` timestamp, and optional metadata (encrypted the same way as the payload) — plus an ownership commitment that authorizes later `update()`/`removeReference()` calls. It never holds raw data or plaintext keys, only encrypted pointers and cryptographic commitments. ### What does the SDK store on-chain vs off-chain? * **Off-chain (the storage network — currently Arweave)**: the encrypted data payload. * **On-chain (Midnight DataRegistry contract)**: an encrypted storage pointer (`storageId`), an encrypted key envelope (`keyEnvelope`), the encryption scheme label, and optional metadata (encrypted the same way as the payload; unavailable for public uploads). Raw user data and plaintext keys never appear on-chain. ### What happens, step by step, when I call `store()`? Your data is encrypted on-device, then (if a payment adapter is configured) the storage payment is made, the encrypted blob is uploaded to the storage network, the chain payment is made (if applicable), and finally the encrypted pointer is written on-chain as a reference. `store()` returns a `storageId` (the storage-network pointer) and, when a chain adapter is configured, a `chainRefId` (the on-chain reference handle). ### Can dStorage operators read my data? No. Encryption happens on the user's device using keys derived locally. The managed payment service (`portal.dstorage.pro`) only receives a cryptographic hash — never content. The storage network (currently Arweave) holds ciphertext. No party in the pipeline holds an unencrypted copy. ### What is a `refId`? A `refId` is a 32-byte identifier (returned as a hex string) produced by the Midnight `DataRegistry` contract when a reference is stored. Pass it to `retrieveByRefId()` to look up and decrypt the data later. It is the primary retrieval handle; `storageId` is a lower-level identifier for the raw storage blob. ### What is a `storageId`? A 43-character base64url string (32 decoded bytes) that identifies the encrypted blob on the storage network. For chunked uploads it points to the manifest, not individual chunks. You can retrieve directly via `retrieveByStorageId()` but this bypasses the on-chain record — useful for direct storage access when no chain adapter is configured. ### Does dStorage work without a blockchain? Yes. Omit `chainAdapters` (or pass `[]`) from `DStorageConfig` to run in storage-only mode. Uploads go directly to the storage network; no on-chain reference is written. `store()` still returns a `storageId` for later retrieval, but there is no verifiable on-chain ownership record. ### Does dStorage support large files? Yes. Files larger than 10 MB are automatically split into independently encrypted 10 MB chunks. A small manifest ties them together; the manifest's `storageId` is the single pointer written on-chain. `store()`, `retrieveByRefId()`, and `retrieveByStorageId()` behave identically regardless of file size. ### What networks and storage providers are supported today? "Storage network" refers to the off-chain layer where encrypted blobs live. dStorage's storage adapter is pluggable, but Arweave (via the adapter family below) is the only storage network supported today — support for additional storage networks is planned for future releases. | Area | Available now | Stub / planned | | ------- | ---------------------------------------------------------------| -------------- | | Storage | Mock, Arweave Local, Arweave, Arweave Bundler (managed) | | Chain | Mock, MidnightSimulator, Midnight (connector + provider modes), HTTP Gateway (custom REST backend) | | Payment | Mock, Arweave (JWK), Managed (remote signing server) | ### What tokens are used for payments? | Token | Network | Used for | | ------ | -------- | -------------------------------------------------------------| | `AR` | Arweave | Storage payment via `ArweavePaymentAdapter` or managed flow | | `DUST` | Midnight | Chain fees — paid by the Midnight wallet, or sponsored via dStorage Pro's managed flow | | `MOCK` | Mock | Development & testing (simulated, no real funds needed) | ### What does `isPublic: true` do? It is an explicit opt-in that skips both encryption layers. The data payload is stored as raw bytes; the `storageId` is written to the chain in plaintext. Use it only for world-readable content. **Public mode is permanent and irreversible** — once data is stored unencrypted on the storage network (currently Arweave) it cannot be made private retroactively. --- --- url: https://dstorage.pro/docs/faq/developers.md --- # Developers ### What is the minimal working setup? ```typescript import { DStorage, MockStorageAdapter, MockChainAdapter, PasswordEncryptionAdapter, } from "@dstorage-tech/dstorage-sdk"; const sdk = new DStorage({ storageAdapters: [new MockStorageAdapter()], chainAdapters: [new MockChainAdapter()], encryptionAdapters: [ new PasswordEncryptionAdapter({ password: "Correct-Horse-Battery!", salt: "myapp:v1", }), ], }); await sdk.init(); const { chainRefId } = await sdk.store(new TextEncoder().encode("hello")); const { bytes } = await sdk.retrieveByRefId(chainRefId); ``` Mock adapters are fully in-memory — no network, no Docker, no tokens required. ### What does `sdk.init()` do? `init()` prepares the SDK for use: it derives the chain encryption key and, if `chainAdapters` is configured, calls each configured chain adapter's `init()` in order — stopping as soon as one succeeds — which either deploys a new `DataRegistry` contract or joins an existing one. You must call `init()` before `store()` or `retrieveByRefId()` when using private (encrypted) data. ### Should I call `sdk.destroy()` when I'm done, and why? Yes, calling `destroy()` is strongly recommended whenever the user logs out, navigates away, or the SDK instance is no longer needed. It immediately wipes all sensitive in-memory state: * **Encryption providers** — calls each configured `EncryptionAdapter`'s own `destroy()` method (for adapters that implement it, e.g. `KeypairEncryptionAdapter` zeroes its secret key bytes) before dropping the SDK's references to them * **Wallet reference** — the in-memory wallet info including the raw seed used to derive keys * **Initialized flag** — prevents accidental further use of a stale instance Without an explicit `destroy()` call, this material stays in the JavaScript heap for the lifetime of the process or page. In a browser context that means it persists across route changes and is accessible to any code running in the same tab. In a Node.js server context it stays in memory for the lifetime of the process, potentially across multiple users' sessions if the instance is inadvertently shared. ```typescript // React example — destroy on logout or unmount useEffect(() => { return () => sdk.destroy(); }, []); // or on explicit logout function handleLogout() { sdk.destroy(); router.push("/login"); } ``` `destroy()` does not delete any on-chain data or stored content — it only clears the local key material. You can call `init()` again on the same instance to reinitialise if needed. ### What happens if `store()` succeeds at the storage layer but fails to write the on-chain reference? `store()` is not atomic: the content is uploaded first, then the chain reference is written. If the chain write fails (network error, timeout, insufficient DUST), the content is permanently on the storage network — paid for — but has no on-chain record, making it unretrievable via `retrieveByRefId()`. `store()` throws a `StorePartialError` (a `DStorageError` subclass) that includes a `recovery` object with the information needed to retry only the chain write, without re-uploading: ```typescript import { isStorePartialError } from "@dstorage-tech/dstorage-sdk"; // Reactive recovery — catch the error: try { const result = await sdk.store(bytes); } catch (err) { if (isStorePartialError(err)) { const { storageId, storageProvider, keyEnvelope, contentHash } = err.recovery; // Persist recovery to localStorage or DB — keyEnvelope is safe to store localStorage.setItem("pending-store", JSON.stringify(err.recovery)); // Retry the chain write: const { chainRefId } = await sdk.registerReference({ storageId, storageProvider, keyEnvelope, contentHash, // preserves the on-chain integrity hash for the recovered reference }); } } ``` For extra resilience, use the proactive path: `onProgress` fires a `"stored"` phase event after the storage write succeeds and **before** the chain write begins. Persist the recovery payload at that point — it covers crashes and page reloads before the chain write completes: ```typescript await sdk.store(bytes, { onProgress(p) { if (p.phase === "stored" && p.recovery) { // Safe to persist — keyEnvelope is the encryption key already wrapped by your configured adapter localStorage.setItem("pending-store", JSON.stringify(p.recovery)); } }, }); localStorage.removeItem("pending-store"); // clear on success ``` On app startup, check for a pending recovery: ```typescript const pending = localStorage.getItem("pending-store"); if (pending) { const { storageId, storageProvider, keyEnvelope, contentHash } = JSON.parse(pending); await sdk.registerReference({ storageId, storageProvider, keyEnvelope, contentHash, }); localStorage.removeItem("pending-store"); } ``` `keyEnvelope` is the per-upload encryption key already wrapped by your configured encryption adapter — it is safe to persist to localStorage or a database. The raw encryption key is never exposed. For public uploads, `keyEnvelope` is `undefined` and the content remains publicly accessible via `retrieveByStorageId(recovery.storageId)`. `contentHash` is the BLAKE3 hex digest of the stored ciphertext — pass it to `registerReference()` so the recovered reference carries the same on-chain integrity commitment. ### How do I preserve a deployed contract address across runs? `init()` returns the `contractAddress` of the deployed (or joined) `DataRegistry` contract. Save it and pass it as `contractAddress` to `MidnightChainAdapter` on subsequent runs to reconnect instead of deploying a new one each time. ### Can I estimate costs before uploading? Yes: ```typescript const estimate = await sdk.estimateCost(bytes.byteLength); // { storageCost: { amount, token }, chainCost: { amount, token }, fileSizeBytes } ``` ### How do I pass metadata to an upload? Use the second argument to `store()`: ```typescript await sdk.store(bytes, { metadata: { filename: "report.pdf", mimeType: "application/pdf" }, onProgress: ({ phase, chunksUploaded, totalChunks }) => { /* … */ }, }); ``` Metadata is encrypted with the same per-upload key as the payload before being written on-chain, and decrypted automatically on retrieval — it is never stored in plaintext. Because there's no encryption key in public mode, passing `metadata` together with `isPublic: true` throws rather than silently storing it unencrypted. ### What scrypt params should I use in tests? ```typescript new PasswordEncryptionAdapter({ password: "dstorage-test-passphrase-v1!", salt: "test-salt", params: { N: 32768, r: 8, p: 1 }, // minimum allowed N — fastest params tests can use }); ``` ### Can I integrate dStorage into an existing Midnight app? Yes. dStorage is designed to sit alongside an existing Midnight app rather than replace anything — the `DataRegistry` contract it uses is independent of your own contract(s), so integrating it is mostly additive. A few ideas for approaching it: * Reuse the app's existing wallet connection and proof server when configuring `MidnightChainAdapter` (`walletProvider` in Node.js, `connectorName` in the browser) instead of standing up a second wallet flow. * Deploy `DataRegistry` once and save the resulting `contractAddress` alongside your app's other deployed contract addresses, so later runs join it rather than redeploying. * Prototype the calling code first with `MockStorageAdapter`/`MockChainAdapter`, then swap in real adapters once `store()`/`retrieveByRefId()` are wired in at the points where your app handles sensitive user data. The [Midnight Bulletin Board](https://github.com/midnightntwrk/example-bboard) is a small example dApp — a single Compact contract with `post`/`takeDown` circuits for posting and removing one message at a time, with its own wallet and proof server setup — that's a good, low-friction target for practicing exactly this kind of integration: adding dStorage's independent `DataRegistry` contract alongside an app's existing one. ### How do I update the content stored at an existing reference? Use `sdk.update(refId, newBytes, options?)`. It encrypts the new content with a fresh per-upload encryption key, uploads it to the storage network, then locates whichever configured chain adapter actually holds the reference and calls its `updateReference()` — proving ownership with the old `ownerSecret` and registering the new one atomically. * The on-chain pointer is updated in place; the `refId` stays the same. * Old content is **not** deleted from the storage network — only the on-chain pointer changes. * Chunking is **not** applied by `update()`. For large replacements, use `store()` to create a new chunked reference and then `removeReference()` on the old one. * Requires the chain adapter to implement `updateReference()`. Both `MockChainAdapter` and `MidnightChainAdapter` support it; storage-only mode does not. ```typescript const { chainRefId, storageId } = await sdk.update(refId, newBytes); ``` ### How do I rotate encryption adapters without re-uploading content? Use `sdk.rotateKeys(refId)`. It re-wraps the existing per-upload encryption key using all adapters currently in the instance's `encryptionAdapters` list, then writes the new `keyEnvelope` back on-chain. The storage-network ciphertext is **never touched** and `writtenAt` is preserved to reflect the original upload time. Common use cases: * **Add a recovery key** — include both the primary and the new recovery adapter in the instance, then call `rotateKeys`. Both adapters will be able to decrypt going forward. * **Change password (full revocation)** — two steps: (1) init with `[old, new]` so `rotateKeys` can unwrap with `old` and re-wrap under both; (2) init a fresh instance with `[new]` only and call `rotateKeys` again to prune the old wrapper from the envelope. Requires the chain adapter to implement `updateReference()` (same as `update()`). ```typescript // Add a recovery key to an existing reference const sdk = new DStorage({ storageAdapters: [storageAdapter], chainAdapters: [chainAdapter], encryptionAdapters: [ new PasswordEncryptionAdapter({ password, salt }), // existing new MnemonicEncryptionAdapter({ mnemonic: backupPhrase }), // new recovery ], }); await sdk.init(); await sdk.rotateKeys(refId); ``` ### How do I list all my on-chain references? Call `sdk.listReferences()`. It returns a `ListedDataReference[]` — one entry per reference in the DataRegistry contract — containing the `refId`, decrypted `storageId`, `storageProvider`, `encryptionScheme`, `ownerCommitment`, and any attached (decrypted) `metadata`. The SDK automatically decrypts each entry using the configured adapters; references whose envelope cannot be decrypted are returned with a `decryptionError` field rather than throwing. The no-filter (aggregate) path queries every configured chain adapter instead of stopping at the first success, so results may span multiple chains. Each returned reference is tagged with `sourceAdapterIndex`, the zero-based index into `chainAdapters` it came from — useful since `ChainAdapter.name`/`.providerName` isn't guaranteed unique per instance. To look up a known subset without fetching the entire list, pass `refIds`: ```typescript const all = await sdk.listReferences(); const specific = await sdk.listReferences({ refIds: [refId1, refId2] }); ``` Requires at least one chain adapter. The full (no-filter) path additionally requires at least one configured adapter to implement `listReferences()`. ### How do I remove an on-chain reference? Call `sdk.removeReference(refId)`. The SDK re-derives the `ownerSecret` from the reference and submits a ZK ownership proof. Only the original owner can remove a reference — the chain contract enforces this. Removing a reference does **not** delete the data from the storage network; it only removes the on-chain pointer. Whether the underlying blob can be reclaimed later depends on the storage provider. Any caller who saved the old `storageId` can still fetch the ciphertext via `retrieveByStorageId()`, but without the `keyEnvelope` they cannot decrypt it. Requires the chain adapter to implement `removeReference()`. ```typescript await sdk.removeReference(refId); ``` ### What Node.js version is required? Node.js 22 or later. ### Does the SDK work in the browser? Yes. A plain `import ... from "@dstorage-tech/dstorage-sdk"` already resolves to a browser-safe build automatically in bundlers that honor the package's `"browser"` exports condition (Vite, webpack 5+, Rollup, esbuild) — no separate import needed. If your tooling doesn't resolve conditional exports, or you just want it explicit, the same build is also available at `@dstorage-tech/dstorage-sdk/browser`. The main constraint is `MidnightChainAdapter`'s `walletMode: "provider"` (facade mode), which only works in Node.js — use `walletMode: "connector"` instead to delegate to a browser wallet extension (leave `connectorName` unset to use whichever wallet is first detected, or set it to prefer a specific one; any wallet implementing the dApp Connector API works). Pass `zkConfigBaseUrl: window.location.origin` so ZK artifacts are fetched over HTTP from the `public/` directory rather than read from disk. --- --- url: https://dstorage.pro/docs/faq/encryption-security.md --- # Encryption & Security ### Does the SDK keep or hold any secrets, including wallet secrets? Some secret material is held in memory for as long as the SDK instance is alive, but none of it is persisted to disk or on-chain: * Encryption adapter secrets (passwords, mnemonics/seeds, keypairs) are held by their adapter. `MnemonicEncryptionAdapter` zeroes the raw seed from the JS heap immediately after construction; `KeypairEncryptionAdapter` holds its secret key until cleared. * `MidnightChainAdapter`'s wallet reference — including the raw wallet seed used to derive keys — is held in-memory by the SDK. * `ownerSecret` (used to prove on-chain ownership) is never stored at all — it's re-derived on demand from the DEK and `storageId` each time it's needed. Call `sdk.destroy()` to wipe all in-memory secret material at once. See [Should I call `sdk.destroy()` when I'm done, and why?](./developers#should-i-call-sdk-destroy-when-i-m-done-and-why) for the full breakdown of what gets cleared, and [What is `ownerSecret` and why is it not stored?](#what-is-ownersecret-and-why-is-it-not-stored) for detail on that value specifically. ### What encryption scheme does the SDK use? XChaCha20-Poly1305 with a 24-byte random nonce. Its 192-bit nonce space pushes the birthday bound to 2⁸⁰, making random nonce collision a non-issue in practice. ### What is the DEK/KEK model? A fresh random **Data Encryption Key (DEK)** is generated per upload. The DEK is then wrapped (encrypted) under each adapter's **Key Encryption Key (KEK)** and stored in the `keyEnvelope` field on-chain. To decrypt, the SDK retrieves the key envelope, uses one of the registered adapters to unwrap the DEK, then decrypts the payload. ### What encryption adapters are available? | Adapter | Key source | Post-quantum at key layer | Cross-device portable | | ---------------------------- | ------------------------------------ | -------------------------------- | ------------------------------------------------------------------ | | `PasswordEncryptionAdapter` | scrypt(password, salt) | Conditional on password entropy | Yes — same password + salt on any device | | `MnemonicEncryptionAdapter` | BIP-39 (24-word) or 64-byte hex seed | 128-bit PQ (unconditional) | Yes — same mnemonic on any device | | `KeypairEncryptionAdapter` | ML-KEM768 public/secret keypair | 192-bit PQ for a random `generateKeypair()`; ~128-bit when derived via `fromPassword()` / `fromMnemonic()` (bounded by the derivation input's Grover ceiling) | Requires SK; re-derivable via `fromPassword()` / `fromMnemonic()` | | `WebAuthnPrfEncryptionAdapter` | WebAuthn PRF passkey (wraps an internal `MnemonicEncryptionAdapter` + `KeypairEncryptionAdapter`) | 128-bit PQ (unconditional — same tier as `MnemonicEncryptionAdapter`) | Depends on platform passkey sync (e.g. iCloud Keychain, Google Password Manager) — not portable for a device-bound hardware key | ### Why does `MnemonicEncryptionAdapter` require 24 words? A 12-word BIP-39 mnemonic provides only 128 bits of entropy, which Grover's algorithm halves to 64-bit post-quantum security — below NIST's 128-bit minimum. The adapter rejects anything shorter than 24 words at construction time. It also validates the full BIP-39 checksum, not just word-count and wordlist membership, so a single mistyped word that still happens to be a real wordlist entry is rejected instead of silently deriving a different key. ### Can multiple adapters decrypt the same upload? Yes. Supply an array to `encryptionAdapters`. The SDK wraps the DEK under every adapter's KEK independently. Any single registered adapter can decrypt any upload made while it was in the list. This is useful for adding a recovery key or shared access: ```typescript encryptionAdapters: [ new PasswordEncryptionAdapter({ password, salt }), // primary new MnemonicEncryptionAdapter({ mnemonic: backupPhrase }), // recovery ]; ``` ### What happens if I lose my encryption key / password? Data is permanently unrecoverable. The SDK stores no key material — keys are derived deterministically from the adapter's inputs each time. There is no key escrow or recovery mechanism. Follow standard backup practices for whichever secret your adapter uses (password + salt, BIP-39 mnemonic, or SK bytes). ### Is `PasswordEncryptionAdapter` post-quantum safe? At the data layer, yes — XChaCha20-Poly1305 with a 256-bit key gives 128-bit post-quantum security (NIST minimum). At the key encapsulation layer, it depends on password entropy. Human-chosen passwords cannot reach the 256-bit entropy required to resist Grover's. To reach the 128-bit (NIST-minimum) PQ level, use `generateHighEntropyPassword()` with `KeypairEncryptionAdapter.fromPassword()`, or switch to `MnemonicEncryptionAdapter` (24 words). ### What is `generateHighEntropyPassword()`? A helper that produces a cryptographically random 43-character base64url string (32 bytes = 256 bits of real entropy). Pass the result to `KeypairEncryptionAdapter.fromPassword()` to get **128-bit** (NIST-minimum) PQ protection at the key layer with no secret key to store. Note the derived keypair is bounded at ~128-bit by the password's Grover ceiling, not ML-KEM-768's ~192-bit — for the full 192-bit use a random `generateKeypair()` and back up its secret key separately. Store the generated string — it is the only secret needed to re-derive the keypair. ### What makes a password acceptable to `PasswordEncryptionAdapter`? The adapter enforces five rules at construction time: 1. **Length** — at least 12 characters (configurable via `minPasswordLength`). 2. **Character diversity** — at least 3 of 4 character classes: uppercase, lowercase, digits, special characters. 3. **No sequential runs** — patterns like `abcde` or `12345` are rejected. 4. **No keyboard-walk patterns** — sequences like `qwerty` or `asdfg` are rejected. 5. **Entropy** — charset-based entropy estimate must reach 60 bits (configurable via `minEntropyBits`). If your use case requires bypassing these checks — for example, a machine-generated credential — use `generateHighEntropyPassword()` to obtain a cryptographically random 43-character base64url string (256 bits of real entropy), then pass it to `KeypairEncryptionAdapter.fromPassword()` for 128-bit (NIST-minimum) PQ protection at the key layer. ### How does the SDK prevent swapping chunks or blobs in transit? Each ciphertext is bound to its role via Additional Authenticated Data (AAD). A single-shot (non-chunked) upload uses the payload label; once a file is large enough to trigger chunking (see [Does dStorage support large files?](./concepts#does-dstorage-support-large-files)), the manifest and chunk labels come into play instead. The storage pointer label applies to every upload either way. Labels used: | Label | Protects | | ---------------------------- | ---------------------------------------- | | `dstorage:payload:v1` | Single-shot (non-chunked) uploads | | `dstorage:manifest:v1` | Chunked upload manifest (large files only) | | `dstorage:chunk:v1:/` | Each chunk bound to its index and total (large files only) | | `dstorage:storageId:v1` | Storage pointer encrypted on-chain | A reordered chunk, a substituted manifest, or a ciphertext moved from one role to another causes MAC verification to fail before any data is returned. ### How does the SDK verify that an Arweave gateway hasn't tampered with the retrieved data? Every Arweave transaction includes an `X-Content-Hash` tag containing the BLAKE3 hex digest of the uploaded bytes (ciphertext for private uploads, plaintext for public ones). On retrieval, `ArweaveStorageAdapter` fetches the transaction metadata, recomputes the hash, and throws `[dStorage/arweave] Content hash mismatch` if they differ. When retrieval goes through `@dstorage/core` (the normal path via `retrieveByRefId()`/`retrieveByStorageId()`), the hash it verifies against is anchored to the on-chain reference rather than the gateway's own tag — so the check holds even against a gateway that controls both the returned bytes and its `X-Content-Hash` tag and could otherwise make them consistent with each other. Using an Arweave adapter directly, without going through core, falls back to trusting the gateway-supplied tag alone. For private uploads this is defence-in-depth on top of XChaCha20-Poly1305 AEAD authentication. For public uploads it is the primary integrity guarantee. To opt out when using a fully trusted private gateway, pass `skipIntegrityCheck: true` to `ArweaveStorageAdapter`. ### Is the storage pointer on-chain encrypted? Yes. The `storageId` (the storage network's content address — currently an Arweave transaction ID) is encrypted with the same per-upload DEK using a fresh nonce, then stored in the `DataRegistry` contract. A blockchain observer cannot learn which storage resource belongs to which wallet even though the on-chain record is public. ### What is the on-chain content hash and what does it protect against? When a reference is written (via `store()` or `registerReference()`), the SDK computes a BLAKE3 hash of the ciphertext bytes — the exact bytes as written to the storage network — and stores it in the `DataRegistry` contract alongside the encrypted storage pointer. On `retrieveByRefId()`, the SDK re-downloads those bytes, recomputes the hash, and compares it to the on-chain value before attempting decryption. This catches: * A compromised or manipulated storage gateway returning bytes that have been altered since upload. * Any divergence between what was committed to the chain and what the storage network currently serves. For private uploads, AEAD authentication (XChaCha20-Poly1305) provides a second independent tamper check during decryption. For public uploads (no encryption), the on-chain hash is the primary integrity guarantee beyond the storage adapter's own tag. If the check fails, `retrieveByStorageId()` throws a `DStorageError` with code `CONTENT_HASH_MISMATCH` before any decryption is attempted. `retrieveByRefId()` delegates to `retrieveByStorageId()`, passing the hash from the on-chain reference — so the check fires automatically on every `retrieveByRefId()` call when a hash is present. You can also call `retrieveByStorageId(storageId, dekScheme, knownHash)` directly to perform the same check without going through the chain adapter. ### Is the on-chain content hash computed from the plaintext or the ciphertext? Always the ciphertext — the bytes exactly as stored on the storage network. For private uploads this is the encrypted blob; because a fresh random nonce is generated per upload, the ciphertext is non-deterministic even for identical plaintext, so the on-chain hash reveals nothing about content. For public uploads (`isPublic: true`) the ciphertext equals the plaintext, which is acceptable since the data is world-readable by design. For chunked uploads, the hash covers the **manifest ciphertext** (the small encrypted JSON at the `storageId` pointer), not the concatenated chunks. Individual chunks are already independently protected by their AEAD authentication tags and position-binding AAD. ### How does the on-chain hash differ from the `X-Content-Hash` Arweave tag? | | `X-Content-Hash` (Arweave tag) | On-chain hash (`DataRegistry`) | | ------------------------ | ------------------------------------------ | ---------------------------------------------------------------| | **Where stored** | Arweave transaction metadata | Midnight `DataRegistry` contract | | **Who verifies** | Storage adapter (`ArweaveStorageAdapter`) | Core SDK (`retrieveByStorageId`, called by `retrieveByRefId`) | | **Applies to** | Arweave adapters only | All storage adapters | | **Skippable** | Yes — `skipIntegrityCheck: true` | No — always checked when present | | **Independent of chain** | Yes | Anchored to the on-chain reference | Both checks run on `retrieveByRefId()` with Arweave adapters, providing two independent commitments. The on-chain hash covers storage adapters that do not implement their own tag (Mock, future adapters). ### What is `ownerSecret` and why is it not stored? `ownerSecret` is an HKDF derivative of the DEK and the raw `storageId`. It is needed to remove or update a reference on-chain. Because it is derived deterministically from values the SDK already holds, it never needs to be persisted — it is recomputed on demand during `removeReference()` and `update()`. `update()` re-derives the *old* `ownerSecret` to prove ownership of the existing reference, and derives a *new* one (from the fresh per-upload DEK) to register alongside the updated content — both in the same atomic call. ### What exactly changes when I call `rotateKeys()`, and what stays the same? `rotateKeys(refId)` re-wraps an existing reference's per-upload DEK under the encryption adapters currently configured on the SDK instance, then writes the new `keyEnvelope` back on-chain — without re-uploading or re-encrypting the content itself. It's the mechanism behind adding/removing a recovery key and changing a password (see [How do I rotate encryption adapters without re-uploading content?](./developers#how-do-i-rotate-encryption-adapters-without-re-uploading-content) in the Developers section). | Field | Changes? | | -------------------------------------- | ----------------------------------------------------------| | `keyEnvelope` on-chain | Yes — re-wrapped under the new adapter set | | Encrypted `storageId` on-chain | No | | `writtenAt` on-chain | No — preserved from the original upload | | `encryptionScheme` | No | | Ciphertext on the storage network | No — bytes are never re-uploaded | | `ownerSecret` / ownership commitment | No — derived from the same DEK + storageId, so identical | Because the DEK itself is unchanged, any party who previously obtained the DEK retains the ability to decrypt until the corresponding adapter wrapper is pruned from the envelope (see the two-step password-change flow in the Developers section). ### If I call `update()`, what happens to the original content on the storage network? Nothing — it stays there. `update()` only changes the on-chain pointer (the `storageId` field in the DataRegistry reference) to point at the newly encrypted blob. The old blob remains at its original `storageId` on the storage network; whether it can be garbage-collected depends on the storage provider (Arweave uploads, for example, are permanent and immutable). Today the SDK doesn't track old `storageId`s once `update()` overwrites the on-chain pointer, so recovering a prior version means having saved its `storageId` yourself beforehand. Native content versioning — the SDK keeping a history of prior `storageId`s per reference and exposing an API to list or retrieve them — is a possible future addition, not something the SDK supports today. * Anyone who saved the old `storageId` can still download the old ciphertext — but without the old `keyEnvelope` they cannot decrypt it, as the key envelope on-chain now covers the new content's DEK. * `update()` is best suited for mutable application state (user profiles, document drafts) where preserving old versions is not required. --- --- url: https://dstorage.pro/docs/faq/deployment-configuration.md --- # Deployment & Configuration ### What are the prerequisites for running the SDK locally? * Node.js 22+, npm * Docker (for Midnight proof server and Arweave Local) * 1AM wallet extension (latest version) — or Lace, or another dApp Connector-compatible wallet (browser connector flows only) * Wander wallet extension (Arweave browser flows only) The `DataRegistry` contract ships pre-compiled with the SDK package (compiled circuits and keys included) — the Midnight Compact compiler is only needed if you're modifying the contract itself, not for normal SDK usage. ### How do I start the local Midnight proof server? ```sh docker run -p 6300:6300 midnightntwrk/proof-server:8.1.0 -- midnight-proof-server -v ``` ### How do I start a local Arweave node for testing? ```sh docker run -d --rm -p 1984:1984 textury/arlocal # or: npx arlocal ``` No real AR tokens are needed — arlocal auto-funds test wallets. ### What Midnight networks does the adapter support? Any Midnight network — `undeployed` (localhost), `preprod`, `preview`, or another — passed as the required `network` field. The adapter has no built-in default endpoints: * **Provider mode** requires `indexerEndpoint`, `indexerWsEndpoint`, and `proofServerEndpoint`; the constructor throws if any is missing. * **Connector mode** uses the indexer endpoints reported by the connected wallet unless you set `indexerEndpoint`/`indexerWsEndpoint` yourself, and `init()` throws if the wallet is on a different network than `network`. With the default `prover` (`PROVER_MODE.PROOF_SERVER_URL`), `proofServerEndpoint` is required too; the wallet prover modes use the wallet's proof server or the wallet itself instead (see [Where are ZK proofs generated in connector mode?](/faq/adapters#where-are-zk-proofs-generated-in-connector-mode)). ### How do I configure `MidnightChainAdapter` for browser vs Node.js? Use the `walletMode` discriminated union: Node.js (provider mode) — caller supplies a pre-built, synced WalletFacade: ```typescript { walletMode: "provider", walletProvider, // AdapterWalletProvider built and synced by the app privateStatePassword, // password for local LevelDB state encryption network: "undeployed", indexerEndpoint, // required: indexer GraphQL (HTTP) URL indexerWsEndpoint, // required: indexer GraphQL WebSocket URL proofServerEndpoint: "http://localhost:6300", // required zkArtifactsPath, // optional: absolute path to keys/ and zkir/ dirs — the SDK // ships its own compiled copy (see below), no separate compile needed } ``` Browser (connector mode) — delegates to a wallet extension (1AM, Lace, or another dApp Connector wallet): ```typescript { walletMode: "connector", connectorName: "1am", // wallet to prefer under window.midnight zkConfigBaseUrl: window.location.origin, // base URL serving keys/ and zkir/ network: "preprod", // must match the network the wallet is set to proofServerEndpoint: "http://localhost:6300", // required with the default prover // indexer endpoints come from the wallet unless set here } ``` `walletMode` and `network` are required. ### Does `WebAuthnPrfEncryptionAdapter` work in a Node.js app? Only partially. `register()` and `authenticate()` require real browser globals — `window`, `navigator.credentials`, `PublicKeyCredential` — and throw `WEBAUTHN_PRF_UNAVAILABLE` if they're missing, which is always the case in a plain Node.js process. `isSupported()` behaves the same way: it returns `false` (rather than throwing) whenever `window` is undefined. So the interactive passkey ceremony only runs in a real browser tab, or in an Electron **renderer** process (which exposes those same browser globals) — never in a Node.js backend, CLI, or Electron **main** process. The one part of the adapter that does work in Node is `fromPublicKey()`: it only needs raw public-key bytes and never touches WebAuthn, so a Node.js backend can use it for upload-only delegation on behalf of a browser-registered passkey owner (see [`WebAuthnPrfEncryptionAdapter`](/faq/adapters#webauthnprfencryptionadapter) in Adapters). ### How do I configure `ArweaveBundlerStorageAdapter` for managed uploads? ```typescript new ArweaveBundlerStorageAdapter({ managedPayment: { signingServerUrl: "https://portal.dstorage.pro", // Token format: . — issued by the signing server. // The modulus (512 bytes, base64url) is the server's RSA-4096 public key and is // used to pin the expected signing key at construction time. authToken: process.env.DSTORAGE_AUTH_TOKEN ?? "", }, }); ``` ### How do I reuse an existing DataRegistry contract? Pass the previously deployed address: ```typescript new MidnightChainAdapter({ contractAddress: "a1b2c3…", // 64 lowercase hex chars // … rest of config }); ``` Omit `contractAddress` to deploy a new contract on first `init()`. ### Where do ZK artifacts live and how are they served? `DataRegistry` is dStorage's own fixed contract — not something you compile yourself. Its compiled ZK artifacts (prover/verifier keys and ZKIR) ship inside the `@dstorage-tech/dstorage-sdk` package at `dist/contracts/dataregistry/managed/{keys,zkir}`. * **Node.js**: pass an absolute path via `zkArtifactsPath`. The adapter reads `keys/` and `zkir/` from that directory. Resolve the bundled copy via the package's own `package.json` rather than hardcoding a `node_modules` path: ```typescript import { createRequire } from "node:module"; import path from "node:path"; const zkArtifactsPath = path.join( path.dirname(createRequire(import.meta.url).resolve("@dstorage-tech/dstorage-sdk/package.json")), "dist/contracts/dataregistry/managed", ); ``` * **Browser**: copy `keys/` and `zkir/` from `node_modules/@dstorage-tech/dstorage-sdk/dist/contracts/dataregistry/managed/` into your application's public assets directory, then pass `zkConfigBaseUrl: window.location.origin`. Artifacts are fetched over HTTP. (The [`dstorage-starter-template`](https://github.com/dStorageTech/dstorage-starter-template)'s `npm run dev` does this automatically — see `scripts/copy-zk-artifacts.mjs`.) --- --- url: https://dstorage.pro/docs/faq/adapters.md --- # Adapters ### What is an adapter and how do the three types compose? Adapters are the pluggable components you pass to `DStorage` to tell it *where* to store data, *where* to write the on-chain record, and *how* to encrypt the per-upload key. There are three adapter slots: | Slot | Config field | Role | Required? | | ---------- | ---------------------- | -------------------------------------------------------- | -------------------------------------------------------| | Storage | `storageAdapters` | Receives encrypted bytes and returns a `storageId` | Always — at least one | | Chain | `chainAdapters` | Writes the encrypted pointer and key envelope on-chain | Optional — omit or pass `[]` for storage-only mode | | Encryption | `encryptionAdapters` | Wraps the per-upload DEK under each adapter's KEK | Optional — omit for public (`isPublic: true`) uploads | `storageAdapters` and `chainAdapters` are both arrays, tried in priority order. Reads and writes fall back to the next configured adapter on failure and succeed as soon as one works. `listReferences()` is the one exception: it aggregates results across every configured chain adapter instead of stopping at the first success, since references may legitimately live on more than one. See [How do multiple storage/chain adapters and fallback work?](#how-do-multiple-storage-chain-adapters-and-fallback-work) below. You can supply multiple encryption adapters; any single registered adapter can independently decrypt any upload made while it was in the list. ### How do multiple storage/chain adapters and fallback work? `storageAdapters: StorageAdapter[]` and `chainAdapters?: ChainAdapter[]` are ordered lists of fallback candidates, not a single adapter each: ```typescript const sdk = new DStorage({ storageAdapters: [primaryArweaveAdapter, mirrorArweaveAdapter], chainAdapters: [midnightAdapter], // omit or [] for storage-only mode encryptionAdapters: [adapter], }); ``` * **Reads and writes** try each configured adapter in order and fall back to the next on failure, succeeding as soon as one works. If every candidate fails, the SDK throws an aggregate error (`STORAGE_READ_ALL_FAILED`/`STORAGE_WRITE_ALL_FAILED` for storage, `CHAIN_READ_ALL_FAILED`/`CHAIN_WRITE_ALL_FAILED` for chain) whose `.cause` is an array of `{ adapterName, error }` — one entry per candidate tried. * **`listReferences()`** aggregates results from every configured chain adapter instead of stopping at the first success. Each returned reference is tagged with `sourceAdapterIndex`, the zero-based index into `chainAdapters` it came from — useful because `ChainAdapter.name`/`.providerName` isn't guaranteed unique per instance (e.g. two Midnight configs pointed at different networks both report `"midnight"`). * **Single-reference mutations** (`update()`, `retryUpdate()`, `rotateKeys()`, `removeReference()`) locate whichever configured chain adapter actually holds the reference and target it directly. * **`init()`** stops trying chain adapters as soon as one succeeds. * **Chunked uploads** (>10 MB) retry the *whole* upload — not per-chunk — against the next storage adapter candidate if the first fails partway through. `StoreProgress.attemptIndex` tells you which candidate a given progress event belongs to, so a rewind to `chunksUploaded: 0` reads as "retrying against the next adapter" rather than data corruption. Chunk *retrieval* stays pinned to whichever adapter served the manifest. * Retrieval tries each configured storage adapter in order regardless of a reference's recorded `storageProvider` field. ### `MockStorageAdapter` In-memory storage for development and testing. Stores encrypted bytes in an in-memory `Map` on Node.js or `localStorage` in the browser. No network, no tokens, no Docker required. Generates 43-character base64url `storageId` values in the same format as real Arweave IDs, so code that handles `storageId` values works identically against real and mock storage. Uses a MOCK pricing model for `estimateCost()` calls. Optionally accepts a `managedPayment` config (`{ signingServerUrl, authToken }`) to exercise managed payment request/response round-trips in tests without touching real funds. ```typescript const sdk = new DStorage({ storageAdapters: [new MockStorageAdapter()], chainAdapters: [new MockChainAdapter()], encryptionAdapters: [adapter], }); ``` ### `ArweaveLocalStorageAdapter` Local Arweave integration testing. A drop-in replacement for `ArweaveStorageAdapter` that connects to a local arlocal node (`localhost:1984`) instead of the Arweave mainnet. No real AR tokens are needed. The recommended setup is the `createWithTestWallet()` factory, which generates a fresh JWK wallet and auto-funds it: ```typescript const { adapter } = await ArweaveLocalStorageAdapter.createWithTestWallet({ fundAr: 5, }); const sdk = new DStorage({ storageAdapters: [adapter] /* … */ }); ``` The adapter exposes a `.testnet` helper with utilities for controlling the local node: `mine(blocks)` to fast-forward the blockchain, `mintTokens(address, winstons)` to fund wallets, `getBalance(address)`, `isRunning()`, and `reset()` for a clean slate between test runs. arlocal must be running before using `ArweaveLocalStorageAdapter`: ```sh docker run -d --rm -p 1984:1984 textury/arlocal # or: npx arlocal ``` ### `ArweaveStorageAdapter` Permanent Arweave storage. Uploads to the Arweave permaweb: pay once, store forever. Signing is handled by the ArConnect browser extension (browser) or a JWK wallet file (Node.js). The adapter tags every upload with a BLAKE3 `X-Content-Hash` and verifies it on retrieval — pass `skipIntegrityCheck: true` only when using a fully trusted private gateway. Uploads are optimistic (HTTP 202); if you need to block until the transaction is confirmed on-chain, set `waitForConfirmation: true` (expect 2–20 minutes). For development against the Arweave testnet, use `gateway: { host: "arweave.dev", port: 443, protocol: "https" }`. ```typescript // Browser — ArConnect handles signing const sdk = new DStorage({ storageAdapters: [new ArweaveStorageAdapter()], // … }); // Node.js — JWK wallet file const sdk = new DStorage({ storageAdapters: [ new ArweaveStorageAdapter({ walletKey: JSON.parse(fs.readFileSync("wallet.json", "utf8")), }), ], // … }); ``` Requires a funded AR wallet by default (local signing via `walletKey`). To avoid requiring end-users to hold AR, pass a `managedPayment` config (`{ signingServerUrl, authToken }`) instead to delegate signing to a managed service (see [What is dStorage Pro?](./managed-payments#what-is-dstorage-pro) in the Managed Payments section) — the same mechanism `ArweaveBundlerStorageAdapter` requires unconditionally (it has no local-signing mode; omitting `managedPayment` throws). The Bundler adapter's advantage isn't avoiding a wallet — both need a managed signing service to do that — it's near-instant ANS-104 finality instead of the 2–20 minute L1 confirmation wait. `retrieve()` — the method that downloads previously stored content back from Arweave — is bounded by the `maxRetrieveSizeBytes` constructor option (default 256 MiB): the adapter rejects the download upfront if the transaction metadata reports a larger `data_size`, and rejects again after loading if the actual byte count exceeds the limit anyway. This guards against a gateway serving a misleadingly small `data_size` or an unexpectedly huge blob. ### `ArweaveBundlerStorageAdapter` Fast finality via managed signing. Uploads via the ANS-104 Arweave bundler, which delivers near-instant finality compared to the 2–20 minute wait for Arweave L1 transactions. The signing server holds a funded bundler account, so end-users need no AR wallet — see [What is dStorage Pro?](./managed-payments#what-is-dstorage-pro) in the Managed Payments section for the managed service this adapter relies on. Privacy is preserved: only a 48-byte ANS-104 `deep_hash` is sent to the server — the file bytes never leave the client. ```typescript const sdk = new DStorage({ storageAdapters: [ new ArweaveBundlerStorageAdapter({ managedPayment: { signingServerUrl: process.env.DSTORAGE_SERVICE_URL ?? "https://portal.dstorage.pro", authToken: process.env.DSTORAGE_AUTH_TOKEN ?? "", }, }), ], // … }); ``` The `authToken` uses the compound `ds_.` format. The modulus is the server's RSA-4096 public key and is pinned at construction time — if the server rotates its key the adapter immediately detects the mismatch, and the token must be re-issued. `retrieve()` — the method that downloads previously stored content back from Arweave — makes two network calls to the gateway: one for the transaction metadata/tags (used to look up and verify the content), and one for the actual data bytes. Two constructor options bound both against a slow or misbehaving gateway: `requestTimeoutMs` (default 30 s, `0` disables it) aborts either call if it takes too long; `maxMetadataResponseBytes` (default 1 MiB) caps how large the metadata/tags JSON response is allowed to be, since a hostile gateway could otherwise return an oversized body to exhaust memory even though real metadata/tags are always tiny. ### `MockChainAdapter` In-memory chain adapter for development and testing. Stores on-chain references in an in-memory `Map` on Node.js or in `localStorage` in the browser (browser state survives page reloads). Enforces the same SHA-256 `ownerSecret` commitment check as the real DataRegistry contract, so ownership-gated operations (`removeReference`, `updateReference`) behave correctly in tests. `refId` values are UUID-style strings rather than the Bytes<32> format produced by the real circuit. Requires no Midnight node, proof server, indexer, or DUST tokens. Optionally accepts a `managedPayment` config to exercise managed payment request/response round-trips in tests without touching real funds. ```typescript const sdk = new DStorage({ storageAdapters: [new MockStorageAdapter()], chainAdapters: [new MockChainAdapter()], encryptionAdapters: [adapter], }); ``` ### `HttpGatewayChainAdapter` Remote REST backend. Delegates reference operations to a custom HTTP/HTTPS service instead of a blockchain. Configure it with four paths (write, read, list, delete) appended to a shared `baseUrl`: ```typescript new HttpGatewayChainAdapter({ baseUrl: "https://my-backend.example.com", writePath: "/api/refs", readPath: "/api/refs", listPath: "/api/refs", deletePath: "/api/refs", }); ``` All four CRUD operations (`POST /{refId}`, `GET /{refId}`, `GET`, `DELETE /{refId}`) are issued against those paths. **Fixed request headers** — use the optional `headers` field to send the same headers on every request. This is the standard way to pass API tokens or any other authentication material required by the backend: ```typescript new HttpGatewayChainAdapter({ baseUrl: "https://my-backend.example.com", writePath: "/api/refs", readPath: "/api/refs", listPath: "/api/refs", deletePath: "/api/refs", headers: { Authorization: "Bearer my-api-token", "X-Api-Key": "abc123", }, }); ``` For gateways that use a custom CA or mutual-TLS client certificate, pass a preconfigured `fetch`-compatible function via `customFetch`: ```typescript new HttpGatewayChainAdapter({ // … customFetch: (url, init) => fetch(url, { ...init, agent: myCustomHttpsAgent }), }); ``` Every operation applies a `requestTimeoutMs` (default 30 s, `0` disables it), and `readReference`/`listReferences` cap their JSON response at `maxResponseBytes` (default 10 MiB) — both guard against a hung or hostile gateway. One important security note: `ownerSecret` is an internal ZK witness that is deliberately stripped before any network call — it is never transmitted to the gateway. Ownership enforcement (whether to honour a remove or update request) is therefore the responsibility of the backend service, not the adapter. By default, `refId` values are client-generated UUID-style strings (via `crypto.randomUUID()`), matching `MockChainAdapter`'s format — the adapter picks the ID before writing, and the backend just stores it under that key. This isn't enforced on read/list, though: the backend is free to store and return `refId` in any string format (including circuit-derived `Bytes<32>` hex), and a caller can also supply their own `refId` on write to override the client-generated default. No Midnight infrastructure (proof server, indexer, DUST wallet) is needed — cost estimates always return zero. Typical use cases: * Staging environments backed by a custom database * Non-Midnight deployments where a simple REST service is preferable to a blockchain * Integration testing against a real HTTP service * Bridging to another blockchain's own reference registry, by implementing the REST backend as a thin proxy in front of that chain ### `MidnightSimulatorChainAdapter` Real DataRegistry circuits without a live network. Runs the actual DataRegistry Compact circuits in-process via `DataRegistrySimulator`. Ownership enforcement, `ownerSecret`/`ownerCommitment` semantics, and `refId` derivation all match the on-chain behavior exactly — unlike `MockChainAdapter`, which uses simplified UUID-style IDs. No Midnight node, indexer, proof server, or DUST tokens are required. Optionally accepts a `managedPayment` config to exercise the managed payment flow end-to-end in tests. Use this adapter when you need realistic ZK circuit behavior (correct `refId` format, real commitment checks) in integration tests without standing up Docker infrastructure. ```typescript const sdk = new DStorage({ storageAdapters: [new ArweaveLocalStorageAdapter(/* … */)], chainAdapters: [new MidnightSimulatorChainAdapter()], encryptionAdapters: [adapter], }); ``` ### `MidnightChainAdapter` Real Midnight network. Connects to the Midnight blockchain named by the required `network` field (e.g. `preprod`, `preview`, `undeployed`). The adapter has no built-in default endpoints — which ones you must supply depends on the `walletMode` you choose for your runtime: * **`"provider"` (Node.js)**: you build and sync a `WalletFacade` yourself, then pass it as `walletProvider`. Also requires `privateStatePassword` (LevelDB encryption), `indexerEndpoint`, `indexerWsEndpoint`, and `proofServerEndpoint` — the constructor throws if any is missing — and optionally `zkArtifactsPath` (absolute path to the `keys/` and `zkir/` directories). * **`"connector"` (browser)**: delegates all key management to a Midnight wallet extension — by default the first wallet discovered under `window.midnight`, or pass `connectorName` (e.g. `"1am"`) to prefer a specific one (matched by rdns, then declared name, then the injected key). Lace, 1AM, or any other wallet implementing the dApp Connector API works. Requires `zkConfigBaseUrl` set to the base URL from which the ZK artifacts are served. The indexer endpoints default to the ones the connected wallet reports (set `indexerEndpoint`/`indexerWsEndpoint` to override them). The `prover` option selects where ZK proofs are generated (see [Where are ZK proofs generated in connector mode?](#where-are-zk-proofs-generated-in-connector-mode) below); by default that's the proof server at `proofServerEndpoint`, which is then required. `init()` either deploys a fresh DataRegistry contract or, if you pass `contractAddress`, reconnects to an existing one. Save the returned address across runs to avoid redeploying. ```typescript import { createRequire } from "node:module"; import path from "node:path"; // The compiled DataRegistry ZK artifacts ship inside the SDK package itself — // resolve the bundled copy via its package.json rather than hardcoding a path. const zkArtifactsPath = path.join( path.dirname(createRequire(import.meta.url).resolve("@dstorage-tech/dstorage-sdk/package.json")), "dist/contracts/dataregistry/managed", ); // Node.js (provider mode) new MidnightChainAdapter({ walletMode: "provider", walletProvider, // pre-built, synced AdapterWalletProvider privateStatePassword, zkArtifactsPath, network: "undeployed", indexerEndpoint: process.env.MIDNIGHT_INDEXER_URL!, // indexer GraphQL (HTTP) URL indexerWsEndpoint: process.env.MIDNIGHT_INDEXER_WS_URL!, // indexer GraphQL WebSocket URL proofServerEndpoint: "http://localhost:6300", }); // Browser (connector mode) new MidnightChainAdapter({ walletMode: "connector", connectorName: "1am", zkConfigBaseUrl: window.location.origin, network: "preprod", proofServerEndpoint: "http://localhost:6300", // required with the default prover }); ``` Requires a running Midnight proof server (unless a wallet prover mode is used in the browser) and a wallet funded with DUST. DUST chain fees are handled internally by the connected wallet — no explicit payment config is needed. Optionally pass a `managedPayment` config (`{ signingServerUrl, authToken }`) to route Midnight transaction balancing through [dStorage Pro](./managed-payments#what-is-dstorage-pro) instead of the local wallet — the wallet then needs no DUST or NIGHT (see [Managed Payments on Midnight](/guide/managed-payments-midnight)). #### Where are ZK proofs generated in connector mode? In connector mode, the `prover` option selects where ZK proofs are generated. Its values are exported as `PROVER_MODE`: | `prover` | Proofs generated by | `proofServerEndpoint` | | -------------------------------------------- | -------------------------------------------------------------------------------- | --------------------- | | `PROVER_MODE.PROOF_SERVER_URL` (default) | the proof server at `proofServerEndpoint` | required | | `PROVER_MODE.WALLET_CONFIGURED_PROOF_SERVER` | the proof server the wallet reports (`proverServerUri`), called by the adapter | must not be set | | `PROVER_MODE.WALLET_DELEGATED` | the wallet itself, via the dApp Connector `getProvingProvider()` | must not be set | ```typescript import { MidnightChainAdapter, PROVER_MODE } from "@dstorage-tech/dstorage-sdk/browser"; new MidnightChainAdapter({ walletMode: "connector", zkConfigBaseUrl: window.location.origin, // the wallet fetches keys/ and zkir/ from here network: "preprod", prover: PROVER_MODE.WALLET_DELEGATED, // no proof server to run }); ``` The adapter never falls back from one prover mode to another: if the selected one is unavailable, writes fail with an error. Both wallet modes hand the ZK witness inputs — including the `ownerSecret` that authorizes updating and removing a reference — to something the wallet controls, so use them only with a wallet extension you trust. `prover` is connector-only: passing it in provider mode throws. The `allowWalletProofServer` option was removed in v0.3.0 and now throws `MIDNIGHT_INVALID_PROVER_CONFIG` — replace `allowWalletProofServer: true` with `prover: PROVER_MODE.WALLET_CONFIGURED_PROOF_SERVER`, and remove `proofServerEndpoint`. Start a local proof server with Docker (see [How do I start the local Midnight proof server?](./deployment-configuration#how-do-i-start-the-local-midnight-proof-server) in the Deployment section): ```sh docker run -p 6300:6300 midnightntwrk/proof-server:8.1.0 -- midnight-proof-server -v ``` ### `PasswordEncryptionAdapter` Password-derived encryption. Derives a 64-byte KEK from a user-supplied password using scrypt (memory-hard; default: 128 MB RAM per attempt, matching OWASP 2024 recommendations). Because derivation is deterministic, the same `password` and `salt` always produce the same key on any device — no key backup is needed beyond those two values. The `salt` is mandatory and must be supplied by the caller. Use an app-scoped domain label (`"myapp:v1"`), a per-user identifier, or a combination. The SDK never supplies a default to avoid silent cross-app key collisions when passwords are reused. Passwords are validated at construction: minimum 12 characters, at least 3 of 4 character classes (uppercase, lowercase, digits, special), no sequential runs, no keyboard-walk patterns (e.g. `qwerty`), and at least 60 bits of estimated entropy. For constrained devices (e.g. mobile browsers) where 128 MB is too expensive, pass `preset: "v1-lite"` (64 MB). In tests, pass `params: { N: 32768, r: 8, p: 1 }` for fast derivation — that's the fastest params the SDK allows. The SDK enforces a hard floor on any custom `N` (must be ≥ 32768 and a power of two — the OWASP 2024 minimum); anything weaker throws at construction time rather than silently producing a brute-forceable KDF. ```typescript new PasswordEncryptionAdapter({ password: "Correct-Horse-Battery!", salt: "myapp:v1", // preset: "v1-lite", // constrained devices only }); ``` ::: warning Post-quantum note Human-chosen passwords rarely reach 256-bit entropy. For the full ~192-bit post-quantum safety at the key layer, use a randomly generated `KeypairEncryptionAdapter` (`generateKeypair()`, secret key backed up separately). A password- or mnemonic-derived keypair (`fromPassword()` / `fromMnemonic()`) is bounded at ~128-bit by the derivation input's Grover ceiling — still the NIST minimum, but not 192-bit. If you need a machine-generated credential instead of a human-chosen password, `generateHighEntropyPassword()` (see [What is `generateHighEntropyPassword()`?](./encryption-security#what-is-generatehighentropypassword) in the Encryption & Security section) produces a cryptographically random 43-character string with 256 bits of real entropy, reaching that same ~128-bit PQ floor when passed to `KeypairEncryptionAdapter.fromPassword()`. ::: ### `MnemonicEncryptionAdapter` BIP-39 mnemonic or hex seed. Derives a KEK via HKDF from a BIP-39 seed phrase or a raw 64-byte hex seed. Requires a 24-word mnemonic — 12-word phrases are rejected at construction because they provide only 64-bit post-quantum security (below NIST's 128-bit minimum). With 24 words the adapter is unconditionally post-quantum safe. The raw seed bytes are imported into a non-extractable `CryptoKey` and zeroed from the JS heap immediately — they are never readable from JavaScript after construction. Like `PasswordEncryptionAdapter`, derivation is deterministic: the same mnemonic always recovers the same key on any device. Mnemonic validation checks the full BIP-39 checksum, not just that each word is in the wordlist — a phrase with a single-word typo that still happens to land on another real wordlist word is rejected rather than silently deriving a different, wrong key. ```typescript new MnemonicEncryptionAdapter({ mnemonic: "word1 word2 … word24" }); // or from a hex seed: new MnemonicEncryptionAdapter({ seedHex: "a1b2c3…" /* 64 bytes / 128 hex chars */, }); ``` ### `KeypairEncryptionAdapter` ML-KEM768 asymmetric wrapping. Uses CRYSTALS-Kyber (ML-KEM, NIST FIPS 203) for asymmetric DEK encapsulation. The default variant is `mlkem768` (192-bit post-quantum security); `mlkem512` (128-bit) and `mlkem1024` (256-bit) are also available. The asymmetric design enables upload-only parties: an uploader who holds only the public key can wrap a DEK, but cannot unwrap it — decryption requires the secret key. This is useful for delegated upload scenarios where the client should not be able to read back what it stored. ```typescript // Generate a fresh keypair: const { adapter, secretKey } = KeypairEncryptionAdapter.generateKeypair("myapp:v1"); // adapter holds PK+SK — use for full read/write access // share adapter.publicKey with upload-only parties: const uploaderOnly = KeypairEncryptionAdapter.fromPublicKey( adapter.publicKey, "myapp:v1", ); ``` Key sizes for `mlkem768`: public key 1184 bytes, secret key 2400 bytes, KEM ciphertext 1088 bytes. A keypair can also be derived deterministically from a password instead of generated randomly, the same way `PasswordEncryptionAdapter` derives a symmetric KEK — same `password` + `salt` always recover the same ML-KEM keypair on any device, so there's no secret key to back up separately, only the password and salt: ```typescript const adapter = await KeypairEncryptionAdapter.fromPassword( "s3cr3t!", "myapp:user-42", // salt "myapp:kek:v1", // context — domain-separation string bound into the KEK derivation ); ``` `KeypairEncryptionAdapter.fromMnemonic()` works the same way for a 24-word BIP-39 mnemonic or hex seed instead of a password: ```typescript const adapter = await KeypairEncryptionAdapter.fromMnemonic( { mnemonic: "word1 word2 … word24" }, "myapp:kek:v1", // context — domain-separation string bound into the KEK derivation ); // or from a hex seed: const sameAdapter = await KeypairEncryptionAdapter.fromMnemonic( { seedHex: "a1b2c3…" /* 32 or 64 bytes */ }, "myapp:kek:v1", ); ``` ### `WebAuthnPrfEncryptionAdapter` Passkey-based encryption via the WebAuthn PRF extension. Instead of a password or mnemonic, key material is derived from a platform passkey's PRF output — so there's nothing for the user to remember or type. Internally, it derives a seed from your passkey and uses it to build two things: a `MnemonicEncryptionAdapter` that locks and unlocks your uploads, and a fixed `KeypairEncryptionAdapter` whose public key you can safely share. Anyone holding that shared public key can encrypt something for you without ever touching your passkey — only you can unlock it, and only by using the passkey again. Two async static factories cover the two points where a user interacts with their passkey: ```typescript // First visit — registers a new passkey const adapter = await WebAuthnPrfEncryptionAdapter.register(); localStorage.setItem( "credentialId", btoa(String.fromCharCode(...adapter.credentialId)), ); // Returning visits — re-authenticate with the stored credential const stored = localStorage.getItem("credentialId")!; const adapter = await WebAuthnPrfEncryptionAdapter.authenticate([ Uint8Array.from(atob(stored), (c) => c.charCodeAt(0)), ]); ``` The adapter doesn't persist `credentialId` for you — your app is responsible for saving it (e.g. `localStorage`) and passing it back into `authenticate()` on later sessions, the same way it's responsible for `contractAddress` after `init()`. For upload-only delegation without a WebAuthn ceremony — e.g. a Node.js backend encrypting content on behalf of a browser-registered passkey owner — use the shared public key directly: ```typescript const uploaderOnly = WebAuthnPrfEncryptionAdapter.fromPublicKey(adapter.publicKey); ``` `fromPublicKey()` also accepts an optional second argument, `{ shareContext, shareVariant }`, needed only if the owner customized those away from their defaults (see below) — it must match whatever the owner's `register()`/`authenticate()` call used. Check support before calling `register()`, since PRF isn't available everywhere: ```typescript if (!(await WebAuthnPrfEncryptionAdapter.isSupported())) { // fall back to PasswordEncryptionAdapter, MnemonicEncryptionAdapter, etc. } ``` Browser/authenticator support: WebAuthn PRF extension support in the browser, or a CTAP2 hardware key with `hmac-secret` support (e.g. a YubiKey 5). Call `isSupported()` first to check the current browser — it returns `false` rather than throwing where the PRF extension isn't available. For a flow with nothing to persist client-side at all, register with a resident (discoverable) credential and skip storing `credentialId`: ```typescript // Register with a resident (discoverable) credential const adapter = await WebAuthnPrfEncryptionAdapter.register({ residentKey: "required", }); // Later — no stored credentialId needed; the browser's native picker // shows any resident passkey for this origin const adapter = await WebAuthnPrfEncryptionAdapter.authenticate([], { discoverable: true, }); ``` `discoverable: true` omits `allowCredentials` from the WebAuthn call entirely, so the browser can present its native picker across any resident passkey instead of requiring a `credentialId` you supplied. It only works for a credential registered with `residentKey: "required"` (or `"preferred"`, depending on the authenticator) — the `credentialId`-based flow above remains the default. `register()`/`authenticate()` also accept a few advanced options for interoperating with a pre-existing, non-SDK passkey-encryption scheme — all default to the SDK's own current behavior, so they're only needed if you're matching an external derivation: `seedDerivation` (`"hkdf-expand"` default, or `"raw"` to use the PRF output directly as the seed) and `seedExpandInfo` (overrides the HKDF info label) control how the seed is derived; `shareContext` and `shareVariant` customize the internally-derived ML-KEM sharing keypair behind `.publicKey`/`fromPublicKey()` away from the SDK's fixed defaults. ::: warning `register()` and `authenticate()` let the underlying `DOMException` (user cancellation, no matching credential, etc.) bubble up unwrapped rather than converting it to a `DStorageError` — catch `DOMException` directly around these calls. ::: --- --- url: https://dstorage.pro/docs/faq/managed-payments.md --- # Managed Payments & dStorage Pro ### What is dStorage Pro? dStorage Pro (`https://portal.dstorage.pro`) is a managed service that handles storage and chain payment signing on behalf of applications. Instead of requiring every end-user to hold AR tokens and manage an Arweave JWK wallet, the service signs and submits transactions server-side. Developers sign up for an API auth token and configure the SDK to point at the service URL — no client-side wallet management required. Delegating signing to a third-party service doesn't weaken the SDK's privacy model: encryption always happens on the user's device first, and the service only ever receives what it needs to construct and sign a transaction — never plaintext content, encryption keys, or (for Arweave uploads) the raw file bytes. See [What does the managed service see?](#what-does-the-managed-service-see) for the exact data sent per network. ### What does the managed service see? Only a cryptographic hash of the content and the request metadata needed to construct the transaction. It never receives the plaintext payload or encryption keys. Encryption happens entirely on the user's device before the SDK makes any network call. ### What is the dStorage Pro portal? The dStorage Pro portal (`https://portal.dstorage.pro`) is the web dashboard where customers manage the account behind the managed payment service — it's the human-facing counterpart to the `managedPayment` config (`signingServerUrl` + `authToken`) the SDK talks to. Log in with Google, Lace, MetaMask (SIWE), or the 1AM wallet extension and a customer account lets you: * **Fund a balance** via the Liquidity Manager, using a card, bank transfer, local payment methods, or a coupon — this prepaid balance is what the managed service deducts from on every signed transaction. Some free DUST is also credited to customers for Midnight fee sponsorship. [x402](#will-dstorage-pro-support-funding-accounts-via-x402) support for funding is planned. * **Create and manage API Tokens** — secret `ds_*` tokens for server-side use. * **Create and manage JWT Tokens** — scoped, revocable, ES256-signed tokens safe to embed in browser code. * **Monitor usage** on the Dashboard (request volume, success rate, liquidity consumed, network breakdown) and the Transaction History feed (service calls and payments, filterable, with status per entry). ### Will dStorage Pro support funding accounts via x402? Yes — x402 support is planned as an additional way to fund a dStorage Pro account balance. [x402](https://www.x402.org/) is an open, HTTP-native micropayment protocol (built around the HTTP 402 "Payment Required" status code) that lets a client pay for a resource or top up a balance with a stablecoin transfer as part of the HTTP request/response flow, without a traditional checkout page. See the [x402 specification](https://github.com/coinbase/x402) for the protocol details. This isn't available yet — see [What is the dStorage Pro portal?](#what-is-the-dstorage-pro-portal) for the funding methods supported today. ### How do I configure the managed payment flow? Pass a `managedPayment` object — `{ signingServerUrl, authToken }` — to any adapter that supports managed signing. On the storage side that's `ArweaveStorageAdapter`, `ArweaveBundlerStorageAdapter`, and `ArweaveLocalStorageAdapter`; on the chain side, `MidnightChainAdapter`. The Mock and Simulator adapters accept it too, for testing (see [How do I test the managed payment code path without spending real funds?](#how-do-i-test-the-managed-payment-code-path-without-spending-real-funds)). ```typescript import { DStorage, ArweaveBundlerStorageAdapter, MidnightChainAdapter, } from "@dstorage-tech/dstorage-sdk"; const managedPayment = { signingServerUrl: process.env.DSTORAGE_SERVICE_URL ?? "https://portal.dstorage.pro", authToken: process.env.DSTORAGE_AUTH_TOKEN ?? "", }; const sdk = new DStorage({ storageAdapters: [new ArweaveBundlerStorageAdapter({ managedPayment })], chainAdapters: [ new MidnightChainAdapter({ // … chain config … managedPayment, }), ], encryptionAdapters: [ /* … */ ], }); ``` The same `managedPayment` object can be shared by the storage and chain adapters. It also accepts an optional `requestTimeoutMs` (default 30 s, `0` disables it). Prefer environment variables over hard-coding credentials. ### Where do I get an auth token? Sign up at [portal.dstorage.pro](https://portal.dstorage.pro) and obtain a **secret `ds_*` API token** via the portal UI (API Tokens tab) or `POST /api/tokens`. This is the standard credential, meant for server-side use only. Tokens use a compound format: ``` ds_. ``` `` is the authentication secret sent as the `Authorization: Bearer` header. `` is the 512-byte RSA-4096 public key modulus of the server's Arweave signing key, base64url-encoded (~683 chars). The adapter parses and pins this key at construction time — if the server rotates its signing key the token must be re-issued to reflect the new modulus. ```typescript new ArweaveBundlerStorageAdapter({ managedPayment: { signingServerUrl: "https://portal.dstorage.pro", authToken: process.env.DSTORAGE_AUTH_TOKEN ?? "", }, }); ``` `ds_*` tokens carry **full account access** (manage other tokens, payment history, coupons). Treat them like private keys, and always supply them via an environment variable — never hard-code one. Tokens expire after 90 days by default; re-issue before expiry in the portal. If you need to call the managed service directly from browser JavaScript, don't use a `ds_*` token — use a JWT token instead, see [What is a JWT token and why does the service support it?](#what-is-a-jwt-token-and-why-does-the-service-support-it) below. dStorage Pro is live and open to everyone: claim your Pro Pass at [portal.dstorage.pro](https://portal.dstorage.pro) by signing in with Google, Lace, MetaMask, or 1AM, top up your balance, then generate a token from the dashboard. x402 funding is planned as well. Follow [@dStorageTech on X](https://x.com/dStorageTech) for updates. ### How do I set the credentials via environment variables? ```sh export DSTORAGE_SERVICE_URL=https://portal.dstorage.pro export DSTORAGE_AUTH_TOKEN=ds_your_token_here ``` Or add them to a `.env` file in your project root (loaded via `dotenv` or equivalent). The SDK does not load `.env` files automatically — your application loader must do that. ### What does the managed Midnight chain payment flow look like? DUST chain fees are sponsored by the managed service too — not just Arweave storage. When `MidnightChainAdapter` is configured with `managedPayment`, the SDK wraps the wallet's transaction-balancing step so the signing server balances *and* signs the transaction on the wallet's behalf. The local wallet only needs to expose its public keys (for the ZK circuit) — it never needs to hold DUST. This is automatic: no configuration beyond `managedPayment` on `MidnightChainAdapter` is needed. See the [Managed Payments on Midnight](/guide/managed-payments-midnight) guide for a walkthrough. ### Is there a known security limitation with the managed payment response? The signing server response (`txId`, `signature`) is validated for schema correctness but carries no cryptographic MAC. The adapter relies on TLS for transport integrity. A compromised CDN or network-level MITM could substitute a valid-shaped response and schema validation would still pass. The signing key substitution attack (where a MITM replaces the server's public key to corrupt the on-chain ownership record) is mitigated: the expected key is embedded in the auth token and pinned at construction time. Each sign-tx request asserts the pinned key; the server rejects any mismatch. ::: warning Mitigation for the response MAC gap Always use a `managedPayment.signingServerUrl` with HTTPS and a valid certificate issued to the operator-controlled domain. Never use this adapter over plain HTTP or with self-signed certificates in production. A proper fix (an `HMAC-SHA256` response MAC) is deferred pending a stable signing-server API. ::: ### What payment tokens does the managed service handle? The managed service covers Arweave storage costs billed in AR, and — when `MidnightChainAdapter` is configured with `managedPayment` — Midnight chain fees billed in DUST too. In that case the wallet configured in `MidnightChainAdapter` never needs to hold DUST itself. ### How do I test the managed payment code path without spending real funds? dStorage Pro has a `TEST` sandbox: requests sent with the `"TEST"` network identifier are recorded as sandbox payments against your real account and token, without spending real funds. This exercises the full managed payment request/response round-trip — including auth token pinning and error handling. There are three ways to use it. **`ArweaveLocalStorageAdapter` with `testMode: true`** — set `managedPayment.testMode: true` to send each upload's payment to the `TEST` sandbox while the upload itself still goes to your local arlocal instance (the transaction is signed locally with the test wallet so arlocal accepts it). `testMode` is only supported by `ArweaveLocalStorageAdapter` — `ArweaveStorageAdapter` and `ArweaveBundlerStorageAdapter` reject it at construction. The [Managed Payments Service](/guide/managed-payments-service) guide walks through this setup: ```typescript const managedPayment = { signingServerUrl: "https://portal.dstorage.pro", authToken: process.env.DSTORAGE_AUTH_TOKEN ?? "", testMode: true, }; const { adapter: storageAdapter } = await ArweaveLocalStorageAdapter.createWithTestWallet({ managedPayment }); ``` **Mock and Simulator adapters** — `MockStorageAdapter`, `MockChainAdapter`, and `MidnightSimulatorChainAdapter` always use the `managedmock` payment network when given `managedPayment`, which sends `"TEST"` to the server instead of a real network name — no `testMode` flag needed. `MockChainAdapter` is the lighter-weight option — an in-memory chain simulation with no real circuit logic. Use `MidnightSimulatorChainAdapter` instead when you also want to exercise the real DataRegistry Compact circuits (ownership checks, ref-id derivation) locally. ```typescript const managedPayment = { signingServerUrl: "https://portal.dstorage.pro", authToken: process.env.DSTORAGE_AUTH_TOKEN ?? "", }; const sdk = new DStorage({ storageAdapters: [new MockStorageAdapter({ managedPayment })], chainAdapters: [new MidnightSimulatorChainAdapter({ managedPayment })], encryptionAdapters: [ /* … */ ], }); ``` **`TEST` tokens on a real Midnight network** — `TEST` tokens are regularly shared on our socials ([X](https://x.com/dStorageTech), [Reddit](https://www.reddit.com/r/dStorage), [LinkedIn](https://www.linkedin.com/company/dstorage-tech)). Once claimed, dStorage Pro sponsors your DUST fees for free, so the real `MidnightChainAdapter` with `managedPayment` runs on Preprod without spending real funds — and your wallet needs no DUST or NIGHT. See the [Managed Payments on Midnight](/guide/managed-payments-midnight) guide. ### Can I use a `ds_*` token in browser-side JavaScript? No. A `ds_*` token grants full account control — anyone who can read your bundle can extract it and manage your entire account. Never ship a `ds_*` token in a browser bundle, React app, or any public-facing code. For frontends that need to call the managed service directly, use a **JWT token** instead (see below). ### What is a JWT token and why does the service support it? JWT tokens exist to solve one problem: `ds_*` tokens cannot safely leave a secure server environment, but some applications need to call the managed `sign-tx` endpoint directly from browser JavaScript — for example, a dApp that submits transactions on behalf of users without a backend proxy. The portal issues ES256-signed JWT tokens that are specifically designed to be public: * They are accepted **only** on `POST /api/service/sign-tx`. No other endpoint will honour them — they cannot list tokens, access payment history, redeem coupons, or do anything else. * Their constraints (allowed origin, spend cap, request cap) are **baked into the cryptographic signature**. They cannot be forged or modified without invalidating the JWT. * Each JWT has a unique `jti` identifier checked against the database on every request, so revocation is instant — delete the token in the portal and it stops working immediately. This gives you a credential that is safe to embed in a browser bundle because the blast radius of a leak is mathematically bounded. ### What do JWT tokens provide that a `ds_*` token does not? | Capability | `ds_*` API token | JWT token | | ----------------------------------------- | ------------------ | ---------------------------- | | Call `sign-tx` | Yes | Yes | | Origin restriction (lock to one domain) | No | Optional — `origin` claim | | Per-token spend cap | No | `maxBalance` (USD) | | Per-token request cap | No | `maxRequests` | | Safe to embed in browser JavaScript | No | Yes | | Account management endpoints | Yes | No | Create a JWT token in the portal (JWT Tokens tab) or via `POST /api/jwt-tokens` using a `ds_*` token. Required fields: `name`, `expiresAt`, `maxBalance`. The `origin` and `maxRequests` constraints are optional but recommended for public deployments. Use a JWT token in the SDK exactly the same way as a `ds_*` token — `managedPayment.authToken` accepts either format: ```typescript new ArweaveBundlerStorageAdapter({ managedPayment: { signingServerUrl: "https://portal.dstorage.pro", authToken: process.env.DSTORAGE_JWT_TOKEN ?? "", }, }); ``` Lock a JWT to your production domain to limit exposure if the token is scraped: ``` origin: "https://myapp.com" maxBalance: 50 // stop after $50 of Arweave storage charged to your account maxRequests: 10000 // or after 10 000 sign-tx calls, whichever comes first ``` If a JWT is compromised, an attacker can only call `sign-tx` within those configured bounds on that one origin — and you can revoke it instantly from the dStorage Pro portal. --- --- url: https://dstorage.pro/docs/faq/troubleshooting.md --- # Troubleshooting ### `Failed to parse result provided by node` / `instanceof BN` failure `bn.js@5.2.3` appears more than once in `node_modules` — `instanceof BN` fails across module boundaries. Fix: pin `bn.js@5.2.3` in root `package.json` `dependencies` and delete any nested copies under `node_modules`. Run `npm install` afterwards. ### Wallet extension not detected in browser connector mode Confirm a Midnight wallet extension (1AM, Lace, or another wallet implementing the dApp Connector API) is installed and enabled in your browser. The connector flow enumerates `window.midnight` and calls `connect()` on the discovered wallet — if no extension is installed or enabled, this call rejects. Reload the page after installing/enabling the extension. ### `MIDNIGHT_INVALID_PROVER_CONFIG` after upgrading to v0.3.0 `MidnightChainAdapter`'s connector-mode proving options changed in v0.3.0. The constructor throws this error if the config still sets the removed `allowWalletProofServer` option, sets neither `proofServerEndpoint` nor a wallet `prover` mode, sets `proofServerEndpoint` together with a wallet `prover` mode, or passes `prover` in provider mode. See [Where are ZK proofs generated in connector mode?](/faq/adapters#where-are-zk-proofs-generated-in-connector-mode) for the valid combinations. ### `Proof server at … failed to …` (`MIDNIGHT_PROOF_SERVER_FAILED`) The adapter couldn't reach the proof server, or it returned an error. The message names the proof server URL and the reason (connection refused, timeout, or an HTTP error status). Check that the proof server is running — e.g. the `midnightntwrk/proof-server` Docker container from the [Midnight Network Adapter](/guide/midnight-network-adapter#step-1-—-start-the-midnight-proof-server) guide — and that `proofServerEndpoint` (or the wallet's configured proof server) points at it. ### `Wallet failed to …` / `Wallet did not provide a proving provider` (`MIDNIGHT_WALLET_PROVING_FAILED`) With `prover: PROVER_MODE.WALLET_DELEGATED`, the wallet extension failed to generate the proof, didn't provide a proving provider, or the user rejected its prompt. Check that the wallet supports delegated proving and can fetch the ZK artifacts from `zkConfigBaseUrl`, or switch back to `PROVER_MODE.PROOF_SERVER_URL` with your own proof server. --- --- url: https://dstorage.pro/docs/features.md --- # Features A complete list of what dStorage offers, split into three groups: capabilities you get as a developer integrating the SDK, guarantees your end-users get as a result, and what changes when you route payments through dStorage Pro's managed service. ## Developer Features The SDK is a set of independently swappable adapters (storage, chain, encryption, and payment) wired together behind one `store()` / `retrieve()` call pattern — pick the adapters that fit your environment without changing your application code. ### Architecture & Core Capabilities * Pluggable adapter architecture — storage, chain, encryption, and payment are each configured independently and can be swapped without touching call sites. See [Core Concepts](/guide/core-concepts). * Multiple storage/chain adapters with automatic fallback — configure `storageAdapters`/`chainAdapters` as ordered lists; reads and writes fall back to the next candidate on failure. See [Adapters](/faq/adapters#how-do-multiple-storage-chain-adapters-and-fallback-work). * No custom on-chain contract to write or deploy — the DataRegistry contract ships pre-compiled with the SDK, so integrating dStorage doesn't require Compact expertise. * `estimateCost()` — a pre-upload cost estimate covering both the storage and chain side, before you commit to a `store()` call. * Storage-only mode — omit `chainAdapters` (or pass `[]`) to skip on-chain references entirely. * Works in both Node.js and the browser, with a dedicated browser entry point. * TypeScript-first, with strict typing throughout. ### Encryption & Recovery * Four encryption adapters: `PasswordEncryptionAdapter`, `MnemonicEncryptionAdapter` (BIP-39), `KeypairEncryptionAdapter` (ML-KEM768, post-quantum), and `WebAuthnPrfEncryptionAdapter` (WebAuthn passkeys). See [FAQ: Encryption & Security](/faq/encryption-security#encryption-security). * Multi-key encryption — register multiple encryption adapters (e.g. a password plus a recovery mnemonic); any one can independently decrypt. * Key rotation (`rotateKeys()`) — add or remove encryption adapters without re-uploading content. * `generateHighEntropyPassword()` generates a machine-random, 256-bit password. Combined with `KeypairEncryptionAdapter`, it gives 128-bit (NIST-minimum) post-quantum protection at the key-encapsulation layer, while the password itself stays human-recoverable. * Upgrade a password or mnemonic to a keypair adapter in place with `intoKeypairEncryptionAdapter()` (or `KeypairEncryptionAdapter.fromPassword()`), without changing how existing content was encrypted. ### Data Integrity & Resilience * Automatic large-file chunking — files over 10 MB are transparently split into independently encrypted chunks. See [FAQ: Concepts & Features](/faq/concepts#does-dstorage-support-large-files). * Explicit public-mode opt-in (`isPublic: true`) for world-readable content — never the default. * Tamper detection on every `retrieve()` call: the SDK recomputes the BLAKE3 hash of the retrieved content and compares it against the hash recorded on-chain — not one reported by the storage gateway itself — so it catches tampering even by a gateway that controls both the returned bytes and its own metadata. * AAD-bound ciphertexts — payloads, manifests, chunks, and storage pointers each carry additional authenticated data (AAD) tying the ciphertext to its specific role, preventing one from being substituted for another. * Partial-failure recovery — `StorePartialError` and `onProgress` recovery payloads let you retry just the on-chain write if it fails after a successful upload. * In-place content updates (`update()`) that preserve the `refId`. * `listReferences()` and `removeReference()`, the latter backed by a ZK ownership proof. ### Adapters for Every Environment * Fully in-memory Mock adapters — no network, Docker, or tokens required for local development. * `MidnightSimulatorChainAdapter` — real DataRegistry circuit behavior without a live network. See [Local & Simulator Adapters](/guide/local-simulator-adapters). * `ArweaveLocalStorageAdapter` with an auto-funded test wallet for local integration testing. * Production storage adapters — `ArweaveBundlerStorageAdapter` (near-instant finality via the ANS-104 bundler protocol) and `ArweaveStorageAdapter` (direct Arweave L1 submission) — plus `MidnightChainAdapter` for the real Midnight network. * Choice of where ZK proofs are generated in the browser — your own proof server, the wallet's configured proof server, or the wallet itself — via `MidnightChainAdapter`'s `prover` option. See [FAQ: Adapters](/faq/adapters#where-are-zk-proofs-generated-in-connector-mode). * `HttpGatewayChainAdapter` for delegating chain operations to any custom REST/gateway backend instead of talking to a chain directly. ## End-User Features None of this requires end-users to understand blockchains, hold tokens, or trust dStorage or any single storage/chain provider with their data. * Client-side encryption — data is encrypted on-device before it ever leaves. * Non-custodial — encryption keys are held only by the user; operators, backend servers, and storage/chain networks never see the user's data or plaintext. * A tamper-proof, verifiable on-chain ownership receipt for every upload. * Permanent, censorship-resistant decentralised storage. * Post-quantum protection options, guarding against "harvest now, decrypt later" threats. See [FAQ: Encryption & Security](/faq/encryption-security#encryption-security). * Cross-device recovery via password, mnemonic phrase, or keypair — no vendor account needed. See [FAQ: Adapters](/faq/adapters#adapters). * Passwordless unlock via WebAuthn passkeys — nothing to remember or type. See [Passkey Encryption](/guide/passkey-encryption). * Portable identity — the same credentials restore access to your data on any device. * Shared or delegated access, by registering more than one encryption adapter (e.g. a trusted recovery contact) — including upload-only delegation, where a party can encrypt content for you using your public key alone, without ever being able to decrypt it. * No vendor lock-in — the same data model works across multiple storage and chain providers. * Private by default — making data public requires an explicit, clearly-irreversible opt-in. ## Managed Payments Service [dStorage Pro](https://portal.dstorage.pro) is a managed service that signs Arweave and Midnight transactions on your app's behalf, so end-users never need their own AR wallet or a DUST-funded Midnight wallet. Configure it via a `managedPayment` config (`signingServerUrl` + `authToken`) on the storage and chain adapters — see the [Managed Payments Service](/guide/managed-payments-service) guide for a complete walkthrough. dStorage Pro is live — claim your Pro Pass at [portal.dstorage.pro](https://portal.dstorage.pro) to top up and get your API token. * Managed Arweave signing — no AR wallet or JWK key file needed client-side. * Two managed storage-signing options: near-instant finality via the ANS-104 bundler protocol (`ArweaveBundlerStorageAdapter`), or direct Arweave L1 submission (`ArweaveStorageAdapter`) for callers who prefer L1's 2–20 minute confirmation semantics. * Managed Midnight DUST-fee payment — no funded Midnight wallet needed either, with sponsorship in some cases. See [Managed Payments on Midnight](/guide/managed-payments-midnight). * Privacy-preserving signing — your file bytes never leave the client and are never sent to or seen by the signing server. Only a cryptographic hash of the content, plus the metadata needed to construct the transaction, is transmitted; the content itself and your encryption keys are never shared with dStorage Pro. * Two token types: `ds_*` secret tokens (full account access, server-side only) and scoped JWT tokens (browser-safe, with origin/spend/request caps and instant revocation). * Signing-key pinning — the auth token embeds the server's public key, so a key rotation or substitution is detected immediately. * A `TEST` sandbox for exercising the full managed-payment round trip without spending real funds — via `managedPayment.testMode: true` on `ArweaveLocalStorageAdapter`, or automatically with the Mock and Simulator adapters — still using your real dStorage Pro account and token. * Balance funding via card, bank transfer, local payment methods, or by redeeming a coupon code — with a DUST bonus auto-credited on deposit. * A per-account stats dashboard — total requests, success rate, balance spent, and network breakdown, with a 7-day request-activity chart and an all-time network-distribution chart. * Unified transaction and payment history — a paginated, filterable feed with a per-transaction breakdown of native network cost vs. service fee, plus live on-chain status tracking (submitted → confirming → confirmed/expired) for non-Midnight transactions. * An account-management REST API, separate from the SDK's signing endpoint, for automating balance/profile lookups, stats and history queries, and full CRUD on both secret and JWT API tokens — including per-JWT spend and request caps. See [Managed Payments FAQ](/faq/managed-payments#where-do-i-get-an-auth-token). --- --- url: https://dstorage.pro/docs/reference.md --- # Reference API reference docs are coming soon. In the meantime, the [guides](/guide/) walk through the full `init()` / `store()` / `retrieveByRefId()` call pattern with working code for every adapter combination.