create-sia-app 0.1.11 → 0.1.13
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/dist/index.js +2 -2
- package/package.json +1 -1
- package/template/CLAUDE.md +50 -36
- package/template/README.md +7 -6
- package/template/biome.json +1 -1
- package/template/package.json +4 -5
- package/template/src/components/Navbar.tsx +3 -3
- package/template/src/components/auth/ApproveScreen.tsx +1 -1
- package/template/src/components/auth/AuthFlow.tsx +8 -13
- package/template/src/components/auth/ConnectScreen.tsx +15 -60
- package/template/src/components/auth/RecoveryScreen.tsx +6 -9
- package/template/src/components/upload/UploadZone.tsx +109 -88
- package/template/src/lib/constants.ts +9 -4
- package/template/src/stores/auth.ts +6 -6
- package/template/src/types/uint8array-hex.d.ts +10 -0
- package/template/tsconfig.app.json +0 -3
- package/template/vite.config.ts +4 -11
- package/template/AGENTS.md +0 -143
package/dist/index.js
CHANGED
|
@@ -54,7 +54,7 @@ async function runPrompts() {
|
|
|
54
54
|
}
|
|
55
55
|
const indexerUrl = await p.text({
|
|
56
56
|
message: "Indexer URL",
|
|
57
|
-
initialValue: "https://
|
|
57
|
+
initialValue: "https://sia.storage"
|
|
58
58
|
});
|
|
59
59
|
if (p.isCancel(indexerUrl)) {
|
|
60
60
|
p.cancel("Cancelled.");
|
|
@@ -173,7 +173,7 @@ function getDefaultOptions(projectName) {
|
|
|
173
173
|
return {
|
|
174
174
|
projectName,
|
|
175
175
|
appKey: crypto2.randomBytes(32).toString("hex"),
|
|
176
|
-
indexerUrl: "https://
|
|
176
|
+
indexerUrl: "https://sia.storage",
|
|
177
177
|
appDescription: "A Sia storage app"
|
|
178
178
|
};
|
|
179
179
|
}
|
package/package.json
CHANGED
package/template/CLAUDE.md
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
## Overview
|
|
4
4
|
|
|
5
|
-
A starter template for building decentralized storage apps on the Sia network. Uses
|
|
5
|
+
A starter template for building decentralized storage apps on the Sia network. Uses [`sia-storage`](https://www.npmjs.com/package/sia-storage) — a TypeScript SDK that ships a pre-compiled WASM binary for encryption, uploads, downloads, and key management. WASM runs on the main thread (Rust async — no workers required).
|
|
6
6
|
|
|
7
|
-
**Tech stack:** React 19, TypeScript, Vite, Tailwind CSS 4, Zustand,
|
|
7
|
+
**Tech stack:** React 19, TypeScript, Vite, Tailwind CSS 4, Zustand, `sia-storage`
|
|
8
8
|
|
|
9
9
|
## Architecture
|
|
10
10
|
|
|
@@ -17,79 +17,93 @@ loading → connect → approve → recovery → connected
|
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
- **loading**: WASM initializes, checks for stored app key
|
|
20
|
-
- **connect**: User enters indexer URL, app
|
|
21
|
-
- **approve**: User visits approval URL in another tab
|
|
22
|
-
- **recovery**: User generates or enters 12-word recovery phrase
|
|
23
|
-
- **connected**:
|
|
20
|
+
- **connect**: User enters indexer URL, app constructs a `Builder` and calls `requestConnection()`
|
|
21
|
+
- **approve**: User visits approval URL in another tab; app polls `builder.waitForApproval()`
|
|
22
|
+
- **recovery**: User generates or enters a 12-word BIP-39 recovery phrase; `builder.register(phrase)` returns the `Sdk`
|
|
23
|
+
- **connected**: `Sdk` is ready, main app UI renders
|
|
24
|
+
|
|
25
|
+
### Returning users
|
|
26
|
+
|
|
27
|
+
Returning users skip `requestConnection`/`waitForApproval`/`register` entirely. `AuthFlow` constructs a `Builder` and calls `builder.connected(appKey)` with the persisted key — it returns an `Sdk` if the key is still valid, or `undefined` to fall back to the `connect` step.
|
|
24
28
|
|
|
25
29
|
### SDK
|
|
26
30
|
|
|
27
|
-
|
|
31
|
+
`sia-storage` handles:
|
|
28
32
|
- Encrypted file uploads/downloads (erasure coding + encryption)
|
|
29
33
|
- Key derivation from recovery phrases (BIP-39)
|
|
30
34
|
- Object pinning and metadata management
|
|
31
35
|
- Connection auth with indexers
|
|
32
|
-
-
|
|
36
|
+
- Direct streaming to/from Sia hosts (no worker pool needed)
|
|
33
37
|
|
|
34
38
|
### Zustand Persistence
|
|
35
39
|
|
|
36
|
-
Auth state persists to localStorage via Zustand's `persist` middleware. The storage key is `{app-name}-auth`. Persisted fields: `storedKeyHex`, `indexerUrl`.
|
|
40
|
+
Auth state persists to localStorage via Zustand's `persist` middleware. The storage key is `{app-name}-auth`. Persisted fields: `storedKeyHex`, `indexerUrl`. The app key is stored as hex via the TC39 `Uint8Array.prototype.toHex` method.
|
|
37
41
|
|
|
38
42
|
## Key Files
|
|
39
43
|
|
|
40
44
|
| File | Description |
|
|
41
45
|
|------|-------------|
|
|
42
|
-
| `src/lib/constants.ts` | App key, app name, indexer URL, app metadata |
|
|
43
|
-
| `src/stores/auth.ts` | Auth state machine (Zustand + persist) |
|
|
46
|
+
| `src/lib/constants.ts` | App key, app name, indexer URL, app metadata (typed `AppMetadata`) |
|
|
47
|
+
| `src/stores/auth.ts` | Auth state machine (Zustand + persist), holds the `Sdk` |
|
|
44
48
|
| `src/stores/toast.ts` | Toast notification store (auto-dismiss) |
|
|
45
49
|
| `src/components/Navbar.tsx` | App navbar with title, public key, sign out |
|
|
46
50
|
| `src/components/Toast.tsx` | Toast overlay component |
|
|
47
51
|
| `src/components/CopyButton.tsx` | Copy-to-clipboard button with toast |
|
|
48
|
-
| `src/components/auth/AuthFlow.tsx` | Auth orchestrator |
|
|
49
|
-
| `src/components/auth/ConnectScreen.tsx` | Indexer
|
|
52
|
+
| `src/components/auth/AuthFlow.tsx` | Auth orchestrator — `initSia()`, returning-user reconnect via `Builder.connected` |
|
|
53
|
+
| `src/components/auth/ConnectScreen.tsx` | Indexer URL form; constructs `new Builder(url, APP_META)` and calls `requestConnection()` |
|
|
50
54
|
| `src/components/auth/ApproveScreen.tsx` | Approval polling (auto-polls on mount) |
|
|
51
|
-
| `src/components/auth/RecoveryScreen.tsx` | Recovery phrase generation/import |
|
|
55
|
+
| `src/components/auth/RecoveryScreen.tsx` | Recovery phrase generation/import; `builder.register(phrase)` → `Sdk` |
|
|
52
56
|
| `src/components/upload/UploadZone.tsx` | File upload/download dropzone + file list |
|
|
53
|
-
| `src/components/DevNote.tsx` | Developer callout component
|
|
57
|
+
| `src/components/DevNote.tsx` | Developer callout component |
|
|
58
|
+
| `src/types/uint8array-hex.d.ts` | Ambient types for TC39 `Uint8Array.toHex`/`fromHex` (drop once TS lib ships them) |
|
|
54
59
|
|
|
55
60
|
## SDK Usage Patterns
|
|
56
61
|
|
|
57
62
|
### Upload a file
|
|
58
63
|
|
|
59
64
|
```ts
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
65
|
+
import { PinnedObject } from 'sia-storage'
|
|
66
|
+
|
|
67
|
+
const object = new PinnedObject()
|
|
68
|
+
const pinnedObject = await sdk.upload(object, file.stream(), {
|
|
69
|
+
maxInflight: 10,
|
|
70
|
+
onShardUploaded: (progress) => {
|
|
71
|
+
// progress: { hostKey, shardSize, shardIndex, slabIndex, elapsedMs }
|
|
72
|
+
console.log(`shard ${progress.shardIndex} uploaded (${progress.shardSize}b)`)
|
|
73
|
+
},
|
|
63
74
|
})
|
|
75
|
+
|
|
64
76
|
pinnedObject.updateMetadata(new TextEncoder().encode(JSON.stringify({
|
|
65
77
|
name: 'file.txt',
|
|
66
78
|
type: 'text/plain',
|
|
67
79
|
size: file.size,
|
|
68
80
|
hash: '...',
|
|
69
81
|
})))
|
|
70
|
-
await
|
|
71
|
-
await
|
|
82
|
+
await sdk.pinObject(pinnedObject)
|
|
83
|
+
await sdk.updateObjectMetadata(pinnedObject)
|
|
72
84
|
```
|
|
73
85
|
|
|
74
86
|
### Download a file
|
|
75
87
|
|
|
88
|
+
`sdk.download` returns a `ReadableStream` of `Uint8Array` chunks. Buffer it, or stream it directly into a `Response`/`Blob`:
|
|
89
|
+
|
|
76
90
|
```ts
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
91
|
+
const stream = sdk.download(pinnedObject, {
|
|
92
|
+
maxInflight: 10,
|
|
93
|
+
onShardDownloaded: (progress) => {
|
|
94
|
+
console.log(`shard ${progress.shardIndex} downloaded`)
|
|
95
|
+
},
|
|
80
96
|
})
|
|
81
|
-
|
|
97
|
+
const blob = await new Response(stream).blob()
|
|
82
98
|
```
|
|
83
99
|
|
|
84
100
|
### List files
|
|
85
101
|
|
|
86
102
|
```ts
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const events = await client.objectEvents(null, 100)
|
|
103
|
+
const events = await sdk.objectEvents(undefined, 100)
|
|
90
104
|
for (const event of events) {
|
|
91
105
|
if (!event.deleted && event.object) {
|
|
92
|
-
const meta =
|
|
106
|
+
const meta = JSON.parse(new TextDecoder().decode(event.object.metadata()))
|
|
93
107
|
console.log(meta.name, event.object.size())
|
|
94
108
|
}
|
|
95
109
|
}
|
|
@@ -98,21 +112,21 @@ for (const event of events) {
|
|
|
98
112
|
### Delete a file
|
|
99
113
|
|
|
100
114
|
```ts
|
|
101
|
-
await
|
|
115
|
+
await sdk.deleteObject(objectId)
|
|
102
116
|
```
|
|
103
117
|
|
|
104
118
|
### Share a file
|
|
105
119
|
|
|
106
120
|
```ts
|
|
107
|
-
const validUntil = Date.now() + 7 * 24 * 60 * 60 * 1000 // 7 days
|
|
108
|
-
const shareUrl =
|
|
121
|
+
const validUntil = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) // 7 days
|
|
122
|
+
const shareUrl = sdk.shareObject(pinnedObject, validUntil)
|
|
109
123
|
```
|
|
110
124
|
|
|
111
|
-
### Download shared file
|
|
125
|
+
### Download a shared file
|
|
112
126
|
|
|
113
127
|
```ts
|
|
114
|
-
const object = await
|
|
115
|
-
const
|
|
128
|
+
const object = await sdk.sharedObject(shareUrl)
|
|
129
|
+
const stream = sdk.download(object)
|
|
116
130
|
```
|
|
117
131
|
|
|
118
132
|
## Customization
|
|
@@ -122,12 +136,12 @@ const data = await client.download(object)
|
|
|
122
136
|
Edit `src/lib/constants.ts`. The app key is a 32-byte hex string that identifies your app to the indexer. Generate one with:
|
|
123
137
|
|
|
124
138
|
```ts
|
|
125
|
-
crypto.
|
|
139
|
+
crypto.getRandomValues(new Uint8Array(32)).toHex()
|
|
126
140
|
```
|
|
127
141
|
|
|
128
142
|
### Replace the upload UI
|
|
129
143
|
|
|
130
|
-
The post-auth UI is rendered in `src/App.tsx`. Replace `<UploadZone />` with your own component. The `
|
|
144
|
+
The post-auth UI is rendered in `src/App.tsx`. Replace `<UploadZone />` with your own component. The `Sdk` is available via `useAuthStore((s) => s.sdk)`. All SDK methods live directly on `Sdk` — `sdk.upload()`, `sdk.download()`, `sdk.objectEvents()`, etc.
|
|
131
145
|
|
|
132
146
|
### Add routes
|
|
133
147
|
|
package/template/README.md
CHANGED
|
@@ -45,14 +45,14 @@ Indexer service
|
|
|
45
45
|
|
|
46
46
|
### Auth Flow
|
|
47
47
|
|
|
48
|
-
1. **Connect** — Enter an indexer URL (default: `https://
|
|
48
|
+
1. **Connect** — Enter an indexer URL (default: `https://sia.storage`). The app sends your app metadata to request a connection.
|
|
49
49
|
2. **Approve** — Visit the approval URL in another tab to authorize your app.
|
|
50
50
|
3. **Recovery Phrase** — Generate a new 12-word phrase or enter an existing one. This deterministically derives all cryptographic keys.
|
|
51
51
|
4. **Connected** — The SDK is ready. Your app key is saved to localStorage for future sessions.
|
|
52
52
|
|
|
53
53
|
### Upload Flow
|
|
54
54
|
|
|
55
|
-
Files are encrypted in the browser, erasure coded into shards, and
|
|
55
|
+
Files are encrypted in the browser, erasure coded into shards, and streamed directly to Sia hosts. The SDK handles all of this — your code just calls `sdk.upload(object, file.stream(), { onShardUploaded })`. Metadata (filename, type, size) is also encrypted and pinned to the indexer.
|
|
56
56
|
|
|
57
57
|
## Customization
|
|
58
58
|
|
|
@@ -65,7 +65,7 @@ Edit `src/lib/constants.ts` to set your app key, name, description, and indexer
|
|
|
65
65
|
The main post-auth component is `src/components/upload/UploadZone.tsx`. Replace it with your own UI — the SDK is available via:
|
|
66
66
|
|
|
67
67
|
```tsx
|
|
68
|
-
const
|
|
68
|
+
const sdk = useAuthStore((s) => s.sdk)
|
|
69
69
|
```
|
|
70
70
|
|
|
71
71
|
## Tech Stack
|
|
@@ -76,14 +76,15 @@ const client = useAuthStore((s) => s.client)
|
|
|
76
76
|
- [Tailwind CSS](https://tailwindcss.com) 4
|
|
77
77
|
- [Zustand](https://zustand.docs.pmnd.rs) (state management)
|
|
78
78
|
- [Biome](https://biomejs.dev) (linting & formatting)
|
|
79
|
-
- [
|
|
79
|
+
- [sia-storage](https://www.npmjs.com/package/sia-storage) (Sia SDK — encryption, erasure coding, direct host transfers via WASM)
|
|
80
80
|
|
|
81
81
|
## Project Structure
|
|
82
82
|
|
|
83
83
|
```
|
|
84
84
|
src/
|
|
85
85
|
├── lib/ # Constants, utilities
|
|
86
|
-
├── stores/ # Zustand auth
|
|
86
|
+
├── stores/ # Zustand stores (auth + toast)
|
|
87
|
+
├── types/ # Ambient type declarations (TC39 Uint8Array.toHex/fromHex)
|
|
87
88
|
└── components/
|
|
88
89
|
├── auth/ # Auth flow screens
|
|
89
90
|
├── upload/ # Upload dropzone
|
|
@@ -93,4 +94,4 @@ src/
|
|
|
93
94
|
## Learn More
|
|
94
95
|
|
|
95
96
|
- [Sia Documentation](https://docs.sia.tech)
|
|
96
|
-
- [
|
|
97
|
+
- [sia-storage](https://www.npmjs.com/package/sia-storage)
|
package/template/biome.json
CHANGED
package/template/package.json
CHANGED
|
@@ -10,21 +10,20 @@
|
|
|
10
10
|
"check": "biome check ."
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"@siafoundation/sia": "^0.6.2",
|
|
14
13
|
"react": "^19.2.0",
|
|
15
14
|
"react-dom": "^19.2.0",
|
|
15
|
+
"sia-storage": "^0.0.8",
|
|
16
16
|
"zustand": "^5.0.11"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
|
-
"@biomejs/biome": "^2.4.
|
|
19
|
+
"@biomejs/biome": "^2.4.12",
|
|
20
|
+
"@playwright/test": "^1.59.1",
|
|
20
21
|
"@tailwindcss/vite": "^4.1.18",
|
|
21
22
|
"@types/react": "^19.2.7",
|
|
22
23
|
"@types/react-dom": "^19.2.3",
|
|
23
24
|
"@vitejs/plugin-react": "^5.1.1",
|
|
24
25
|
"tailwindcss": "^4.1.18",
|
|
25
26
|
"typescript": "~5.9.3",
|
|
26
|
-
"vite": "^7.3.1"
|
|
27
|
-
"vite-plugin-top-level-await": "^1.6.0",
|
|
28
|
-
"vite-plugin-wasm": "^3.5.0"
|
|
27
|
+
"vite": "^7.3.1"
|
|
29
28
|
}
|
|
30
29
|
}
|
|
@@ -5,17 +5,17 @@ import { CopyButton } from './CopyButton'
|
|
|
5
5
|
|
|
6
6
|
export function Navbar() {
|
|
7
7
|
const step = useAuthStore((s) => s.step)
|
|
8
|
-
const
|
|
8
|
+
const sdk = useAuthStore((s) => s.sdk)
|
|
9
9
|
const reset = useAuthStore((s) => s.reset)
|
|
10
10
|
const isConnected = step === 'connected'
|
|
11
11
|
|
|
12
12
|
const publicKey = useMemo(() => {
|
|
13
13
|
try {
|
|
14
|
-
return
|
|
14
|
+
return sdk?.appKey().publicKey() ?? null
|
|
15
15
|
} catch {
|
|
16
16
|
return null
|
|
17
17
|
}
|
|
18
|
-
}, [
|
|
18
|
+
}, [sdk])
|
|
19
19
|
|
|
20
20
|
function handleSignOut() {
|
|
21
21
|
reset()
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { Builder } from '@siafoundation/sia'
|
|
2
1
|
import { useEffect, useRef, useState } from 'react'
|
|
2
|
+
import type { Builder } from 'sia-storage'
|
|
3
3
|
import { useAuthStore } from '../../stores/auth'
|
|
4
4
|
import { CopyButton } from '../CopyButton'
|
|
5
5
|
import { DevNote } from '../DevNote'
|
|
@@ -1,11 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
AppKey,
|
|
3
|
-
type Builder,
|
|
4
|
-
connect,
|
|
5
|
-
fromHex,
|
|
6
|
-
initSia,
|
|
7
|
-
} from '@siafoundation/sia'
|
|
8
1
|
import { useEffect, useRef } from 'react'
|
|
2
|
+
import { AppKey, Builder, initSia } from 'sia-storage'
|
|
3
|
+
import { APP_META } from '../../lib/constants'
|
|
9
4
|
import { useAuthStore } from '../../stores/auth'
|
|
10
5
|
import { ApproveScreen } from './ApproveScreen'
|
|
11
6
|
import { ConnectScreen } from './ConnectScreen'
|
|
@@ -22,19 +17,19 @@ export function AuthFlow() {
|
|
|
22
17
|
let cancelled = false
|
|
23
18
|
|
|
24
19
|
async function init() {
|
|
25
|
-
const { storedKeyHex, indexerUrl,
|
|
20
|
+
const { storedKeyHex, indexerUrl, setSdk, setStep } =
|
|
26
21
|
useAuthStore.getState()
|
|
27
22
|
try {
|
|
28
23
|
await initSia()
|
|
29
24
|
|
|
30
25
|
if (storedKeyHex && indexerUrl) {
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
const
|
|
26
|
+
const appKey = new AppKey(Uint8Array.fromHex(storedKeyHex))
|
|
27
|
+
const builder = new Builder(indexerUrl, APP_META)
|
|
28
|
+
const sdk = await builder.connected(appKey)
|
|
34
29
|
|
|
35
30
|
if (cancelled) return
|
|
36
|
-
if (
|
|
37
|
-
|
|
31
|
+
if (sdk) {
|
|
32
|
+
setSdk(sdk)
|
|
38
33
|
return
|
|
39
34
|
}
|
|
40
35
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Builder } from '@siafoundation/sia'
|
|
2
1
|
import { useState } from 'react'
|
|
3
|
-
import {
|
|
2
|
+
import { Builder } from 'sia-storage'
|
|
3
|
+
import { APP_META, DEFAULT_INDEXER_URL } from '../../lib/constants'
|
|
4
4
|
import { useAuthStore } from '../../stores/auth'
|
|
5
5
|
import { DevNote } from '../DevNote'
|
|
6
6
|
|
|
@@ -13,24 +13,26 @@ export function ConnectScreen({
|
|
|
13
13
|
useAuthStore()
|
|
14
14
|
const [url, setUrl] = useState(indexerUrl || DEFAULT_INDEXER_URL)
|
|
15
15
|
const [loading, setLoading] = useState(false)
|
|
16
|
-
const [showCurl, setShowCurl] = useState(false)
|
|
17
|
-
const [curlResponse, setCurlResponse] = useState('')
|
|
18
16
|
|
|
19
17
|
async function handleConnect() {
|
|
20
18
|
setLoading(true)
|
|
21
19
|
setError(null)
|
|
22
20
|
try {
|
|
23
|
-
const b = new Builder(url)
|
|
21
|
+
const b = new Builder(url, APP_META)
|
|
24
22
|
builder.current = b
|
|
25
23
|
setIndexerUrl(url)
|
|
26
24
|
|
|
27
25
|
try {
|
|
28
|
-
await b.requestConnection(
|
|
26
|
+
await b.requestConnection()
|
|
29
27
|
const approvalUrl = b.responseUrl()
|
|
30
28
|
setApprovalUrl(approvalUrl)
|
|
31
29
|
setStep('approve')
|
|
32
|
-
} catch {
|
|
33
|
-
|
|
30
|
+
} catch (e) {
|
|
31
|
+
setError(
|
|
32
|
+
e instanceof Error
|
|
33
|
+
? `Connection failed: ${e.message}. Check the indexer URL and that it allows requests from this origin (CORS).`
|
|
34
|
+
: 'Connection failed. Check the indexer URL and CORS configuration.',
|
|
35
|
+
)
|
|
34
36
|
}
|
|
35
37
|
} catch (e) {
|
|
36
38
|
setError(e instanceof Error ? e.message : 'Failed to connect')
|
|
@@ -39,26 +41,6 @@ export function ConnectScreen({
|
|
|
39
41
|
}
|
|
40
42
|
}
|
|
41
43
|
|
|
42
|
-
function handleCurlSubmit() {
|
|
43
|
-
try {
|
|
44
|
-
const b = builder.current
|
|
45
|
-
if (!b) {
|
|
46
|
-
setError('No builder instance')
|
|
47
|
-
return
|
|
48
|
-
}
|
|
49
|
-
b.setConnectionResponse(APP_KEY, curlResponse)
|
|
50
|
-
const approvalUrl = b.responseUrl()
|
|
51
|
-
setApprovalUrl(approvalUrl)
|
|
52
|
-
setStep('approve')
|
|
53
|
-
} catch (e) {
|
|
54
|
-
setError(e instanceof Error ? e.message : 'Invalid response')
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const curlCommand = `curl -X POST ${url}/auth/connect \\
|
|
59
|
-
-H "Content-Type: application/json" \\
|
|
60
|
-
-d '${APP_META}'`
|
|
61
|
-
|
|
62
44
|
return (
|
|
63
45
|
<div className="flex flex-col items-center justify-center flex-1 px-4">
|
|
64
46
|
<div className="w-full max-w-md space-y-6">
|
|
@@ -74,14 +56,14 @@ export function ConnectScreen({
|
|
|
74
56
|
<DevNote title="Indexer URL & App Key">
|
|
75
57
|
<p>
|
|
76
58
|
The indexer URL points to your Sia storage provider. The default is{' '}
|
|
77
|
-
<code className="text-amber-300">https://
|
|
78
|
-
|
|
59
|
+
<code className="text-amber-300">https://sia.storage</code>. Your
|
|
60
|
+
app key (set in{' '}
|
|
79
61
|
<code className="text-amber-300">src/lib/constants.ts</code>)
|
|
80
62
|
uniquely identifies your app to the indexer.
|
|
81
63
|
</p>
|
|
82
64
|
<p className="mt-1">
|
|
83
|
-
If the
|
|
84
|
-
|
|
65
|
+
If the connection fails with a CORS error, the indexer must allow
|
|
66
|
+
requests from this app's origin.
|
|
85
67
|
</p>
|
|
86
68
|
</DevNote>
|
|
87
69
|
|
|
@@ -90,7 +72,7 @@ export function ConnectScreen({
|
|
|
90
72
|
type="url"
|
|
91
73
|
value={url}
|
|
92
74
|
onChange={(e) => setUrl(e.target.value)}
|
|
93
|
-
placeholder="https://
|
|
75
|
+
placeholder="https://sia.storage"
|
|
94
76
|
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"
|
|
95
77
|
/>
|
|
96
78
|
|
|
@@ -103,33 +85,6 @@ export function ConnectScreen({
|
|
|
103
85
|
{loading ? 'Connecting...' : 'Connect'}
|
|
104
86
|
</button>
|
|
105
87
|
</div>
|
|
106
|
-
|
|
107
|
-
{showCurl && (
|
|
108
|
-
<div className="space-y-4 p-4 bg-neutral-900 rounded-lg border border-neutral-700">
|
|
109
|
-
<p className="text-sm text-neutral-300">
|
|
110
|
-
Direct connection failed (CORS). Run this command and paste the
|
|
111
|
-
response:
|
|
112
|
-
</p>
|
|
113
|
-
<pre className="text-xs bg-neutral-950 p-3 rounded overflow-x-auto text-green-400">
|
|
114
|
-
{curlCommand}
|
|
115
|
-
</pre>
|
|
116
|
-
<textarea
|
|
117
|
-
value={curlResponse}
|
|
118
|
-
onChange={(e) => setCurlResponse(e.target.value)}
|
|
119
|
-
placeholder="Paste the JSON response here..."
|
|
120
|
-
rows={4}
|
|
121
|
-
className="w-full px-3 py-2 bg-neutral-950 border border-neutral-700 rounded text-sm text-white placeholder-neutral-500 focus:outline-none focus:border-green-500 font-mono"
|
|
122
|
-
/>
|
|
123
|
-
<button
|
|
124
|
-
type="button"
|
|
125
|
-
onClick={handleCurlSubmit}
|
|
126
|
-
disabled={!curlResponse}
|
|
127
|
-
className="w-full py-2 bg-green-600 hover:bg-green-700 disabled:bg-neutral-700 disabled:text-neutral-500 text-white text-sm font-medium rounded-lg transition-colors"
|
|
128
|
-
>
|
|
129
|
-
Submit Response
|
|
130
|
-
</button>
|
|
131
|
-
</div>
|
|
132
|
-
)}
|
|
133
88
|
</div>
|
|
134
89
|
</div>
|
|
135
90
|
)
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
1
2
|
import {
|
|
2
3
|
type Builder,
|
|
3
4
|
generateRecoveryPhrase,
|
|
4
|
-
toHex,
|
|
5
5
|
validateRecoveryPhrase,
|
|
6
|
-
} from '
|
|
7
|
-
import { useState } from 'react'
|
|
6
|
+
} from 'sia-storage'
|
|
8
7
|
import { useAuthStore } from '../../stores/auth'
|
|
9
8
|
import { CopyButton } from '../CopyButton'
|
|
10
9
|
import { DevNote } from '../DevNote'
|
|
@@ -14,7 +13,7 @@ export function RecoveryScreen({
|
|
|
14
13
|
}: {
|
|
15
14
|
builder: React.RefObject<Builder | null>
|
|
16
15
|
}) {
|
|
17
|
-
const {
|
|
16
|
+
const { setSdk, setStoredKeyHex, setError } = useAuthStore()
|
|
18
17
|
const [mode, setMode] = useState<'choose' | 'generate' | 'import'>('choose')
|
|
19
18
|
const [phrase, setPhrase] = useState('')
|
|
20
19
|
const [generatedPhrase, setGeneratedPhrase] = useState('')
|
|
@@ -57,11 +56,9 @@ export function RecoveryScreen({
|
|
|
57
56
|
|
|
58
57
|
setLoading(true)
|
|
59
58
|
try {
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
setStoredKeyHex(toHex(exported))
|
|
64
|
-
setClient(client)
|
|
59
|
+
const sdk = await b.register(mnemonic)
|
|
60
|
+
setStoredKeyHex(sdk.appKey().export().toHex())
|
|
61
|
+
setSdk(sdk)
|
|
65
62
|
} catch (e) {
|
|
66
63
|
setError(e instanceof Error ? e.message : 'Registration failed')
|
|
67
64
|
} finally {
|
|
@@ -1,11 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type DownloadProgress,
|
|
3
|
-
decodeMetadata,
|
|
4
|
-
type PinnedObject,
|
|
5
|
-
toHex,
|
|
6
|
-
type UploadProgress,
|
|
7
|
-
} from '@siafoundation/sia'
|
|
8
1
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
2
|
+
import { PinnedObject, type ShardProgress } from 'sia-storage'
|
|
9
3
|
import { APP_KEY } from '../../lib/constants'
|
|
10
4
|
import { useAuthStore } from '../../stores/auth'
|
|
11
5
|
import { DevNote } from '../DevNote'
|
|
@@ -27,26 +21,40 @@ function formatBytes(bytes: number): string {
|
|
|
27
21
|
return `${parseFloat((bytes / k ** i).toFixed(1))} ${sizes[i]}`
|
|
28
22
|
}
|
|
29
23
|
|
|
24
|
+
function decodeMetadata(bytes: Uint8Array): FileMetadata | null {
|
|
25
|
+
try {
|
|
26
|
+
return JSON.parse(new TextDecoder().decode(bytes)) as FileMetadata
|
|
27
|
+
} catch {
|
|
28
|
+
return null
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
30
32
|
type UploadedFile = {
|
|
31
33
|
id: string
|
|
32
34
|
metadata: FileMetadata
|
|
33
35
|
object: PinnedObject
|
|
34
36
|
}
|
|
35
37
|
|
|
36
|
-
type
|
|
38
|
+
type UploadProgress = {
|
|
37
39
|
fileName: string
|
|
38
|
-
|
|
40
|
+
shardsDone: number
|
|
41
|
+
bytesUploaded: number
|
|
42
|
+
totalBytes: number
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
type DownloadProgress = {
|
|
46
|
+
shardsDone: number
|
|
47
|
+
bytesDownloaded: number
|
|
48
|
+
totalBytes: number
|
|
39
49
|
}
|
|
40
50
|
|
|
41
|
-
// Detect unconfigured template placeholder. The string is split so the
|
|
42
|
-
// scaffolder's {{APP_KEY}} replacement doesn't rewrite this check.
|
|
43
51
|
const isPlaceholderKey = APP_KEY.startsWith('{' + '{')
|
|
44
52
|
|
|
45
53
|
export function UploadZone() {
|
|
46
|
-
const
|
|
54
|
+
const sdk = useAuthStore((s) => s.sdk)
|
|
47
55
|
const [files, setFiles] = useState<UploadedFile[]>([])
|
|
48
56
|
const [uploading, setUploading] = useState(false)
|
|
49
|
-
const [activeUpload, setActiveUpload] = useState<
|
|
57
|
+
const [activeUpload, setActiveUpload] = useState<UploadProgress | null>(null)
|
|
50
58
|
const [dragOver, setDragOver] = useState(false)
|
|
51
59
|
const [error, setError] = useState<string | null>(null)
|
|
52
60
|
const [downloading, setDownloading] = useState<string | null>(null)
|
|
@@ -55,14 +63,14 @@ export function UploadZone() {
|
|
|
55
63
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
|
56
64
|
|
|
57
65
|
const loadFiles = useCallback(async () => {
|
|
58
|
-
if (!
|
|
66
|
+
if (!sdk) return
|
|
59
67
|
try {
|
|
60
|
-
const events = await
|
|
68
|
+
const events = await sdk.objectEvents(undefined, 100)
|
|
61
69
|
const loaded: UploadedFile[] = []
|
|
62
70
|
for (const event of events) {
|
|
63
71
|
if (event.deleted || !event.object) continue
|
|
64
|
-
const meta = decodeMetadata(event.object.metadata())
|
|
65
|
-
if (meta
|
|
72
|
+
const meta = decodeMetadata(event.object.metadata())
|
|
73
|
+
if (meta?.name) {
|
|
66
74
|
loaded.push({ id: event.id, metadata: meta, object: event.object })
|
|
67
75
|
}
|
|
68
76
|
}
|
|
@@ -70,37 +78,45 @@ export function UploadZone() {
|
|
|
70
78
|
} catch (e) {
|
|
71
79
|
console.error('Failed to load files:', e)
|
|
72
80
|
}
|
|
73
|
-
}, [
|
|
81
|
+
}, [sdk])
|
|
74
82
|
|
|
75
83
|
useEffect(() => {
|
|
76
84
|
loadFiles()
|
|
77
85
|
}, [loadFiles])
|
|
78
86
|
|
|
79
87
|
async function uploadFile(file: File) {
|
|
80
|
-
if (!
|
|
88
|
+
if (!sdk) return
|
|
81
89
|
setUploading(true)
|
|
82
90
|
setError(null)
|
|
83
91
|
setActiveUpload({
|
|
84
92
|
fileName: file.name,
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
slabsTotal: 0,
|
|
89
|
-
shardsComplete: 0,
|
|
90
|
-
shardsTotal: 0,
|
|
91
|
-
},
|
|
93
|
+
shardsDone: 0,
|
|
94
|
+
bytesUploaded: 0,
|
|
95
|
+
totalBytes: file.size,
|
|
92
96
|
})
|
|
93
97
|
|
|
94
98
|
try {
|
|
95
|
-
// Compute SHA-256 hash for metadata
|
|
96
99
|
const hashBuffer = await crypto.subtle.digest(
|
|
97
100
|
'SHA-256',
|
|
98
101
|
await file.arrayBuffer(),
|
|
99
102
|
)
|
|
100
|
-
const hash =
|
|
103
|
+
const hash = new Uint8Array(hashBuffer).toHex()
|
|
101
104
|
|
|
102
|
-
const
|
|
103
|
-
|
|
105
|
+
const object = new PinnedObject()
|
|
106
|
+
let shardsDone = 0
|
|
107
|
+
let bytesUploaded = 0
|
|
108
|
+
const pinnedObject = await sdk.upload(object, file.stream(), {
|
|
109
|
+
maxInflight: 10,
|
|
110
|
+
onShardUploaded: (progress: ShardProgress) => {
|
|
111
|
+
shardsDone++
|
|
112
|
+
bytesUploaded += progress.shardSize
|
|
113
|
+
setActiveUpload({
|
|
114
|
+
fileName: file.name,
|
|
115
|
+
shardsDone,
|
|
116
|
+
bytesUploaded,
|
|
117
|
+
totalBytes: file.size,
|
|
118
|
+
})
|
|
119
|
+
},
|
|
104
120
|
})
|
|
105
121
|
|
|
106
122
|
const metadata: FileMetadata = {
|
|
@@ -114,8 +130,8 @@ export function UploadZone() {
|
|
|
114
130
|
pinnedObject.updateMetadata(
|
|
115
131
|
new TextEncoder().encode(JSON.stringify(metadata)),
|
|
116
132
|
)
|
|
117
|
-
await
|
|
118
|
-
await
|
|
133
|
+
await sdk.pinObject(pinnedObject)
|
|
134
|
+
await sdk.updateObjectMetadata(pinnedObject)
|
|
119
135
|
|
|
120
136
|
setFiles((prev) => [
|
|
121
137
|
{ id: pinnedObject.id(), metadata, object: pinnedObject },
|
|
@@ -130,14 +146,43 @@ export function UploadZone() {
|
|
|
130
146
|
}
|
|
131
147
|
|
|
132
148
|
async function downloadFile(file: UploadedFile) {
|
|
133
|
-
if (!
|
|
149
|
+
if (!sdk) return
|
|
134
150
|
setDownloading(file.id)
|
|
135
|
-
setDownloadProgress(
|
|
151
|
+
setDownloadProgress({
|
|
152
|
+
shardsDone: 0,
|
|
153
|
+
bytesDownloaded: 0,
|
|
154
|
+
totalBytes: file.metadata.size,
|
|
155
|
+
})
|
|
136
156
|
try {
|
|
137
|
-
|
|
138
|
-
|
|
157
|
+
let shardsDone = 0
|
|
158
|
+
const stream = sdk.download(file.object, {
|
|
159
|
+
maxInflight: 10,
|
|
160
|
+
onShardDownloaded: () => {
|
|
161
|
+
shardsDone++
|
|
162
|
+
setDownloadProgress((prev) => ({
|
|
163
|
+
shardsDone,
|
|
164
|
+
bytesDownloaded: prev?.bytesDownloaded ?? 0,
|
|
165
|
+
totalBytes: file.metadata.size,
|
|
166
|
+
}))
|
|
167
|
+
},
|
|
139
168
|
})
|
|
140
|
-
|
|
169
|
+
|
|
170
|
+
const reader = stream.getReader()
|
|
171
|
+
const chunks: Uint8Array[] = []
|
|
172
|
+
let bytesDownloaded = 0
|
|
173
|
+
while (true) {
|
|
174
|
+
const { done, value } = await reader.read()
|
|
175
|
+
if (done) break
|
|
176
|
+
chunks.push(value)
|
|
177
|
+
bytesDownloaded += value.length
|
|
178
|
+
setDownloadProgress((prev) => ({
|
|
179
|
+
shardsDone: prev?.shardsDone ?? 0,
|
|
180
|
+
bytesDownloaded,
|
|
181
|
+
totalBytes: file.metadata.size,
|
|
182
|
+
}))
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const blob = new Blob(chunks as BlobPart[], { type: file.metadata.type })
|
|
141
186
|
const url = URL.createObjectURL(blob)
|
|
142
187
|
const a = document.createElement('a')
|
|
143
188
|
a.href = url
|
|
@@ -166,30 +211,14 @@ export function UploadZone() {
|
|
|
166
211
|
}
|
|
167
212
|
}
|
|
168
213
|
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
? Math.round((progress.slabsComplete / progress.slabsTotal) * 100)
|
|
178
|
-
: 0
|
|
179
|
-
|
|
180
|
-
function progressLabel(): string {
|
|
181
|
-
if (!progress) return ''
|
|
182
|
-
switch (progress.phase) {
|
|
183
|
-
case 'connecting':
|
|
184
|
-
return 'Connecting to hosts...'
|
|
185
|
-
case 'uploading':
|
|
186
|
-
return `${progress.slabsComplete}/${progress.slabsTotal} slabs \u00B7 ${progressPercent}%`
|
|
187
|
-
case 'assembling':
|
|
188
|
-
return 'Assembling object...'
|
|
189
|
-
case 'pinning':
|
|
190
|
-
return 'Pinning object...'
|
|
191
|
-
}
|
|
192
|
-
}
|
|
214
|
+
const uploadPercent = activeUpload
|
|
215
|
+
? Math.min(
|
|
216
|
+
100,
|
|
217
|
+
Math.round(
|
|
218
|
+
(activeUpload.bytesUploaded / activeUpload.totalBytes) * 100,
|
|
219
|
+
),
|
|
220
|
+
)
|
|
221
|
+
: 0
|
|
193
222
|
|
|
194
223
|
return (
|
|
195
224
|
<div className="flex-1 p-6 space-y-5 max-w-5xl mx-auto w-full">
|
|
@@ -208,14 +237,14 @@ export function UploadZone() {
|
|
|
208
237
|
<DevNote title="Upload & Download">
|
|
209
238
|
<p>
|
|
210
239
|
<code className="text-amber-300">
|
|
211
|
-
|
|
240
|
+
sdk.upload(object, file.stream(), opts)
|
|
212
241
|
</code>{' '}
|
|
213
|
-
encrypts, erasure-codes, and
|
|
214
|
-
|
|
215
|
-
<code className="text-amber-300">
|
|
216
|
-
|
|
217
|
-
</code>{' '}
|
|
218
|
-
|
|
242
|
+
encrypts, erasure-codes, and streams shards directly to Sia hosts.{' '}
|
|
243
|
+
<code className="text-amber-300">sdk.download(object, opts)</code>{' '}
|
|
244
|
+
returns a <code className="text-amber-300">ReadableStream</code> of
|
|
245
|
+
decrypted bytes. Per-shard progress is reported via{' '}
|
|
246
|
+
<code className="text-amber-300">onShardUploaded</code> /{' '}
|
|
247
|
+
<code className="text-amber-300">onShardDownloaded</code>.
|
|
219
248
|
</p>
|
|
220
249
|
</DevNote>
|
|
221
250
|
|
|
@@ -266,30 +295,23 @@ export function UploadZone() {
|
|
|
266
295
|
{activeUpload ? (
|
|
267
296
|
<div className="space-y-4">
|
|
268
297
|
<p className="text-neutral-300 text-sm">
|
|
269
|
-
{
|
|
270
|
-
|
|
271
|
-
Preparing upload for{' '}
|
|
272
|
-
<span className="text-white">{activeUpload.fileName}</span>
|
|
273
|
-
</>
|
|
274
|
-
) : (
|
|
275
|
-
<>
|
|
276
|
-
Uploading{' '}
|
|
277
|
-
<span className="text-white">{activeUpload.fileName}</span>
|
|
278
|
-
</>
|
|
279
|
-
)}
|
|
298
|
+
Uploading{' '}
|
|
299
|
+
<span className="text-white">{activeUpload.fileName}</span>
|
|
280
300
|
</p>
|
|
281
301
|
<div className="w-full max-w-xs mx-auto bg-neutral-800 rounded-full h-1.5 overflow-hidden">
|
|
282
|
-
{
|
|
302
|
+
{activeUpload.shardsDone === 0 ? (
|
|
283
303
|
<div className="bg-green-500 h-full rounded-full w-1/4 animate-indeterminate" />
|
|
284
304
|
) : (
|
|
285
305
|
<div
|
|
286
306
|
className="bg-green-500 h-full rounded-full transition-all duration-300"
|
|
287
|
-
style={{ width: `${
|
|
307
|
+
style={{ width: `${uploadPercent}%` }}
|
|
288
308
|
/>
|
|
289
309
|
)}
|
|
290
310
|
</div>
|
|
291
311
|
<p className="text-neutral-600 text-xs font-mono">
|
|
292
|
-
{
|
|
312
|
+
{activeUpload.shardsDone} shards ·{' '}
|
|
313
|
+
{formatBytes(activeUpload.bytesUploaded)} /{' '}
|
|
314
|
+
{formatBytes(activeUpload.totalBytes)}
|
|
293
315
|
</p>
|
|
294
316
|
</div>
|
|
295
317
|
) : (
|
|
@@ -340,12 +362,11 @@ export function UploadZone() {
|
|
|
340
362
|
)}
|
|
341
363
|
{isDownloading && downloadProgress && (
|
|
342
364
|
<span>
|
|
343
|
-
{'
|
|
344
|
-
{
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
: 'Assembling...'}
|
|
365
|
+
{' '}
|
|
366
|
+
·{' '}
|
|
367
|
+
{formatBytes(downloadProgress.bytesDownloaded)} /{' '}
|
|
368
|
+
{formatBytes(downloadProgress.totalBytes)} (
|
|
369
|
+
{downloadProgress.shardsDone} shards)
|
|
349
370
|
</span>
|
|
350
371
|
)}
|
|
351
372
|
</p>
|
|
@@ -1,9 +1,14 @@
|
|
|
1
|
+
import type { AppMetadata } from 'sia-storage'
|
|
2
|
+
|
|
3
|
+
// biome-ignore format: scaffolder substitutes a 64-char hex string here
|
|
1
4
|
export const APP_KEY = '{{APP_KEY}}'
|
|
2
5
|
export const APP_NAME = '{{APP_NAME}}'
|
|
3
6
|
export const DEFAULT_INDEXER_URL = '{{INDEXER_URL}}'
|
|
4
|
-
export const APP_META =
|
|
5
|
-
|
|
7
|
+
export const APP_META: AppMetadata = {
|
|
8
|
+
appId: APP_KEY,
|
|
6
9
|
name: APP_NAME,
|
|
7
10
|
description: '{{APP_DESCRIPTION}}',
|
|
8
|
-
|
|
9
|
-
|
|
11
|
+
serviceUrl: '{{INDEXER_URL}}',
|
|
12
|
+
logoUrl: undefined,
|
|
13
|
+
callbackUrl: undefined,
|
|
14
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Sdk } from 'sia-storage'
|
|
2
2
|
import { create } from 'zustand'
|
|
3
3
|
import { persist } from 'zustand/middleware'
|
|
4
4
|
|
|
@@ -10,13 +10,13 @@ export type AuthStep =
|
|
|
10
10
|
| 'connected'
|
|
11
11
|
|
|
12
12
|
type AuthState = {
|
|
13
|
-
|
|
13
|
+
sdk: Sdk | null
|
|
14
14
|
storedKeyHex: string | null
|
|
15
15
|
indexerUrl: string
|
|
16
16
|
step: AuthStep
|
|
17
17
|
error: string | null
|
|
18
18
|
approvalUrl: string | null
|
|
19
|
-
|
|
19
|
+
setSdk: (sdk: Sdk) => void
|
|
20
20
|
setStep: (step: AuthStep) => void
|
|
21
21
|
setError: (error: string | null) => void
|
|
22
22
|
setStoredKeyHex: (hex: string) => void
|
|
@@ -28,13 +28,13 @@ type AuthState = {
|
|
|
28
28
|
export const useAuthStore = create<AuthState>()(
|
|
29
29
|
persist(
|
|
30
30
|
(set) => ({
|
|
31
|
-
|
|
31
|
+
sdk: null,
|
|
32
32
|
storedKeyHex: null,
|
|
33
33
|
indexerUrl: '',
|
|
34
34
|
step: 'loading',
|
|
35
35
|
error: null,
|
|
36
36
|
approvalUrl: null,
|
|
37
|
-
|
|
37
|
+
setSdk: (sdk) => set({ sdk, step: 'connected', error: null }),
|
|
38
38
|
setStep: (step) => set({ step, error: null }),
|
|
39
39
|
setError: (error) => set({ error }),
|
|
40
40
|
setStoredKeyHex: (hex) => set({ storedKeyHex: hex }),
|
|
@@ -42,7 +42,7 @@ export const useAuthStore = create<AuthState>()(
|
|
|
42
42
|
setApprovalUrl: (url) => set({ approvalUrl: url }),
|
|
43
43
|
reset: () =>
|
|
44
44
|
set({
|
|
45
|
-
|
|
45
|
+
sdk: null,
|
|
46
46
|
storedKeyHex: null,
|
|
47
47
|
step: 'loading',
|
|
48
48
|
error: null,
|
package/template/vite.config.ts
CHANGED
|
@@ -1,17 +1,10 @@
|
|
|
1
1
|
import tailwindcss from '@tailwindcss/vite'
|
|
2
2
|
import react from '@vitejs/plugin-react'
|
|
3
3
|
import { defineConfig } from 'vite'
|
|
4
|
-
import topLevelAwait from 'vite-plugin-top-level-await'
|
|
5
|
-
import wasm from 'vite-plugin-wasm'
|
|
6
4
|
|
|
7
5
|
export default defineConfig({
|
|
8
|
-
plugins: [react(), tailwindcss()
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
},
|
|
12
|
-
resolve: {
|
|
13
|
-
alias: {
|
|
14
|
-
'sia-wasm': '@siafoundation/sia/wasm',
|
|
15
|
-
},
|
|
16
|
-
},
|
|
6
|
+
plugins: [react(), tailwindcss()],
|
|
7
|
+
// sia-storage loads its WASM via `new URL(..., import.meta.url)`; excluding
|
|
8
|
+
// it from the deps pre-bundler keeps that URL pointing at the real file.
|
|
9
|
+
optimizeDeps: { exclude: ['sia-storage'] },
|
|
17
10
|
})
|
package/template/AGENTS.md
DELETED
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
# Sia Starter — AI Assistant Guide
|
|
2
|
-
|
|
3
|
-
## Overview
|
|
4
|
-
|
|
5
|
-
A starter template for building decentralized storage apps on the Sia network. Uses `@siafoundation/sia` — a TypeScript SDK that ships a pre-compiled WASM binary for encryption, uploads, downloads, and key management.
|
|
6
|
-
|
|
7
|
-
**Tech stack:** React 19, TypeScript, Vite, Tailwind CSS 4, Zustand, `@siafoundation/sia`
|
|
8
|
-
|
|
9
|
-
## Architecture
|
|
10
|
-
|
|
11
|
-
### Auth Flow State Machine
|
|
12
|
-
|
|
13
|
-
The app uses a step-based auth flow managed by Zustand (`src/stores/auth.ts`):
|
|
14
|
-
|
|
15
|
-
```
|
|
16
|
-
loading → connect → approve → recovery → connected
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
- **loading**: WASM initializes, checks for stored app key
|
|
20
|
-
- **connect**: User enters indexer URL, app requests connection
|
|
21
|
-
- **approve**: User visits approval URL in another tab
|
|
22
|
-
- **recovery**: User generates or enters 12-word recovery phrase
|
|
23
|
-
- **connected**: SDK is ready, main app UI renders
|
|
24
|
-
|
|
25
|
-
### SDK
|
|
26
|
-
|
|
27
|
-
The SDK (`@siafoundation/sia`) is an npm package that handles:
|
|
28
|
-
- Encrypted file uploads/downloads (erasure coding + encryption)
|
|
29
|
-
- Key derivation from recovery phrases (BIP-39)
|
|
30
|
-
- Object pinning and metadata management
|
|
31
|
-
- Connection auth with indexers
|
|
32
|
-
- Parallel multi-worker uploads and downloads
|
|
33
|
-
|
|
34
|
-
### Zustand Persistence
|
|
35
|
-
|
|
36
|
-
Auth state persists to localStorage via Zustand's `persist` middleware. The storage key is `{app-name}-auth`. Persisted fields: `storedKeyHex`, `indexerUrl`.
|
|
37
|
-
|
|
38
|
-
## Key Files
|
|
39
|
-
|
|
40
|
-
| File | Description |
|
|
41
|
-
|------|-------------|
|
|
42
|
-
| `src/lib/constants.ts` | App key, app name, indexer URL, app metadata |
|
|
43
|
-
| `src/stores/auth.ts` | Auth state machine (Zustand + persist) |
|
|
44
|
-
| `src/stores/toast.ts` | Toast notification store (auto-dismiss) |
|
|
45
|
-
| `src/components/Navbar.tsx` | App navbar with title, public key, sign out |
|
|
46
|
-
| `src/components/Toast.tsx` | Toast overlay component |
|
|
47
|
-
| `src/components/CopyButton.tsx` | Copy-to-clipboard button with toast |
|
|
48
|
-
| `src/components/auth/AuthFlow.tsx` | Auth orchestrator |
|
|
49
|
-
| `src/components/auth/ConnectScreen.tsx` | Indexer connection + CORS fallback |
|
|
50
|
-
| `src/components/auth/ApproveScreen.tsx` | Approval polling (auto-polls on mount) |
|
|
51
|
-
| `src/components/auth/RecoveryScreen.tsx` | Recovery phrase generation/import |
|
|
52
|
-
| `src/components/upload/UploadZone.tsx` | File upload/download dropzone + file list |
|
|
53
|
-
| `src/components/DevNote.tsx` | Developer callout component (unused, available for customization) |
|
|
54
|
-
|
|
55
|
-
## SDK Usage Patterns
|
|
56
|
-
|
|
57
|
-
### Upload a file
|
|
58
|
-
|
|
59
|
-
```ts
|
|
60
|
-
const pinnedObject = await client.upload(file, (progress) => {
|
|
61
|
-
// progress.phase: 'connecting' | 'uploading' | 'assembling' | 'pinning'
|
|
62
|
-
console.log(`${progress.slabsComplete}/${progress.slabsTotal} slabs`)
|
|
63
|
-
})
|
|
64
|
-
pinnedObject.updateMetadata(new TextEncoder().encode(JSON.stringify({
|
|
65
|
-
name: 'file.txt',
|
|
66
|
-
type: 'text/plain',
|
|
67
|
-
size: file.size,
|
|
68
|
-
hash: '...',
|
|
69
|
-
})))
|
|
70
|
-
await client.pinObject(pinnedObject)
|
|
71
|
-
await client.updateObjectMetadata(pinnedObject)
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
### Download a file
|
|
75
|
-
|
|
76
|
-
```ts
|
|
77
|
-
const data = await client.download(pinnedObject, (progress) => {
|
|
78
|
-
// progress.phase: 'connecting' | 'downloading' | 'assembling'
|
|
79
|
-
console.log(`${progress.slabsComplete}/${progress.slabsTotal} slabs`)
|
|
80
|
-
})
|
|
81
|
-
// data is Uint8Array
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
### List files
|
|
85
|
-
|
|
86
|
-
```ts
|
|
87
|
-
import { decodeMetadata } from '@siafoundation/sia'
|
|
88
|
-
|
|
89
|
-
const events = await client.objectEvents(null, 100)
|
|
90
|
-
for (const event of events) {
|
|
91
|
-
if (!event.deleted && event.object) {
|
|
92
|
-
const meta = decodeMetadata(event.object.metadata())
|
|
93
|
-
console.log(meta.name, event.object.size())
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
### Delete a file
|
|
99
|
-
|
|
100
|
-
```ts
|
|
101
|
-
await client.deleteObject(objectId)
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
### Share a file
|
|
105
|
-
|
|
106
|
-
```ts
|
|
107
|
-
const validUntil = Date.now() + 7 * 24 * 60 * 60 * 1000 // 7 days
|
|
108
|
-
const shareUrl = client.shareObject(pinnedObject, validUntil)
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
### Download shared file
|
|
112
|
-
|
|
113
|
-
```ts
|
|
114
|
-
const object = await client.sharedObject(shareUrl)
|
|
115
|
-
const data = await client.download(object)
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
## Customization
|
|
119
|
-
|
|
120
|
-
### Change app key
|
|
121
|
-
|
|
122
|
-
Edit `src/lib/constants.ts`. The app key is a 32-byte hex string that identifies your app to the indexer. Generate one with:
|
|
123
|
-
|
|
124
|
-
```ts
|
|
125
|
-
crypto.randomBytes(32).toString('hex')
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
### Replace the upload UI
|
|
129
|
-
|
|
130
|
-
The post-auth UI is rendered in `src/App.tsx`. Replace `<UploadZone />` with your own component. The `SiaClient` is available via `useAuthStore((s) => s.client)`. `SiaClient` exposes all SDK methods directly — use `client.upload()`, `client.download()`, `client.objectEvents()`, etc.
|
|
131
|
-
|
|
132
|
-
### Add routes
|
|
133
|
-
|
|
134
|
-
Install `react-router-dom` and wrap your app. The auth flow should gate all routes.
|
|
135
|
-
|
|
136
|
-
## Common Commands
|
|
137
|
-
|
|
138
|
-
```bash
|
|
139
|
-
bun install # Install dependencies
|
|
140
|
-
bun dev # Start dev server
|
|
141
|
-
bun run build # Production build
|
|
142
|
-
bun run check # Lint with Biome
|
|
143
|
-
```
|