Skip to content

Spike: run @mysten/seal encrypt+decrypt inside a Cloudflare Worker #5

Open

Description

@harrymove-ctrl

Context

The Harbor HTTP API is plain fetch + multipart and is Worker-friendly, but @mysten/seal leans on WebCrypto + @noble curves + CPU-intensive BLS12-381. The Worker runs with compatibility_flags: ["nodejs_compat"] and compatibility_date: 2026-05-21 (apps/api/cloudflare/wrangler.jsonc:17), and @mysten/sui ^2.9.1 is already a dependency of apps/api (apps/api/package.json). Whether SEAL fits the Worker's compressed-bundle cap and CPU limit is the single highest-risk unknown gating the entire private-memory workstream (roadmap "The key runtime unknown (must spike early)" + §1 spike row).

Goal / user story

As an engineer, I want a throwaway Worker that imports @mysten/seal, encrypts one blob and decrypts it back, so we can decide WHERE encryption runs (Worker vs queue consumer vs Node CLI/MCP) before investing in HarborClient/seal.ts.

Acceptance criteria

  • A minimal Worker (or apps/api test entry) imports @mysten/seal + @mysten/sui/keypairs/ed25519, and encrypts a small (<10KB) Uint8Array against a Seal policy id on Sui testnet, returning ciphertext.
  • The same Worker builds a SessionKey + seal_approve PTB and decrypts the ciphertext back to the original bytes (round-trip asserted byte-equal).
  • wrangler deploy --dry-run (or a real deploy) reports the compressed bundle size, recorded against the Workers size cap; CPU ms for one encrypt+decrypt is noted.
  • A written finding recommends where encryption runs (Worker / queue consumer / Node-only) backed by the bundle + CPU numbers.
  • If the Worker busts limits, the fallback (encrypt in the queue consumer or Node CLI/MCP) is documented so the Harbor client/encrypted-path issues target the right runtime.

Implementation notes

  • Add @mysten/seal to a scratch package or apps/api (do NOT keep it in the prod bundle if the spike fails). @mysten/sui is already present.
  • You need a real seal_policy_id on Sui testnet — create one via Harbor (a bucket create yields a Seal policy) or reuse the seal-docs.wal.app example policy; the demo sample target already points there (CONTEXTMEM_DEMO_SAMPLE_TARGET in wrangler.jsonc).
  • SessionKey requires an Ed25519 signer: decodeSuiPrivateKey(suiprivkey1...) → Ed25519Keypair. Use SuiGrpcClient (already used in packages/walrus/src/proof.ts/history.ts/resolve.ts) for the seal_approve devInspect.
  • Gotchas: nodejs_compat polyfills crypto, but BLS in @noble/curves is CPU-heavy (Worker CPU limit ~30s paid, lower on free) and the compressed-bundle cap is small. This is throwaway — do not ship to prod. Keep tar-based packProofBundle (packages/walrus/src/storage.ts:231) out; it's Node-only regardless.

Sui Overflow angle

This de-risks the whole Sui-native private-memory demo. The answer (edge-encrypt vs offload) determines the live-demo architecture: whether the hackathon judges see encryption happen in the same Worker that serves recall, or in a queue/Node tier.

Dependencies

None — do this first. Blocks the HarborClient issue and the private SEAL encrypted remember/recall issue.

