Resources
Soulidity stores every content blob — soul.md, memory entries, skill versions, sprites, audio — on Walrus, a decentralized blob storage network on Sui. Access is gated by Seal, which uses threshold-encrypted key shares released only after on-chain approval. This page documents the Phase 2 unified content encryption model.
Content is encrypted client-side with a freshly generated 256-bit AES-GCM data encryption key (DEK). The DEK is threshold-encrypted by the Seal key-server network and can only be reconstructed by parties Seal approves on-chain.
blobId and a registered Blob object on Sui.Every slot keeps seal_encrypted = true. Public slots remain Seal-encrypted at rest — they use a separate public approval entry, not a bypass.
Phase 2 collapsed the per-channel document IDs (soul-seal, soul-memory, soul-skill) into a single deterministic format for every slot under SoulContent. The Move module verifies the exact byte layout with == (not >=) for every seal_approve_content_* entry — any off-by-one length is rejected.
"soul-content:" // 13-byte domain prefix
+ version_byte(1) // document-id schema version
+ kind_be(4) // KindRegistry kind id, big-endian u32
+ content_object_id(32) // SoulContent shared-object ID bytes
+ name_bytes(..) // canonical name bytes for this kind/slot
+ 0x00 // NUL terminator
+ version_index_be(8) // slot version index, big-endian u64
+ nonce(16) // per-encrypt random nonceThe TypeScript builder for this lives in web/lib/soulidity/content-document-id.ts. Always use it — hand-rolled clients risk byte-misalignment that the Move check will reject.
Four Seal-callable entries cover every read path. Each takes the document ID plus the typed-content references and either an owner-sender check, a SoulGrant object, an explicit public-mode check, or a per-(buyer, kind) paid-access lookup.
| Module | Function | Required slot read mode |
|---|---|---|
| content | seal_approve_content_owner | READ_OWNER (mandatory on every slot) |
| content | seal_approve_content_granted_agent | READ_GRANT (+ SoulGrant covering slot's scope_mask) |
| content | seal_approve_content_public | READ_PUBLIC (slot keeps Seal encryption) |
| paid_access | seal_approve_content_paid_access | READ_PAID (+ KindPaidEntry with sufficient scope) |
Slots cache read_mode_mask and grant_scope_maskat append time — the approval functions consult only those caches, never the registry. So a kind being later deprecated does not retroactively invalidate older slots' approvals.
Granted-agent approval requires the SoulGrant.scope_mask to cover the slot's cached grant_scope_mask. Each built-in kind has a single grant-scope bit:
The bit map is enforced by kind_registry::assert_valid_default_grant_scope at descriptor registration. Combined masks for a single kind are rejected. See Kind Registry.
The sidecar is a JSON object stored alongside each slot. It contains everything the client needs to decrypt — excluding the DEK itself, which lives inside the Seal ciphertext.
{
encryptedDek: string, // base64 — Seal ciphertext of DEK + contentHash
iv: string, // base64 — 12-byte AES-GCM IV
cipher: "AES-GCM-256",
fileName: string,
mimeType: string,
contentHash: string, // hex — SHA-256 of plaintext; bound inside encryptedDek
}The contentHash is bound inside the Seal-encrypted DEK envelope (DEK_BYTES || CONTENT_HASH_BYTES). After decryption the client verifies the decrypted hash matches the sidecar hash, and verifies the plaintext SHA-256 matches the same value — preventing substitution attacks.
Mint and append flows encrypt in the browser, ask the wallet to pay Walrus storage, then build sidecar objects after the on-chain TX exposes the final SoulContent object ID. Mirror APIs verify and store the sidecars; they never receive raw DEKs.
encryptedDek does not decrypt against the expected document ID at access time.GET /api/souls/[id]/content/[kind]/[name]/[versionIndex]/access for the exact slot. The route returns the sidecar, Walrus blob URL, Seal server config, and an approval policy (module + function + required object IDs). The legacy /api/souls/[id]/access route resolves only the canonical Soul document.SessionKey via SessionKey.create, sign the personal message with the viewer wallet.seal_approve_content_* entries).SealClient.decrypt with the sidecar encryptedDek — the Seal key servers verify the approval TX before releasing key shares.contentHash.