Skip to content

Sealed Storage — Integration guide ​

Examples use the seal controller and registry from the SDK setup, a Walrus client walrus from createWalrusClient, and an executor exec that signs and executes transactions (in Vue: @meddleware/wallet-adapter).

Encrypt to an access gate and store on Walrus ​

ts
import { createBlobUploadFlow } from '@meddleware/walrus-storage-client'
import type { SealedManifest } from '@meddleware/sealed-storage-client'

// 1. Encrypt locally: anyone holding a valid pass for `gateId` will be able to decrypt. The
//    controller (SDK setup) refuses a gate that mints transferable passes: seal to a soulbound gate.
const { id, ciphertext } = await seal.encrypt('nft-gate', { gateId }, plaintextBytes)

// 2. Store the ciphertext (register → upload → certify; see Walrus Storage)
const flow = createBlobUploadFlow(walrus, ciphertext)
await flow.encode()
const reg = await exec.signAndExecute(flow.register({ epochs: 53, owner: address, deletable: false }))
await exec.waitForTransaction(reg.digest)
await flow.upload({ digest: reg.digest })
const cert = await exec.signAndExecute(flow.certify())
await exec.waitForTransaction(cert.digest)
const { blobId } = await flow.getBlob()

// 3. Keep the manifest — it is all a reader needs
const manifest: SealedManifest = { policyType: 'nft-gate', id, blobId, network: 'testnet', params: { gateId } }

Sealing to a gate whose passes are transferable is refused unless the controller was created with allowTransferableGates: true: a holder can freeze or share such a pass, and the content becomes effectively public. The check needs accessGateOriginalId in the controller config (see Create a controller).

A time lock instead: seal.encrypt('time-lock', { unlockMs: Date.parse('2027-01-01') }, bytes).

Decrypt ​

ts
import { parseSealedManifest, SealedManifestError } from '@meddleware/sealed-storage-client'
import { WALRUS_AGGREGATOR_HOSTS } from '@meddleware/walrus-storage-client'
import { readBlob } from '@meddleware/walrus-storage-client/http'

const m = parseSealedManifest(untrustedJson, registry) // `registry` from the SDK setup: it also validates the policy's params
if (m.network !== 'testnet') throw new Error('Manifest is for another network')

// Timeout, size cap (100 MiB by default) and the aggregator's strict consistency check.
const ciphertext = await readBlob(m.blobId, { aggregator: WALRUS_AGGREGATOR_HOSTS.testnet })

// nft-gate needs the reader's pass: its object id, and whether the gate is soulbound
const plaintext = await seal.decrypt(
  m.policyType,
  { ...m.params, nftId: passId, soulbound },
  m.id,
  ciphertext,
  { address, signPersonalMessage: (message) => wallet.signPersonalMessage({ message }) },
)

The first decrypt for an address asks the wallet for one personal-message signature (the Seal session key); later decrypts reuse it until it expires. Find the reader's pass with fetchAccessNfts(client, address, nftType, gateId) from @meddleware/access-gate-client.

Publish a discovery pointer (optional) ​

sealed_content::publish records an on-chain pointer (gate, blob, identity, label) so unlock UIs can list what a gate protects. It is not a policy and grants nothing:

ts
import { Transaction } from '@mysten/sui/transactions'
import { buildPublishSealedContentTx, sealedContentEventType } from '@meddleware/sealed-storage-client'

const tx = new Transaction()
// { publishedAt, policyConfigId } from sealPoliciesDeployment(network)
buildPublishSealedContentTx(tx, { publishedAt, policyConfigId }, { gateId, blobId, sealId: id, label: 'Chapter 1' })
await exec.signAndExecute(tx)

Anyone can publish a pointer under any gate with any label. When listing pointers (events of type sealedContentEventType(pkg)), show only those whose publisher is the gate's operator or a list you curate.

What access means ​

  • Membership, not consumption. Decrypting never spends a single-use pass; an exhausted (zero-use) pass cannot decrypt.
  • No revocation. A released key stays usable, and a decrypted file stays decrypted. Transferring a pass moves future access with it.
  • Pause. If the gate was created with the policy pause_blocks_decryption, a paused gate denies new decryptions (nft_gate abort 4) until it is unpaused — tell the user it is paused rather than that their pass is invalid. Otherwise pausing only stops sales.

Errors ​

WhereMeaningHandling
nft_gate abort 1identity not namespaced to this gatewrong manifest/gate pairing
nft_gate abort 2pass is for another gatepick the pass for params.gateId
nft_gate abort 3pass has no uses leftbuy another pass
nft_gate abort 4gate paused and its policy blocks decryptionretry after the operator unpauses
timelock abort 2before the unlock timeshow the unlock date
SealedManifestErrormalformed manifestreject the input
key-server / network errorscommittee unavailable or below thresholdretry; decryption fails closed

Codes repeat across modules — always key on (module, code) (PTB patterns).

2026 Meddleware. All jams preserved.