create-sia-app 0.1.1

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.
Files changed (112) hide show
  1. package/dist/index.d.ts +1 -0
  2. package/dist/index.js +179 -0
  3. package/package.json +29 -0
  4. package/template/CLAUDE.md +160 -0
  5. package/template/README.md +102 -0
  6. package/template/_gitignore +5 -0
  7. package/template/biome.json +40 -0
  8. package/template/index.html +13 -0
  9. package/template/package.json +30 -0
  10. package/template/rust/README.md +16 -0
  11. package/template/rust/sia-sdk-rs/.changeset/added_cancel_function_to_cancel_inflight_packed_uploads.md +6 -0
  12. package/template/rust/sia-sdk-rs/.changeset/check_if_we_have_enough_hosts_prior_to_encoding_in_upload_slabs.md +16 -0
  13. package/template/rust/sia-sdk-rs/.changeset/fix_slab_length_in_packed_object.md +5 -0
  14. package/template/rust/sia-sdk-rs/.changeset/fix_upload_racing_race_conditon.md +13 -0
  15. package/template/rust/sia-sdk-rs/.changeset/improved_parallelism_of_packed_uploads.md +5 -0
  16. package/template/rust/sia-sdk-rs/.changeset/progress_callback_will_now_be_called_as_expected_for_packed_uploads.md +5 -0
  17. package/template/rust/sia-sdk-rs/.github/dependabot.yml +10 -0
  18. package/template/rust/sia-sdk-rs/.github/workflows/main.yml +36 -0
  19. package/template/rust/sia-sdk-rs/.github/workflows/prepare-release.yml +34 -0
  20. package/template/rust/sia-sdk-rs/.github/workflows/release.yml +30 -0
  21. package/template/rust/sia-sdk-rs/.rustfmt.toml +4 -0
  22. package/template/rust/sia-sdk-rs/Cargo.lock +4127 -0
  23. package/template/rust/sia-sdk-rs/Cargo.toml +3 -0
  24. package/template/rust/sia-sdk-rs/LICENSE +21 -0
  25. package/template/rust/sia-sdk-rs/README.md +30 -0
  26. package/template/rust/sia-sdk-rs/indexd/CHANGELOG.md +79 -0
  27. package/template/rust/sia-sdk-rs/indexd/Cargo.toml +79 -0
  28. package/template/rust/sia-sdk-rs/indexd/benches/upload.rs +258 -0
  29. package/template/rust/sia-sdk-rs/indexd/src/app_client.rs +1710 -0
  30. package/template/rust/sia-sdk-rs/indexd/src/builder.rs +354 -0
  31. package/template/rust/sia-sdk-rs/indexd/src/download.rs +379 -0
  32. package/template/rust/sia-sdk-rs/indexd/src/hosts.rs +659 -0
  33. package/template/rust/sia-sdk-rs/indexd/src/lib.rs +827 -0
  34. package/template/rust/sia-sdk-rs/indexd/src/mock.rs +162 -0
  35. package/template/rust/sia-sdk-rs/indexd/src/object_encryption.rs +125 -0
  36. package/template/rust/sia-sdk-rs/indexd/src/quic.rs +575 -0
  37. package/template/rust/sia-sdk-rs/indexd/src/rhp4.rs +52 -0
  38. package/template/rust/sia-sdk-rs/indexd/src/slabs.rs +497 -0
  39. package/template/rust/sia-sdk-rs/indexd/src/upload.rs +629 -0
  40. package/template/rust/sia-sdk-rs/indexd/src/wasm_time.rs +41 -0
  41. package/template/rust/sia-sdk-rs/indexd/src/web_transport.rs +398 -0
  42. package/template/rust/sia-sdk-rs/indexd_ffi/CHANGELOG.md +76 -0
  43. package/template/rust/sia-sdk-rs/indexd_ffi/Cargo.toml +47 -0
  44. package/template/rust/sia-sdk-rs/indexd_ffi/examples/python/README.md +10 -0
  45. package/template/rust/sia-sdk-rs/indexd_ffi/examples/python/example.py +130 -0
  46. package/template/rust/sia-sdk-rs/indexd_ffi/src/bin/uniffi-bindgen.rs +3 -0
  47. package/template/rust/sia-sdk-rs/indexd_ffi/src/builder.rs +377 -0
  48. package/template/rust/sia-sdk-rs/indexd_ffi/src/io.rs +155 -0
  49. package/template/rust/sia-sdk-rs/indexd_ffi/src/lib.rs +1039 -0
  50. package/template/rust/sia-sdk-rs/indexd_ffi/src/logging.rs +58 -0
  51. package/template/rust/sia-sdk-rs/indexd_ffi/src/tls.rs +23 -0
  52. package/template/rust/sia-sdk-rs/indexd_wasm/Cargo.toml +33 -0
  53. package/template/rust/sia-sdk-rs/indexd_wasm/src/lib.rs +818 -0
  54. package/template/rust/sia-sdk-rs/knope.toml +54 -0
  55. package/template/rust/sia-sdk-rs/sia_derive/CHANGELOG.md +38 -0
  56. package/template/rust/sia-sdk-rs/sia_derive/Cargo.toml +19 -0
  57. package/template/rust/sia-sdk-rs/sia_derive/src/lib.rs +278 -0
  58. package/template/rust/sia-sdk-rs/sia_sdk/CHANGELOG.md +91 -0
  59. package/template/rust/sia-sdk-rs/sia_sdk/Cargo.toml +59 -0
  60. package/template/rust/sia-sdk-rs/sia_sdk/benches/merkle_root.rs +12 -0
  61. package/template/rust/sia-sdk-rs/sia_sdk/src/blake2.rs +22 -0
  62. package/template/rust/sia-sdk-rs/sia_sdk/src/consensus.rs +767 -0
  63. package/template/rust/sia-sdk-rs/sia_sdk/src/encoding/v1.rs +257 -0
  64. package/template/rust/sia-sdk-rs/sia_sdk/src/encoding/v2.rs +291 -0
  65. package/template/rust/sia-sdk-rs/sia_sdk/src/encoding.rs +26 -0
  66. package/template/rust/sia-sdk-rs/sia_sdk/src/encoding_async/v2.rs +367 -0
  67. package/template/rust/sia-sdk-rs/sia_sdk/src/encoding_async.rs +6 -0
  68. package/template/rust/sia-sdk-rs/sia_sdk/src/encryption.rs +303 -0
  69. package/template/rust/sia-sdk-rs/sia_sdk/src/erasure_coding.rs +347 -0
  70. package/template/rust/sia-sdk-rs/sia_sdk/src/lib.rs +15 -0
  71. package/template/rust/sia-sdk-rs/sia_sdk/src/macros.rs +435 -0
  72. package/template/rust/sia-sdk-rs/sia_sdk/src/merkle.rs +112 -0
  73. package/template/rust/sia-sdk-rs/sia_sdk/src/rhp/merkle.rs +357 -0
  74. package/template/rust/sia-sdk-rs/sia_sdk/src/rhp/rpc.rs +1507 -0
  75. package/template/rust/sia-sdk-rs/sia_sdk/src/rhp/types.rs +146 -0
  76. package/template/rust/sia-sdk-rs/sia_sdk/src/rhp.rs +7 -0
  77. package/template/rust/sia-sdk-rs/sia_sdk/src/seed.rs +278 -0
  78. package/template/rust/sia-sdk-rs/sia_sdk/src/signing.rs +236 -0
  79. package/template/rust/sia-sdk-rs/sia_sdk/src/types/common.rs +677 -0
  80. package/template/rust/sia-sdk-rs/sia_sdk/src/types/currency.rs +450 -0
  81. package/template/rust/sia-sdk-rs/sia_sdk/src/types/specifier.rs +110 -0
  82. package/template/rust/sia-sdk-rs/sia_sdk/src/types/spendpolicy.rs +778 -0
  83. package/template/rust/sia-sdk-rs/sia_sdk/src/types/utils.rs +117 -0
  84. package/template/rust/sia-sdk-rs/sia_sdk/src/types/v1.rs +1737 -0
  85. package/template/rust/sia-sdk-rs/sia_sdk/src/types/v2.rs +1726 -0
  86. package/template/rust/sia-sdk-rs/sia_sdk/src/types/work.rs +59 -0
  87. package/template/rust/sia-sdk-rs/sia_sdk/src/types.rs +16 -0
  88. package/template/scripts/setup-rust.js +29 -0
  89. package/template/src/App.tsx +13 -0
  90. package/template/src/components/DevNote.tsx +21 -0
  91. package/template/src/components/auth/ApproveScreen.tsx +84 -0
  92. package/template/src/components/auth/AuthFlow.tsx +77 -0
  93. package/template/src/components/auth/ConnectScreen.tsx +214 -0
  94. package/template/src/components/auth/LoadingScreen.tsx +8 -0
  95. package/template/src/components/auth/RecoveryScreen.tsx +182 -0
  96. package/template/src/components/upload/UploadZone.tsx +314 -0
  97. package/template/src/index.css +9 -0
  98. package/template/src/lib/constants.ts +8 -0
  99. package/template/src/lib/format.ts +35 -0
  100. package/template/src/lib/hex.ts +13 -0
  101. package/template/src/lib/sdk.ts +25 -0
  102. package/template/src/lib/wasm-env.ts +5 -0
  103. package/template/src/main.tsx +12 -0
  104. package/template/src/stores/auth.ts +86 -0
  105. package/template/tsconfig.app.json +31 -0
  106. package/template/tsconfig.json +7 -0
  107. package/template/tsconfig.node.json +26 -0
  108. package/template/vite.config.ts +18 -0
  109. package/template/wasm/indexd_wasm/indexd_wasm.d.ts +309 -0
  110. package/template/wasm/indexd_wasm/indexd_wasm.js +1507 -0
  111. package/template/wasm/indexd_wasm/indexd_wasm_bg.wasm +0 -0
  112. package/template/wasm/indexd_wasm/package.json +31 -0