Part of the ContextMEM roadmap (#4) • Sui Overflow build.

Activity

  1. added this to the Sui Overflow milestone on Jun 16, 2026
  2. added
    P0Demo-blocking: required for a working Sui Overflow demo
    harborHarbor encrypted Walrus storage on Sui (Space/Bucket/File)
    seal@mysten/seal client-side encryption + on-chain access control
    spikeTime-boxed throwaway investigation to de-risk a decision
    on Jun 16, 2026
  3. harrymove-ctrl commented on Jun 19, 2026

    @harrymove-ctrl
    OwnerAuthor

    Implementation Plan

    Approach
    Build a throwaway, standalone Worker in a new spikes/seal-worker/ directory (NOT a bun workspace member, so it never enters the prod bundle or CI) that imports @mysten/seal + @mysten/sui/keypairs/ed25519, runs one encrypt → decrypt round-trip against a real Sui testnet Seal policy, and reports the gzipped bundle size and CPU/wall-clock ms. The single design decision this spike answers: does @mysten/seal (WebCrypto + @noble BLS12-381) fit the Worker compressed-bundle cap and CPU budget, and therefore whether encryption runs in the Worker, the queue consumer, or Node-only. Keep it self-contained — get the seal_policy_id from a minimal self-published seal_approve Move package on testnet (zero dependency on the unbuilt HarborClient), with "create a Harbor bucket" as the fallback source.

    Prerequisites & dependencies

    Step-by-step

    1. Scaffold the throwaway package (excluded from workspaces → invisible to bun run check/build and prod):

      • Create spikes/seal-worker/package.json with "private": true, deps @mysten/seal@^1.2.1, @mysten/sui@^2.19.0. Run bun install from inside spikes/seal-worker/ (root package.json workspaces: ["apps/*","packages/*"] deliberately omits spikes/).
      • Create spikes/seal-worker/wrangler.jsonc: name: "seal-spike", main: "src/index.ts", compatibility_date: "2026-05-21", compatibility_flags: ["nodejs_compat"] (mirror apps/api/cloudflare/wrangler.jsonc:16-17 so the polyfill surface is identical to prod), workers_dev: true, and "observability": { "enabled": true } so cpuTime shows up in wrangler tail. Bind nothing else.
      • Create spikes/seal-worker/.dev.vars (gitignored) for SEAL_SERVICE_PRIVATE_KEY, SEAL_PACKAGE_ID, SEAL_POLICY_OBJECT_ID; for real deploy use wrangler secret put.
    2. Get a testnet Seal policy (self-contained route). Author a minimal spikes/seal-worker/seal-policy/ Move package (Move.toml + sources/policy.move) exposing entry fun seal_approve(id: vector<u8>, /* policy obj */) that authorizes unconditionally (this is a throwaway whose only job is to make decrypt's seal_approve PTB succeed — it is NOT the production ACL). sui client publish --gas-budget ... → record the packageId and the created policy object id into SEAL_PACKAGE_ID / SEAL_POLICY_OBJECT_ID. Fallback if you'd rather not publish Move: reuse the seal-docs example policy or create a Harbor bucket to harvest its seal_policy_id (note this re-introduces a Harbor dependency the self-publish route avoids).

    3. Write the encrypt path in spikes/seal-worker/src/index.ts (single fetch handler):

      • const suiClient = new SuiClient({ url: getFullnodeUrl('testnet') }) (use the JSON-RPC SuiClient from @mysten/sui/client for SEAL — it needs devInspect/tx.build; the repo's SuiGrpcClient at packages/walrus/src/proof.ts:1 is for plain reads, so prefer JSON-RPC here and flag grpc-compat as a verify-later note).
      • const sealClient = new SealClient({ suiClient, serverConfigs: getAllowlistedKeyServers('testnet').map(id => ({ objectId: id, weight: 1 })), verifyKeyServers: false }).
      • Build a <10KB Uint8Array test blob; pick an identity id (hex bytes). const { encryptedObject, key } = await sealClient.encrypt({ threshold: 1, packageId: SEAL_PACKAGE_ID, id, data }). Time it with Date.now() around the call.
    4. Write the decrypt path in the same handler:

      • const keypair = Ed25519Keypair.fromSecretKey(decodeSuiPrivateKey(env.SEAL_SERVICE_PRIVATE_KEY).secretKey).
      • const sessionKey = await SessionKey.create({ address: keypair.toSuiAddress(), packageId: SEAL_PACKAGE_ID, ttlMin: 10, signer: keypair, suiClient }) (or new SessionKey(...) → getPersonalMessage() → keypair.signPersonalMessage() → setPersonalMessageSignature()).
      • Build the seal_approve PTB: const tx = new Transaction(); tx.moveCall({ target: \${SEAL_PACKAGE_ID}::policy::seal_approve`, arguments: [tx.pure.vector('u8', fromHex(id)), tx.object(SEAL_POLICY_OBJECT_ID)] }); const txBytes = await tx.build({ client: suiClient, onlyTransactionKind: true })`.
      • const decrypted = await sealClient.decrypt({ data: encryptedObject, sessionKey, txBytes }). Time it.
      • Assert decrypted is byte-equal to the original blob; throw if not.
    5. Measure & report. Return JSON { ok, byteEqual, ciphertextBytes, encryptMs, decryptMs } from the handler. Run wrangler deploy --dry-run --outdir dist and record the "Total Upload: X / gzip: Y" number against the Workers cap (Free ≈ 3 MiB gzip, Paid ≈ 10 MiB gzip — confirm current limits). Then do a real wrangler deploy to workers.dev, hit the URL once, and read cpuTime from wrangler tail --format json (wall-clock from the body is a sanity check; the BLS work is CPU-bound so they track closely, but cpuTime is the number that matters vs the limit).

    6. Write the finding (post as the issue comment / a spikes/seal-worker/FINDINGS.md): the gzip bundle size, encrypt/decrypt cpuTime, byte-equal result, and a recommendation — Worker if comfortably under both caps; queue consumer if CPU is the bottleneck (consumers get longer CPU budgets); Node CLI/MCP only if the bundle busts the compressed cap. Document the chosen fallback so Build HarborClient: reserve→sign→finalize bucket, multipart upload, status poll, download #6/Namespace→Space/Bucket/Seal mapping + Harbor service-key management in Worker secrets #7 target the right runtime. Note explicitly that tar-based packProofBundle (packages/walrus/src/storage.ts:231) stays Node-only regardless and is out of scope.

    New / changed files

    • spikes/seal-worker/package.json (new — standalone, pins @mysten/seal@^1.2.1 + @mysten/sui@^2.19.0)
    • spikes/seal-worker/wrangler.jsonc (new — nodejs_compat, observability on)
    • spikes/seal-worker/src/index.ts (new — encrypt+decrypt+measure fetch handler)
    • spikes/seal-worker/.dev.vars (new, gitignored)
    • spikes/seal-worker/seal-policy/Move.toml + spikes/seal-worker/seal-policy/sources/policy.move (new — minimal seal_approve; optional if reusing an existing policy)
    • spikes/seal-worker/FINDINGS.md (new — the recorded numbers + runtime recommendation)
    • .gitignore (edit — add spikes/seal-worker/.dev.vars, spikes/seal-worker/node_modules, spikes/seal-worker/dist)
    • No edits to apps/api, packages/*, root package.json, or CI.

    Test plan

    • Round-trip assert (AC Memory knowledge graph: 3D neural constellation + grounded entity panel #2): handler throws unless decrypted is byte-equal to the original <10KB blob; a 200 with { byteEqual: true } proves it.
    • Real testnet proof: the publish in Step 2 yields a real Sui testnet tx digest (the package-publish digest) and a seal_policy_id object id — paste both into the finding as evidence the policy is genuinely on-chain. getAllowlistedKeyServers('testnet') confirms decrypt actually fetched key shares from live testnet key servers (not a local mock).
    • Bundle measurement (AC fix(ci): force bun + pin wrangler in Deploy workflow #3): wrangler deploy --dry-run --outdir dist gzip number recorded vs cap.
    • CPU measurement (AC fix(ci): force bun + pin wrangler in Deploy workflow #3): real deploy + one request + wrangler tail cpuTime for encrypt and decrypt.
    • Manual: wrangler dev locally first (runs in workerd, the real Worker runtime — proves it's not silently falling back to Node) before the real deploy.

    Risks & gotchas

    • @mysten/sui peer mismatch: seal needs ^2.19.0, repo has 2.17.0. Pin in the spike only; a root bump for Build HarborClient: reserve→sign→finalize bucket, multipart upload, status poll, download #6/Namespace→Space/Bucket/Seal mapping + Harbor service-key management in Worker secrets #7 is a separate compat exercise (the 8.2k-line apps/api/src/worker.ts already imports @mysten/sui).
    • Bundle cap is the Walrus storage + Memory via Tatum, plus CI deploy #1 kill risk: @noble/curves@2 + @mysten/sui + @mysten/seal may approach/exceed the ~3 MiB gzip Free cap. If dry-run is over, that alone forces encryption out of the Worker (or onto Workers Paid's 10 MiB cap) — exactly the decision this spike exists to make.
    • CPU limit: BLS12-381 in @noble/curves is bigint-heavy; Free CPU budget is small (~10ms, burstable), Paid up to ~30s. Decrypt (key-share fetch + combine + BLS) is the expensive op — measure it specifically.
    • nodejs_compat polyfill gaps: verify globalThis.crypto.getRandomValues/subtle and Buffer resolve under the polyfill; a missing primitive surfaces only at runtime, so always exercise via wrangler dev/deploy, never just typecheck.
    • SessionKey/PTB shape: seal_approve arg order and id encoding must match between encrypt and the decrypt PTB exactly, or decrypt 403s with no-access. Use EncryptedObject.parse(encryptedObject) to recover the id if needed.
    • SuiClient vs SuiGrpcClient: the issue suggests grpc for seal_approve devInspect, but SEAL's decrypt/tx.build path is JSON-RPC-shaped — start with SuiClient (@mysten/sui/client) and only try grpc if you specifically want to reuse the repo's client; flag the result in the finding for Build HarborClient: reserve→sign→finalize bucket, multipart upload, status poll, download #6.
    • Network split: spike is testnet only (Tatum prod is mainnet). No migration, no prod data touched — that's intentional and keeps blast radius zero.
    • Harbor sponsor-sig / mirror_missing_grant: only relevant if you choose the "create a Harbor bucket" route for the policy; the self-published Move route sidesteps it entirely (preferred for a clean spike).

    Acceptance-criteria mapping

    • Worker imports @mysten/seal + @mysten/sui/keypairs/ed25519, encrypts a <10KB Uint8Array against a testnet Seal policy → ciphertext → Steps 1, 2, 3.
    • Same Worker builds SessionKey + seal_approve PTB and decrypts back, round-trip byte-equal asserted → Step 4 (assert), Test plan round-trip.
    • wrangler deploy --dry-run reports compressed bundle size vs cap; CPU ms noted → Step 5.
    • Written finding recommends where encryption runs, backed by bundle + CPU numbers → Step 6.
    • If limits bust, fallback (queue consumer / Node CLI/MCP) documented → Step 6 (recommendation branches + explicit fallback).

    Effort
    M, ~2-3 person-days. ~0.5d scaffold + version pinning; ~0.5d publish the minimal Move policy (sui CLI + faucet + verify on-chain); ~1d wire encrypt/decrypt/SessionKey/PTB and debug Worker polyfill/runtime issues; ~0.5d measure (dry-run + tail) and write the finding. Risk skews toward the upper end if the bundle busts the cap and the fallback runtime needs its own quick re-measure.


    Implementation plan generated for execution (planning phase). Part of the roadmap (#4).

  4. added a commit that references this issue on Jun 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P0Demo-blocking: required for a working Sui Overflow demoharborHarbor encrypted Walrus storage on Sui (Space/Bucket/File)seal@mysten/seal client-side encryption + on-chain access controlspikeTime-boxed throwaway investigation to de-risk a decision

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions