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,818 @@
1
+ use std::io::Cursor;
2
+ use std::pin::Pin;
3
+ use std::sync::{Arc, Mutex};
4
+ use std::task::{Context, Poll};
5
+
6
+ use indexd::app_client::{RegisterAppRequest, RegisterAppResponse};
7
+ use js_sys::Uint8Array;
8
+ use tokio::io::AsyncWrite;
9
+ use sia::seed::Seed;
10
+ use sia::signing::{PrivateKey, Signature};
11
+ use sia::types::Hash256;
12
+ use std::str::FromStr;
13
+ use std::time::SystemTime;
14
+ use chrono::{TimeZone, Utc};
15
+ use wasm_bindgen::prelude::*;
16
+
17
+ /// Install a panic hook and logging bridge so that Rust panics show a proper
18
+ /// stack trace and `log::debug!()` / `log::info!()` etc. appear in the browser
19
+ /// console.
20
+ #[wasm_bindgen(start)]
21
+ fn init_panic_hook() {
22
+ console_error_panic_hook::set_once();
23
+ console_log::init_with_level(log::Level::Trace).ok();
24
+ log::info!("indexd WASM initialized - logging at TRACE level");
25
+ }
26
+
27
+ /// Converts a JsValue error context into a JsError.
28
+ fn to_js_err(e: impl std::fmt::Display) -> JsError {
29
+ JsError::new(&e.to_string())
30
+ }
31
+
32
+ // ── AppKey ──────────────────────────────────────────────────────────────
33
+
34
+ #[wasm_bindgen]
35
+ pub struct AppKey(PrivateKey);
36
+
37
+ #[wasm_bindgen]
38
+ impl AppKey {
39
+ /// Imports an AppKey from a 64-byte ed25519 keypair or a 32-byte seed.
40
+ #[wasm_bindgen(constructor)]
41
+ pub fn new(key: &[u8]) -> Result<AppKey, JsError> {
42
+ match key.len() {
43
+ 64 => {
44
+ let mut keypair = [0u8; 64];
45
+ keypair.copy_from_slice(key);
46
+ Ok(AppKey(PrivateKey::from(keypair)))
47
+ }
48
+ 32 => {
49
+ let mut seed = [0u8; 32];
50
+ seed.copy_from_slice(key);
51
+ Ok(AppKey(PrivateKey::from_seed(&seed)))
52
+ }
53
+ _ => Err(JsError::new("app key must be 64 bytes (keypair) or 32 bytes (seed)")),
54
+ }
55
+ }
56
+
57
+ /// Exports the full 64-byte ed25519 keypair.
58
+ pub fn export(&self) -> Uint8Array {
59
+ Uint8Array::from(self.0.as_ref())
60
+ }
61
+
62
+ /// Returns the hex-encoded public key.
63
+ #[wasm_bindgen(js_name = "publicKey")]
64
+ pub fn public_key(&self) -> String {
65
+ self.0.public_key().to_string()
66
+ }
67
+
68
+ /// Signs a message, returning the 64-byte signature.
69
+ pub fn sign(&self, message: &[u8]) -> Uint8Array {
70
+ let sig = self.0.sign(message);
71
+ Uint8Array::from(sig.as_ref() as &[u8])
72
+ }
73
+
74
+ /// Verifies a signature against a message.
75
+ #[wasm_bindgen(js_name = "verifySignature")]
76
+ pub fn verify_signature(&self, message: &[u8], signature: &[u8]) -> Result<bool, JsError> {
77
+ if signature.len() != 64 {
78
+ return Err(JsError::new("signatures must be 64 bytes"));
79
+ }
80
+ let mut sig_bytes = [0u8; 64];
81
+ sig_bytes.copy_from_slice(signature);
82
+ Ok(self
83
+ .0
84
+ .public_key()
85
+ .verify(message, &Signature::from(sig_bytes)))
86
+ }
87
+ }
88
+
89
+ // ── PinnedObject ────────────────────────────────────────────────────────
90
+
91
+ #[wasm_bindgen]
92
+ pub struct PinnedObject {
93
+ inner: Arc<Mutex<indexd::Object>>,
94
+ }
95
+
96
+ #[wasm_bindgen]
97
+ impl PinnedObject {
98
+ /// Opens a sealed object (JSON) using the provided app key.
99
+ pub fn open(app_key: &AppKey, sealed_json: &str) -> Result<PinnedObject, JsError> {
100
+ let sealed: indexd::SealedObject =
101
+ serde_json::from_str(sealed_json).map_err(to_js_err)?;
102
+ let obj = sealed.open(&app_key.0).map_err(to_js_err)?;
103
+ Ok(PinnedObject {
104
+ inner: Arc::new(Mutex::new(obj)),
105
+ })
106
+ }
107
+
108
+ /// Seals the object for offline storage, returning JSON.
109
+ pub fn seal(&self, app_key: &AppKey) -> Result<String, JsError> {
110
+ let inner = self.inner.lock().map_err(to_js_err)?;
111
+ let sealed = inner.seal(&app_key.0);
112
+ serde_json::to_string(&sealed).map_err(to_js_err)
113
+ }
114
+
115
+ /// Returns the object's ID as a hex string.
116
+ pub fn id(&self) -> Result<String, JsError> {
117
+ let inner = self.inner.lock().map_err(to_js_err)?;
118
+ Ok(inner.id().to_string())
119
+ }
120
+
121
+ /// Returns the total size of the object in bytes.
122
+ pub fn size(&self) -> Result<f64, JsError> {
123
+ let inner = self.inner.lock().map_err(to_js_err)?;
124
+ Ok(inner.size() as f64)
125
+ }
126
+
127
+ /// Returns the metadata as a Uint8Array.
128
+ pub fn metadata(&self) -> Result<Uint8Array, JsError> {
129
+ let inner = self.inner.lock().map_err(to_js_err)?;
130
+ Ok(Uint8Array::from(inner.metadata.as_slice()))
131
+ }
132
+
133
+ /// Updates the metadata.
134
+ #[wasm_bindgen(js_name = "updateMetadata")]
135
+ pub fn update_metadata(&self, metadata: &[u8]) -> Result<(), JsError> {
136
+ let mut inner = self.inner.lock().map_err(to_js_err)?;
137
+ inner.metadata = metadata.to_vec();
138
+ Ok(())
139
+ }
140
+ }
141
+
142
+ // ── SDK ─────────────────────────────────────────────────────────────────
143
+
144
+ #[wasm_bindgen]
145
+ pub struct SDK {
146
+ inner: indexd::SDK,
147
+ }
148
+
149
+ #[wasm_bindgen]
150
+ impl SDK {
151
+ /// Returns the app key used by this SDK instance.
152
+ #[wasm_bindgen(js_name = "appKey")]
153
+ pub fn app_key(&self) -> AppKey {
154
+ AppKey(self.inner.app_key().clone())
155
+ }
156
+
157
+ /// Uploads a Uint8Array to the Sia network.
158
+ ///
159
+ /// Returns a PinnedObject containing the metadata needed to download the data.
160
+ pub async fn upload(&self, data: &[u8]) -> Result<PinnedObject, JsError> {
161
+ log::info!("upload: starting ({} bytes)", data.len());
162
+ let rt = tokio::runtime::Builder::new_current_thread()
163
+ .build()
164
+ .map_err(to_js_err)?;
165
+ let _guard = rt.enter();
166
+ let local = tokio::task::LocalSet::new();
167
+ let cursor = Cursor::new(data.to_vec());
168
+ let object: indexd::Object = local
169
+ .run_until(self.inner.upload(cursor, indexd::UploadOptions::default()))
170
+ .await
171
+ .map_err(to_js_err)?;
172
+ log::info!("upload: complete");
173
+ Ok(PinnedObject {
174
+ inner: Arc::new(Mutex::new(object)),
175
+ })
176
+ }
177
+
178
+ /// Downloads an object's data, returning a Uint8Array.
179
+ pub async fn download(&self, object: &PinnedObject) -> Result<Uint8Array, JsError> {
180
+ let rt = tokio::runtime::Builder::new_current_thread()
181
+ .build()
182
+ .map_err(to_js_err)?;
183
+ let _guard = rt.enter();
184
+ let local = tokio::task::LocalSet::new();
185
+ let obj = object.inner.lock().map_err(to_js_err)?.clone();
186
+ let size = obj.size() as usize;
187
+ let mut buf = vec![0u8; size];
188
+ local
189
+ .run_until(async {
190
+ self.inner.download(
191
+ &mut Cursor::new(&mut buf),
192
+ &obj,
193
+ indexd::DownloadOptions::default(),
194
+ ).await
195
+ })
196
+ .await
197
+ .map_err(to_js_err)?;
198
+ Ok(Uint8Array::from(buf.as_slice()))
199
+ }
200
+
201
+ /// Uploads a Uint8Array with per-shard progress reporting.
202
+ ///
203
+ /// The `on_progress` callback receives `(current_shards, total_shards)`.
204
+ #[wasm_bindgen(js_name = "uploadWithProgress")]
205
+ pub async fn upload_with_progress(
206
+ &self,
207
+ data: &[u8],
208
+ on_progress: &js_sys::Function,
209
+ ) -> Result<PinnedObject, JsError> {
210
+ log::info!("upload_with_progress: starting ({} bytes)", data.len());
211
+ let slab_data_size = 10usize * 4_194_304; // data_shards(10) * SECTOR_SIZE(4 MiB)
212
+ let num_slabs = if data.is_empty() { 0 } else { data.len().div_ceil(slab_data_size) };
213
+ let total_shards = (num_slabs as u32) * 30; // 30 shards per slab (10 data + 20 parity)
214
+
215
+ let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
216
+ let options = indexd::UploadOptions {
217
+ shard_uploaded: Some(tx),
218
+ ..self.inner.default_upload_options()
219
+ };
220
+
221
+ let rt = tokio::runtime::Builder::new_current_thread()
222
+ .build()
223
+ .map_err(to_js_err)?;
224
+ let _guard = rt.enter();
225
+ let local = tokio::task::LocalSet::new();
226
+ let cursor = Cursor::new(data.to_vec());
227
+ let on_progress = on_progress.clone();
228
+ let object: indexd::Object = local
229
+ .run_until(async {
230
+ tokio::task::spawn_local(async move {
231
+ let mut count: u32 = 0;
232
+ while rx.recv().await.is_some() {
233
+ count += 1;
234
+ let _ = on_progress.call2(
235
+ &JsValue::NULL,
236
+ &JsValue::from(count),
237
+ &JsValue::from(total_shards),
238
+ );
239
+ }
240
+ });
241
+ self.inner.upload(cursor, options).await
242
+ })
243
+ .await
244
+ .map_err(to_js_err)?;
245
+ log::info!("upload_with_progress: complete");
246
+ Ok(PinnedObject {
247
+ inner: Arc::new(Mutex::new(object)),
248
+ })
249
+ }
250
+
251
+ /// Downloads an object's data with per-slab progress reporting.
252
+ ///
253
+ /// The `on_progress` callback receives `(current_slabs, total_slabs)`.
254
+ #[wasm_bindgen(js_name = "downloadWithProgress")]
255
+ pub async fn download_with_progress(
256
+ &self,
257
+ object: &PinnedObject,
258
+ on_progress: &js_sys::Function,
259
+ ) -> Result<Uint8Array, JsError> {
260
+ let rt = tokio::runtime::Builder::new_current_thread()
261
+ .build()
262
+ .map_err(to_js_err)?;
263
+ let _guard = rt.enter();
264
+ let local = tokio::task::LocalSet::new();
265
+ let obj = object.inner.lock().map_err(to_js_err)?.clone();
266
+ let size = obj.size() as usize;
267
+ let total_slabs = obj.slabs().len() as u32;
268
+ let mut buf = vec![0u8; size];
269
+
270
+ let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
271
+ let options = indexd::DownloadOptions {
272
+ slab_downloaded: Some(tx),
273
+ ..self.inner.default_download_options()
274
+ };
275
+
276
+ let on_progress = on_progress.clone();
277
+ local
278
+ .run_until(async {
279
+ tokio::task::spawn_local(async move {
280
+ let mut count: u32 = 0;
281
+ while rx.recv().await.is_some() {
282
+ count += 1;
283
+ let _ = on_progress.call2(
284
+ &JsValue::NULL,
285
+ &JsValue::from(count),
286
+ &JsValue::from(total_slabs),
287
+ );
288
+ }
289
+ });
290
+ self.inner
291
+ .download(&mut Cursor::new(&mut buf), &obj, options)
292
+ .await
293
+ })
294
+ .await
295
+ .map_err(to_js_err)?;
296
+ Ok(Uint8Array::from(buf.as_slice()))
297
+ }
298
+
299
+ /// Downloads an object with streaming chunks.
300
+ /// Fires `on_chunk(bytes)` after each slab is decoded and `on_progress(current, total)` for progress.
301
+ #[wasm_bindgen(js_name = "downloadStreaming")]
302
+ pub async fn download_streaming(
303
+ &self,
304
+ object: &PinnedObject,
305
+ on_chunk: &js_sys::Function,
306
+ on_progress: &js_sys::Function,
307
+ ) -> Result<(), JsError> {
308
+ /// Custom AsyncWrite that calls JS callback with each chunk
309
+ struct ChunkWriter {
310
+ callback: js_sys::Function,
311
+ }
312
+
313
+ impl AsyncWrite for ChunkWriter {
314
+ fn poll_write(
315
+ self: Pin<&mut Self>,
316
+ _cx: &mut Context<'_>,
317
+ buf: &[u8],
318
+ ) -> Poll<Result<usize, std::io::Error>> {
319
+ let array = Uint8Array::from(buf);
320
+ let _ = self.callback.call1(&JsValue::NULL, &array);
321
+ Poll::Ready(Ok(buf.len()))
322
+ }
323
+
324
+ fn poll_flush(
325
+ self: Pin<&mut Self>,
326
+ _cx: &mut Context<'_>,
327
+ ) -> Poll<Result<(), std::io::Error>> {
328
+ Poll::Ready(Ok(()))
329
+ }
330
+
331
+ fn poll_shutdown(
332
+ self: Pin<&mut Self>,
333
+ _cx: &mut Context<'_>,
334
+ ) -> Poll<Result<(), std::io::Error>> {
335
+ Poll::Ready(Ok(()))
336
+ }
337
+ }
338
+
339
+ let rt = tokio::runtime::Builder::new_current_thread()
340
+ .build()
341
+ .map_err(to_js_err)?;
342
+ let _guard = rt.enter();
343
+ let local = tokio::task::LocalSet::new();
344
+ let obj = object.inner.lock().map_err(to_js_err)?.clone();
345
+ let total_slabs = obj.slabs().len() as u32;
346
+
347
+ let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
348
+ let options = indexd::DownloadOptions {
349
+ slab_downloaded: Some(tx),
350
+ ..self.inner.default_download_options()
351
+ };
352
+
353
+ let mut writer = ChunkWriter {
354
+ callback: on_chunk.clone(),
355
+ };
356
+
357
+ let on_progress = on_progress.clone();
358
+ local
359
+ .run_until(async {
360
+ tokio::task::spawn_local(async move {
361
+ let mut count: u32 = 0;
362
+ while rx.recv().await.is_some() {
363
+ count += 1;
364
+ let _ = on_progress.call2(
365
+ &JsValue::NULL,
366
+ &JsValue::from(count),
367
+ &JsValue::from(total_slabs),
368
+ );
369
+ }
370
+ });
371
+ self.inner.download(&mut writer, &obj, options).await
372
+ })
373
+ .await
374
+ .map_err(to_js_err)?;
375
+ Ok(())
376
+ }
377
+
378
+ /// Returns hosts as a JSON array.
379
+ pub async fn hosts(&self) -> Result<JsValue, JsError> {
380
+ let hosts = self
381
+ .inner
382
+ .hosts(Default::default())
383
+ .await
384
+ .map_err(to_js_err)?;
385
+ serde_wasm_bindgen::to_value(&hosts).map_err(to_js_err)
386
+ }
387
+
388
+ /// Returns account information as a JS object.
389
+ pub async fn account(&self) -> Result<JsValue, JsError> {
390
+ let a = self.inner.account().await.map_err(to_js_err)?;
391
+ let obj = serde_json::json!({
392
+ "accountKey": a.account_key.to_string(),
393
+ "connectKey": a.connect_key,
394
+ "maxPinnedData": a.max_pinned_data,
395
+ "pinnedData": a.pinned_data,
396
+ "app": {
397
+ "id": a.app.id.to_string(),
398
+ "description": a.app.description,
399
+ "serviceUrl": a.app.service_url,
400
+ "logoUrl": a.app.logo_url,
401
+ },
402
+ "lastUsed": a.last_used.timestamp_millis(),
403
+ });
404
+ serde_wasm_bindgen::to_value(&obj).map_err(to_js_err)
405
+ }
406
+
407
+ /// Retrieves a pinned object by its hex-encoded key.
408
+ pub async fn object(&self, key: &str) -> Result<PinnedObject, JsError> {
409
+ let key = Hash256::from_str(key).map_err(to_js_err)?;
410
+ let obj = self.inner.object(&key).await.map_err(to_js_err)?;
411
+ Ok(PinnedObject {
412
+ inner: Arc::new(Mutex::new(obj)),
413
+ })
414
+ }
415
+
416
+ /// Pins an object to the indexer.
417
+ #[wasm_bindgen(js_name = "pinObject")]
418
+ pub async fn pin_object(&self, object: &PinnedObject) -> Result<(), JsError> {
419
+ let obj = object.inner.lock().map_err(to_js_err)?.clone();
420
+ self.inner.pin_object(&obj).await.map_err(to_js_err)
421
+ }
422
+
423
+ /// Updates the metadata of an object already stored in the indexer.
424
+ #[wasm_bindgen(js_name = "updateObjectMetadata")]
425
+ pub async fn update_object_metadata(&self, object: &PinnedObject) -> Result<(), JsError> {
426
+ let obj = object.inner.lock().map_err(to_js_err)?.clone();
427
+ self.inner
428
+ .update_object_metadata(&obj)
429
+ .await
430
+ .map_err(to_js_err)
431
+ }
432
+
433
+ /// Deletes an object from the indexer by its hex-encoded key.
434
+ #[wasm_bindgen(js_name = "deleteObject")]
435
+ pub async fn delete_object(&self, key: &str) -> Result<(), JsError> {
436
+ let key = Hash256::from_str(key).map_err(to_js_err)?;
437
+ self.inner.delete_object(&key).await.map_err(to_js_err)
438
+ }
439
+
440
+ /// Prunes unused slabs from the indexer.
441
+ #[wasm_bindgen(js_name = "pruneSlabs")]
442
+ pub async fn prune_slabs(&self) -> Result<(), JsError> {
443
+ self.inner.prune_slabs().await.map_err(to_js_err)
444
+ }
445
+
446
+ /// Returns object events for syncing. Supports cursor-based pagination.
447
+ ///
448
+ /// `cursor_json` is an optional JSON string: `{"id": "hex...", "after": <epoch_ms>}`
449
+ /// `limit` is the maximum number of events to return.
450
+ ///
451
+ /// Returns a JS array of objects:
452
+ /// `[{ id: string, deleted: bool, updatedAt: number, object: PinnedObject | null }]`
453
+ #[wasm_bindgen(js_name = "objectEvents")]
454
+ pub async fn object_events(
455
+ &self,
456
+ cursor_json: Option<String>,
457
+ limit: u32,
458
+ ) -> Result<JsValue, JsError> {
459
+ let cursor = match cursor_json {
460
+ Some(json) => {
461
+ let parsed: serde_json::Value = serde_json::from_str(&json).map_err(to_js_err)?;
462
+ let id_str = parsed["id"].as_str().ok_or_else(|| JsError::new("cursor missing 'id'"))?;
463
+ let after_ms = parsed["after"].as_i64().ok_or_else(|| JsError::new("cursor missing 'after'"))?;
464
+ let id = sia::types::Hash256::from_str(id_str).map_err(to_js_err)?;
465
+ let after = Utc.timestamp_millis_opt(after_ms)
466
+ .single()
467
+ .ok_or_else(|| JsError::new("invalid 'after' timestamp"))?;
468
+ Some(indexd::app_client::ObjectsCursor { id, after })
469
+ }
470
+ None => None,
471
+ };
472
+
473
+ let events = self.inner.object_events(cursor, Some(limit as usize)).await.map_err(to_js_err)?;
474
+
475
+ let arr = js_sys::Array::new();
476
+ let js_err = |_| JsError::new("failed to set property on object");
477
+ for event in events {
478
+ let obj = js_sys::Object::new();
479
+ js_sys::Reflect::set(&obj, &"id".into(), &event.id.to_string().into())
480
+ .map_err(js_err)?;
481
+ js_sys::Reflect::set(&obj, &"deleted".into(), &event.deleted.into())
482
+ .map_err(js_err)?;
483
+ js_sys::Reflect::set(
484
+ &obj,
485
+ &"updatedAt".into(),
486
+ &JsValue::from_f64(event.updated_at.timestamp_millis() as f64),
487
+ )
488
+ .map_err(js_err)?;
489
+
490
+ let pinned_val = match event.object {
491
+ Some(o) => {
492
+ let pinned = PinnedObject {
493
+ inner: Arc::new(Mutex::new(o)),
494
+ };
495
+ pinned.into()
496
+ }
497
+ None => JsValue::NULL,
498
+ };
499
+ js_sys::Reflect::set(&obj, &"object".into(), &pinned_val)
500
+ .map_err(js_err)?;
501
+
502
+ arr.push(&obj);
503
+ }
504
+ Ok(arr.into())
505
+ }
506
+
507
+ /// Creates a share URL for an object, valid until the given timestamp (ms since epoch).
508
+ #[wasm_bindgen(js_name = "shareObject")]
509
+ pub fn share_object(
510
+ &self,
511
+ object: &PinnedObject,
512
+ valid_until_ms: f64,
513
+ ) -> Result<String, JsError> {
514
+ let obj = object.inner.lock().map_err(to_js_err)?.clone();
515
+ let duration = std::time::Duration::from_millis(valid_until_ms as u64);
516
+ let valid_until = SystemTime::UNIX_EPOCH + duration;
517
+ let url = self
518
+ .inner
519
+ .share_object(&obj, valid_until.into())
520
+ .map_err(to_js_err)?;
521
+ // sia:// URLs can open the mobile app when clicked
522
+ // FIXME Alright - could leave this for the app to implement
523
+ let url_str = url.to_string();
524
+ Ok(if let Some(rest) = url_str.strip_prefix("https://") {
525
+ format!("sia://{rest}")
526
+ } else {
527
+ url_str
528
+ })
529
+ }
530
+
531
+ /// Retrieves a shared object from a signed share URL.
532
+ /// Accepts both `https://` and `sia://` schemes.
533
+ #[wasm_bindgen(js_name = "sharedObject")]
534
+ pub async fn shared_object(&self, share_url: &str) -> Result<PinnedObject, JsError> {
535
+ let url = if share_url.starts_with("sia://") {
536
+ format!("https://{}", &share_url[6..])
537
+ } else {
538
+ share_url.to_string()
539
+ };
540
+ let obj = self
541
+ .inner
542
+ .shared_object(url)
543
+ .await
544
+ .map_err(to_js_err)?;
545
+ Ok(PinnedObject {
546
+ inner: Arc::new(Mutex::new(obj)),
547
+ })
548
+ }
549
+ }
550
+
551
+ // ── Builder ─────────────────────────────────────────────────────────────
552
+
553
+ #[wasm_bindgen]
554
+ pub struct Builder {
555
+ state: Arc<Mutex<Option<BuilderState>>>,
556
+ }
557
+
558
+ enum BuilderState {
559
+ Disconnected(indexd::Builder<indexd::DisconnectedState>),
560
+ RequestingApproval(indexd::Builder<indexd::RequestingApprovalState>),
561
+ Approved(indexd::Builder<indexd::ApprovedState>),
562
+ Finalized,
563
+ }
564
+
565
+ #[wasm_bindgen]
566
+ impl Builder {
567
+ /// Creates a new SDK builder for the given indexer URL.
568
+ #[wasm_bindgen(constructor)]
569
+ pub fn new(indexer_url: &str) -> Result<Builder, JsError> {
570
+ let builder = indexd::Builder::new(indexer_url).map_err(to_js_err)?;
571
+ Ok(Builder {
572
+ state: Arc::new(Mutex::new(Some(BuilderState::Disconnected(builder)))),
573
+ })
574
+ }
575
+
576
+ /// Sets the maximum number of concurrent price fetches (default: 1).
577
+ /// Lower values = more stable, higher values = faster but may crash browser.
578
+ #[wasm_bindgen(js_name = "withMaxPriceFetches")]
579
+ pub fn with_max_price_fetches(&self, max: usize) -> Result<(), JsError> {
580
+ let mut state = self.state.lock().map_err(to_js_err)?;
581
+ if let Some(BuilderState::Disconnected(builder)) = state.take() {
582
+ let builder = builder.with_max_price_fetches(max);
583
+ *state = Some(BuilderState::Disconnected(builder));
584
+ Ok(())
585
+ } else {
586
+ Err(JsError::new("Can only set concurrency on disconnected builder"))
587
+ }
588
+ }
589
+
590
+ /// Sets the maximum number of concurrent downloads (default: 2).
591
+ /// Lower values = more stable, higher values = faster but may crash browser.
592
+ #[wasm_bindgen(js_name = "withMaxDownloads")]
593
+ pub fn with_max_downloads(&self, max: usize) -> Result<(), JsError> {
594
+ let mut state = self.state.lock().map_err(to_js_err)?;
595
+ if let Some(BuilderState::Disconnected(builder)) = state.take() {
596
+ let builder = builder.with_max_downloads(max);
597
+ *state = Some(BuilderState::Disconnected(builder));
598
+ Ok(())
599
+ } else {
600
+ Err(JsError::new("Can only set concurrency on disconnected builder"))
601
+ }
602
+ }
603
+
604
+ /// Sets the maximum number of concurrent uploads (default: 3).
605
+ /// Lower values = more stable, higher values = faster but may crash browser.
606
+ #[wasm_bindgen(js_name = "withMaxUploads")]
607
+ pub fn with_max_uploads(&self, max: usize) -> Result<(), JsError> {
608
+ let mut state = self.state.lock().map_err(to_js_err)?;
609
+ if let Some(BuilderState::Disconnected(builder)) = state.take() {
610
+ let builder = builder.with_max_uploads(max);
611
+ *state = Some(BuilderState::Disconnected(builder));
612
+ Ok(())
613
+ } else {
614
+ Err(JsError::new("Can only set concurrency on disconnected builder"))
615
+ }
616
+ }
617
+
618
+ /// Attempts to connect using an existing app key.
619
+ ///
620
+ /// Returns the SDK if authenticated, or null if the key is not recognized.
621
+ /// Call `requestConnection` if null is returned.
622
+ pub async fn connected(&self, app_key: &AppKey) -> Result<JsValue, JsError> {
623
+ let state = self
624
+ .state
625
+ .lock()
626
+ .map_err(to_js_err)?
627
+ .take()
628
+ .ok_or_else(|| JsError::new("invalid builder state"))?;
629
+
630
+ match state {
631
+ BuilderState::Disconnected(builder) => {
632
+ match builder.connected(&app_key.0).await.map_err(to_js_err)? {
633
+ Some(sdk) => {
634
+ *self.state.lock().map_err(to_js_err)? = Some(BuilderState::Finalized);
635
+ Ok(SDK { inner: sdk }.into())
636
+ }
637
+ None => {
638
+ *self.state.lock().map_err(to_js_err)? =
639
+ Some(BuilderState::Disconnected(builder));
640
+ Ok(JsValue::NULL)
641
+ }
642
+ }
643
+ }
644
+ other => {
645
+ *self.state.lock().map_err(to_js_err)? = Some(other);
646
+ Err(JsError::new("invalid state: expected Disconnected"))
647
+ }
648
+ }
649
+ }
650
+
651
+ /// Requests a new app connection. Pass app metadata as a JSON object:
652
+ /// ```json
653
+ /// {
654
+ /// "app_id": [32 bytes as hex],
655
+ /// "name": "My App",
656
+ /// "description": "...",
657
+ /// "service_url": "https://...",
658
+ /// "logo_url": "https://..." (optional),
659
+ /// "callback_url": "https://..." (optional)
660
+ /// }
661
+ /// ```
662
+ #[wasm_bindgen(js_name = "requestConnection")]
663
+ pub async fn request_connection(&self, app_meta_json: &str) -> Result<(), JsError> {
664
+ let meta: RegisterAppRequest =
665
+ serde_json::from_str(app_meta_json).map_err(to_js_err)?;
666
+
667
+ let state = self
668
+ .state
669
+ .lock()
670
+ .map_err(to_js_err)?
671
+ .take()
672
+ .ok_or_else(|| JsError::new("invalid builder state"))?;
673
+
674
+ match state {
675
+ BuilderState::Disconnected(builder) => {
676
+ let builder = builder
677
+ .request_connection(&meta)
678
+ .await
679
+ .map_err(to_js_err)?;
680
+ *self.state.lock().map_err(to_js_err)? =
681
+ Some(BuilderState::RequestingApproval(builder));
682
+ Ok(())
683
+ }
684
+ other => {
685
+ *self.state.lock().map_err(to_js_err)? = Some(other);
686
+ Err(JsError::new("invalid state: expected Disconnected"))
687
+ }
688
+ }
689
+ }
690
+
691
+ /// Transitions the builder using a pre-fetched connection response.
692
+ /// Use this when the `POST /auth/connect` call was made out-of-band
693
+ /// (e.g. via curl) to work around CORS restrictions.
694
+ ///
695
+ /// `app_id_hex` is the hex-encoded app ID used in the request.
696
+ /// `response_json` is the JSON response from `POST /auth/connect`.
697
+ #[wasm_bindgen(js_name = "setConnectionResponse")]
698
+ pub fn set_connection_response(
699
+ &self,
700
+ app_id_hex: &str,
701
+ response_json: &str,
702
+ ) -> Result<(), JsError> {
703
+ let app_id = Hash256::from_str(app_id_hex).map_err(to_js_err)?;
704
+ let response: RegisterAppResponse =
705
+ serde_json::from_str(response_json).map_err(to_js_err)?;
706
+
707
+ let state = self
708
+ .state
709
+ .lock()
710
+ .map_err(to_js_err)?
711
+ .take()
712
+ .ok_or_else(|| JsError::new("invalid builder state"))?;
713
+
714
+ match state {
715
+ BuilderState::Disconnected(builder) => {
716
+ let builder = builder
717
+ .with_connection_response(app_id, response)
718
+ .map_err(to_js_err)?;
719
+ *self.state.lock().map_err(to_js_err)? =
720
+ Some(BuilderState::RequestingApproval(builder));
721
+ Ok(())
722
+ }
723
+ other => {
724
+ *self.state.lock().map_err(to_js_err)? = Some(other);
725
+ Err(JsError::new("invalid state: expected Disconnected"))
726
+ }
727
+ }
728
+ }
729
+
730
+ /// Returns the response URL the user must visit to authorize the connection.
731
+ #[wasm_bindgen(js_name = "responseUrl")]
732
+ pub fn response_url(&self) -> Result<String, JsError> {
733
+ let state = self.state.lock().map_err(to_js_err)?;
734
+ match state.as_ref() {
735
+ Some(BuilderState::RequestingApproval(builder)) => {
736
+ Ok(builder.response_url().to_owned())
737
+ }
738
+ _ => Err(JsError::new(
739
+ "invalid state: expected RequestingApproval",
740
+ )),
741
+ }
742
+ }
743
+
744
+ /// Polls for approval. Resolves when the user approves.
745
+ #[wasm_bindgen(js_name = "waitForApproval")]
746
+ pub async fn wait_for_approval(&self) -> Result<(), JsError> {
747
+ let state = self
748
+ .state
749
+ .lock()
750
+ .map_err(to_js_err)?
751
+ .take()
752
+ .ok_or_else(|| JsError::new("invalid builder state"))?;
753
+
754
+ match state {
755
+ BuilderState::RequestingApproval(builder) => {
756
+ let builder = builder.wait_for_approval().await.map_err(to_js_err)?;
757
+ *self.state.lock().map_err(to_js_err)? = Some(BuilderState::Approved(builder));
758
+ Ok(())
759
+ }
760
+ other => {
761
+ *self.state.lock().map_err(to_js_err)? = Some(other);
762
+ Err(JsError::new(
763
+ "invalid state: expected RequestingApproval",
764
+ ))
765
+ }
766
+ }
767
+ }
768
+
769
+ /// Registers the app using the user's recovery phrase and returns the SDK.
770
+ pub async fn register(&self, mnemonic: &str) -> Result<SDK, JsError> {
771
+ let state = self
772
+ .state
773
+ .lock()
774
+ .map_err(to_js_err)?
775
+ .take()
776
+ .ok_or_else(|| JsError::new("invalid builder state"))?;
777
+
778
+ match state {
779
+ BuilderState::Approved(builder) => {
780
+ let sdk = builder.register(mnemonic).await.map_err(to_js_err)?;
781
+ *self.state.lock().map_err(to_js_err)? = Some(BuilderState::Finalized);
782
+ Ok(SDK { inner: sdk })
783
+ }
784
+ other => {
785
+ *self.state.lock().map_err(to_js_err)? = Some(other);
786
+ Err(JsError::new("invalid state: expected Approved"))
787
+ }
788
+ }
789
+ }
790
+ }
791
+
792
+ // ── Free functions ──────────────────────────────────────────────────────
793
+
794
+ /// Generates a new 12-word BIP-32 recovery phrase.
795
+ #[wasm_bindgen(js_name = "generateRecoveryPhrase")]
796
+ pub fn generate_recovery_phrase() -> String {
797
+ let seed: [u8; 16] = rand::random();
798
+ Seed::from_seed(seed).to_string()
799
+ }
800
+
801
+ /// Validates a BIP-32 recovery phrase.
802
+ #[wasm_bindgen(js_name = "validateRecoveryPhrase")]
803
+ pub fn validate_recovery_phrase(phrase: &str) -> Result<(), JsError> {
804
+ Seed::new(phrase).map_err(to_js_err)?;
805
+ Ok(())
806
+ }
807
+
808
+ /// Connects to a host via WebTransport and fetches its settings/prices.
809
+ ///
810
+ /// `address` should be a host address like `host.example.com:9883`.
811
+ /// Returns the host settings as a JS object.
812
+ #[wasm_bindgen(js_name = "fetchHostSettings")]
813
+ pub async fn fetch_host_settings(address: &str) -> Result<JsValue, JsError> {
814
+ let settings = indexd::web_transport::fetch_host_settings(address)
815
+ .await
816
+ .map_err(to_js_err)?;
817
+ serde_wasm_bindgen::to_value(&settings).map_err(to_js_err)
818
+ }