install
source · Clone the upstream repo
git clone https://github.com/jpedrosa94/DeBOX
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/jpedrosa94/DeBOX "$T" && mkdir -p ~/.claude/skills && cp -r "$T/.claude/skill" ~/.claude/skills/jpedrosa94-debox-skill && rm -rf "$T"
manifest:
.claude/skill/skill.mdsafety · automated scan (low risk)
This is a pattern-based risk scan, not a security review. Our crawler flagged:
- references .env files
- references API keys
Always read a skill's source content before installing. Patterns alone don't mean the skill is malicious — but they warrant attention.
source content
Domain Expertise — Walrus Blob Storage Application
This document captures the full set of domain knowledge required to work on this application. It spans the Sui blockchain ecosystem, cryptographic protocols, decentralized storage, and the specific patterns and pitfalls discovered in this codebase.
1. Sui Blockchain Fundamentals
Object Model
- Sui uses an object-centric model (not account-based like Ethereum). Every on-chain entity is an object with a unique ID, version, and digest.
- Objects are owned (by an address or another object) or shared.
- Gas payments reference specific coin objects:
.{ objectId, version, digest }
Addresses
- Sui addresses are 32 bytes (64 hex characters), always prefixed with
.0x - Derived deterministically from the auth scheme — for zkLogin, the address depends on the OAuth provider, key claim (sub), salt, and audience (client ID).
Transactions
- Programmable Transaction Blocks (PTBs): Sui's native transaction format. A PTB contains
andinputs[]
, executed atomically.commands[]
class fromTransaction
is the builder.@mysten/sui/transactions
returns fulltx.build()
BCS bytes (includes gas, sender, expiration).TransactionData
returns just thetx.build({ onlyTransactionKind: true })
BCS bytes (no gas/sender envelope).TransactionKind
BCS (Binary Canonical Serialization)
- Sui's wire format. All on-chain data and transactions are BCS-encoded.
- Vectors are length-prefixed:
[ULEB128 length][element0][element1]... - Enums are tag-prefixed:
[variant_index][variant_data]
BCS:TransactionKind[0x00 = ProgrammableTransaction tag][ProgrammableTransaction struct]
BCS:TransactionData[0x00 = V1 tag][TransactionKind][sender][gasData][expiration]- Import
fromfromHex
to convert hex strings to byte arrays.@mysten/bcs - Import
frombcs
for parsing/serializing Sui-specific types.@mysten/sui/bcs
SuiClient
fromSuiJsonRpcClient
— current JSON-RPC client (replaces old@mysten/sui/jsonRpc
fromSuiClient
).@mysten/sui.js
returns the public testnet RPC URL.getJsonRpcFullnodeUrl("testnet")- Methods:
,getBalance()
,getObject()
, etc.dryRunTransactionBlock()
2. zkLogin (Zero-Knowledge Login)
Concept
- zkLogin maps an OAuth identity (e.g., Google
claim) to a deterministic Sui address — no seed phrase needed.sub - Uses a ZK proof to prove OAuth token ownership without revealing the JWT on-chain.
- Components: ephemeral keypair (short-lived, signs transactions), salt (links OAuth to address), ZK proof (proves JWT knowledge).
Address Derivation
whereaddress = hash(iss, aud_or_addressSeed)
.addressSeed = hash(salt, sub)- Different salt → different address for the same Google account. Enoki manages a consistent salt per user.
Prover Infrastructure
— uses devnet SNARK verification key. Proofs fail on testnet.prover-dev.mystenlabs.com
— uses testnet key but requires whitelisted Google client ID.prover.mystenlabs.com- Enoki — manages its own prover compatible with testnet. This is what this app uses.
Enoki (@mysten/enoki
)
@mysten/enoki
— manages the entire zkLogin lifecycle: OAuth redirect, salt retrieval, ZK proof generation, session management.EnokiFlow- Requires
(Enoki API key) and aapiKey
for persistence (this app usesstore
).localStorage
— generates Google OAuth redirect URL.enokiFlow.createAuthorizationURL()
— processes theenokiFlow.handleAuthCallback(hash)
fragment after OAuth redirect.#id_token=...
— returnsenokiFlow.$zkLoginState.get()
after successful auth.{ address }
— returns anenokiFlow.getKeypair({ network })
for signing.EnokiKeypair
is marked deprecated in favor ofEnokiFlow
, but is correct for non-dapp-kit integrations.registerEnokiWallets
Signer Compatibility
returnsEnokiKeypair.signPersonalMessage(bytes)
— must destructure to get plain base64 string.{ signature, bytes }
— same return shape.EnokiKeypair.signTransaction(bytes)- Seal's
expectsSessionKey
to return a plain base64 string, notsignPersonalMessage
.{ signature } - The
wrapper ingetZkLoginSigner()
handles this destructuring.useZkLogin.js
3. Seal Encryption (@mysten/seal
)
@mysten/sealArchitecture
- Identity-based encryption (IBE) built on Sui. Files are encrypted to an identity (in this app, a Sui address).
- Decryption requires threshold key shares from key servers — servers validate an on-chain policy (Move function) before releasing shares.
- Threshold scheme: t-of-n key servers must respond for decryption. This app uses 2-of-3.
SealClient
— initialized lazily.new SealClient({ suiClient, serverConfigs, verifyKeyServers })
: array ofserverConfigs
describing key servers.{ objectId, weight, aggregatorUrl? }
for testnet (setverifyKeyServers: false
for mainnet).true
— encryptsclient.encrypt({ threshold, packageId, id, data })
(Uint8Array) to identitydata
.id
— decrypts using session key + approval transaction.client.decrypt({ data, sessionKey, txBytes })
Key Servers (Testnet)
- Mysten #1:
— independent server, no0x73d05d62...
.aggregatorUrl - Mysten #2:
— independent server, no0xf5d14a81...
.aggregatorUrl - Mysten Committee:
— 3-of-5 nodes behind an aggregator, requires0xb012378c...
in config.aggregatorUrlaggregatorUrl: "https://seal-aggregator-testnet.mystenlabs.com"- SDK's
validates that committee servers haveretrieveKeyServers()
.aggregatorUrl
- NodeInfra (
) — removed because it sends0x5466b7...
(double wildcard), which browsers reject.Access-Control-Allow-Origin: *, *
SessionKey
— creates a session certificate.SessionKey.create({ address, packageId, ttlMin, suiClient })- Do NOT pass
—signer
would callSessionKey
which returns the ephemeral address, not the zkLogin address.signer.getPublicKey().toSuiAddress()
→ bytes to sign with zkLogin signer.sessionKey.getPersonalMessage()
→ attach the zkLogin signature.sessionKey.setPersonalMessageSignature(signature)
Seal Approval Transaction (Critical)
The decryption flow requires building a "dry-run" transaction that calls
seal_approve on the Move contract. Key servers validate this PTB to decide whether to release key shares.
Correct approach:
const tx = new Transaction(); tx.moveCall({ target: `${PACKAGE_ID}::identity_allowlist::seal_approve`, arguments: [tx.pure.vector("u8", fromHex(userAddress))], }); const txBytes = await tx.build({ client: suiClient, onlyTransactionKind: true }); // Pass txBytes directly to client.decrypt()
Why
is required:onlyTransactionKind: true
- Returns
BCS:TransactionKind[0x00 (PTx tag), ProgrammableTransaction...] - Seal SDK internally does
before sending to key serverstxBytes.slice(1) - After slice:
— correct[0x01 (inputs_count=1), inputs, commands...]
formatProgrammableTransaction
Why full
fails:TransactionData
returnstx.build()[0x00 (V1 tag), 0x00 (PTx kind), 0x01 (inputs), ...]- After SDK's
:slice(1)[0x00 (PTx kind), 0x01, ...] - Key server reads first byte as
instead ofinputs_count = 0
→ "Invalid PTB: Invalid BCS"1
What NOT to do:
- No
,setSender()
,setGasPrice()
,setGasBudget()
— none needed withsetGasPayment()
.onlyTransactionKind: true - No manual prefix/slice manipulation — pass
directly totxBytes
.client.decrypt()
4. Walrus Blob Storage
Concept
- Decentralized blob storage on Sui. Data is erasure-coded across storage nodes.
- Blobs are immutable and content-addressed by blob ID.
- Storage is paid for in epochs (time periods). This app stores for 5 epochs.
API
- Publisher (
):https://publisher.walrus-testnet.walrus.space
— upload blob, body is raw bytes, content-typePUT /v1/blobs?epochs=N
.application/octet-stream- Response:
or{ newlyCreated: { blobObject: { blobId } } }
.{ alreadyCertified: { blobId } }
- Aggregator (
):https://aggregator.walrus-testnet.walrus.space
— download blob bytes.GET /v1/blobs/{blobId}
Upload Flow (This App)
- Frontend optionally encrypts file bytes with Seal.
- Frontend sends encrypted/raw bytes to backend via
(multipart form).POST /api/upload - Backend
s bytes to Walrus publisher, extracts blob ID from response.PUT - Backend returns blob ID + metadata to frontend.
- Frontend saves file entry to backend index via
.POST /api/files/{address}
Download Flow
- Frontend requests blob via
(proxied through backend).GET /api/blob/{blobId} - Backend fetches from Walrus aggregator, streams back to frontend.
- If encrypted: frontend decrypts with Seal using zkLogin signer.
- Browser downloads the plaintext file.
5. Move Smart Contract (identity_allowlist
)
identity_allowlistPurpose
Provides the on-chain policy that Seal key servers evaluate during decryption. The
seal_approve function checks that the transaction sender matches the encryption identity.
Contract Code
module identity_allowlist::identity_allowlist { use sui::bcs; const ENotAuthorized: u64 = 0; public fun seal_approve(id: vector<u8>, ctx: &TxContext) { let sender_bytes = bcs::to_bytes(&ctx.sender()); assert!(id == sender_bytes, ENotAuthorized); } }
Key Points
parameter: raw 32 bytes of the owner's Sui address (set at encryption time).id
: the address of the user requesting decryption.ctx.sender()- If
, aborts withid != sender_bytes
— key servers refuse to release shares.ENotAuthorized - BCS encoding of a Sui address is just the raw 32 bytes (no length prefix for fixed-size arrays).
Deployment
- Published on Sui testnet at
.0x1d1bc0019d623cc5d1c0e67e3f024a531197378c3ea32d34a36fb2f49541ebe9 - Upgrade capability object exists — contract can be upgraded.
- To redeploy:
fromsui client publish --gas-budget 100000000
directory.move/
6. Go Backend Expertise
Design Principles
- stdlib only — no external Go packages. Uses
,net/http
,encoding/json
,sync
,io
.os - Go 1.22+ routing with method+pattern:
.mux.HandleFunc("POST /api/upload", handler) - Path parameters via
(Go 1.22 feature).r.PathValue("name")
Data Persistence
- JSON file at
— keyed by wallet address, values are arrays ofbackend/data/files.json
.FileEntry - Protected by
for concurrent access.sync.Mutex
/readDB()
handle file I/O + JSON marshal/unmarshal.writeDB()
CORS
- Custom
wraps the mux, setscorsHandler
.Access-Control-Allow-Origin: http://localhost:5173 - Handles
preflight with 204 No Content.OPTIONS - Allowed methods: GET, POST, DELETE, OPTIONS.
Walrus Proxy Pattern
- Upload: receives multipart form from frontend, re-sends raw bytes to Walrus publisher via PUT.
- Download: fetches from Walrus aggregator, streams response to frontend.
- Blob responses cached with
(blobs are immutable).Cache-Control: public, max-age=31536000, immutable
Walrus Response Parsing
- Two possible response shapes:
ornewlyCreated.blobObject.blobId
.alreadyCertified.blobId - Status values:
,"newly_created"
,"already_certified"
(for manual imports)."imported"
7. React Frontend Patterns
Component Architecture
- Functional components with hooks throughout. No class components.
- Custom hooks in
:hooks/
,useZkLogin
,useFiles
.useBalance - Single-file components in
.components/
extension for all React files (no TypeScript)..jsx
State Management
- Local state with
— no Redux, no Context (beyond what hooks provide).useState
hook manages all file operations and file list state.useFiles
hook manages auth session, signer, and login/logout.useZkLogin
polls SUI balance every 30 seconds.useBalance
File Operations
- Upload:
— optionally encrypts, uploads to Walrus, saves to index.useFiles.uploadFile(file, encrypt) - Download:
— fetches blob, optionally decrypts, triggers browser download.useFiles.downloadFile(file) - Send:
— decrypt → re-encrypt for recipient → upload → save.useFiles.sendFile(file, recipientAddress, onSuccess) - Import:
— add existing blob to user's index.useFiles.importFile(blobId, filename, isEncrypted) - Delete:
— removes from index (blob remains on Walrus — immutable).useFiles.deleteFile(file)
OAuth Flow
- User clicks login →
→ redirects to Google.useZkLogin.initLogin() - Google redirects back with
in URL hash.#id_token=...
detects hash on mount → callsApp.jsx
.handleCallback(hash)- Enoki processes JWT → generates ZK proof → derives Sui address.
- Session stored in state + localStorage.
8. Testing Expertise (Vitest)
Configuration
v4, config atvitest
.frontend/vitest.config.js
—globals: true
,describe
,it
,expect
available without import.vi
— no jsdom.environment: "node"
set in config'sVITE_SEAL_PACKAGE_ID
block for tests.env
Mocking Patterns
Constructor mocks must use regular functions (not arrows):
// CORRECT — `this` binds to the new instance vi.mock('@mysten/seal', () => ({ SealClient: vi.fn(function () { this.decrypt = mockDecrypt; this.encrypt = mockEncrypt; }), })); // WRONG — arrow function: `this` is undefined vi.mock('@mysten/seal', () => ({ SealClient: vi.fn(() => { /* this.decrypt won't work */ }), }));
Mocking
(callable + has methods):tx.pure
this.pure = Object.assign( vi.fn().mockReturnValue({ kind: 'Input', index: 0 }), { vector: mockPureVector } );
Hoisted constants for use inside
factories:vi.mock()
const { FAKE_TX_BYTES } = vi.hoisted(() => { const FAKE_TX_BYTES = new Uint8Array(10); return { FAKE_TX_BYTES }; });
Dynamic import after mocks are active:
let encryptFile, decryptFile; beforeAll(async () => { const svc = await import('../sealService.js'); encryptFile = svc.encryptFile; decryptFile = svc.decryptFile; });
Test Categories
- Pure unit tests: byte-level validation with fake data, no network.
- Mock integration tests: full function calls with all dependencies mocked.
- Real SDK integration tests: actual
calls (skipped in CI viatx.build()
).process.env.CI
9. Common Pitfalls & Gotchas
Seal
is mandatory. Full TransactionData → "Invalid PTB: Invalid BCS".onlyTransactionKind: true- Do not pass
tosigner
— it would use the ephemeral address.SessionKey.create()
return value: Enoki returnssignPersonalMessage
, Seal expects plain string.{ signature, bytes }- Key server CORS: NodeInfra sends double wildcard
— browsers block it. Only use Mysten servers.*, * - Committee server needs
— omitting it causes SDK validation error inaggregatorUrl
.retrieveKeyServers() - Old encrypted files: files encrypted under [Mysten1, Mysten2, NodeInfra] can only decrypt with Mysten1+Mysten2 (threshold 2, only 2 of original 3 reachable).
zkLogin
- Salt determines address: changing salt (or switching from manual to Enoki) changes the derived address. Files uploaded under old address won't appear.
- Prover compatibility: devnet prover proofs fail on testnet. Enoki handles this correctly.
- JWT profile extraction: must handle base64url → base64 conversion (
→-
,+
→_
)./
Walrus
- Blobs are immutable: delete only removes from local index, not from Walrus.
- Response shapes differ:
vsnewlyCreated
— must handle both.alreadyCertified - Epochs: storage duration is epoch-based, not time-based. Currently set to 5.
Go Backend
- Mutex scope:
+readDB
must be inside the samewriteDB
section for atomic read-modify-write.mu.Lock() - CORS origin hardcoded:
— must change for production.http://localhost:5173 - No auth on backend: anyone can read/write file entries if they know the address. Security relies on Seal encryption.
Testing
- Constructor mocks: must use
, never arrow functions.vi.fn(function() {...})
dual nature: must be both callable and havetx.pure
method — use.vector()
.Object.assign- Module import timing:
must be imported dynamically aftersealService.js
calls.vi.mock()
10. SDK Version Awareness
Current Packages
| Package | Version | Notes |
|---|---|---|
| ^2.7.0 | JSON-RPC client, Transaction builder, BCS |
| ^1.1.0 | Encryption/decryption, SessionKey, SealClient |
| ^1.0.4 | zkLogin flow management |
| (peer) | , utilities |
Import Paths (Current SDK)
import { SuiJsonRpcClient, getJsonRpcFullnodeUrl } from "@mysten/sui/jsonRpc"; import { Transaction } from "@mysten/sui/transactions"; import { bcs } from "@mysten/sui/bcs"; import { fromHex } from "@mysten/bcs"; import { SealClient, SessionKey } from "@mysten/seal"; import { EnokiFlow } from "@mysten/enoki";
Deprecated Patterns (Avoid)
fromSuiClient
→ use@mysten/sui.js
fromSuiJsonRpcClient
.@mysten/sui/jsonRpc
→ usenew TransactionBlock()
fromnew Transaction()
.@mysten/sui/transactions
for pure args → usebcs.vector(bcs.u8(), ...)
.tx.pure.vector("u8", bytes)
→ only for dapp-kit;registerEnokiWallets
is correct for this app's direct integration.EnokiFlow
11. Security Model
Threat Model
- Backend is untrusted for confidentiality: it only stores metadata and proxies encrypted blobs. It never sees plaintext.
- Encryption at rest: Seal encrypts before upload. Key servers enforce access policy.
- Authentication: zkLogin proves Google identity → Sui address. No passwords stored.
- Authorization: Move contract (
) ensures only the owner's address can decrypt.seal_approve
Limitations
- Backend has no authentication — any client can call API endpoints.
- File index (JSON) has no integrity protection — a compromised backend could alter metadata.
- Seal key server availability: if 2+ servers are down, decryption fails (threshold not met).
- Walrus blobs persist beyond epoch expiry as long as storage nodes retain them — no guaranteed deletion.