Sealed Storage — SDK setup
Sealed Storage encrypts data client-side with Seal threshold encryption and stores the ciphertext on Walrus. A Seal key server releases its key share only if an on-chain policy (sealed_storage::<module>::seal_approve*) succeeds in a dry run with the requester as sender, so only addresses that satisfy the policy can decrypt.
@meddleware/sealed-storage-client is the client half:
- Policy providers — one per Move policy module. Each builds the Seal identity for new content and the approve call for decryption. Built in:
nft-gate(holders of an access-gate pass) andtime-lock(anyone, after a time). PolicyRegistry— the set of providers an app offers (createDefaultRegistry()registers both built-ins).SealController— thresholdencrypt/decryptover a key-server committee, and the per-address session key (one wallet signature, cached until it expires).
Install
npm install @meddleware/sealed-storage-client @mysten/seal @mysten/sui@mysten/seal and @mysten/sui are peer dependencies.
Create a controller
import { SuiGrpcClient } from '@mysten/sui/grpc'
import { createDefaultRegistry } from '@meddleware/sealed-storage-client'
import { SealController } from '@meddleware/sealed-storage-client/controller' // also on the main entry; the subpath lets apps lazy-load @mysten/seal
import { sealPoliciesDeployment } from '@meddleware/sealed-storage-client/deployments'
import { accessGateDeployment } from '@meddleware/access-gate-client/deployments'
const suiClient = new SuiGrpcClient({ network: 'testnet', baseUrl: 'https://fullnode.testnet.sui.io:443' })
// originalId: identities are bound to it (never changes); publishedAt: the latest version, the
// target of seal_approve calls; policyConfigId: the shared version gate every seal_approve reads.
// All three come from the package's recorded deployments.
const { originalId, publishedAt, policyConfigId } = sealPoliciesDeployment('testnet')
// The access_gate package that sealed_storage links: with it set, `encrypt('nft-gate', …)` reads the gate
// first and refuses one that is not a Gate of that package, or whose passes are transferable.
const accessGateOriginalId = accessGateDeployment('testnet').originalId
const registry = createDefaultRegistry(accessGateOriginalId)
const seal = new SealController(
{
suiClient,
originalId,
publishedAt,
policyConfigId,
accessGateOriginalId,
threshold: 2,
serverConfigs: [
// Mysten testnet committee (decentralised server, via the aggregator) + two independent servers
{ objectId: '0xb012378c9f3799fb5b1a7083da74a4069e3c3f1c93de0b27212a5799ce1e1e98', weight: 1,
aggregatorUrl: 'https://seal-aggregator-testnet.mystenlabs.com' },
{ objectId: '0x73d05d62c18d9374e3ea529e8e0ed6161da1a141a94d3f76ae3fe4e99356db75', weight: 1 },
{ objectId: '0xf5d14a81a982144ae441cd7d64b09027f116a468bd36e7eca494f750591623c8', weight: 1 },
],
},
registry,
)Seal to soulbound gates only
With accessGateOriginalId set, encrypt('nft-gate', …) refuses a gate that mints transferable passes (allowTransferableGates: true opts in). The holder of a transferable pass can freeze or share it, after which anyone can present it and the policy approves — the content is then effectively public. Seal to a soulbound gate, and leave accessGateOriginalId unset only if you check this yourself: without it the controller does not read the gate.
These are the committee defaults sealed-storage-ui uses on testnet. On mainnet the Mysten committee aggregator requires an API key; see the Seal docs.
The flow
- Encrypt —
seal.encrypt(policyType, params, bytes)returns{ id, ciphertext }.id(hex) is the Seal identity; it contains a random nonce, so it cannot be recomputed later. - Store — upload
ciphertextto Walrus and keep a manifest that points to it. - Decrypt —
seal.decrypt(policyType, params, id, ciphertext, { address, signPersonalMessage })builds the policy's approve transaction, obtains key shares from the committee and decrypts locally.
The manifest
SealedManifest is the interchange format between encrypting and decrypting apps. It contains no secrets and can be stored anywhere:
interface SealedManifest {
policyType: string // 'nft-gate' | 'time-lock' | …
id: string // Seal identity (hex, no 0x) — required verbatim to decrypt
blobId: string // Walrus blob id of the ciphertext
network: string // where the policy package + key servers live
params?: Record<string, unknown> // non-secret policy params, e.g. { gateId }
label?: string
}Validate untrusted manifests with parseSealedManifest(raw, registry) (throws SealedManifestError; the registry also validates the policy's params), then check network matches your app.
See the integration guide for end-to-end code and Writing policies for adding a policy.