@meddleware/seal-ui 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,15 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@meddleware/seal-ui` are documented here.
4
+ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versioning follows [Semantic Versioning](https://semver.org/).
5
+
6
+ ## [0.1.0] - 2026-09-02
7
+
8
+ ### Added
9
+
10
+ - Initial release. Sealed Storage SPA:
11
+ - Encrypt a file under a registry-driven policy (nft-gate, time-lock), store the ciphertext on
12
+ Walrus (HTTP publisher), and download a portable manifest.
13
+ - Decrypt from a manifest: fetch ciphertext, mint a Seal SessionKey via one wallet signature, and
14
+ download the plaintext.
15
+ - Testnet-first with a mainnet-pending notice (Seal committee mode is testnet-only).
package/LICENSE ADDED
@@ -0,0 +1,14 @@
1
+ BSD Zero Clause License
2
+
3
+ Copyright (c) 2026 MeddleWare
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted.
7
+
8
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
9
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
10
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
11
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
12
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
13
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
14
+ PERFORMANCE OF THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,54 @@
1
+ # @meddleware/seal-ui
2
+
3
+ Sealed Storage — a standalone Vue 3 SPA for client-side [Seal](https://seal-docs.wal.app/) encryption
4
+ with access-gated, decentralised [Walrus](https://walrus.xyz) storage on Sui.
5
+
6
+ Encrypt a file under a policy (access-gate NFT ownership, time-lock, …), store the ciphertext on
7
+ Walrus, and share a small **manifest**. Anyone with the manifest can decrypt — but only if the
8
+ on-chain policy allows (e.g. they hold a valid gate pass, or the unlock time has passed).
9
+
10
+ ## How it works
11
+
12
+ 1. **Encrypt** in the browser via `@meddleware/seal-client` (threshold Seal). No key material ever
13
+ touches a server.
14
+ 2. **Store** the opaque ciphertext on Walrus over HTTP; keep the returned blob id in the manifest.
15
+ 3. **Decrypt** by signing one wallet message (mints a short-lived Seal SessionKey); a threshold
16
+ committee of independent key servers releases key shares only if the on-chain `seal_approve`
17
+ passes.
18
+
19
+ The policies live in the [`seal_policies`](https://github.com/meddleware-org/seal-policies-sui) Move
20
+ package; the client seam is [`@meddleware/seal-client`](https://github.com/meddleware-org/seal-client).
21
+
22
+ ## Configure (build-time `VITE_*`)
23
+
24
+ | Var | Meaning |
25
+ | --- | --- |
26
+ | `VITE_NETWORK` | `testnet` (default) or `mainnet` (pending) |
27
+ | `VITE_SEAL_PACKAGE_ID_{NET}` | Published `seal_policies` package id |
28
+ | `VITE_SEAL_SERVER_OBJECT_IDS_{NET}` | CSV of key-server object ids (the committee) |
29
+ | `VITE_SEAL_AGGREGATOR_URLS_{NET}` | CSV of aggregator URLs (index-aligned) |
30
+ | `VITE_SEAL_THRESHOLD` | `t` in t-of-n (default 2) |
31
+ | `VITE_WALRUS_PUBLISHER_{NET}` / `VITE_WALRUS_AGGREGATOR_{NET}` | Walrus HTTP endpoints (defaults provided) |
32
+ | `VITE_RPC_{NET}` | Sui RPC URL override |
33
+
34
+ ## Develop
35
+
36
+ ```sh
37
+ # @meddleware/seal-client must be linked or published:
38
+ ( cd ../seal-client && npm link )
39
+ npm install && npm link @meddleware/seal-client
40
+ npm run dev
41
+ ```
42
+
43
+ ## Build
44
+
45
+ ```sh
46
+ npm run build # vue-tsc + vite → dist/
47
+ docker build -t seal-ui .
48
+ ```
49
+
50
+ Committee mode is testnet-only today; mainnet stays disabled with an in-app notice until it ships.
51
+
52
+ ## License
53
+
54
+ BSD Zero Clause License (`0BSD`). See [LICENSE](LICENSE).
package/index.html ADDED
@@ -0,0 +1,13 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <link rel="icon" href="/favicon.ico">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Sealed Storage — Meddleware</title>
8
+ </head>
9
+ <body>
10
+ <div id="app"></div>
11
+ <script type="module" src="/src/main.ts"></script>
12
+ </body>
13
+ </html>
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@meddleware/seal-ui",
3
+ "version": "0.0.3",
4
+ "description": "Standalone Vue 3 SPA for Sealed Storage — client-side Seal encryption + access-gated, decentralised Walrus storage on Sui.",
5
+ "author": "MeddleWare <meddleware@proton.me>",
6
+ "license": "0BSD",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/meddleware-org/seal-ui.git"
10
+ },
11
+ "type": "module",
12
+ "sideEffects": [
13
+ "*.css",
14
+ "*.vue"
15
+ ],
16
+ "exports": {
17
+ ".": {
18
+ "types": "./src/index.ts",
19
+ "default": "./src/index.ts"
20
+ },
21
+ "./package.json": "./package.json"
22
+ },
23
+ "files": [
24
+ "src",
25
+ "index.html",
26
+ "vite.config.ts",
27
+ "tsconfig.json",
28
+ "CHANGELOG.md"
29
+ ],
30
+ "scripts": {
31
+ "dev": "vite",
32
+ "build": "vue-tsc --noEmit && vite build",
33
+ "preview": "vite preview",
34
+ "type-check": "vue-tsc --noEmit"
35
+ },
36
+ "publishConfig": {
37
+ "access": "public"
38
+ },
39
+ "dependencies": {
40
+ "@meddleware/design-tokens": "^0.1.2",
41
+ "@meddleware/seal-client": "^0.0.2",
42
+ "@meddleware/ui": "^0.1.6",
43
+ "@meddleware/wallet-adapter": "^0.0.2",
44
+ "@mysten/seal": "^1.4.7",
45
+ "@mysten/sui": "^2.28.0",
46
+ "@mysten/wallet-standard": "^0.20.0",
47
+ "vue": "^3.5.40"
48
+ },
49
+ "devDependencies": {
50
+ "@types/node": "~24.12.2",
51
+ "@vitejs/plugin-vue": "^6.0.8",
52
+ "typescript": "~6.0.0",
53
+ "vite": "^8.1.5",
54
+ "vue-tsc": "~3.3.0"
55
+ },
56
+ "engines": {
57
+ "node": "^22.18.0 || >=24.12.0"
58
+ }
59
+ }
package/src/App.vue ADDED
@@ -0,0 +1,44 @@
1
+ <script setup lang="ts">
2
+ // Standalone shell for the Sealed Storage SPA: app header + footer wrapping the core tool view.
3
+ // The core UI lives in SealView.vue (also exported for inline embedding in the dashboard).
4
+ import { AppHeader, AppFooter, ColorModeControl, useColorMode } from '@meddleware/ui'
5
+ import { NETWORK } from './config.js'
6
+ import SealView from './components/SealView.vue'
7
+
8
+ const { mode, set } = useColorMode('dark')
9
+ </script>
10
+
11
+ <template>
12
+ <div class="app">
13
+ <AppHeader variant="dark">
14
+ <template #brand>
15
+ <span>🔒 Sealed Storage</span>
16
+ </template>
17
+ <template #actions>
18
+ <span class="badge">{{ NETWORK }}</span>
19
+ <ColorModeControl :model-value="mode" @update:model-value="set" />
20
+ </template>
21
+ </AppHeader>
22
+
23
+ <SealView />
24
+
25
+ <AppFooter />
26
+ </div>
27
+ </template>
28
+
29
+ <style scoped>
30
+ .app {
31
+ min-height: 100vh;
32
+ display: flex;
33
+ flex-direction: column;
34
+ }
35
+
36
+ .badge {
37
+ font-size: 0.75rem;
38
+ padding: 0.2rem 0.5rem;
39
+ border-radius: 4px;
40
+ background: var(--surface);
41
+ color: var(--muted);
42
+ border: 1px solid var(--border);
43
+ }
44
+ </style>
@@ -0,0 +1,559 @@
1
+ <script setup lang="ts">
2
+ // Core Sealed Storage tool UI (Encrypt / Decrypt / Unlock), free of any app shell.
3
+ // Rendered standalone by seal-ui's App.vue and inline by the dashboard. Wallet state comes from
4
+ // the shared @meddleware/wallet-adapter singleton (via ./wallet.js): connecting here or in any
5
+ // other inline tool view (or the dashboard header) reflects everywhere.
6
+ //
7
+ // Styles are scoped to this component so the dashboard can import it without pulling seal-ui's
8
+ // global stylesheet (which restyles body / #app / bare inputs). The standalone app keeps those
9
+ // globals via main.ts → styles.css.
10
+ import { computed, onMounted, ref } from 'vue'
11
+ import { Transaction } from '@mysten/sui/transactions'
12
+ import {
13
+ buildPublishSealedContentTx,
14
+ type SealedManifest,
15
+ type SealPolicyProvider,
16
+ type SealedContentPointer,
17
+ } from '@meddleware/seal-client'
18
+ import { useWallet } from '../wallet.js'
19
+ import { registry, getSealController } from '../seal.js'
20
+ import { storeBlob, readBlob } from '../walrus.js'
21
+ import { discoverSealedContent } from '../sealed-content.js'
22
+ import { NETWORK, MAINNET_PENDING, SEAL_CONFIGURED, SEAL_PACKAGE_ID } from '../config.js'
23
+
24
+ const { wallets, account, connect, disconnect, signPersonalMessage, signAndExecute } = useWallet()
25
+
26
+ const providers = registry.list() as SealPolicyProvider[]
27
+ const disabled = computed(() => MAINNET_PENDING || !SEAL_CONFIGURED)
28
+
29
+ type Tab = 'encrypt' | 'decrypt' | 'unlock'
30
+ const tab = ref<Tab>('encrypt')
31
+
32
+ // Deep-link from access-gate-ui: ?gate=<id> preselects the nft-gate policy + gate.
33
+ onMounted(() => {
34
+ const gate = new URLSearchParams(location.search).get('gate')
35
+ if (gate && providers.some((p) => p.type === 'nft-gate')) {
36
+ encType.value = 'nft-gate'
37
+ encValues.value = { ...encValues.value, gateId: gate }
38
+ unlockGateId.value = gate
39
+ }
40
+ })
41
+ const busy = ref(false)
42
+ const status = ref<string | null>(null)
43
+ const errorMsg = ref<string | null>(null)
44
+
45
+ function coerce(kind: string, raw: string | boolean): unknown {
46
+ if (kind === 'boolean') return Boolean(raw)
47
+ if (kind === 'datetime') return Date.parse(String(raw))
48
+ return String(raw)
49
+ }
50
+
51
+ function triggerDownload(blob: Blob, name: string): void {
52
+ const url = URL.createObjectURL(blob)
53
+ const a = document.createElement('a')
54
+ a.href = url
55
+ a.download = name
56
+ a.click()
57
+ URL.revokeObjectURL(url)
58
+ }
59
+
60
+ // ── Encrypt ──────────────────────────────────────────────────────────────────
61
+ const encType = ref(providers[0]?.type ?? '')
62
+ const encProvider = computed(() => providers.find((p) => p.type === encType.value) ?? null)
63
+ const encValues = ref<Record<string, string | boolean>>({})
64
+ const encFile = ref<File | null>(null)
65
+ const encLabel = ref('')
66
+ const manifest = ref<SealedManifest | null>(null)
67
+
68
+ function onEncFile(e: Event): void {
69
+ encFile.value = (e.target as HTMLInputElement).files?.[0] ?? null
70
+ }
71
+
72
+ async function performEncrypt(): Promise<void> {
73
+ errorMsg.value = null
74
+ status.value = null
75
+ manifest.value = null
76
+ const provider = encProvider.value
77
+ if (!provider) return
78
+ if (!encFile.value) {
79
+ errorMsg.value = 'Choose a file to encrypt.'
80
+ return
81
+ }
82
+ busy.value = true
83
+ try {
84
+ const params: Record<string, unknown> = {}
85
+ for (const f of provider.describe().encryptFields) {
86
+ const v = encValues.value[f.name]
87
+ if (f.required && (v === undefined || v === '')) throw new Error(`${f.label} is required.`)
88
+ if (v !== undefined && v !== '') params[f.name] = coerce(f.kind, v)
89
+ }
90
+ status.value = 'Encrypting…'
91
+ const data = new Uint8Array(await encFile.value.arrayBuffer())
92
+ const { id, ciphertext } = await (await getSealController()).encrypt(provider.type, params, data)
93
+ status.value = 'Storing ciphertext on Walrus…'
94
+ const blobId = await storeBlob(ciphertext)
95
+ manifest.value = {
96
+ policyType: provider.type,
97
+ id,
98
+ blobId,
99
+ network: NETWORK,
100
+ params,
101
+ label: encLabel.value || encFile.value.name,
102
+ }
103
+ status.value = 'Sealed. Keep the manifest below — you need it to decrypt.'
104
+ } catch (e) {
105
+ errorMsg.value = e instanceof Error ? e.message : String(e)
106
+ } finally {
107
+ busy.value = false
108
+ }
109
+ }
110
+
111
+ function downloadManifest(): void {
112
+ if (!manifest.value) return
113
+ const blob = new Blob([JSON.stringify(manifest.value, null, 2)], { type: 'application/json' })
114
+ triggerDownload(blob, `${manifest.value.label ?? 'sealed'}.seal.json`)
115
+ }
116
+
117
+ // ── Decrypt ──────────────────────────────────────────────────────────────────
118
+ const decManifestText = ref('')
119
+ const decValues = ref<Record<string, string | boolean>>({})
120
+ const decManifest = computed<SealedManifest | null>(() => {
121
+ if (!decManifestText.value.trim()) return null
122
+ try {
123
+ return JSON.parse(decManifestText.value) as SealedManifest
124
+ } catch {
125
+ return null
126
+ }
127
+ })
128
+ const decProvider = computed(() =>
129
+ decManifest.value ? (providers.find((p) => p.type === decManifest.value?.policyType) ?? null) : null,
130
+ )
131
+
132
+ async function onManifestFile(e: Event): Promise<void> {
133
+ const f = (e.target as HTMLInputElement).files?.[0]
134
+ if (f) decManifestText.value = await f.text()
135
+ }
136
+
137
+ async function performDecrypt(): Promise<void> {
138
+ errorMsg.value = null
139
+ status.value = null
140
+ const m = decManifest.value
141
+ const provider = decProvider.value
142
+ if (!m || !provider) {
143
+ errorMsg.value = 'Paste or upload a valid manifest.'
144
+ return
145
+ }
146
+ if (!account.value) {
147
+ errorMsg.value = 'Connect your wallet to decrypt.'
148
+ return
149
+ }
150
+ busy.value = true
151
+ try {
152
+ const params: Record<string, unknown> = { ...(m.params ?? {}) }
153
+ for (const f of provider.describe().decryptFields) {
154
+ const v = decValues.value[f.name]
155
+ if (v !== undefined && v !== '') params[f.name] = coerce(f.kind, v)
156
+ if (f.required && (params[f.name] === undefined || params[f.name] === '')) {
157
+ throw new Error(`${f.label} is required.`)
158
+ }
159
+ }
160
+ status.value = 'Fetching ciphertext from Walrus…'
161
+ const ciphertext = await readBlob(m.blobId)
162
+ status.value = 'Requesting decryption keys — approve the signature in your wallet…'
163
+ const plaintext = await (await getSealController()).decrypt(m.policyType, params, m.id, ciphertext, {
164
+ address: account.value.address,
165
+ signPersonalMessage,
166
+ })
167
+ triggerDownload(new Blob([plaintext.slice().buffer as ArrayBuffer]), m.label ?? 'decrypted')
168
+ status.value = 'Decrypted — download started.'
169
+ } catch (e) {
170
+ errorMsg.value = e instanceof Error ? e.message : String(e)
171
+ } finally {
172
+ busy.value = false
173
+ }
174
+ }
175
+
176
+ // ── Publish an on-chain discovery pointer (nft-gate content only) ─────────────
177
+ const publishDigest = ref<string | null>(null)
178
+
179
+ async function performPublish(): Promise<void> {
180
+ errorMsg.value = null
181
+ const m = manifest.value
182
+ if (!m || m.policyType !== 'nft-gate' || !m.params?.gateId) {
183
+ errorMsg.value = 'On-chain publishing is available for access-gate content only.'
184
+ return
185
+ }
186
+ if (!account.value) {
187
+ errorMsg.value = 'Connect your wallet to publish.'
188
+ return
189
+ }
190
+ busy.value = true
191
+ try {
192
+ status.value = 'Publishing on-chain pointer — approve the transaction in your wallet…'
193
+ const tx = new Transaction()
194
+ buildPublishSealedContentTx(tx, SEAL_PACKAGE_ID, {
195
+ gateId: String(m.params.gateId),
196
+ blobId: m.blobId,
197
+ sealId: m.id,
198
+ label: m.label ?? '',
199
+ })
200
+ const { digest } = await signAndExecute(tx)
201
+ publishDigest.value = digest
202
+ status.value = 'Published — gate pass-holders can now discover and unlock this content.'
203
+ } catch (e) {
204
+ errorMsg.value = e instanceof Error ? e.message : String(e)
205
+ } finally {
206
+ busy.value = false
207
+ }
208
+ }
209
+
210
+ // ── Unlock: discover a gate's sealed content and decrypt ──────────────────────
211
+ const unlockGateId = ref('')
212
+ const discovered = ref<SealedContentPointer[]>([])
213
+ const unlockNftId = ref('')
214
+ const unlockSoulbound = ref(false)
215
+
216
+ async function performDiscover(): Promise<void> {
217
+ errorMsg.value = null
218
+ discovered.value = []
219
+ if (!unlockGateId.value) {
220
+ errorMsg.value = 'Enter a gate id to search.'
221
+ return
222
+ }
223
+ busy.value = true
224
+ try {
225
+ status.value = 'Searching for sealed content…'
226
+ discovered.value = await discoverSealedContent(unlockGateId.value)
227
+ status.value = discovered.value.length
228
+ ? `Found ${discovered.value.length} item(s).`
229
+ : 'No sealed content published for this gate.'
230
+ } catch (e) {
231
+ errorMsg.value = e instanceof Error ? e.message : String(e)
232
+ } finally {
233
+ busy.value = false
234
+ }
235
+ }
236
+
237
+ async function performUnlock(item: SealedContentPointer): Promise<void> {
238
+ errorMsg.value = null
239
+ if (!account.value) {
240
+ errorMsg.value = 'Connect your wallet to decrypt.'
241
+ return
242
+ }
243
+ if (!unlockNftId.value) {
244
+ errorMsg.value = 'Enter your pass NFT id.'
245
+ return
246
+ }
247
+ busy.value = true
248
+ try {
249
+ status.value = 'Fetching ciphertext from Walrus…'
250
+ const ciphertext = await readBlob(item.blobId)
251
+ status.value = 'Requesting decryption keys — approve the signature in your wallet…'
252
+ const plaintext = await (await getSealController()).decrypt(
253
+ 'nft-gate',
254
+ { gateId: item.gateId, nftId: unlockNftId.value, soulbound: unlockSoulbound.value },
255
+ item.sealId,
256
+ ciphertext,
257
+ { address: account.value.address, signPersonalMessage },
258
+ )
259
+ triggerDownload(new Blob([plaintext.slice().buffer as ArrayBuffer]), item.label || 'decrypted')
260
+ status.value = 'Decrypted — download started.'
261
+ } catch (e) {
262
+ errorMsg.value = e instanceof Error ? e.message : String(e)
263
+ } finally {
264
+ busy.value = false
265
+ }
266
+ }
267
+
268
+ const shortAddr = computed(() => {
269
+ const a = account.value?.address
270
+ return a ? `${a.slice(0, 6)}…${a.slice(-4)}` : ''
271
+ })
272
+ </script>
273
+
274
+ <template>
275
+ <div class="page">
276
+ <p class="muted">Client-side encrypted, access-gated storage on Walrus + Sui.</p>
277
+
278
+ <div v-if="MAINNET_PENDING" class="notice notice--warn">
279
+ Mainnet support is pending — Seal committee mode is currently testnet-only. Switch to testnet to
280
+ seal content.
281
+ </div>
282
+ <div v-else-if="!SEAL_CONFIGURED" class="notice notice--warn">
283
+ This deployment has no Seal policy package or key-server committee configured
284
+ (<code>VITE_SEAL_PACKAGE_ID_*</code> / <code>VITE_SEAL_SERVER_OBJECT_IDS_*</code>).
285
+ </div>
286
+
287
+ <section class="card" style="display:flex;align-items:center;justify-content:space-between;gap:1rem">
288
+ <template v-if="account">
289
+ <span class="muted">Connected: {{ shortAddr }}</span>
290
+ <button class="link" @click="disconnect">Disconnect</button>
291
+ </template>
292
+ <template v-else>
293
+ <span class="muted">Wallet needed only to decrypt.</span>
294
+ <span>
295
+ <button
296
+ v-for="w in wallets"
297
+ :key="w.name"
298
+ class="primary"
299
+ style="margin-left:0.4rem"
300
+ @click="connect(w)"
301
+ >
302
+ Connect {{ w.name }}
303
+ </button>
304
+ <span v-if="!wallets.length" class="muted">No Sui wallet detected.</span>
305
+ </span>
306
+ </template>
307
+ </section>
308
+
309
+ <nav class="tabs">
310
+ <button :class="{ active: tab === 'encrypt' }" @click="tab = 'encrypt'">Encrypt</button>
311
+ <button :class="{ active: tab === 'decrypt' }" @click="tab = 'decrypt'">Decrypt</button>
312
+ <button :class="{ active: tab === 'unlock' }" @click="tab = 'unlock'">Unlock</button>
313
+ </nav>
314
+
315
+ <div v-if="errorMsg" class="notice notice--error">{{ errorMsg }}</div>
316
+
317
+ <!-- Encrypt -->
318
+ <section v-show="tab === 'encrypt'" class="card">
319
+ <label class="field">
320
+ <span>Policy</span>
321
+ <select v-model="encType" :disabled="disabled">
322
+ <option v-for="p in providers" :key="p.type" :value="p.type">
323
+ {{ p.describe().label }}
324
+ </option>
325
+ </select>
326
+ </label>
327
+ <p v-if="encProvider" class="muted">{{ encProvider.describe().help }}</p>
328
+
329
+ <template v-if="encProvider">
330
+ <label
331
+ v-for="f in encProvider.describe().encryptFields"
332
+ :key="f.name"
333
+ class="field"
334
+ :class="{ checkbox: f.kind === 'boolean' }"
335
+ >
336
+ <span>{{ f.label }}<template v-if="f.required"> *</template></span>
337
+ <input v-if="f.kind === 'datetime'" type="datetime-local" v-model="encValues[f.name]" />
338
+ <input v-else-if="f.kind === 'boolean'" type="checkbox" v-model="encValues[f.name]" />
339
+ <input v-else type="text" v-model="encValues[f.name]" :placeholder="f.help" />
340
+ </label>
341
+ </template>
342
+
343
+ <label class="field">
344
+ <span>Label (optional)</span>
345
+ <input type="text" v-model="encLabel" placeholder="A name to recognise this later" />
346
+ </label>
347
+
348
+ <label class="field">
349
+ <span>File</span>
350
+ <input type="file" @change="onEncFile" />
351
+ </label>
352
+
353
+ <button class="primary" :disabled="busy || disabled" @click="performEncrypt">
354
+ {{ busy ? 'Working…' : 'Encrypt & store' }}
355
+ </button>
356
+
357
+ <div v-if="manifest" style="margin-top:1rem">
358
+ <p class="muted">Manifest — save this; it's required to decrypt (it holds no secrets):</p>
359
+ <pre class="manifest">{{ JSON.stringify(manifest, null, 2) }}</pre>
360
+ <button class="link" @click="downloadManifest">Download manifest</button>
361
+
362
+ <div v-if="manifest.policyType === 'nft-gate'" style="margin-top:0.75rem">
363
+ <p class="muted">
364
+ Optional: publish an on-chain pointer so this gate's pass-holders can discover and unlock
365
+ this content in the Unlock tab (no need to share the manifest).
366
+ </p>
367
+ <button class="primary" :disabled="busy || !account" @click="performPublish">
368
+ {{ busy ? 'Working…' : 'Publish discovery pointer' }}
369
+ </button>
370
+ <span v-if="!account" class="muted"> — connect a wallet first.</span>
371
+ <p v-if="publishDigest" class="muted">Published in tx {{ publishDigest.slice(0, 10) }}…</p>
372
+ </div>
373
+ </div>
374
+ </section>
375
+
376
+ <!-- Unlock -->
377
+ <section v-show="tab === 'unlock'" class="card">
378
+ <p class="muted">
379
+ Discover content sealed to an access gate and decrypt it with a pass you hold.
380
+ </p>
381
+ <label class="field">
382
+ <span>Access gate ID</span>
383
+ <input type="text" v-model="unlockGateId" placeholder="0x… gate object id" />
384
+ </label>
385
+ <button class="primary" :disabled="busy || !unlockGateId" @click="performDiscover">
386
+ {{ busy ? 'Working…' : 'Find sealed content' }}
387
+ </button>
388
+
389
+ <div v-if="discovered.length" style="margin-top:1rem">
390
+ <label class="field">
391
+ <span>Your pass NFT ID *</span>
392
+ <input type="text" v-model="unlockNftId" placeholder="0x… your AccessNFT for this gate" />
393
+ </label>
394
+ <label class="field checkbox">
395
+ <input type="checkbox" v-model="unlockSoulbound" />
396
+ <span>Pass is soulbound</span>
397
+ </label>
398
+
399
+ <div v-for="item in discovered" :key="item.contentId" class="card" style="margin:0.5rem 0">
400
+ <strong>{{ item.label || '(untitled)' }}</strong>
401
+ <p class="muted" style="word-break:break-all">blob {{ item.blobId }}</p>
402
+ <button class="primary" :disabled="busy || !account || !unlockNftId" @click="performUnlock(item)">
403
+ Unlock &amp; download
404
+ </button>
405
+ <span v-if="!account" class="muted"> — connect a wallet to decrypt.</span>
406
+ </div>
407
+ </div>
408
+ </section>
409
+
410
+ <!-- Decrypt -->
411
+ <section v-show="tab === 'decrypt'" class="card">
412
+ <label class="field">
413
+ <span>Manifest (paste JSON or upload)</span>
414
+ <textarea v-model="decManifestText" placeholder='{ "policyType": "...", "id": "...", "blobId": "..." }'></textarea>
415
+ </label>
416
+ <input type="file" accept="application/json,.json" @change="onManifestFile" />
417
+
418
+ <template v-if="decProvider">
419
+ <p class="muted" style="margin-top:0.75rem">
420
+ Policy: {{ decProvider.describe().label }} — {{ decProvider.describe().help }}
421
+ </p>
422
+ <label
423
+ v-for="f in decProvider.describe().decryptFields"
424
+ :key="f.name"
425
+ class="field"
426
+ :class="{ checkbox: f.kind === 'boolean' }"
427
+ >
428
+ <span>{{ f.label }}<template v-if="f.required"> *</template></span>
429
+ <input v-if="f.kind === 'datetime'" type="datetime-local" v-model="decValues[f.name]" />
430
+ <input v-else-if="f.kind === 'boolean'" type="checkbox" v-model="decValues[f.name]" />
431
+ <input
432
+ v-else
433
+ type="text"
434
+ v-model="decValues[f.name]"
435
+ :placeholder="decManifest?.params?.[f.name] != null ? String(decManifest.params[f.name]) : f.help"
436
+ />
437
+ </label>
438
+ </template>
439
+ <p v-else-if="decManifestText.trim()" class="muted">Unrecognised or invalid manifest.</p>
440
+
441
+ <button class="primary" :disabled="busy || !decProvider" @click="performDecrypt">
442
+ {{ busy ? 'Working…' : 'Decrypt' }}
443
+ </button>
444
+ </section>
445
+
446
+ <p v-if="status" class="status muted">{{ status }}</p>
447
+
448
+ <p class="muted disclaimer">
449
+ Decryption keys are released by a threshold committee of independent key servers. If enough
450
+ servers are unreachable, decryption pauses — storage and retrieval are unaffected.
451
+ </p>
452
+ </div>
453
+ </template>
454
+
455
+ <style scoped>
456
+ .page {
457
+ max-width: 780px;
458
+ margin: 0 auto;
459
+ flex: 1;
460
+ }
461
+
462
+ .notice {
463
+ border: 1px solid color-mix(in srgb, currentColor 20%, transparent);
464
+ border-radius: 10px;
465
+ padding: 0.7rem 0.9rem;
466
+ margin: 0.75rem 0;
467
+ font-size: 0.9rem;
468
+ }
469
+ .notice--warn { background: color-mix(in srgb, #d29922 15%, transparent); }
470
+ .notice--error { background: color-mix(in srgb, #f85149 18%, transparent); }
471
+
472
+ .tabs {
473
+ display: flex;
474
+ gap: 0.25rem;
475
+ margin: 1rem 0;
476
+ border-bottom: 1px solid color-mix(in srgb, currentColor 15%, transparent);
477
+ }
478
+ .tabs button {
479
+ background: transparent;
480
+ border: 0;
481
+ color: inherit;
482
+ padding: 0.5rem 0.9rem;
483
+ cursor: pointer;
484
+ opacity: 0.7;
485
+ border-bottom: 2px solid transparent;
486
+ }
487
+ .tabs button.active {
488
+ opacity: 1;
489
+ border-bottom-color: var(--accent, #c0503f);
490
+ }
491
+
492
+ .card {
493
+ background: var(--surface, #1e1917);
494
+ border: 1px solid color-mix(in srgb, currentColor 12%, transparent);
495
+ border-radius: 12px;
496
+ padding: 1rem 1.1rem;
497
+ margin: 0.75rem 0;
498
+ }
499
+
500
+ label.field {
501
+ display: flex;
502
+ flex-direction: column;
503
+ gap: 0.3rem;
504
+ margin: 0.6rem 0;
505
+ font-size: 0.9rem;
506
+ }
507
+ label.field.checkbox {
508
+ flex-direction: row;
509
+ align-items: center;
510
+ gap: 0.5rem;
511
+ }
512
+
513
+ input[type='text'],
514
+ input[type='datetime-local'],
515
+ textarea,
516
+ select {
517
+ background: var(--bg, #14100f);
518
+ color: inherit;
519
+ border: 1px solid color-mix(in srgb, currentColor 25%, transparent);
520
+ border-radius: 8px;
521
+ padding: 0.5rem 0.6rem;
522
+ font: inherit;
523
+ width: 100%;
524
+ box-sizing: border-box;
525
+ }
526
+ textarea { min-height: 6rem; resize: vertical; font-family: ui-monospace, monospace; font-size: 0.8rem; }
527
+
528
+ button.primary {
529
+ background: var(--accent, #c0503f);
530
+ color: #fff;
531
+ border: 0;
532
+ border-radius: 8px;
533
+ padding: 0.55rem 1.1rem;
534
+ font-weight: 600;
535
+ cursor: pointer;
536
+ }
537
+ button.primary:disabled { opacity: 0.5; cursor: default; }
538
+
539
+ button.link {
540
+ background: transparent;
541
+ border: 0;
542
+ color: var(--accent, #c0503f);
543
+ cursor: pointer;
544
+ padding: 0;
545
+ font: inherit;
546
+ }
547
+
548
+ pre.manifest {
549
+ background: var(--bg, #14100f);
550
+ border-radius: 8px;
551
+ padding: 0.7rem;
552
+ overflow-x: auto;
553
+ font-size: 0.75rem;
554
+ }
555
+
556
+ .muted { color: var(--muted, #a89b96); font-size: 0.85rem; }
557
+ .status { font-size: 0.85rem; margin-top: 0.5rem; }
558
+ .disclaimer { margin-top: 2rem; }
559
+ </style>
package/src/config.ts ADDED
@@ -0,0 +1,69 @@
1
+ // Build-time configuration (Vite inlines VITE_* at build). All values are baked into the static
2
+ // bundle via Docker --build-arg. See Dockerfile for the full list.
3
+ import type { KeyServerConfig } from '@meddleware/seal-client'
4
+
5
+ export type Network = 'testnet' | 'mainnet'
6
+
7
+ const env = (import.meta as unknown as { env?: Record<string, string | undefined> }).env ?? {}
8
+
9
+ export const NETWORK: Network = (env.VITE_NETWORK as Network) || 'testnet'
10
+
11
+ /** Read a per-network var, e.g. netEnv('VITE_SEAL_PACKAGE_ID') → VITE_SEAL_PACKAGE_ID_TESTNET. */
12
+ function netEnv(base: string): string | undefined {
13
+ return env[`${base}_${NETWORK.toUpperCase()}`]
14
+ }
15
+
16
+ function csv(v: string | undefined): string[] {
17
+ return (v ?? '').split(',').map((s) => s.trim()).filter(Boolean)
18
+ }
19
+
20
+ export const RPC_URLS: Record<Network, string> = {
21
+ testnet: env.VITE_RPC_TESTNET || 'https://fullnode.testnet.sui.io:443',
22
+ mainnet: env.VITE_RPC_MAINNET || 'https://fullnode.mainnet.sui.io:443',
23
+ }
24
+
25
+ /** Published `seal_policies` package id for this network (required to encrypt/decrypt). */
26
+ export const SEAL_PACKAGE_ID: string =
27
+ netEnv('VITE_SEAL_PACKAGE_ID') ||
28
+ // Meddleware's canonical testnet deployment — override with VITE_SEAL_PACKAGE_ID_TESTNET
29
+ (NETWORK === 'testnet'
30
+ ? '0x9f0563bfe42fbd29932cd280cc47efe17f5339b4dc569eb110114665eecc231e'
31
+ : '')
32
+
33
+ /** Threshold `t` in the t-of-n committee (default 2). */
34
+ export const SEAL_THRESHOLD = Number(env.VITE_SEAL_THRESHOLD || '2')
35
+
36
+ /**
37
+ * The key-server committee. Object ids and aggregator URLs are supplied as parallel CSV lists
38
+ * (index-aligned); aggregatorUrl is required for committee-mode servers.
39
+ */
40
+ export const SEAL_SERVERS: KeyServerConfig[] = (() => {
41
+ const ids = csv(netEnv('VITE_SEAL_SERVER_OBJECT_IDS'))
42
+ const aggs = csv(netEnv('VITE_SEAL_AGGREGATOR_URLS'))
43
+ return ids.map((objectId, i) => ({ objectId, weight: 1, aggregatorUrl: aggs[i] }))
44
+ })()
45
+
46
+ /** Walrus HTTP publisher/aggregator for storing/reading the opaque ciphertext blob. */
47
+ export const WALRUS_PUBLISHER: string =
48
+ netEnv('VITE_WALRUS_PUBLISHER') ||
49
+ (NETWORK === 'mainnet'
50
+ ? 'https://publisher.walrus.space'
51
+ : 'https://publisher.walrus-testnet.walrus.space')
52
+
53
+ export const WALRUS_AGGREGATOR: string =
54
+ netEnv('VITE_WALRUS_AGGREGATOR') ||
55
+ (NETWORK === 'mainnet'
56
+ ? 'https://aggregator.walrus.space'
57
+ : 'https://aggregator.walrus-testnet.walrus.space')
58
+
59
+ /** Default blob lifetime (Walrus storage epochs). */
60
+ export const WALRUS_EPOCHS = Number(env.VITE_WALRUS_EPOCHS || '5')
61
+
62
+ /**
63
+ * Seal committee mode is testnet-only today. On mainnet the app shows a "pending" notice and
64
+ * disables sealing until the committee is available there.
65
+ */
66
+ export const MAINNET_PENDING = NETWORK === 'mainnet'
67
+
68
+ /** True when the on-chain policy package + committee are configured for the active network. */
69
+ export const SEAL_CONFIGURED = Boolean(SEAL_PACKAGE_ID) && SEAL_SERVERS.length > 0
package/src/env.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ declare module '*.vue' {
4
+ import type { DefineComponent } from 'vue'
5
+ const component: DefineComponent<Record<string, unknown>, Record<string, unknown>, unknown>
6
+ export default component
7
+ }
package/src/index.ts ADDED
@@ -0,0 +1,11 @@
1
+ // @meddleware/seal-ui — library entry.
2
+ //
3
+ // Exports the core Sealed Storage tool view (no app shell) for inline embedding in the dashboard.
4
+ // The standalone SPA (App.vue + main.ts) is unaffected and still builds/deploys as before.
5
+ //
6
+ // SealView's styles are scoped, so consumers need only the shared design tokens + UI base at
7
+ // their entry (not seal-ui's global styles.css):
8
+ // import '@meddleware/design-tokens/tokens.css'
9
+ // import '@meddleware/ui/base.css'
10
+
11
+ export { default as SealView } from './components/SealView.vue'
package/src/main.ts ADDED
@@ -0,0 +1,7 @@
1
+ import { createApp } from 'vue'
2
+ import '@meddleware/design-tokens/tokens.css'
3
+ import '@meddleware/ui/base.css'
4
+ import './styles.css'
5
+ import App from './App.vue'
6
+
7
+ createApp(App).mount('#app')
package/src/seal.ts ADDED
@@ -0,0 +1,31 @@
1
+ // Wires the shared @meddleware/seal-client into this app: one registry (drives the policy picker)
2
+ // and one SealController (encrypt/decrypt over the configured committee). Module singletons.
3
+ //
4
+ // SealController is loaded lazily (dynamic import) so @mysten/seal and @noble/curves are kept
5
+ // out of the initial bundle — they're only needed when the user triggers encrypt/decrypt.
6
+ import { createDefaultRegistry, type PolicyRegistry } from '@meddleware/seal-client'
7
+ import type { SealController } from '@meddleware/seal-client/controller'
8
+ import { getSuiClient } from './wallet.js'
9
+ import { NETWORK, SEAL_PACKAGE_ID, SEAL_THRESHOLD, SEAL_SERVERS } from './config.js'
10
+
11
+ /** The policy registry (nft-gate + time-lock as peers). Iterate `list()` to render the picker. */
12
+ export const registry: PolicyRegistry = createDefaultRegistry()
13
+
14
+ let controller: SealController | null = null
15
+
16
+ /** Lazily build the SealController from build-time config + the shared registry. */
17
+ export async function getSealController(): Promise<SealController> {
18
+ if (!controller) {
19
+ const { SealController } = await import('@meddleware/seal-client/controller')
20
+ controller = new SealController(
21
+ {
22
+ suiClient: getSuiClient(NETWORK),
23
+ packageId: SEAL_PACKAGE_ID,
24
+ threshold: SEAL_THRESHOLD,
25
+ serverConfigs: SEAL_SERVERS,
26
+ },
27
+ registry,
28
+ )
29
+ }
30
+ return controller
31
+ }
@@ -0,0 +1,45 @@
1
+ // Discover on-chain SealedContent pointers for a gate by indexing the SealedContentPublished
2
+ // event. Uses the gRPC client's listEvents + a client-side gate_id filter (no indexer needed).
3
+ import { sealedContentEventType, type SealedContentPointer } from '@meddleware/seal-client'
4
+ import { getSuiClient } from './wallet.js'
5
+ import { SEAL_PACKAGE_ID } from './config.js'
6
+
7
+ function normId(id: string): string {
8
+ const lower = id.toLowerCase()
9
+ return lower.startsWith('0x') ? lower : `0x${lower}`
10
+ }
11
+
12
+ interface RawEvent {
13
+ content_id?: string
14
+ gate_id?: string
15
+ blob_id?: string
16
+ seal_id?: string
17
+ label?: string
18
+ publisher?: string
19
+ }
20
+
21
+ /** All published pointers whose `gate_id` matches, newest first. */
22
+ export async function discoverSealedContent(gateId: string): Promise<SealedContentPointer[]> {
23
+ if (!SEAL_PACKAGE_ID) throw new Error('Seal policy package is not configured.')
24
+ const client = getSuiClient()
25
+ // gRPC listEvents returns ascending by default (no cursor → no descending option); reverse at end.
26
+ const res = await client.listEvents({
27
+ filter: { eventType: sealedContentEventType(SEAL_PACKAGE_ID) },
28
+ limit: 200,
29
+ })
30
+ const target = normId(gateId)
31
+ const out: SealedContentPointer[] = []
32
+ for (const ev of res.events) {
33
+ const p = ev.json as RawEvent | undefined
34
+ if (!p?.gate_id || normId(p.gate_id) !== target) continue
35
+ out.push({
36
+ contentId: p.content_id ?? '',
37
+ gateId: p.gate_id,
38
+ blobId: p.blob_id ?? '',
39
+ sealId: p.seal_id ?? '',
40
+ label: p.label ?? '',
41
+ publisher: p.publisher ?? '',
42
+ })
43
+ }
44
+ return out.reverse()
45
+ }
package/src/styles.css ADDED
@@ -0,0 +1,29 @@
1
+ /* Sealed Storage standalone-app globals. Colours come from @meddleware/design-tokens.
2
+ *
3
+ * Only genuinely global element styling lives here (body reset, #app centering). The tool UI's
4
+ * component styles (.card, .field, .primary, .tabs, .notice, .manifest, .muted, inputs, …) are
5
+ * SCOPED inside src/components/SealView.vue so the view can be embedded in the dashboard without
6
+ * this stylesheet — importing it globally would restyle the host's body / #app / bare inputs.
7
+ * This file is imported only by the standalone entry (src/main.ts). */
8
+
9
+ :root {
10
+ color-scheme: dark;
11
+ }
12
+
13
+ body {
14
+ margin: 0;
15
+ font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
16
+ background: var(--bg, #14100f);
17
+ color: var(--text, #f3ece9);
18
+ }
19
+
20
+ #app {
21
+ max-width: 780px;
22
+ margin: 0 auto;
23
+ padding: 1.5rem 1.25rem 4rem;
24
+ }
25
+
26
+ h1 {
27
+ font-size: 1.4rem;
28
+ margin: 0;
29
+ }
package/src/wallet.ts ADDED
@@ -0,0 +1,52 @@
1
+ // Thin seal-ui shim over the shared @meddleware/wallet-adapter singleton.
2
+ //
3
+ // The adapter is network-agnostic (RPC URL passed per call); this shim binds seal-ui's RPC_URLS
4
+ // so call sites keep the ergonomics (`getSuiClient()` defaulting to the active network,
5
+ // `signAndExecute(tx)`). Because the adapter is a module singleton, the wallet connection is
6
+ // shared with any other tool view rendered in the same window (e.g. the dashboard).
7
+ import {
8
+ useWallet as useWalletBase,
9
+ getSuiClient as getSuiClientBase,
10
+ buildExecutor as buildExecutorBase,
11
+ } from '@meddleware/wallet-adapter'
12
+ import type { Transaction } from '@mysten/sui/transactions'
13
+ import { NETWORK, RPC_URLS, type Network } from './config.js'
14
+
15
+ /** Memoised Sui JSON-RPC client for the network (defaults to the active network). */
16
+ export function getSuiClient(network: Network = NETWORK) {
17
+ return getSuiClientBase(network, RPC_URLS[network])
18
+ }
19
+
20
+ /** Sign a personal message with the connected wallet (mints a Seal SessionKey). */
21
+ export function signPersonalMessage(message: Uint8Array): Promise<{ signature: string }> {
22
+ return useWalletBase().signPersonalMessage(message)
23
+ }
24
+
25
+ /** Sign + execute a PTB with the connected wallet, returning the transaction digest. */
26
+ export async function signAndExecute(
27
+ tx: Transaction,
28
+ network: Network = NETWORK,
29
+ ): Promise<{ digest: string }> {
30
+ const executor = await buildExecutorBase(network, RPC_URLS[network])
31
+ return executor.signAndExecute(tx)
32
+ }
33
+
34
+ /**
35
+ * Wallet composable bound to seal-ui's network config. Delegates to the shared adapter singleton;
36
+ * Seal needs `sui:signPersonalMessage` (SessionKey) and `sui:signTransaction` (publish pointer),
37
+ * so both are requested for discovery.
38
+ */
39
+ export function useWallet() {
40
+ const base = useWalletBase({ requiredFeatures: ['sui:signPersonalMessage', 'sui:signTransaction'] })
41
+ return {
42
+ wallets: base.wallets,
43
+ currentWallet: base.currentWallet,
44
+ account: base.account,
45
+ connecting: base.connecting,
46
+ error: base.error,
47
+ connect: base.connect,
48
+ disconnect: base.disconnect,
49
+ signPersonalMessage,
50
+ signAndExecute,
51
+ }
52
+ }
package/src/walrus.ts ADDED
@@ -0,0 +1,30 @@
1
+ // Ciphertext storage via the Walrus HTTP publisher/aggregator. The blob is opaque (already
2
+ // Seal-encrypted), so we need no wallet, no wasm, and no @mysten/walrus SDK here — just fetch.
3
+ // (Routing uploads through the Meddleware relay to capture the tip/commission is a documented
4
+ // follow-up; see the Sealed Storage plan.)
5
+ import { WALRUS_PUBLISHER, WALRUS_AGGREGATOR, WALRUS_EPOCHS } from './config.js'
6
+
7
+ interface PublishResponse {
8
+ newlyCreated?: { blobObject?: { blobId?: string } }
9
+ alreadyCertified?: { blobId?: string }
10
+ }
11
+
12
+ /** Store bytes on Walrus; returns the blob id. */
13
+ export async function storeBlob(bytes: Uint8Array, epochs = WALRUS_EPOCHS): Promise<string> {
14
+ const res = await fetch(`${WALRUS_PUBLISHER}/v1/blobs?epochs=${epochs}`, {
15
+ method: 'PUT',
16
+ body: bytes as BodyInit,
17
+ })
18
+ if (!res.ok) throw new Error(`Walrus publisher error ${res.status}: ${await res.text()}`)
19
+ const json = (await res.json()) as PublishResponse
20
+ const blobId = json.newlyCreated?.blobObject?.blobId ?? json.alreadyCertified?.blobId
21
+ if (!blobId) throw new Error('Walrus publisher returned no blob id.')
22
+ return blobId
23
+ }
24
+
25
+ /** Read bytes back from Walrus by blob id. */
26
+ export async function readBlob(blobId: string): Promise<Uint8Array> {
27
+ const res = await fetch(`${WALRUS_AGGREGATOR}/v1/blobs/${encodeURIComponent(blobId)}`)
28
+ if (!res.ok) throw new Error(`Walrus aggregator error ${res.status}`)
29
+ return new Uint8Array(await res.arrayBuffer())
30
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "lib": ["ESNext", "DOM", "DOM.Iterable"],
7
+ "strict": true,
8
+ "esModuleInterop": true,
9
+ "skipLibCheck": true,
10
+ "resolveJsonModule": true,
11
+ "isolatedModules": true,
12
+ "verbatimModuleSyntax": true,
13
+ "jsx": "preserve",
14
+ "types": ["node"],
15
+ "noEmit": true
16
+ },
17
+ "include": ["src/**/*.ts", "src/**/*.vue", "src/**/*.d.ts", "vite.config.ts"]
18
+ }
package/vite.config.ts ADDED
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from 'vite'
2
+ import vue from '@vitejs/plugin-vue'
3
+
4
+ // @mysten/seal is pure JS (no wasm). Ciphertext storage uses the Walrus HTTP publisher/aggregator
5
+ // (plain fetch), so no @mysten/walrus wasm to exclude here.
6
+ export default defineConfig({
7
+ plugins: [vue()],
8
+ })