@@ -0,0 +1,182 @@
1
+ import { useState } from 'react'
2
+ import { toHex } from '../../lib/hex'
3
+ import {
4
+ type Builder,
5
+ generateRecoveryPhrase,
6
+ validateRecoveryPhrase,
7
+ } from '../../lib/sdk'
8
+ import { useAuthStore } from '../../stores/auth'
9
+ import { DevNote } from '../DevNote'
10
+
11
+ export function RecoveryScreen({
12
+ builder,
13
+ }: {
14
+ builder: React.RefObject<Builder | null>
15
+ }) {
16
+ const { setSdk, setStoredKeyHex, setError } = useAuthStore()
17
+ const [mode, setMode] = useState<'choose' | 'generate' | 'import'>('choose')
18
+ const [phrase, setPhrase] = useState('')
19
+ const [generatedPhrase, setGeneratedPhrase] = useState('')
20
+ const [loading, setLoading] = useState(false)
21
+ const [phraseError, setPhraseError] = useState<string | null>(null)
22
+
23
+ function handleGenerate() {
24
+ const mnemonic = generateRecoveryPhrase()
25
+ setGeneratedPhrase(mnemonic)
26
+ setPhrase(mnemonic)
27
+ setMode('generate')
28
+ }
29
+
30
+ function handleValidatePhrase(value: string) {
31
+ setPhrase(value)
32
+ setPhraseError(null)
33
+ if (value.trim()) {
34
+ try {
35
+ validateRecoveryPhrase(value.trim())
36
+ } catch {
37
+ setPhraseError('Invalid recovery phrase')
38
+ }
39
+ }
40
+ }
41
+
42
+ async function handleRegister() {
43
+ const b = builder.current
44
+ if (!b) {
45
+ setError('No builder instance')
46
+ return
47
+ }
48
+
49
+ const mnemonic = phrase.trim()
50
+ try {
51
+ validateRecoveryPhrase(mnemonic)
52
+ } catch {
53
+ setPhraseError('Invalid recovery phrase')
54
+ return
55
+ }
56
+
57
+ setLoading(true)
58
+ try {
59
+ const sdk = await b.register(mnemonic)
60
+ const appKey = sdk.appKey()
61
+ const exported = appKey.export()
62
+ setStoredKeyHex(toHex(exported))
63
+ setSdk(sdk)
64
+ } catch (e) {
65
+ setError(e instanceof Error ? e.message : 'Registration failed')
66
+ } finally {
67
+ setLoading(false)
68
+ }
69
+ }
70
+
71
+ if (mode === 'choose') {
72
+ return (
73
+ <div className="flex flex-col items-center justify-center min-h-screen px-4">
74
+ <div className="w-full max-w-md space-y-6">
75
+ <div className="text-center space-y-2">
76
+ <h1 className="text-2xl font-semibold text-white">
77
+ Recovery Phrase
78
+ </h1>
79
+ <p className="text-neutral-400 text-sm">
80
+ Generate a new recovery phrase or enter an existing one.
81
+ </p>
82
+ </div>
83
+
84
+ <DevNote title="Recovery Phrases & Key Derivation">
85
+ <p>
86
+ The 12-word BIP-39 recovery phrase deterministically derives the
87
+ user&apos;s cryptographic keys. The same phrase always produces
88
+ the same keys, enabling account recovery. The derived app key is
89
+ exported and stored in localStorage (via Zustand persist) so users
90
+ don&apos;t need to re-enter it.
91
+ </p>
92
+ </DevNote>
93
+
94
+ <div className="space-y-3">
95
+ <button
96
+ type="button"
97
+ onClick={handleGenerate}
98
+ className="w-full py-3 bg-green-600 hover:bg-green-700 text-white font-medium rounded-lg transition-colors"
99
+ >
100
+ Generate New Phrase
101
+ </button>
102
+ <button
103
+ type="button"
104
+ onClick={() => setMode('import')}
105
+ className="w-full py-3 bg-neutral-800 hover:bg-neutral-700 text-white font-medium rounded-lg transition-colors"
106
+ >
107
+ Enter Existing Phrase
108
+ </button>
109
+ </div>
110
+ </div>
111
+ </div>
112
+ )
113
+ }
114
+
115
+ return (
116
+ <div className="flex flex-col items-center justify-center min-h-screen px-4">
117
+ <div className="w-full max-w-md space-y-6">
118
+ <div className="text-center space-y-2">
119
+ <h1 className="text-2xl font-semibold text-white">
120
+ {mode === 'generate'
121
+ ? 'Save Your Recovery Phrase'
122
+ : 'Enter Recovery Phrase'}
123
+ </h1>
124
+ <p className="text-neutral-400 text-sm">
125
+ {mode === 'generate'
126
+ ? 'Write down these 12 words in order. You will need them to recover your account.'
127
+ : 'Enter your 12-word recovery phrase.'}
128
+ </p>
129
+ </div>
130
+
131
+ {mode === 'generate' ? (
132
+ <div className="grid grid-cols-3 gap-2 p-4 bg-neutral-900 rounded-lg border border-neutral-700">
133
+ {generatedPhrase.split(' ').map((word, i) => (
134
+ <div
135
+ key={`${word}-${i}`}
136
+ className="text-center py-2 bg-neutral-800 rounded text-sm"
137
+ >
138
+ <span className="text-neutral-500 mr-1">{i + 1}.</span>
139
+ <span className="text-white">{word}</span>
140
+ </div>
141
+ ))}
142
+ </div>
143
+ ) : (
144
+ <div className="space-y-2">
145
+ <textarea
146
+ value={phrase}
147
+ onChange={(e) => handleValidatePhrase(e.target.value)}
148
+ placeholder="Enter your 12-word recovery phrase..."
149
+ rows={3}
150
+ className="w-full px-4 py-3 bg-neutral-900 border border-neutral-700 rounded-lg text-white placeholder-neutral-500 focus:outline-none focus:border-green-500"
151
+ />
152
+ {phraseError && (
153
+ <p className="text-red-400 text-sm">{phraseError}</p>
154
+ )}
155
+ </div>
156
+ )}
157
+
158
+ <button
159
+ type="button"
160
+ onClick={handleRegister}
161
+ disabled={loading || !phrase.trim()}
162
+ className="w-full py-3 bg-green-600 hover:bg-green-700 disabled:bg-neutral-700 disabled:text-neutral-500 text-white font-medium rounded-lg transition-colors"
163
+ >
164
+ {loading ? 'Registering...' : 'Complete Setup'}
165
+ </button>
166
+
167
+ <button
168
+ type="button"
169
+ onClick={() => {
170
+ setMode('choose')
171
+ setPhrase('')
172
+ setGeneratedPhrase('')
173
+ setPhraseError(null)
174
+ }}
175
+ className="w-full py-2 text-neutral-400 hover:text-neutral-300 text-sm transition-colors"
176
+ >
177
+ Back
178
+ </button>
179
+ </div>
180
+ </div>
181
+ )
182
+ }
@@ -0,0 +1,314 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
2
+ import { APP_KEY } from '../../lib/constants'
3
+ import { type FileMetadata, formatBytes } from '../../lib/format'
4
+ import { toHex } from '../../lib/hex'
5
+ import { useAuthStore } from '../../stores/auth'
6
+ import { DevNote } from '../DevNote'
7
+
8
+ type UploadedFile = {
9
+ id: string
10
+ metadata: FileMetadata
11
+ }
12
+
13
+ type UploadProgress = {
14
+ fileName: string
15
+ current: number
16
+ total: number
17
+ }
18
+
19
+ // Detect unconfigured template placeholder. The string is split so the
20
+ // scaffolder's {{APP_KEY}} replacement doesn't rewrite this check.
21
+ const isPlaceholderKey = APP_KEY.startsWith('{' + '{')
22
+
23
+ export function UploadZone() {
24
+ const sdk = useAuthStore((s) => s.sdk)
25
+ const reset = useAuthStore((s) => s.reset)
26
+ const [files, setFiles] = useState<UploadedFile[]>([])
27
+ const [uploading, setUploading] = useState(false)
28
+ const [progress, setProgress] = useState<UploadProgress | null>(null)
29
+ const [dragOver, setDragOver] = useState(false)
30
+ const [error, setError] = useState<string | null>(null)
31
+ const fileInputRef = useRef<HTMLInputElement>(null)
32
+
33
+ const publicKey = useMemo(() => {
34
+ try {
35
+ return sdk?.appKey().publicKey() ?? null
36
+ } catch {
37
+ return null
38
+ }
39
+ }, [sdk])
40
+
41
+ const loadFiles = useCallback(async () => {
42
+ if (!sdk) return
43
+ try {
44
+ const events = await sdk.objectEvents(null, 100)
45
+ const loaded: UploadedFile[] = []
46
+ for (const event of events) {
47
+ if (event.deleted || !event.object) continue
48
+ try {
49
+ const metaBytes = event.object.metadata()
50
+ const meta = JSON.parse(
51
+ new TextDecoder().decode(metaBytes),
52
+ ) as FileMetadata
53
+ loaded.push({ id: event.id, metadata: meta })
54
+ } catch {
55
+ // skip objects with invalid metadata
56
+ }
57
+ }
58
+ setFiles(loaded)
59
+ } catch (e) {
60
+ console.error('Failed to load files:', e)
61
+ }
62
+ }, [sdk])
63
+
64
+ useEffect(() => {
65
+ loadFiles()
66
+ }, [loadFiles])
67
+
68
+ async function uploadFile(file: File) {
69
+ if (!sdk) return
70
+ setUploading(true)
71
+ setError(null)
72
+ setProgress({ fileName: file.name, current: 0, total: 0 })
73
+
74
+ try {
75
+ const arrayBuffer = await file.arrayBuffer()
76
+ const data = new Uint8Array(arrayBuffer)
77
+
78
+ // Compute SHA-256 hash for metadata
79
+ const hashBuffer = await crypto.subtle.digest('SHA-256', data)
80
+ const hash = toHex(new Uint8Array(hashBuffer))
81
+
82
+ const pinnedObject = await sdk.uploadWithProgress(
83
+ data,
84
+ (current: number, total: number) => {
85
+ setProgress({ fileName: file.name, current, total })
86
+ },
87
+ )
88
+
89
+ const metadata: FileMetadata = {
90
+ name: file.name,
91
+ type: file.type || 'application/octet-stream',
92
+ size: file.size,
93
+ hash,
94
+ createdAt: Date.now(),
95
+ }
96
+
97
+ pinnedObject.updateMetadata(
98
+ new TextEncoder().encode(JSON.stringify(metadata)),
99
+ )
100
+ await sdk.pinObject(pinnedObject)
101
+ await sdk.updateObjectMetadata(pinnedObject)
102
+
103
+ setFiles((prev) => [{ id: pinnedObject.id(), metadata }, ...prev])
104
+ } catch (e) {
105
+ setError(e instanceof Error ? e.message : 'Upload failed')
106
+ } finally {
107
+ setUploading(false)
108
+ setProgress(null)
109
+ }
110
+ }
111
+
112
+ async function handleFiles(fileList: FileList) {
113
+ for (const file of Array.from(fileList)) {
114
+ await uploadFile(file)
115
+ }
116
+ }
117
+
118
+ function handleDrop(e: React.DragEvent) {
119
+ e.preventDefault()
120
+ setDragOver(false)
121
+ if (e.dataTransfer.files.length > 0) {
122
+ handleFiles(e.dataTransfer.files)
123
+ }
124
+ }
125
+
126
+ function handleDisconnect() {
127
+ reset()
128
+ window.location.reload()
129
+ }
130
+
131
+ const progressPercent =
132
+ progress && progress.total > 0
133
+ ? Math.round((progress.current / progress.total) * 100)
134
+ : 0
135
+
136
+ return (
137
+ <div className="min-h-screen flex flex-col">
138
+ {/* Nav */}
139
+ <header className="flex items-center justify-between px-6 py-4 border-b border-neutral-800/60">
140
+ <div className="flex items-center gap-3">
141
+ <h1 className="text-sm font-semibold text-white tracking-tight">
142
+ Sia Storage
143
+ </h1>
144
+ <span className="text-xs text-neutral-600">
145
+ {files.length} file{files.length !== 1 ? 's' : ''}
146
+ </span>
147
+ </div>
148
+ <div className="flex items-center gap-4">
149
+ {publicKey && (
150
+ <span
151
+ className="text-[11px] font-mono text-neutral-500 bg-neutral-900 border border-neutral-800 rounded px-2 py-1 cursor-default"
152
+ title={publicKey}
153
+ >
154
+ {publicKey.slice(0, 8)}...{publicKey.slice(-6)}
155
+ </span>
156
+ )}
157
+ <button
158
+ type="button"
159
+ onClick={handleDisconnect}
160
+ className="text-xs text-neutral-500 hover:text-neutral-300 transition-colors"
161
+ >
162
+ Disconnect
163
+ </button>
164
+ </div>
165
+ </header>
166
+
167
+ <div className="flex-1 p-6 space-y-5 max-w-2xl mx-auto w-full">
168
+ {/* Dev notes — remove these when shipping */}
169
+ {isPlaceholderKey && (
170
+ <DevNote title="Replace Your App Key">
171
+ <p>
172
+ You&apos;re using the template placeholder. Set your own key in{' '}
173
+ <code className="text-amber-300">src/lib/constants.ts</code> or
174
+ scaffold a fresh project with{' '}
175
+ <code className="text-amber-300">bunx create-sia-app</code>.
176
+ </p>
177
+ </DevNote>
178
+ )}
179
+
180
+ <DevNote title="Upload Flow">
181
+ <p>
182
+ <code className="text-amber-300">sdk.uploadWithProgress()</code>{' '}
183
+ encrypts, erasure-codes, and uploads directly to Sia hosts. Metadata
184
+ is attached via{' '}
185
+ <code className="text-amber-300">
186
+ pinnedObject.updateMetadata()
187
+ </code>{' '}
188
+ then pinned with{' '}
189
+ <code className="text-amber-300">sdk.pinObject()</code>.
190
+ </p>
191
+ </DevNote>
192
+
193
+ {error && (
194
+ <div className="flex items-center justify-between px-4 py-2.5 bg-red-950/80 border border-red-900 rounded-lg text-red-300 text-sm">
195
+ <span>{error}</span>
196
+ <button
197
+ type="button"
198
+ onClick={() => setError(null)}
199
+ className="text-red-500 hover:text-red-300 text-xs ml-4 shrink-0"
200
+ >
201
+ Dismiss
202
+ </button>
203
+ </div>
204
+ )}
205
+
206
+ {/* Dropzone */}
207
+ <label
208
+ onDrop={handleDrop}
209
+ onDragOver={(e) => {
210
+ e.preventDefault()
211
+ setDragOver(true)
212
+ }}
213
+ onDragLeave={(e) => {
214
+ e.preventDefault()
215
+ setDragOver(false)
216
+ }}
217
+ className={`relative block border-2 border-dashed rounded-xl p-16 text-center transition-all duration-150 ${
218
+ uploading
219
+ ? 'border-neutral-800 cursor-default'
220
+ : dragOver
221
+ ? 'border-green-500 bg-green-500/5 cursor-pointer'
222
+ : 'border-neutral-800 hover:border-neutral-600 cursor-pointer'
223
+ }`}
224
+ >
225
+ <input
226
+ ref={fileInputRef}
227
+ type="file"
228
+ multiple
229
+ className="hidden"
230
+ disabled={uploading}
231
+ onChange={(e) => {
232
+ if (e.target.files) handleFiles(e.target.files)
233
+ e.target.value = ''
234
+ }}
235
+ />
236
+
237
+ {progress ? (
238
+ <div className="space-y-4">
239
+ <p className="text-neutral-300 text-sm">
240
+ Uploading{' '}
241
+ <span className="text-white">{progress.fileName}</span>
242
+ </p>
243
+ <div className="w-full max-w-xs mx-auto bg-neutral-800 rounded-full h-1.5 overflow-hidden">
244
+ <div
245
+ className="bg-green-500 h-full rounded-full transition-all duration-300"
246
+ style={{ width: `${progressPercent}%` }}
247
+ />
248
+ </div>
249
+ <p className="text-neutral-600 text-xs font-mono">
250
+ {progress.current}/{progress.total} shards &middot;{' '}
251
+ {progressPercent}%
252
+ </p>
253
+ </div>
254
+ ) : (
255
+ <div className="space-y-2">
256
+ <svg
257
+ className="w-8 h-8 mx-auto text-neutral-700"
258
+ viewBox="0 0 24 24"
259
+ fill="none"
260
+ stroke="currentColor"
261
+ strokeWidth="1.5"
262
+ aria-hidden="true"
263
+ >
264
+ <path d="M12 16V4m0 0l-4 4m4-4l4 4" />
265
+ <path d="M20 16v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2" />
266
+ </svg>
267
+ <p className="text-neutral-400 text-sm">
268
+ Drop files here or click to browse
269
+ </p>
270
+ <p className="text-neutral-600 text-xs">
271
+ Encrypted end-to-end and stored on the Sia network
272
+ </p>
273
+ </div>
274
+ )}
275
+ </label>
276
+
277
+ {/* File list */}
278
+ {files.length > 0 && (
279
+ <div className="space-y-2">
280
+ <h2 className="text-xs font-medium text-neutral-500 uppercase tracking-wider">
281
+ Files
282
+ </h2>
283
+ <div className="divide-y divide-neutral-800/60">
284
+ {files.map((file) => (
285
+ <div
286
+ key={file.id}
287
+ className="flex items-center justify-between py-3 group"
288
+ >
289
+ <div className="flex-1 min-w-0 mr-4">
290
+ <p className="text-sm text-neutral-200 truncate">
291
+ {file.metadata.name}
292
+ </p>
293
+ <p className="text-xs text-neutral-600 mt-0.5">
294
+ {formatBytes(file.metadata.size)}
295
+ {file.metadata.type !== 'application/octet-stream' && (
296
+ <span> &middot; {file.metadata.type}</span>
297
+ )}
298
+ </p>
299
+ </div>
300
+ <span
301
+ className="text-[11px] text-neutral-700 font-mono shrink-0 group-hover:text-neutral-500 transition-colors"
302
+ title={file.metadata.hash}
303
+ >
304
+ {file.metadata.hash.slice(0, 8)}...
305
+ </span>
306
+ </div>
307
+ ))}
308
+ </div>
309
+ </div>
310
+ )}
311
+ </div>
312
+ </div>
313
+ )
314
+ }
@@ -0,0 +1,9 @@
1
+ @import "tailwindcss";
2
+
3
+ body {
4
+ margin: 0;
5
+ background-color: #0a0a0a;
6
+ color: #e5e5e5;
7
+ font-family:
8
+ -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
9
+ }
@@ -0,0 +1,8 @@
1
+ export const APP_KEY = '{{APP_KEY}}'
2
+ export const DEFAULT_INDEXER_URL = '{{INDEXER_URL}}'
3
+ export const APP_META = JSON.stringify({
4
+ appID: APP_KEY,
5
+ name: '{{APP_NAME}}',
6
+ description: '{{APP_DESCRIPTION}}',
7
+ serviceURL: '{{INDEXER_URL}}',
8
+ })
@@ -0,0 +1,35 @@
1
+ export type FileMetadata = {
2
+ name: string
3
+ type: string
4
+ size: number
5
+ hash: string
6
+ createdAt?: number
7
+ updatedAt?: number
8
+ }
9
+
10
+ export function formatBytes(bytes: number): string {
11
+ if (bytes === 0) return '0 B'
12
+ const k = 1024
13
+ const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
14
+ const i = Math.floor(Math.log(bytes) / Math.log(k))
15
+ const value = bytes / k ** i
16
+ return `${value.toFixed(i === 0 ? 0 : 1)} ${sizes[i]}`
17
+ }
18
+
19
+ export function formatDate(epochMs: number): string {
20
+ return new Date(epochMs).toLocaleDateString(undefined, {
21
+ year: 'numeric',
22
+ month: 'short',
23
+ day: 'numeric',
24
+ })
25
+ }
26
+
27
+ export function decodeMetadata(
28
+ metadataBytes: Uint8Array,
29
+ ): Record<string, unknown> {
30
+ try {
31
+ return JSON.parse(new TextDecoder().decode(metadataBytes))
32
+ } catch {
33
+ return {}
34
+ }
35
+ }
@@ -0,0 +1,13 @@
1
+ export function toHex(bytes: Uint8Array): string {
2
+ return Array.from(bytes)
3
+ .map((b) => b.toString(16).padStart(2, '0'))
4
+ .join('')
5
+ }
6
+
7
+ export function fromHex(hex: string): Uint8Array {
8
+ const bytes = new Uint8Array(hex.length / 2)
9
+ for (let i = 0; i < hex.length; i += 2) {
10
+ bytes[i / 2] = Number.parseInt(hex.substring(i, i + 2), 16)
11
+ }
12
+ return bytes
13
+ }
@@ -0,0 +1,25 @@
1
+ import init, {
2
+ AppKey,
3
+ Builder,
4
+ generateRecoveryPhrase,
5
+ PinnedObject,
6
+ type SDK,
7
+ validateRecoveryPhrase,
8
+ } from 'indexd_wasm'
9
+
10
+ let initialized = false
11
+
12
+ export async function initWasm(): Promise<void> {
13
+ if (initialized) return
14
+ await init()
15
+ initialized = true
16
+ }
17
+
18
+ export {
19
+ AppKey,
20
+ Builder,
21
+ PinnedObject,
22
+ type SDK,
23
+ generateRecoveryPhrase,
24
+ validateRecoveryPhrase,
25
+ }
@@ -0,0 +1,5 @@
1
+ // WASM environment stub — provides `env.now` required by the WASM binary
2
+ // (used by Rust's std::time / chrono for timestamps in the browser).
3
+ export function now(): number {
4
+ return Date.now()
5
+ }
@@ -0,0 +1,12 @@
1
+ import { StrictMode } from 'react'
2
+ import { createRoot } from 'react-dom/client'
3
+ import App from './App.tsx'
4
+ import './index.css'
5
+
6
+ const root = document.getElementById('root')
7
+ if (!root) throw new Error('Root element not found')
8
+ createRoot(root).render(
9
+ <StrictMode>
10
+ <App />
11
+ </StrictMode>,
12
+ )
@@ -0,0 +1,86 @@
1
+ import { create } from 'zustand'
2
+ import { persist } from 'zustand/middleware'
3
+ import type { Builder, SDK } from '../lib/sdk'
4
+
5
+ export type AuthStep =
6
+ | 'loading'
7
+ | 'connect'
8
+ | 'approve'
9
+ | 'recovery'
10
+ | 'connected'
11
+
12
+ export type PerformancePreset = 'conservative' | 'balanced' | 'fast'
13
+
14
+ export const PRESETS: Record<
15
+ PerformancePreset,
16
+ { priceFetches: number; downloads: number; uploads: number }
17
+ > = {
18
+ conservative: { priceFetches: 1, downloads: 2, uploads: 2 },
19
+ balanced: { priceFetches: 5, downloads: 10, uploads: 8 },
20
+ fast: { priceFetches: 10, downloads: 20, uploads: 16 },
21
+ }
22
+
23
+ export function applyPresetToBuilder(
24
+ builder: Builder,
25
+ preset: PerformancePreset,
26
+ ) {
27
+ const p = PRESETS[preset]
28
+ builder.withMaxPriceFetches(p.priceFetches)
29
+ builder.withMaxDownloads(p.downloads)
30
+ builder.withMaxUploads(p.uploads)
31
+ }
32
+
33
+ type AuthState = {
34
+ sdk: SDK | null
35
+ storedKeyHex: string | null
36
+ indexerUrl: string
37
+ step: AuthStep
38
+ error: string | null
39
+ approvalUrl: string | null
40
+ performancePreset: PerformancePreset
41
+ setSdk: (sdk: SDK) => void
42
+ setStep: (step: AuthStep) => void
43
+ setError: (error: string | null) => void
44
+ setStoredKeyHex: (hex: string) => void
45
+ setIndexerUrl: (url: string) => void
46
+ setApprovalUrl: (url: string | null) => void
47
+ setPerformancePreset: (preset: PerformancePreset) => void
48
+ reset: () => void
49
+ }
50
+
51
+ export const useAuthStore = create<AuthState>()(
52
+ persist(
53
+ (set) => ({
54
+ sdk: null,
55
+ storedKeyHex: null,
56
+ indexerUrl: '',
57
+ step: 'loading',
58
+ error: null,
59
+ approvalUrl: null,
60
+ performancePreset: 'balanced',
61
+ setSdk: (sdk) => set({ sdk, step: 'connected', error: null }),
62
+ setStep: (step) => set({ step, error: null }),
63
+ setError: (error) => set({ error }),
64
+ setStoredKeyHex: (hex) => set({ storedKeyHex: hex }),
65
+ setIndexerUrl: (url) => set({ indexerUrl: url }),
66
+ setApprovalUrl: (url) => set({ approvalUrl: url }),
67
+ setPerformancePreset: (preset) => set({ performancePreset: preset }),
68
+ reset: () =>
69
+ set({
70
+ sdk: null,
71
+ storedKeyHex: null,
72
+ step: 'loading',
73
+ error: null,
74
+ approvalUrl: null,
75
+ }),
76
+ }),
77
+ {
78
+ name: '{{APP_NAME}}-auth',
79
+ partialize: (state) => ({
80
+ storedKeyHex: state.storedKeyHex,
81
+ indexerUrl: state.indexerUrl,
82
+ performancePreset: state.performancePreset,
83
+ }),
84
+ },
85
+ ),
86
+ )