@meddleware/seal-ui 0.0.9 → 0.0.11

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meddleware/seal-ui",
3
- "version": "0.0.9",
3
+ "version": "0.0.11",
4
4
  "description": "Standalone Vue 3 SPA for Sealed Storage — client-side Seal encryption + access-gated, decentralised Walrus storage on Sui.",
5
5
  "author": "MeddleWare <meddleware@proton.me>",
6
6
  "license": "0BSD",
@@ -38,10 +38,9 @@
38
38
  },
39
39
  "dependencies": {
40
40
  "@meddleware/design-tokens": "^0.1.2",
41
- "@meddleware/seal-client": "^0.0.2",
42
- "@meddleware/ui": "^0.1.9",
41
+ "@meddleware/seal-client": "^0.0.3",
42
+ "@meddleware/ui": "^0.1.11",
43
43
  "@meddleware/wallet-adapter": "^0.0.5",
44
- "@mysten/seal": "^1.4.7",
45
44
  "@mysten/sui": "^2.30.0",
46
45
  "@mysten/wallet-standard": "^0.20.0",
47
46
  "vue": "^3.5.40"
@@ -7,16 +7,18 @@
7
7
  // Styles are scoped to this component so the dashboard can import it without pulling seal-ui's
8
8
  // global stylesheet (which restyles body / #app / bare inputs). The standalone app keeps those
9
9
  // globals via main.ts → styles.css.
10
- import { computed, onMounted, ref } from 'vue'
10
+ import { computed, onMounted, ref, watch } from 'vue'
11
+ import { AppTabNav, type AppTab } from '@meddleware/ui'
11
12
  import { Transaction } from '@mysten/sui/transactions'
12
13
  import {
13
14
  buildPublishSealedContentTx,
14
15
  type SealedManifest,
15
16
  type SealPolicyProvider,
16
17
  type SealedContentPointer,
18
+ type FieldSuggestion,
17
19
  } from '@meddleware/seal-client'
18
20
  import { WalletGuard } from '@meddleware/wallet-adapter'
19
- import { useWallet } from '../wallet.js'
21
+ import { useWallet, getSuiClient } from '../wallet.js'
20
22
  import { registry, getSealController } from '../seal.js'
21
23
  import { storeBlob, readBlob } from '../walrus.js'
22
24
  import { discoverSealedContent } from '../sealed-content.js'
@@ -27,16 +29,23 @@ const { account, signPersonalMessage, signAndExecute } = useWallet()
27
29
  const providers = registry.list() as SealPolicyProvider[]
28
30
  const disabled = computed(() => !SEAL_CONFIGURED)
29
31
 
30
- type Tab = 'encrypt' | 'decrypt' | 'unlock'
31
- const tab = ref<Tab>('encrypt')
32
+ const TABS: AppTab[] = [
33
+ { id: 'encrypt', label: 'Encrypt' },
34
+ { id: 'decrypt', label: 'Decrypt' },
35
+ { id: 'unlock', label: 'Unlock' },
36
+ ]
37
+ const tab = ref<string>('encrypt')
32
38
 
33
39
  // Deep-link from access-gate-ui: ?gate=<id> preselects the nft-gate policy + gate.
40
+ // Force manual mode so the pre-set ID renders in the text input immediately, before suggestions load.
34
41
  onMounted(() => {
35
42
  const gate = new URLSearchParams(location.search).get('gate')
36
43
  if (gate && providers.some((p) => p.type === 'nft-gate')) {
37
44
  encType.value = 'nft-gate'
38
45
  encValues.value = { ...encValues.value, gateId: gate }
46
+ encGateManual.value = true
39
47
  unlockGateId.value = gate
48
+ unlockGateManual.value = true
40
49
  }
41
50
  })
42
51
  const busy = ref(false)
@@ -58,6 +67,21 @@ function triggerDownload(blob: Blob, name: string): void {
58
67
  URL.revokeObjectURL(url)
59
68
  }
60
69
 
70
+ // ── Gate suggestions (nft-gate policy) ───────────────────────────────────────
71
+ const encGateSuggestions = ref<FieldSuggestion[]>([])
72
+ const encGateManual = ref(false)
73
+ const encGateLoading = ref(false)
74
+ const unlockGateSuggestions = ref<FieldSuggestion[]>([])
75
+ const unlockGateManual = ref(false)
76
+ const unlockGateLoading = ref(false)
77
+
78
+ async function fetchGateSuggestions(provider: SealPolicyProvider | null): Promise<FieldSuggestion[]> {
79
+ const addr = account.value?.address
80
+ if (!addr || !provider || !provider.suggest) return []
81
+ const result = await provider.suggest({ account: addr, client: getSuiClient() })
82
+ return result.gateId ?? []
83
+ }
84
+
61
85
  // ── Encrypt ──────────────────────────────────────────────────────────────────
62
86
  const encType = ref(providers[0]?.type ?? '')
63
87
  const encProvider = computed(() => providers.find((p) => p.type === encType.value) ?? null)
@@ -70,6 +94,39 @@ function onEncFile(e: Event): void {
70
94
  encFile.value = (e.target as HTMLInputElement).files?.[0] ?? null
71
95
  }
72
96
 
97
+ watch(
98
+ [() => account.value?.address, encProvider],
99
+ async ([, provider]) => {
100
+ if (encType.value !== 'nft-gate') { encGateSuggestions.value = []; return }
101
+ encGateLoading.value = true
102
+ encGateManual.value = false
103
+ try {
104
+ encGateSuggestions.value = await fetchGateSuggestions(provider)
105
+ } catch {
106
+ encGateSuggestions.value = []
107
+ } finally {
108
+ encGateLoading.value = false
109
+ }
110
+ },
111
+ )
112
+
113
+ watch(
114
+ [() => account.value?.address, tab],
115
+ async ([, t]) => {
116
+ if (t !== 'unlock') return
117
+ const nftGateProvider = providers.find((p) => p.type === 'nft-gate') ?? null
118
+ unlockGateLoading.value = true
119
+ unlockGateManual.value = false
120
+ try {
121
+ unlockGateSuggestions.value = await fetchGateSuggestions(nftGateProvider)
122
+ } catch {
123
+ unlockGateSuggestions.value = []
124
+ } finally {
125
+ unlockGateLoading.value = false
126
+ }
127
+ },
128
+ )
129
+
73
130
  async function performEncrypt(): Promise<void> {
74
131
  errorMsg.value = null
75
132
  status.value = null
@@ -286,11 +343,7 @@ async function performUnlock(item: SealedContentPointer): Promise<void> {
286
343
  </div>
287
344
 
288
345
  <WalletGuard message="Connect a Sui wallet to encrypt and decrypt sealed content.">
289
- <nav class="tabs">
290
- <button :class="{ active: tab === 'encrypt' }" @click="tab = 'encrypt'">Encrypt</button>
291
- <button :class="{ active: tab === 'decrypt' }" @click="tab = 'decrypt'">Decrypt</button>
292
- <button :class="{ active: tab === 'unlock' }" @click="tab = 'unlock'">Unlock</button>
293
- </nav>
346
+ <AppTabNav :tabs="TABS" v-model="tab" style="margin: 1rem 0" />
294
347
 
295
348
  <div v-if="errorMsg" class="notice notice--error">{{ errorMsg }}</div>
296
349
 
@@ -307,17 +360,31 @@ async function performUnlock(item: SealedContentPointer): Promise<void> {
307
360
  <p v-if="encProvider" class="muted">{{ encProvider.describe().help }}</p>
308
361
 
309
362
  <template v-if="encProvider">
310
- <label
311
- v-for="f in encProvider.describe().encryptFields"
312
- :key="f.name"
313
- class="field"
314
- :class="{ checkbox: f.kind === 'boolean' }"
315
- >
316
- <span>{{ f.label }}<template v-if="f.required"> *</template></span>
317
- <input v-if="f.kind === 'datetime'" type="datetime-local" v-model="encValues[f.name]" />
318
- <input v-else-if="f.kind === 'boolean'" type="checkbox" v-model="encValues[f.name]" />
319
- <input v-else type="text" v-model="encValues[f.name]" :placeholder="f.help" />
320
- </label>
363
+ <template v-for="f in encProvider.describe().encryptFields" :key="f.name">
364
+ <!-- Gate ID field: show a picker when wallet-owned gates are available -->
365
+ <label v-if="f.name === 'gateId'" class="field">
366
+ <span>{{ f.label }}<template v-if="f.required"> *</template></span>
367
+ <span v-if="encGateLoading" class="muted" style="font-size:0.8rem">Loading your gates…</span>
368
+ <template v-else-if="encGateSuggestions.length && !encGateManual">
369
+ <select v-model="encValues[f.name]">
370
+ <option value="">Select a gate…</option>
371
+ <option v-for="s in encGateSuggestions" :key="s.value" :value="s.value">{{ s.label }}</option>
372
+ </select>
373
+ <button class="link" style="margin-top:0.25rem" @click="encGateManual = true; encValues[f.name] = ''">Enter ID manually</button>
374
+ </template>
375
+ <template v-else>
376
+ <input type="text" v-model="encValues[f.name]" :placeholder="f.help" />
377
+ <button v-if="encGateSuggestions.length" class="link" style="margin-top:0.25rem" @click="encGateManual = false; encValues[f.name] = ''">← Back to picker</button>
378
+ </template>
379
+ </label>
380
+ <!-- All other fields: generic rendering -->
381
+ <label v-else class="field" :class="{ checkbox: f.kind === 'boolean' }">
382
+ <span>{{ f.label }}<template v-if="f.required"> *</template></span>
383
+ <input v-if="f.kind === 'datetime'" type="datetime-local" v-model="encValues[f.name]" />
384
+ <input v-else-if="f.kind === 'boolean'" type="checkbox" v-model="encValues[f.name]" />
385
+ <input v-else type="text" v-model="encValues[f.name]" :placeholder="f.help" />
386
+ </label>
387
+ </template>
321
388
  </template>
322
389
 
323
390
  <label class="field">
@@ -360,7 +427,18 @@ async function performUnlock(item: SealedContentPointer): Promise<void> {
360
427
  </p>
361
428
  <label class="field">
362
429
  <span>Access gate ID</span>
363
- <input type="text" v-model="unlockGateId" placeholder="0x… gate object id" />
430
+ <span v-if="unlockGateLoading" class="muted" style="font-size:0.8rem">Loading your gates…</span>
431
+ <template v-else-if="unlockGateSuggestions.length && !unlockGateManual">
432
+ <select v-model="unlockGateId">
433
+ <option value="">Select a gate…</option>
434
+ <option v-for="s in unlockGateSuggestions" :key="s.value" :value="s.value">{{ s.label }}</option>
435
+ </select>
436
+ <button class="link" style="margin-top:0.25rem" @click="unlockGateManual = true; unlockGateId = ''">Enter ID manually</button>
437
+ </template>
438
+ <template v-else>
439
+ <input type="text" v-model="unlockGateId" placeholder="0x… gate object id" />
440
+ <button v-if="unlockGateSuggestions.length" class="link" style="margin-top:0.25rem" @click="unlockGateManual = false; unlockGateId = ''">← Back to picker</button>
441
+ </template>
364
442
  </label>
365
443
  <button class="primary" :disabled="busy || !unlockGateId" @click="performDiscover">
366
444
  {{ busy ? 'Working…' : 'Find sealed content' }}
@@ -450,25 +528,6 @@ async function performUnlock(item: SealedContentPointer): Promise<void> {
450
528
  .notice--warn { background: color-mix(in srgb, #d29922 15%, transparent); }
451
529
  .notice--error { background: color-mix(in srgb, #f85149 18%, transparent); }
452
530
 
453
- .tabs {
454
- display: flex;
455
- gap: 0.25rem;
456
- margin: 1rem 0;
457
- border-bottom: 1px solid color-mix(in srgb, currentColor 15%, transparent);
458
- }
459
- .tabs button {
460
- background: transparent;
461
- border: 0;
462
- color: inherit;
463
- padding: 0.5rem 0.9rem;
464
- cursor: pointer;
465
- opacity: 0.7;
466
- border-bottom: 2px solid transparent;
467
- }
468
- .tabs button.active {
469
- opacity: 1;
470
- border-bottom-color: var(--accent, #c0503f);
471
- }
472
531
 
473
532
  .card {
474
533
  background: var(--surface, #1e1917);
package/src/config.ts CHANGED
@@ -96,6 +96,16 @@ export const WALRUS_AGGREGATOR: string =
96
96
  /** Default blob lifetime (Walrus storage epochs). */
97
97
  export const WALRUS_EPOCHS = Number(env.VITE_WALRUS_EPOCHS || '5')
98
98
 
99
+ /**
100
+ * Meddleware's deployed `access_gate` package IDs per network.
101
+ * Used by the nft-gate provider's `suggest()` to list AdminCap-owned gates for the connected wallet.
102
+ * Kept in sync with access-gate-ui/src/constants.ts (same deployment).
103
+ */
104
+ export const ACCESS_GATE_PACKAGE_ID: Record<Network, string> = {
105
+ testnet: '0x0bedd0b27d993d3292ca6a5315f7562de8bc0ff3752b445b4c53252c76f2d20d',
106
+ mainnet: '',
107
+ }
108
+
99
109
  /** True when the on-chain policy package + committee are configured for the active network. */
100
110
  export const SEAL_CONFIGURED = Boolean(SEAL_PACKAGE_ID) && SEAL_SERVERS.length > 0
101
111
 
package/src/seal.ts CHANGED
@@ -6,10 +6,10 @@
6
6
  import { createDefaultRegistry, type PolicyRegistry } from '@meddleware/seal-client'
7
7
  import type { SealController } from '@meddleware/seal-client/controller'
8
8
  import { getSuiClient } from './wallet.js'
9
- import { NETWORK, SEAL_PACKAGE_ID, SEAL_THRESHOLD, SEAL_SERVERS } from './config.js'
9
+ import { NETWORK, SEAL_PACKAGE_ID, SEAL_THRESHOLD, SEAL_SERVERS, ACCESS_GATE_PACKAGE_ID } from './config.js'
10
10
 
11
11
  /** The policy registry (nft-gate + time-lock as peers). Iterate `list()` to render the picker. */
12
- export const registry: PolicyRegistry = createDefaultRegistry()
12
+ export const registry: PolicyRegistry = createDefaultRegistry(ACCESS_GATE_PACKAGE_ID[NETWORK])
13
13
 
14
14
  let controller: SealController | null = null
15
15