@meddleware/seal-ui 0.0.21 → 0.0.23
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/README.md +10 -5
- package/package.json +9 -4
- package/src/App.vue +11 -5
- package/src/components/SealView.vue +62 -35
- package/src/config.ts +116 -92
- package/src/main.ts +7 -0
- package/src/seal.ts +43 -14
- package/src/sealed-content.ts +16 -39
- package/src/walrus.ts +27 -25
package/README.md
CHANGED
|
@@ -21,15 +21,20 @@ package; the client seam is [`@meddleware/seal-client`](https://github.com/meddl
|
|
|
21
21
|
|
|
22
22
|
## Configure (build-time `VITE_*`)
|
|
23
23
|
|
|
24
|
+
The `seal_policies` and `access_gate` ids come from the published `deployments` (seal-client and
|
|
25
|
+
access-gate-client) for the active network; they are not configuration.
|
|
26
|
+
|
|
24
27
|
| Var | Meaning |
|
|
25
28
|
| --- | --- |
|
|
26
|
-
| `VITE_NETWORK` | `testnet` (default) or `mainnet
|
|
27
|
-
| `VITE_SEAL_PACKAGE_ID_{NET}` | Published `seal_policies` package id |
|
|
29
|
+
| `VITE_NETWORK` | Network the standalone build selects: `testnet` (default) or `mainnet`. Embedded, the host's selector rules. |
|
|
28
30
|
| `VITE_SEAL_SERVER_OBJECT_IDS_{NET}` | CSV of key-server object ids (the committee) |
|
|
29
31
|
| `VITE_SEAL_AGGREGATOR_URLS_{NET}` | CSV of aggregator URLs (index-aligned) |
|
|
30
|
-
| `
|
|
31
|
-
| `
|
|
32
|
-
| `
|
|
32
|
+
| `VITE_SEAL_AGGREGATOR_API_KEY_{NET}` | Enoki API key sent as `X-API-Key` to aggregator-backed servers (required by the mainnet aggregator; publishable, baked into the bundle) |
|
|
33
|
+
| `VITE_SEAL_THRESHOLD_{NET}` | `t` over the configured servers; a committee behind an aggregator counts as one (default testnet 2, mainnet 1). An out-of-range value disables sealing on that network with an explanation |
|
|
34
|
+
| `VITE_WALRUS_PUBLISHER_{NET}` | Walrus HTTP publisher. Testnet has a default; **mainnet has none** (Walrus runs no public mainnet publisher), so set an operator-run publisher |
|
|
35
|
+
| `VITE_WALRUS_AGGREGATOR_{NET}` | Walrus HTTP aggregator (Mysten reference endpoints by default) |
|
|
36
|
+
| `VITE_WALRUS_MAX_UPLOAD_BYTES` | Largest ciphertext sent to the publisher (default 10 MiB, the public-publisher limit) |
|
|
37
|
+
| `VITE_INDEXER_URL` | Optional read-indexer for sealed-content discovery (display data; falls back to the full node) |
|
|
33
38
|
|
|
34
39
|
## Develop
|
|
35
40
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meddleware/seal-ui",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.23",
|
|
4
4
|
"homepage": "https://docs.meddleware.co.uk/blockchain/sui/sealed-storage/",
|
|
5
5
|
"description": "Standalone Vue 3 SPA for Sealed Storage — client-side Seal encryption + access-gated, decentralised Walrus storage on Sui.",
|
|
6
6
|
"author": "MeddleWare <dev@meddleware.co.uk>",
|
|
@@ -44,15 +44,20 @@
|
|
|
44
44
|
"access": "public"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
+
"@meddleware/access-gate-client": "^0.0.1",
|
|
47
48
|
"@meddleware/design-tokens": "^0.1.7",
|
|
48
|
-
"@meddleware/seal-client": "^0.0.
|
|
49
|
+
"@meddleware/seal-client": "^0.0.9",
|
|
49
50
|
"@meddleware/ui": "^0.1.28",
|
|
50
|
-
"@meddleware/
|
|
51
|
+
"@meddleware/walrus-client": "^0.0.20",
|
|
51
52
|
"@mysten/sui": "^2.33.1",
|
|
52
53
|
"@mysten/wallet-standard": "^0.21.0",
|
|
53
54
|
"vue": "^3.5.43"
|
|
54
55
|
},
|
|
56
|
+
"peerDependencies": {
|
|
57
|
+
"@meddleware/wallet-adapter": ">=0.0.12 <0.2.0"
|
|
58
|
+
},
|
|
55
59
|
"devDependencies": {
|
|
60
|
+
"@meddleware/wallet-adapter": "^0.0.12",
|
|
56
61
|
"@types/node": "~24.12.2",
|
|
57
62
|
"@vitejs/plugin-vue": "^6.0.9",
|
|
58
63
|
"@vue/eslint-config-typescript": "^14.9.0",
|
|
@@ -71,7 +76,7 @@
|
|
|
71
76
|
"stylelint-config-standard": "^40.0.0",
|
|
72
77
|
"typescript": "~6.0.0",
|
|
73
78
|
"vite": "^8.3.0",
|
|
74
|
-
"vitest": "~5.0.
|
|
79
|
+
"vitest": "~5.0.2",
|
|
75
80
|
"vue-eslint-parser": "^10.4.1",
|
|
76
81
|
"vue-tsc": "~3.3.11"
|
|
77
82
|
},
|
package/src/App.vue
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Standalone shell for the Sealed Storage SPA: app header + footer wrapping the core tool view.
|
|
3
3
|
// The core UI lives in SealView.vue (also exported for inline embedding in the dashboard).
|
|
4
4
|
import { AppHeader, AppFooter, ColorModeControl, useColorMode } from '@meddleware/ui'
|
|
5
|
-
import {
|
|
5
|
+
import { network } from './config.js'
|
|
6
6
|
import SealView from './components/SealView.vue'
|
|
7
7
|
|
|
8
8
|
const { mode, set } = useColorMode('dark')
|
|
@@ -14,17 +14,17 @@ const DEV_URL = import.meta.env.VITE_DEV_URL || 'https://dev.meddleware.co.uk/
|
|
|
14
14
|
<div class="app">
|
|
15
15
|
<AppHeader variant="dark">
|
|
16
16
|
<template #brand>
|
|
17
|
-
<
|
|
17
|
+
<h1 class="brand-title">🔒 Sealed Storage</h1>
|
|
18
18
|
</template>
|
|
19
19
|
<template #actions>
|
|
20
|
-
<span class="badge">{{
|
|
20
|
+
<span class="badge">{{ network }}</span>
|
|
21
21
|
<ColorModeControl :model-value="mode" @update:model-value="set" />
|
|
22
22
|
</template>
|
|
23
23
|
</AppHeader>
|
|
24
24
|
|
|
25
|
-
<
|
|
25
|
+
<main class="app__content">
|
|
26
26
|
<SealView />
|
|
27
|
-
</
|
|
27
|
+
</main>
|
|
28
28
|
|
|
29
29
|
<AppFooter :docs-url="DOCS_URL" :dev-url="DEV_URL" />
|
|
30
30
|
</div>
|
|
@@ -56,4 +56,10 @@ const DEV_URL = import.meta.env.VITE_DEV_URL || 'https://dev.meddleware.co.uk/
|
|
|
56
56
|
color: var(--muted);
|
|
57
57
|
border: 1px solid var(--border);
|
|
58
58
|
}
|
|
59
|
+
|
|
60
|
+
/* The app title is the page's h1; keep the header's own type styles. */
|
|
61
|
+
.brand-title {
|
|
62
|
+
font: inherit;
|
|
63
|
+
margin: 0;
|
|
64
|
+
}
|
|
59
65
|
</style>
|
|
@@ -9,9 +9,8 @@
|
|
|
9
9
|
// globals via main.ts → styles.css.
|
|
10
10
|
import { computed, onMounted, ref, watch } from 'vue'
|
|
11
11
|
import { AppTabNav, UiStepper, UiTabPanel, UiToolIntro, type AppTab, type StepperStep } from '@meddleware/ui'
|
|
12
|
-
import { Transaction } from '@mysten/sui/transactions'
|
|
13
12
|
import {
|
|
14
|
-
|
|
13
|
+
buildPublishSealedContentTransaction,
|
|
15
14
|
type SealedManifest,
|
|
16
15
|
type SealPolicyProvider,
|
|
17
16
|
type SealedContentPointer,
|
|
@@ -20,15 +19,18 @@ import {
|
|
|
20
19
|
import { checkManifestForNetwork } from '../manifest-guard.js'
|
|
21
20
|
import { WalletGuard } from '@meddleware/wallet-adapter'
|
|
22
21
|
import { useWallet, getSuiClient } from '../wallet.js'
|
|
23
|
-
import { registry, getSealController } from '../seal.js'
|
|
22
|
+
import { registry, getSealController, clearSealSessions } from '../seal.js'
|
|
24
23
|
import { storeBlob, readBlob } from '../walrus.js'
|
|
25
24
|
import { discoverSealedContent } from '../sealed-content.js'
|
|
26
|
-
import {
|
|
25
|
+
import { activeConfig, network } from '../config.js'
|
|
27
26
|
|
|
28
27
|
const { account, signPersonalMessage, signAndExecute } = useWallet()
|
|
29
28
|
|
|
30
|
-
const providers = registry.list() as SealPolicyProvider[]
|
|
31
|
-
|
|
29
|
+
const providers = computed(() => registry.value.list() as SealPolicyProvider[])
|
|
30
|
+
|
|
31
|
+
/** Shown inside each tab panel until a wallet is connected. */
|
|
32
|
+
const GUARD_MESSAGE = 'Connect a Sui wallet to encrypt and decrypt sealed content.'
|
|
33
|
+
const disabled = computed(() => activeConfig.value.problem !== null)
|
|
32
34
|
|
|
33
35
|
const TABS: AppTab[] = [
|
|
34
36
|
{ id: 'encrypt', label: 'Encrypt' },
|
|
@@ -65,7 +67,7 @@ watch(tab, () => {
|
|
|
65
67
|
// Force manual mode so the pre-set ID renders in the text input immediately, before suggestions load.
|
|
66
68
|
onMounted(() => {
|
|
67
69
|
const gate = new URLSearchParams(location.search).get('gate')
|
|
68
|
-
if (gate && providers.some((p) => p.type === 'nft-gate')) {
|
|
70
|
+
if (gate && providers.value.some((p) => p.type === 'nft-gate')) {
|
|
69
71
|
encType.value = 'nft-gate'
|
|
70
72
|
encValues.value = { ...encValues.value, gateId: gate }
|
|
71
73
|
encGateManual.value = true
|
|
@@ -83,6 +85,15 @@ function coerce(kind: string, raw: string | boolean): unknown {
|
|
|
83
85
|
return String(raw)
|
|
84
86
|
}
|
|
85
87
|
|
|
88
|
+
/**
|
|
89
|
+
* A download filename derived from an untrusted label (on-chain pointers are written by anyone):
|
|
90
|
+
* keep word characters, dots and dashes, cap the length, and never start with a dot.
|
|
91
|
+
*/
|
|
92
|
+
function safeFileName(label: string | undefined, fallback: string): string {
|
|
93
|
+
const cleaned = (label ?? '').replace(/[^\w.-]/g, '_').replace(/^\.+/, '').slice(0, 100)
|
|
94
|
+
return cleaned || fallback
|
|
95
|
+
}
|
|
96
|
+
|
|
86
97
|
function triggerDownload(blob: Blob, name: string): void {
|
|
87
98
|
const url = URL.createObjectURL(blob)
|
|
88
99
|
const a = document.createElement('a')
|
|
@@ -108,8 +119,8 @@ async function fetchGateSuggestions(provider: SealPolicyProvider | null): Promis
|
|
|
108
119
|
}
|
|
109
120
|
|
|
110
121
|
// ── Encrypt ──────────────────────────────────────────────────────────────────
|
|
111
|
-
const encType = ref(providers[0]?.type ?? '')
|
|
112
|
-
const encProvider = computed(() => providers.find((p) => p.type === encType.value) ?? null)
|
|
122
|
+
const encType = ref(providers.value[0]?.type ?? '')
|
|
123
|
+
const encProvider = computed(() => providers.value.find((p) => p.type === encType.value) ?? null)
|
|
113
124
|
const encValues = ref<Record<string, string | boolean>>({})
|
|
114
125
|
|
|
115
126
|
/** One input per policy field; its type follows the field kind (v-model handles each type). */
|
|
@@ -126,6 +137,23 @@ function onEncFile(e: Event): void {
|
|
|
126
137
|
encFile.value = (e.target as HTMLInputElement).files?.[0] ?? null
|
|
127
138
|
}
|
|
128
139
|
|
|
140
|
+
// SessionKeys are per address: drop them whenever the wallet disconnects or switches account.
|
|
141
|
+
watch(
|
|
142
|
+
() => account.value?.address,
|
|
143
|
+
(next, prev) => {
|
|
144
|
+
if (prev && next !== prev) clearSealSessions()
|
|
145
|
+
},
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
// A network switch invalidates everything shown for the previous network.
|
|
149
|
+
watch(network, () => {
|
|
150
|
+
manifest.value = null
|
|
151
|
+
publishDigest.value = null
|
|
152
|
+
discovered.value = []
|
|
153
|
+
status.value = null
|
|
154
|
+
errorMsg.value = null
|
|
155
|
+
})
|
|
156
|
+
|
|
129
157
|
watch(
|
|
130
158
|
[() => account.value?.address, encProvider],
|
|
131
159
|
async ([, provider]) => {
|
|
@@ -143,10 +171,10 @@ watch(
|
|
|
143
171
|
)
|
|
144
172
|
|
|
145
173
|
watch(
|
|
146
|
-
[() => account.value?.address, tab],
|
|
174
|
+
[() => account.value?.address, tab, network],
|
|
147
175
|
async ([, t]) => {
|
|
148
176
|
if (t !== 'unlock') return
|
|
149
|
-
const nftGateProvider = providers.find((p) => p.type === 'nft-gate') ?? null
|
|
177
|
+
const nftGateProvider = providers.value.find((p) => p.type === 'nft-gate') ?? null
|
|
150
178
|
unlockGateLoading.value = true
|
|
151
179
|
unlockGateManual.value = false
|
|
152
180
|
try {
|
|
@@ -181,12 +209,14 @@ async function performEncrypt(): Promise<void> {
|
|
|
181
209
|
const data = new Uint8Array(await encFile.value.arrayBuffer())
|
|
182
210
|
const { id, ciphertext } = await (await getSealController()).encrypt(provider.type, params, data)
|
|
183
211
|
status.value = 'Storing ciphertext on Walrus…'
|
|
184
|
-
const
|
|
212
|
+
const owner = account.value?.address
|
|
213
|
+
if (!owner) throw new Error('Connect your wallet to store the ciphertext.')
|
|
214
|
+
const blobId = await storeBlob(ciphertext, { sendObjectTo: owner })
|
|
185
215
|
manifest.value = {
|
|
186
216
|
policyType: provider.type,
|
|
187
217
|
id,
|
|
188
218
|
blobId,
|
|
189
|
-
network:
|
|
219
|
+
network: network.value,
|
|
190
220
|
params,
|
|
191
221
|
label: encLabel.value || encFile.value.name,
|
|
192
222
|
}
|
|
@@ -202,7 +232,7 @@ async function performEncrypt(): Promise<void> {
|
|
|
202
232
|
function downloadManifest(): void {
|
|
203
233
|
if (!manifest.value) return
|
|
204
234
|
const blob = new Blob([JSON.stringify(manifest.value, null, 2)], { type: 'application/json' })
|
|
205
|
-
triggerDownload(blob, `${manifest.value.label
|
|
235
|
+
triggerDownload(blob, `${safeFileName(manifest.value.label, 'sealed')}.seal.json`)
|
|
206
236
|
}
|
|
207
237
|
|
|
208
238
|
// ── Decrypt ──────────────────────────────────────────────────────────────────
|
|
@@ -212,11 +242,11 @@ const decValues = ref<Record<string, string | boolean>>({})
|
|
|
212
242
|
// AND enforce that it targets the network this app is built for — a manifest sealed on another
|
|
213
243
|
// network references a package + committee that don't exist here and can never decrypt. The
|
|
214
244
|
// `decManifestError` is surfaced next to the input so a rejection explains itself.
|
|
215
|
-
const decCheck = computed(() => checkManifestForNetwork(decManifestText.value,
|
|
245
|
+
const decCheck = computed(() => checkManifestForNetwork(decManifestText.value, network.value))
|
|
216
246
|
const decManifest = computed<SealedManifest | null>(() => decCheck.value.manifest)
|
|
217
247
|
const decManifestError = computed<string | null>(() => decCheck.value.error)
|
|
218
248
|
const decProvider = computed(() =>
|
|
219
|
-
decManifest.value ? (providers.find((p) => p.type === decManifest.value?.policyType) ?? null) : null,
|
|
249
|
+
decManifest.value ? (providers.value.find((p) => p.type === decManifest.value?.policyType) ?? null) : null,
|
|
220
250
|
)
|
|
221
251
|
|
|
222
252
|
async function onManifestFile(e: Event): Promise<void> {
|
|
@@ -254,7 +284,7 @@ async function performDecrypt(): Promise<void> {
|
|
|
254
284
|
address: account.value.address,
|
|
255
285
|
signPersonalMessage,
|
|
256
286
|
})
|
|
257
|
-
triggerDownload(new Blob([plaintext.slice().buffer as ArrayBuffer]), m.label
|
|
287
|
+
triggerDownload(new Blob([plaintext.slice().buffer as ArrayBuffer]), safeFileName(m.label, 'decrypted'))
|
|
258
288
|
status.value = 'Decrypted — download started.'
|
|
259
289
|
} catch (e) {
|
|
260
290
|
errorMsg.value = e instanceof Error ? e.message : String(e)
|
|
@@ -280,8 +310,9 @@ async function performPublish(): Promise<void> {
|
|
|
280
310
|
busy.value = true
|
|
281
311
|
try {
|
|
282
312
|
status.value = 'Publishing on-chain pointer — approve the transaction in your wallet…'
|
|
283
|
-
const
|
|
284
|
-
|
|
313
|
+
const seal = activeConfig.value.seal
|
|
314
|
+
if (!seal) throw new Error(activeConfig.value.problem ?? 'Sealed Storage is not available on this network.')
|
|
315
|
+
const tx = buildPublishSealedContentTransaction(seal.publishedAt, {
|
|
285
316
|
gateId: String(m.params.gateId),
|
|
286
317
|
blobId: m.blobId,
|
|
287
318
|
sealId: m.id,
|
|
@@ -347,7 +378,7 @@ async function performUnlock(item: SealedContentPointer): Promise<void> {
|
|
|
347
378
|
ciphertext,
|
|
348
379
|
{ address: account.value.address, signPersonalMessage },
|
|
349
380
|
)
|
|
350
|
-
triggerDownload(new Blob([plaintext.slice().buffer as ArrayBuffer]), item.label
|
|
381
|
+
triggerDownload(new Blob([plaintext.slice().buffer as ArrayBuffer]), safeFileName(item.label, 'decrypted'))
|
|
351
382
|
status.value = 'Decrypted — download started.'
|
|
352
383
|
} catch (e) {
|
|
353
384
|
errorMsg.value = e instanceof Error ? e.message : String(e)
|
|
@@ -362,26 +393,18 @@ async function performUnlock(item: SealedContentPointer): Promise<void> {
|
|
|
362
393
|
|
|
363
394
|
<UiToolIntro>Client-side encrypted, access-gated storage on Walrus + Sui.</UiToolIntro>
|
|
364
395
|
|
|
365
|
-
<p v-if="
|
|
366
|
-
<template v-if="NETWORK === 'mainnet'">
|
|
367
|
-
Seal is not configured for mainnet. Set
|
|
368
|
-
<code>VITE_SEAL_PACKAGE_ID_MAINNET</code>,
|
|
369
|
-
<code>VITE_SEAL_SERVER_OBJECT_IDS_MAINNET</code>, and
|
|
370
|
-
<code>VITE_SEAL_AGGREGATOR_URLS_MAINNET</code>, then redeploy.
|
|
371
|
-
</template>
|
|
372
|
-
<template v-else>
|
|
373
|
-
This deployment has no Seal policy package or key-server committee configured
|
|
374
|
-
(<code>VITE_SEAL_PACKAGE_ID_*</code> / <code>VITE_SEAL_SERVER_OBJECT_IDS_*</code>).
|
|
375
|
-
</template>
|
|
376
|
-
</p>
|
|
396
|
+
<p v-if="activeConfig.problem" class="notice notice--warn">{{ activeConfig.problem }}</p>
|
|
377
397
|
|
|
378
|
-
|
|
379
|
-
|
|
398
|
+
<!-- The tab list and every panel always render (each tab controls a live panel); the wallet
|
|
399
|
+
prompt replaces only a panel's content until a wallet is connected. Panels stay mounted
|
|
400
|
+
(v-show) so each tab keeps its state. -->
|
|
401
|
+
<AppTabNav v-model="tab" :tabs="TABS" id-prefix="seal" aria-label="Sealed storage" all-panels class="seal-tabs" />
|
|
380
402
|
|
|
381
403
|
<p v-if="errorMsg" class="notice notice--error" role="alert">{{ errorMsg }}</p>
|
|
382
404
|
|
|
383
405
|
<!-- Encrypt -->
|
|
384
406
|
<UiTabPanel v-show="tab === 'encrypt'" id-prefix="seal" tab="encrypt" class="card">
|
|
407
|
+
<WalletGuard :message="GUARD_MESSAGE">
|
|
385
408
|
<UiStepper :steps="ENC_STEPS" v-model="encStep" />
|
|
386
409
|
|
|
387
410
|
<!-- Step 0: Policy -->
|
|
@@ -488,10 +511,12 @@ async function performUnlock(item: SealedContentPointer): Promise<void> {
|
|
|
488
511
|
<button type="button" class="link" @click="encStep--">Back</button>
|
|
489
512
|
</div>
|
|
490
513
|
</template>
|
|
514
|
+
</WalletGuard>
|
|
491
515
|
</UiTabPanel>
|
|
492
516
|
|
|
493
517
|
<!-- Unlock -->
|
|
494
518
|
<UiTabPanel v-show="tab === 'unlock'" id-prefix="seal" tab="unlock" class="card">
|
|
519
|
+
<WalletGuard :message="GUARD_MESSAGE">
|
|
495
520
|
<UiStepper :steps="UNLOCK_STEPS" v-model="unlockStep" />
|
|
496
521
|
|
|
497
522
|
<!-- Step 0: Gate selector -->
|
|
@@ -548,10 +573,12 @@ async function performUnlock(item: SealedContentPointer): Promise<void> {
|
|
|
548
573
|
<button type="button" class="link" @click="unlockStep--; discovered = []">Back</button>
|
|
549
574
|
</div>
|
|
550
575
|
</template>
|
|
576
|
+
</WalletGuard>
|
|
551
577
|
</UiTabPanel>
|
|
552
578
|
|
|
553
579
|
<!-- Decrypt -->
|
|
554
580
|
<UiTabPanel v-show="tab === 'decrypt'" id-prefix="seal" tab="decrypt" class="card">
|
|
581
|
+
<WalletGuard :message="GUARD_MESSAGE">
|
|
555
582
|
<UiStepper :steps="DEC_STEPS" v-model="decStep" />
|
|
556
583
|
|
|
557
584
|
<!-- Step 0: Manifest input -->
|
|
@@ -600,6 +627,7 @@ async function performUnlock(item: SealedContentPointer): Promise<void> {
|
|
|
600
627
|
</button>
|
|
601
628
|
</div>
|
|
602
629
|
</template>
|
|
630
|
+
</WalletGuard>
|
|
603
631
|
</UiTabPanel>
|
|
604
632
|
|
|
605
633
|
<p v-if="status" class="status muted">{{ status }}</p>
|
|
@@ -608,7 +636,6 @@ async function performUnlock(item: SealedContentPointer): Promise<void> {
|
|
|
608
636
|
Decryption keys are released by a threshold committee of independent key servers. If enough
|
|
609
637
|
servers are unreachable, decryption pauses — storage and retrieval are unaffected.
|
|
610
638
|
</p>
|
|
611
|
-
</WalletGuard>
|
|
612
639
|
</template>
|
|
613
640
|
|
|
614
641
|
<style scoped>
|
package/src/config.ts
CHANGED
|
@@ -1,117 +1,141 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
1
|
+
// Configuration. The network is wallet-adapter's shared runtime selector (the standalone build
|
|
2
|
+
// selects VITE_NETWORK in main.ts; embedded, the host's selector rules). Everything network-specific
|
|
3
|
+
// is resolved for that network:
|
|
4
|
+
// - package ids (seal_policies, access_gate) come from the published `deployments` — never env;
|
|
5
|
+
// - the key-server committee, threshold and Walrus endpoints are operator settings baked in as
|
|
6
|
+
// VITE_*_{NET} build vars, with Mysten defaults where they exist.
|
|
7
|
+
import { computed } from 'vue'
|
|
3
8
|
import type { KeyServerConfig } from '@meddleware/seal-client'
|
|
9
|
+
import { SEAL_POLICIES_DEPLOYMENTS, type SealPoliciesDeployment } from '@meddleware/seal-client/deployments'
|
|
10
|
+
import { ACCESS_GATE_DEPLOYMENTS, type AccessGateDeployment } from '@meddleware/access-gate-client/deployments'
|
|
11
|
+
import { useNetwork } from '@meddleware/wallet-adapter'
|
|
4
12
|
|
|
5
|
-
|
|
13
|
+
type EnvSource = Record<string, string | undefined>
|
|
14
|
+
const env: EnvSource = (import.meta as unknown as { env?: EnvSource }).env ?? {}
|
|
6
15
|
|
|
7
|
-
|
|
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
|
-
}
|
|
16
|
+
/** The active network (read-only ref). */
|
|
17
|
+
export const network = useNetwork().network
|
|
15
18
|
|
|
16
19
|
function csv(v: string | undefined): string[] {
|
|
17
20
|
return (v ?? '').split(',').map((s) => s.trim()).filter(Boolean)
|
|
18
21
|
}
|
|
19
22
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Mysten key-server defaults. Testnet: the decentralized committee (via its aggregator) plus two
|
|
25
|
+
* independent servers. Mainnet: the verified 5-of-8 committee behind the mainnet aggregator, which
|
|
26
|
+
* needs an Enoki API key (`VITE_SEAL_AGGREGATOR_API_KEY_MAINNET`, a publishable client key scoped
|
|
27
|
+
* to Seal — it is baked into the bundle). A committee counts as ONE server.
|
|
28
|
+
*/
|
|
29
|
+
const SERVER_DEFAULTS: Record<string, { ids: string[]; aggs: string[]; threshold: number }> = {
|
|
30
|
+
testnet: {
|
|
31
|
+
ids: [
|
|
32
|
+
'0xb012378c9f3799fb5b1a7083da74a4069e3c3f1c93de0b27212a5799ce1e1e98',
|
|
33
|
+
'0x73d05d62c18d9374e3ea529e8e0ed6161da1a141a94d3f76ae3fe4e99356db75',
|
|
34
|
+
'0xf5d14a81a982144ae441cd7d64b09027f116a468bd36e7eca494f750591623c8',
|
|
35
|
+
],
|
|
36
|
+
aggs: ['https://seal-aggregator-testnet.mystenlabs.com'],
|
|
37
|
+
threshold: 2,
|
|
38
|
+
},
|
|
39
|
+
mainnet: {
|
|
40
|
+
ids: ['0x686098f1439237fff9f36b99c7329683c22979d2005c2465cb891acb012a7595'],
|
|
41
|
+
aggs: ['https://seal-aggregator-mainnet.mystenlabs.com'],
|
|
42
|
+
threshold: 1,
|
|
43
|
+
},
|
|
23
44
|
}
|
|
24
45
|
|
|
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
|
-
? '0x42cc181f851ef702c1fddc9b925553f03b71784edff49d80fbc260055f86d612'
|
|
31
|
-
: '')
|
|
32
|
-
|
|
33
46
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
47
|
+
* Walrus HTTP endpoints. Walrus runs no public publisher on mainnet (a publisher pays SUI + WAL
|
|
48
|
+
* itself), so mainnet has no publisher default: set `VITE_WALRUS_PUBLISHER_MAINNET` to an
|
|
49
|
+
* operator-run authenticated publisher.
|
|
37
50
|
*/
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
51
|
+
const WALRUS_DEFAULTS: Record<string, { publisher: string; aggregator: string }> = {
|
|
52
|
+
testnet: {
|
|
53
|
+
publisher: 'https://publisher.walrus-testnet.walrus.space',
|
|
54
|
+
aggregator: 'https://aggregator.walrus-testnet.walrus.space',
|
|
55
|
+
},
|
|
56
|
+
mainnet: { publisher: '', aggregator: 'https://aggregator.walrus-mainnet.walrus.space' },
|
|
57
|
+
}
|
|
41
58
|
|
|
42
59
|
/**
|
|
43
|
-
* The
|
|
44
|
-
* (
|
|
45
|
-
* independent servers derive their URL from the on-chain object and do not need one.
|
|
46
|
-
*
|
|
47
|
-
* Testnet defaults — three verified Mysten Labs servers:
|
|
48
|
-
* [0] Decentralized (Mysten aggregator) — requires aggregatorUrl
|
|
49
|
-
* [1] Independent server 1
|
|
50
|
-
* [2] Independent server 2
|
|
51
|
-
* Override via VITE_SEAL_SERVER_OBJECT_IDS_TESTNET / VITE_SEAL_AGGREGATOR_URLS_TESTNET.
|
|
52
|
-
*
|
|
53
|
-
* Mainnet defaults — Mysten Labs 5-of-8 decentralized committee:
|
|
54
|
-
* Committee object: 0x686098f1439237fff9f36b99c7329683c22979d2005c2465cb891acb012a7595
|
|
55
|
-
* Aggregator: https://seal-aggregator-mainnet.mystenlabs.com
|
|
56
|
-
* NOTE: The mainnet aggregator requires an Enoki API key (X-API-Key header).
|
|
57
|
-
* Override via VITE_SEAL_SERVER_OBJECT_IDS_MAINNET / VITE_SEAL_AGGREGATOR_URLS_MAINNET.
|
|
58
|
-
* When independent open-mode key servers become available on mainnet, they can be added here.
|
|
60
|
+
* The threshold error for a committee, or null when the pair is valid. A threshold no committee could
|
|
61
|
+
* meet (or a non-integer one) is reported instead of failing at the first decrypt.
|
|
59
62
|
*/
|
|
60
|
-
export
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
'0x73d05d62c18d9374e3ea529e8e0ed6161da1a141a94d3f76ae3fe4e99356db75',
|
|
66
|
-
'0xf5d14a81a982144ae441cd7d64b09027f116a468bd36e7eca494f750591623c8',
|
|
67
|
-
],
|
|
68
|
-
aggs: ['https://seal-aggregator-testnet.mystenlabs.com'],
|
|
69
|
-
},
|
|
70
|
-
mainnet: {
|
|
71
|
-
ids: ['0x686098f1439237fff9f36b99c7329683c22979d2005c2465cb891acb012a7595'],
|
|
72
|
-
aggs: ['https://seal-aggregator-mainnet.mystenlabs.com'],
|
|
73
|
-
},
|
|
63
|
+
export function thresholdError(threshold: number, servers: readonly { weight?: number }[]): string | null {
|
|
64
|
+
const total = servers.reduce((sum, s) => sum + (s.weight ?? 1), 0)
|
|
65
|
+
if (servers.length === 0) return null // no committee — reported as its own problem
|
|
66
|
+
if (!Number.isInteger(threshold) || threshold < 1 || threshold > total) {
|
|
67
|
+
return `The Seal threshold must be an integer in [1, ${total}] for ${servers.length} configured server(s); got ${threshold}`
|
|
74
68
|
}
|
|
69
|
+
return null
|
|
70
|
+
}
|
|
75
71
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
72
|
+
/** Everything sealing needs on one network. */
|
|
73
|
+
export interface SealConfig {
|
|
74
|
+
network: string
|
|
75
|
+
/** The `seal_policies` deployment, or null where none is recorded. */
|
|
76
|
+
seal: SealPoliciesDeployment | null
|
|
77
|
+
/** The `access_gate` original id ('' where none is recorded; gate suggestions are then off). */
|
|
78
|
+
accessGateOriginalId: string
|
|
79
|
+
servers: KeyServerConfig[]
|
|
80
|
+
threshold: number
|
|
81
|
+
walrusPublisher: string
|
|
82
|
+
walrusAggregator: string
|
|
83
|
+
/** Why sealing is unavailable on this network, or null when it is configured. */
|
|
84
|
+
problem: string | null
|
|
85
|
+
}
|
|
82
86
|
|
|
83
|
-
/**
|
|
84
|
-
export
|
|
85
|
-
|
|
86
|
-
(
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
/** Resolve the configuration for `net` (pure; `envSource` is injectable for tests). */
|
|
88
|
+
export function sealConfig(net: string, envSource: EnvSource = env): SealConfig {
|
|
89
|
+
const NET = net.toUpperCase()
|
|
90
|
+
const netEnv = (base: string) => envSource[`${base}_${NET}`]
|
|
91
|
+
const seal = (SEAL_POLICIES_DEPLOYMENTS as Record<string, SealPoliciesDeployment>)[net] ?? null
|
|
92
|
+
const accessGate = (ACCESS_GATE_DEPLOYMENTS as Record<string, AccessGateDeployment>)[net] ?? null
|
|
89
93
|
|
|
90
|
-
|
|
91
|
-
netEnv('
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
94
|
+
const defaults = SERVER_DEFAULTS[net]
|
|
95
|
+
const envIds = csv(netEnv('VITE_SEAL_SERVER_OBJECT_IDS'))
|
|
96
|
+
const envAggs = csv(netEnv('VITE_SEAL_AGGREGATOR_URLS'))
|
|
97
|
+
const ids = envIds.length ? envIds : (defaults?.ids ?? [])
|
|
98
|
+
const aggs = envAggs.length ? envAggs : (defaults?.aggs ?? [])
|
|
99
|
+
const apiKey = netEnv('VITE_SEAL_AGGREGATOR_API_KEY')
|
|
100
|
+
const servers: KeyServerConfig[] = ids.map((objectId, i) => {
|
|
101
|
+
const aggregatorUrl = aggs[i]
|
|
102
|
+
return aggregatorUrl && apiKey
|
|
103
|
+
? { objectId, weight: 1, aggregatorUrl, apiKeyName: 'X-API-Key', apiKey }
|
|
104
|
+
: { objectId, weight: 1, aggregatorUrl }
|
|
105
|
+
})
|
|
106
|
+
const rawThreshold = netEnv('VITE_SEAL_THRESHOLD')
|
|
107
|
+
const threshold = rawThreshold ? Number(rawThreshold) : (defaults?.threshold ?? 1)
|
|
108
|
+
|
|
109
|
+
const walrus = WALRUS_DEFAULTS[net]
|
|
110
|
+
const problem = !seal
|
|
111
|
+
? `Sealed Storage is not available on ${net}: no seal_policies deployment is recorded for it.`
|
|
112
|
+
: servers.length === 0
|
|
113
|
+
? `Sealed Storage is not configured on ${net}: set VITE_SEAL_SERVER_OBJECT_IDS_${NET} (and VITE_SEAL_AGGREGATOR_URLS_${NET} for a committee).`
|
|
114
|
+
: thresholdError(threshold, servers)
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
network: net,
|
|
118
|
+
seal,
|
|
119
|
+
accessGateOriginalId: accessGate?.originalId ?? '',
|
|
120
|
+
servers,
|
|
121
|
+
threshold,
|
|
122
|
+
walrusPublisher: netEnv('VITE_WALRUS_PUBLISHER') || walrus?.publisher || '',
|
|
123
|
+
walrusAggregator: netEnv('VITE_WALRUS_AGGREGATOR') || walrus?.aggregator || '',
|
|
124
|
+
problem,
|
|
125
|
+
}
|
|
126
|
+
}
|
|
95
127
|
|
|
96
|
-
/**
|
|
97
|
-
export const
|
|
128
|
+
/** The configuration for the active network. */
|
|
129
|
+
export const activeConfig = computed(() => sealConfig(network.value))
|
|
98
130
|
|
|
99
131
|
/**
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
* Kept in sync with access-gate-ui/src/constants.ts (same deployment).
|
|
132
|
+
* Largest ciphertext the app sends to the publisher. Public publishers cap requests at 10 MiB;
|
|
133
|
+
* an operator-run publisher may allow more via VITE_WALRUS_MAX_UPLOAD_BYTES.
|
|
103
134
|
*/
|
|
104
|
-
export const
|
|
105
|
-
testnet: '0x1a81ca177db039585e575beeeee4759466e55910e936a6733e38dbb65025eea4',
|
|
106
|
-
mainnet: '',
|
|
107
|
-
}
|
|
135
|
+
export const WALRUS_MAX_UPLOAD_BYTES = Number(env.VITE_WALRUS_MAX_UPLOAD_BYTES || String(10 * 1024 * 1024))
|
|
108
136
|
|
|
109
|
-
/**
|
|
110
|
-
export const
|
|
137
|
+
/** Default blob lifetime (Walrus storage epochs). */
|
|
138
|
+
export const WALRUS_EPOCHS = Number(env.VITE_WALRUS_EPOCHS || '5')
|
|
111
139
|
|
|
112
|
-
/**
|
|
113
|
-
|
|
114
|
-
* On mainnet, populate VITE_SEAL_PACKAGE_ID_MAINNET + VITE_SEAL_SERVER_OBJECT_IDS_MAINNET +
|
|
115
|
-
* VITE_SEAL_AGGREGATOR_URLS_MAINNET to enable sealing — no code change required.
|
|
116
|
-
*/
|
|
117
|
-
export const MAINNET_PENDING = !SEAL_CONFIGURED
|
|
140
|
+
/** Optional read-indexer for sealed-content discovery (display data; falls back to the full node). */
|
|
141
|
+
export const INDEXER_URL: string = env.VITE_INDEXER_URL || ''
|
package/src/main.ts
CHANGED
|
@@ -4,7 +4,14 @@ import '@meddleware/design-tokens/seasons.css'
|
|
|
4
4
|
import '@meddleware/ui/base.css'
|
|
5
5
|
import './styles.css'
|
|
6
6
|
import { useSeason } from '@meddleware/ui'
|
|
7
|
+
import { useNetwork } from '@meddleware/wallet-adapter'
|
|
7
8
|
import App from './App.vue'
|
|
8
9
|
|
|
10
|
+
// The standalone build targets one network; select it in the shared selector so the client, the
|
|
11
|
+
// ids, the key servers and the Walrus endpoints all agree. (Embedded, the host's selector rules.)
|
|
12
|
+
const env = (import.meta as unknown as { env?: Record<string, string | undefined> }).env ?? {}
|
|
13
|
+
const built = env.VITE_NETWORK || 'testnet'
|
|
14
|
+
if (built === 'testnet' || built === 'mainnet') useNetwork().setNetwork(built)
|
|
15
|
+
|
|
9
16
|
useSeason()
|
|
10
17
|
createApp(App).mount('#app')
|
package/src/seal.ts
CHANGED
|
@@ -1,31 +1,60 @@
|
|
|
1
|
-
// Wires the shared @meddleware/seal-client into this app:
|
|
2
|
-
// and
|
|
1
|
+
// Wires the shared @meddleware/seal-client into this app: a policy registry (drives the picker)
|
|
2
|
+
// and a SealController (encrypt/decrypt over the configured committee), each per network.
|
|
3
3
|
//
|
|
4
4
|
// SealController is loaded lazily (dynamic import) so @mysten/seal and @noble/curves are kept
|
|
5
5
|
// out of the initial bundle — they're only needed when the user triggers encrypt/decrypt.
|
|
6
|
+
import { computed } from 'vue'
|
|
6
7
|
import { createDefaultRegistry, type PolicyRegistry } from '@meddleware/seal-client'
|
|
7
8
|
import type { SealController } from '@meddleware/seal-client/controller'
|
|
8
9
|
import { getSuiClient } from './wallet.js'
|
|
9
|
-
import {
|
|
10
|
+
import { activeConfig, type SealConfig } from './config.js'
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
const registries = new Map<string, PolicyRegistry>()
|
|
13
|
+
const controllers = new Map<string, SealController>()
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
/** The policy registry for a network (nft-gate + time-lock as peers). */
|
|
16
|
+
export function registryFor(cfg: SealConfig): PolicyRegistry {
|
|
17
|
+
let r = registries.get(cfg.network)
|
|
18
|
+
if (!r) {
|
|
19
|
+
r = createDefaultRegistry(cfg.accessGateOriginalId)
|
|
20
|
+
registries.set(cfg.network, r)
|
|
21
|
+
}
|
|
22
|
+
return r
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The registry for the active network. Iterate `list()` to render the picker. */
|
|
26
|
+
export const registry = computed(() => registryFor(activeConfig.value))
|
|
15
27
|
|
|
16
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* The SealController for the active network, built on first use.
|
|
30
|
+
*
|
|
31
|
+
* @throws {Error} when sealing is not configured on the active network (the reason is the message).
|
|
32
|
+
*/
|
|
17
33
|
export async function getSealController(): Promise<SealController> {
|
|
18
|
-
|
|
34
|
+
const cfg = activeConfig.value
|
|
35
|
+
if (cfg.problem || !cfg.seal) throw new Error(cfg.problem ?? `Sealed Storage is not available on ${cfg.network}.`)
|
|
36
|
+
let c = controllers.get(cfg.network)
|
|
37
|
+
if (!c) {
|
|
19
38
|
const { SealController } = await import('@meddleware/seal-client/controller')
|
|
20
|
-
|
|
39
|
+
c = new SealController(
|
|
21
40
|
{
|
|
22
41
|
suiClient: getSuiClient(),
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
42
|
+
originalId: cfg.seal.originalId,
|
|
43
|
+
publishedAt: cfg.seal.publishedAt,
|
|
44
|
+
threshold: cfg.threshold,
|
|
45
|
+
serverConfigs: cfg.servers,
|
|
26
46
|
},
|
|
27
|
-
|
|
47
|
+
registryFor(cfg),
|
|
28
48
|
)
|
|
49
|
+
controllers.set(cfg.network, c)
|
|
29
50
|
}
|
|
30
|
-
return
|
|
51
|
+
return c
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Drop every cached SessionKey (wallet disconnected or switched account). A no-op before a
|
|
56
|
+
* controller has been loaded, so calling it never pulls `@mysten/seal` into the bundle.
|
|
57
|
+
*/
|
|
58
|
+
export function clearSealSessions(): void {
|
|
59
|
+
for (const c of controllers.values()) c.clearSession()
|
|
31
60
|
}
|
package/src/sealed-content.ts
CHANGED
|
@@ -1,45 +1,22 @@
|
|
|
1
|
-
// Discover on-chain SealedContent pointers for a gate
|
|
2
|
-
// event
|
|
3
|
-
|
|
1
|
+
// Discover on-chain SealedContent pointers for a gate with seal-client's `listSealedContent`
|
|
2
|
+
// (exact event type at the deployment's original id; BCS-decoded). Reads the read-indexer when
|
|
3
|
+
// VITE_INDEXER_URL is set — display data only, falling back to the full node.
|
|
4
|
+
import { listSealedContent, type SealedContentPointer } from '@meddleware/seal-client'
|
|
4
5
|
import { getSuiClient } from './wallet.js'
|
|
5
|
-
import {
|
|
6
|
+
import { activeConfig, INDEXER_URL } from './config.js'
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
}
|
|
8
|
+
/** Most pointers listed for one gate. */
|
|
9
|
+
export const MAX_DISCOVERED = 200
|
|
20
10
|
|
|
21
|
-
/**
|
|
11
|
+
/** Published pointers for `gateId` on the active network, newest first. */
|
|
22
12
|
export async function discoverSealedContent(gateId: string): Promise<SealedContentPointer[]> {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
limit:
|
|
13
|
+
const cfg = activeConfig.value
|
|
14
|
+
if (!cfg.seal) throw new Error(cfg.problem ?? `Sealed Storage is not available on ${cfg.network}.`)
|
|
15
|
+
const page = await listSealedContent(getSuiClient(), {
|
|
16
|
+
originalId: cfg.seal.originalId,
|
|
17
|
+
gateId,
|
|
18
|
+
limit: MAX_DISCOVERED,
|
|
19
|
+
indexer: INDEXER_URL ? { url: INDEXER_URL, network: cfg.network } : undefined,
|
|
29
20
|
})
|
|
30
|
-
|
|
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()
|
|
21
|
+
return page.pointers
|
|
45
22
|
}
|
package/src/walrus.ts
CHANGED
|
@@ -1,30 +1,32 @@
|
|
|
1
|
-
// Ciphertext storage via the Walrus HTTP publisher/aggregator. The
|
|
2
|
-
// Seal-encrypted), so
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
import {
|
|
1
|
+
// Ciphertext storage via the Walrus HTTP publisher/aggregator (@meddleware/walrus-client/http). The
|
|
2
|
+
// blob is opaque (already Seal-encrypted), so no wallet, wasm or @mysten/walrus is involved. This
|
|
3
|
+
// module only applies the active network's endpoints and the app's limits.
|
|
4
|
+
import { readBlob as readViaAggregator, storeBlobViaPublisher } from '@meddleware/walrus-client/http'
|
|
5
|
+
import { activeConfig, WALRUS_EPOCHS, WALRUS_MAX_UPLOAD_BYTES } from './config.js'
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Store the ciphertext as a permanent blob whose `Blob` object goes to `sendObjectTo` (the user, not
|
|
9
|
+
* the publisher). Returns the blob id.
|
|
10
|
+
*/
|
|
11
|
+
export async function storeBlob(bytes: Uint8Array, opts: { sendObjectTo: string }): Promise<string> {
|
|
12
|
+
const cfg = activeConfig.value
|
|
13
|
+
if (!cfg.walrusPublisher) {
|
|
14
|
+
throw new Error(
|
|
15
|
+
`No Walrus publisher is configured for ${cfg.network}. Walrus runs no public mainnet publisher; ` +
|
|
16
|
+
`set VITE_WALRUS_PUBLISHER_${cfg.network.toUpperCase()} to an operator-run publisher.`,
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
return storeBlobViaPublisher(bytes, {
|
|
20
|
+
publisher: cfg.walrusPublisher,
|
|
21
|
+
epochs: WALRUS_EPOCHS,
|
|
22
|
+
sendObjectTo: opts.sendObjectTo,
|
|
23
|
+
maxBytes: WALRUS_MAX_UPLOAD_BYTES,
|
|
17
24
|
})
|
|
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
25
|
}
|
|
24
26
|
|
|
25
|
-
/** Read
|
|
26
|
-
export
|
|
27
|
-
const
|
|
28
|
-
if (!
|
|
29
|
-
return
|
|
27
|
+
/** Read the ciphertext back, with the aggregator's strict consistency check. */
|
|
28
|
+
export function readBlob(blobId: string): Promise<Uint8Array> {
|
|
29
|
+
const cfg = activeConfig.value
|
|
30
|
+
if (!cfg.walrusAggregator) throw new Error(`No Walrus aggregator is configured for ${cfg.network}.`)
|
|
31
|
+
return readViaAggregator(blobId, { aggregator: cfg.walrusAggregator })
|
|
30
32
|
}
|