sui-seal
Use when implementing data encryption, access control, or secrets management on SUI using the Seal protocol. Triggers on threshold encryption, data privacy, token-gated content, encrypted storage, decryption policies, paywall, gated access, encrypted NFT metadata, private data sharing, or any scenario requiring on-chain access control for off-chain data. Also use when the user mentions Seal, pay-to-decrypt, "only NFT holders can see", or subscriber-only content on SUI.
Best use case
sui-seal is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
Use when implementing data encryption, access control, or secrets management on SUI using the Seal protocol. Triggers on threshold encryption, data privacy, token-gated content, encrypted storage, decryption policies, paywall, gated access, encrypted NFT metadata, private data sharing, or any scenario requiring on-chain access control for off-chain data. Also use when the user mentions Seal, pay-to-decrypt, "only NFT holders can see", or subscriber-only content on SUI.
Teams using sui-seal should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/sui-seal/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How sui-seal Compares
| Feature / Agent | sui-seal | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
Use when implementing data encryption, access control, or secrets management on SUI using the Seal protocol. Triggers on threshold encryption, data privacy, token-gated content, encrypted storage, decryption policies, paywall, gated access, encrypted NFT metadata, private data sharing, or any scenario requiring on-chain access control for off-chain data. Also use when the user mentions Seal, pay-to-decrypt, "only NFT holders can see", or subscriber-only content on SUI.
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
SKILL.md Source
# SUI Seal — Decentralized Secrets Management
**On-chain access policies + threshold encryption + decentralized key servers.**
## SDK Versions
Targets: `@mysten/seal` 1.1.3 (^1.1), `@mysten/sui` 2.17.0 (^2.16). Tested: 2026-05-21.
**Compatibility notes:** `@mysten/sui` is a peer dependency of `@mysten/seal`. The `suiClient` must be a v2.x `SuiGrpcClient` (from `@mysten/sui/grpc`) or `SuiJsonRpcClient` (from `@mysten/sui/jsonRpc`) — these satisfy `SealCompatibleClient`. Do not mix `@mysten/sui@1.x` and `@2.x` in the same install — run `npm ls @mysten/sui` before adding seal/walrus/dapp-kit. Seal is NOT a `$extend()` client extension; always instantiate `new SealClient({ ... })` directly.
## What Seal Does
1. **Encrypt** data client-side using Seal SDK
2. **Define access policies** in Move smart contracts via `seal_approve*` entry functions
3. **Threshold decrypt** — key servers release key shares only when the on-chain policy approves the supplied PTB
4. **Storage agnostic** — encrypted blobs can live on Walrus, IPFS, S3, or anywhere
Security:
- Privacy holds as long as fewer than `t` of `n` key servers are compromised
- Liveness holds as long as at least `t` key servers are available
## Core Concepts
| Concept | Description |
|---|---|
| **Identity-Based Encryption (IBE)** | Data encrypted under an `id` derived from on-chain policy |
| **Threshold Key Servers** | Distributed key management — no single point of failure |
| **Session Keys** | Time-limited decryption credentials signed by the user (personal message) |
| **seal_approve PTB** | A built (not executed) PTB calling `seal_approve*` that key servers dry-run to authorize |
| **Access Policy (Move)** | Move module exposing `seal_approve*(id, ...)` entry functions |
## Usage Flow
```
1. App encrypts data with SealClient.encrypt({ packageId, id, threshold, data })
↓
2. Encrypted blob (Uint8Array) stored on Walrus / IPFS / DB
↓
3. User starts a session: SessionKey.create({ address, packageId, ttlMin, signer, suiClient })
↓
4. App builds a Transaction that calls `${packageId}::policy::seal_approve_*` with the id
↓
5. tx.build({ client: suiClient, onlyTransactionKind: true }) → txBytes
↓
6. sealClient.decrypt({ data, sessionKey, txBytes }) → plaintext
```
## TypeScript SDK
### Setup
```typescript
import { SuiGrpcClient } from '@mysten/sui/grpc';
import { SealClient } from '@mysten/seal';
const suiClient = new SuiGrpcClient({
network: 'testnet',
baseUrl: 'https://fullnode.testnet.sui.io:443',
});
// Each entry is a key server *object ID* on-chain — not a URL.
// weight controls how that server counts toward the threshold.
const sealClient = new SealClient({
suiClient,
serverConfigs: [
{ objectId: '0xKEYSERVER_OBJ_1', weight: 1 },
{ objectId: '0xKEYSERVER_OBJ_2', weight: 1 },
{ objectId: '0xKEYSERVER_OBJ_3', weight: 1 },
// optional fields per entry:
// apiKeyName, apiKey, aggregatorUrl (required for committee-mode servers)
],
verifyKeyServers: true,
timeout: 10_000,
});
```
### Encrypt
```typescript
// @check:skip
import { fromHex } from '@mysten/sui/utils';
const PACKAGE_ID = '0xYOUR_POLICY_PKG';
// `id` is the IBE identity. Convention: bytes that the on-chain
// seal_approve* function will validate (e.g. allowlist object id || nonce).
const id = fromHex('deadbeef'); // hex string of the identity bytes
const { encryptedObject, key } = await sealClient.encrypt({
threshold: 2, // 2-of-3 key servers
packageId: PACKAGE_ID,
id: '0xdeadbeef', // hex-encoded identity string
data: new TextEncoder().encode('secret content'),
// optional: aad, kemType, demType (DemType.AesGcm256 by default)
});
// `encryptedObject` is Uint8Array (BCS-serialized) — store wherever you want.
// `key` is the raw symmetric key — DO NOT share; only useful for backup/escrow.
const blobId = await uploadToWalrus(encryptedObject);
```
### Decrypt — full round-trip
```typescript
// @check:skip
import { SessionKey } from '@mysten/seal';
import { Transaction } from '@mysten/sui/transactions';
import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519';
// 1. Create a session key. The user signs a personal message under the hood
// (signer can be any Signer — Ed25519, EnokiSigner, PasskeyKeypair, etc.).
const keypair = Ed25519Keypair.generate();
const sessionKey = await SessionKey.create({
address: keypair.toSuiAddress(),
packageId: PACKAGE_ID,
ttlMin: 10, // minutes, NOT ms
signer: keypair,
suiClient,
});
// 2. Build (don't execute) the seal_approve PTB. The key servers will
// dry-run this transaction; if it succeeds, they release the share.
const tx = new Transaction();
tx.moveCall({
target: `${PACKAGE_ID}::policy::seal_approve_allowlist`,
arguments: [
tx.pure.vector('u8', Array.from(fromHex('deadbeef'))), // the id
tx.object('0xALLOWLIST_OBJ'),
// ...any other args your seal_approve_* function needs
],
});
const txBytes = await tx.build({
client: suiClient,
onlyTransactionKind: true, // REQUIRED — TransactionKind, not full tx
});
// 3. Fetch the encrypted blob and decrypt.
const encryptedBlob: Uint8Array = await fetchFromWalrus(blobId);
const plaintext = await sealClient.decrypt({
data: encryptedBlob,
sessionKey,
txBytes,
});
console.log(new TextDecoder().decode(plaintext));
```
### Batch decrypts (same session, many ids)
```typescript
// @check:skip
// Pre-fetch keys once, then decrypt many objects cheaply.
await sealClient.fetchKeys({
ids: ['0xdeadbeef', '0xcafebabe'],
txBytes, // a single PTB that calls seal_approve* for all ids
sessionKey,
threshold: 2,
});
for (const blob of blobs) {
const pt = await sealClient.decrypt({ data: blob, sessionKey, txBytes });
// ...
}
```
### Persisting a SessionKey (e.g. across page reloads)
```typescript
// @check:skip
const exported = sessionKey.export(); // ExportedSessionKey (JSON-safe)
localStorage.setItem('seal-sk', JSON.stringify(exported));
// Later:
const restored = SessionKey.import(
JSON.parse(localStorage.getItem('seal-sk')!),
suiClient,
keypair, // optional, only needed if you must re-sign
);
```
### Parsing an encrypted blob's metadata
```typescript
// @check:skip
import { EncryptedObject } from '@mysten/seal';
const meta = EncryptedObject.parse(encryptedBlob);
// → { version, packageId, id, services, threshold, ciphertext, ... }
```
## Move Access Policy
The on-chain side exposes `seal_approve*` entry functions. The function name **must** start with `seal_approve`. The first argument is always the IBE `id` as `vector<u8>`. The body should `abort` if access is denied — success means "release the key".
### Token-gated (NFT holder) example
```move
module example::token_gate {
use sui::object::{Self, UID, ID};
public struct GatePolicy has key {
id: UID,
required_collection: ID,
}
/// Seal key servers dry-run this. Aborts → no key. Returns → key released.
/// `id` is the IBE identity supplied by the decryptor (must match what was encrypted).
public fun seal_approve_holder(
id: vector<u8>,
policy: &GatePolicy,
nft: &SomeNFT, // caller passes their NFT
_ctx: &TxContext,
) {
assert!(some_nft::collection(nft) == policy.required_collection, 0);
// (optionally bind `id` to `object::id(nft)` so each NFT has its own key)
}
}
```
### Time-locked example
```move
module example::time_lock {
use sui::clock::Clock;
public struct TimeLockPolicy has key { id: UID, unlock_ms: u64 }
public fun seal_approve_after(
_id: vector<u8>,
policy: &TimeLockPolicy,
clock: &Clock,
) {
assert!(clock::timestamp_ms(clock) >= policy.unlock_ms, 0);
}
}
```
### Pay-to-decrypt (split: payment runs as a real tx, approval as dry-run)
```move
module example::paywall {
use sui::coin::{Self, Coin};
use sui::sui::SUI;
public struct Receipt has key, store { id: UID, owner: address, paid_for: vector<u8> }
/// Real transaction: user pays, gets a Receipt object.
public entry fun pay(price: u64, mut payment: Coin<SUI>, paid_for: vector<u8>, ctx: &mut TxContext) {
assert!(coin::value(&payment) >= price, 0);
// ...transfer payment, mint receipt...
}
/// seal_approve runs against the user's owned Receipt.
public fun seal_approve_with_receipt(
id: vector<u8>,
receipt: &Receipt,
ctx: &TxContext,
) {
assert!(receipt.owner == tx_context::sender(ctx), 0);
assert!(receipt.paid_for == id, 0);
}
}
```
## Common Use Cases
| Use Case | Policy Type |
|---|---|
| Premium content / paywall | Receipt-based `seal_approve` |
| NFT-gated community content | Holder check |
| Time-release announcements | Clock-based |
| Private DAO votes | Membership check |
| Encrypted NFT metadata | Owner-only |
## Best Practices
- Store encrypted blobs on Walrus for decentralized storage
- Keep `ttlMin` short (5–15 min); rotate session keys
- Test policies against testnet key servers before mainnet
- Always retry with at least `threshold` servers reachable
- Bind the IBE `id` to something on-chain (object ID, content hash) so re-using a key isn't possible
## Common Mistakes
**`sealClient.seal.encrypt(...)` / `client.extend(seal())` — neither exists.**
- Seal exports a `SealClient` class. There is no `.seal` namespace and no `$extend()` factory.
- Use `new SealClient({ suiClient, serverConfigs })`, call `sealClient.encrypt(...)` / `.decrypt(...)` directly.
**Calling `decrypt` without `txBytes`.**
- `txBytes` is mandatory. It must be a `TransactionKind` (`tx.build({ client, onlyTransactionKind: true })`) that calls a `seal_approve*` Move function for the given `id`. Key servers dry-run this PTB; if it aborts, the key is denied.
**`KeyServerConfig.url` — wrong field.**
- Servers are referenced by their on-chain object ID: `{ objectId: '0x…', weight: 1 }`. There is no `url` field. (For committee-mode servers, supply `aggregatorUrl`.)
## See Also
- [advanced-apis.md](references/advanced-apis.md) — Read when you need derived keys without full decrypt, session-key export/reuse across reloads, decoding `EncryptedObject` metadata, or enumerating key servers / public keys (seal ≥1.1.3)
## Resources
- [Seal Documentation](https://seal.mystenlabs.com/)
- [Seal SDK reference](https://sdk.mystenlabs.com/seal)
- [GitHub — MystenLabs/seal](https://github.com/MystenLabs/seal)Related Skills
sui-zklogin
Use when implementing zkLogin on SUI — OAuth login (Google, Facebook, Apple, Twitch) with zero-knowledge proofs for privacy-preserving authentication. Triggers on "zkLogin", "social login on SUI", "Google login", "OAuth", "ephemeral keypair", "JWT proof", or any authentication flow that derives a SUI address from an OAuth provider. Also use when the user mentions "login without wallet extension".
sui-walrus
Use when storing or retrieving files using Walrus — SUI's decentralized blob storage. Triggers on "Walrus", "blob storage", "upload file to chain", "decentralized storage", "store NFT image", "IPFS alternative on SUI", "where to store NFT metadata", "host a site on-chain", or any off-chain data storage needs on SUI. Also use for Walrus Sites (decentralized web hosting), storing game assets, media files, or when the user asks "where do I put large files on SUI".
sui-wallet
Use when performing on-chain transactions (transfer, Move call, publish) through the agent's CLI wallet via MCP tools. Triggers on "transfer SUI", "call Move function", "publish package", "wallet status", "sign transaction", or any agent-driven on-chain operation. This is for headless/backend wallet operations — for browser wallet UI (React/Vue), use sui-frontend instead.
sui-tester
Use when writing Move tests, setting up test suites, running gas benchmarks, or planning test strategy for SUI contracts. Triggers on "write tests", "test this module", "#[test]", "test coverage", "gas benchmark", "property-based test", or any Move testing task. Use even for simple "how do I test this function" questions.
sui-suins
Use when integrating SuiNS (SUI Name Service) — resolving .sui names to addresses, reverse lookups, or registering names. Triggers on "SuiNS", ".sui name", "name resolution", "reverse lookup", "human-readable address", or any name service integration. Also use when the user wants to display user-friendly names instead of hex addresses.
sui-security-guard
Use when setting up security scanning, detecting leaked secrets/API keys, implementing pre-commit hooks, or auditing a Sui Move contract for security/architecture/quality issues. Triggers on "security scan", "detect secrets", "pre-commit hook", "security audit setup", "API key leaked", and on contract-level review requests like "audit this contract", "review access control", "is this Move safe", "check for vulnerabilities", "Move security review" — these load the SEC/DES/PAT/TST/QA/CFG finding registry in references/move-security-findings.md. For offensive/adversarial testing (attack vector discovery, writing exploits/PoCs), use sui-red-team instead. For Move style/idiom quality (non-security), use move-code-quality.
sui-red-team
Use when performing adversarial security testing on SUI Move contracts — generating attack tests for access control bypass, integer overflow, object manipulation, economic exploits, reentrancy, and DoS vectors. Triggers on "red team", "attack test", "find vulnerabilities", "exploit", "pentest", "security test", or when the user wants to stress-test their contract's security. For defensive security setup (scanning, hooks, checklists), use sui-security-guard instead.
sui-passkey
Use when implementing WebAuthn passkeys or biometric authentication (Face ID, fingerprint, hardware keys) on SUI. Triggers on "passkey", "WebAuthn", "biometric login", "Face ID", "fingerprint auth", "FIDO2", or passwordless auth that uses device authenticators instead of seed phrases. Different from zkLogin (which uses OAuth providers).
sui-nautilus
Use when building verifiable off-chain computation, integrating external APIs with on-chain proof, or running trusted execution environments on SUI. Triggers on Nautilus, off-chain oracle, "verify API data on-chain", "connect external API to Move", "prove off-chain result", trusted compute, AWS Nitro Enclave, attestation, price feed, weather data on-chain, or any scenario requiring cryptographically verified external data. Also use when the user asks "how do I get real-world data into my SUI contract" or needs an oracle-like pattern.
sui-kiosk
Use when building NFT marketplaces, enforcing royalties, or managing transfer policies using SUI's Kiosk standard. Triggers on "Kiosk", "NFT marketplace", "transfer policy", "royalty enforcement", "list NFT for sale", "purchase rules", or any NFT commerce on SUI. Also use when the user asks about listing, delisting, or trading NFTs with enforced rules.
sui-install
Use when installing or updating the Sui CLI, managing CLI versions with suiup, or resolving environment/setup problems — "install sui", "update sui", "command not found", "sui not found", "client/server api version mismatch", build errors about "old dependencies", switching CLI versions per network, or installing toolchain components (Walrus, MVR, Move Analyzer, site-builder). Also use for first-time client setup, getting faucet tokens, recovering keys from a phrase, or "Cannot find gas coin for signer address". For deploying/upgrading packages use sui-deployer; for on-chain data queries use sui-ts-sdk.
sui-indexer
Use when building custom indexers, data pipelines, or event processors for the SUI blockchain. Triggers on "indexer", "data pipeline", "backfill", "event processor", "index transactions", "analytics dashboard", "aggregate on-chain data", "historical query", "track all trades", or any custom data extraction from SUI chain history. Also use when the user needs to build dashboards from on-chain data, process historical transactions, or set up real-time event streams.