@freqhole/api-client 0.1.30

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 (45) hide show
  1. package/auth-test.html +651 -0
  2. package/package.json +42 -0
  3. package/src/AdminClient.ts +154 -0
  4. package/src/CharnelLocalTransport.ts +598 -0
  5. package/src/CharnelTransport.ts +616 -0
  6. package/src/FreqholeClient.ts +261 -0
  7. package/src/WasmTransport.ts +1077 -0
  8. package/src/automerge/IrohNetworkAdapter.ts +727 -0
  9. package/src/automerge/index.ts +14 -0
  10. package/src/codegen/admin_commands.ts +61 -0
  11. package/src/codegen/routes.ts +213 -0
  12. package/src/codegen/schema.ts +6721 -0
  13. package/src/domains/admin.ts +97 -0
  14. package/src/domains/app.ts +98 -0
  15. package/src/domains/auth.ts +191 -0
  16. package/src/domains/favorites.types.ts +52 -0
  17. package/src/domains/jobs.ts +48 -0
  18. package/src/domains/music.ts +1935 -0
  19. package/src/domains/types.ts +19 -0
  20. package/src/domains/upload.ts +214 -0
  21. package/src/index.ts +48 -0
  22. package/src/permissions.ts +468 -0
  23. package/src/playlistz/convert.ts +267 -0
  24. package/src/playlistz/index.ts +6 -0
  25. package/src/playlistz/mutations.ts +181 -0
  26. package/src/playlistz/protocol.ts +222 -0
  27. package/src/playlistz/schema.ts +138 -0
  28. package/src/playlistz/shareLink.ts +76 -0
  29. package/src/storage/blobs.ts +272 -0
  30. package/src/storage/identity.ts +190 -0
  31. package/src/storage/index.ts +5 -0
  32. package/src/storage/webLocks.ts +92 -0
  33. package/src/test/automerge.ts +796 -0
  34. package/src/test/coverage.ts +105 -0
  35. package/src/test/fixtures.ts +460 -0
  36. package/src/test/integration.ts +320 -0
  37. package/src/test/playlistz.ts +631 -0
  38. package/src/test/stateful.ts +561 -0
  39. package/src/test/storage.ts +443 -0
  40. package/src/test.ts +75 -0
  41. package/src/transport.ts +391 -0
  42. package/src/utils.ts +79 -0
  43. package/src/webauthn.ts +228 -0
  44. package/tsconfig.json +22 -0
  45. package/vite.config.js +9 -0
@@ -0,0 +1,616 @@
1
+ // CharnelTransport - P2P transport for Tauri apps
2
+ //
3
+ // uses Tauri IPC commands to make P2P requests via the server's
4
+ // app iroh endpoint. no WASM needed.
5
+
6
+ import type { BlobData, Transport, TransportResponse } from "./transport.js";
7
+ import type {
8
+ CloseReason,
9
+ EventFilter,
10
+ JobEvent,
11
+ JobStateSnapshot,
12
+ } from "./codegen/schema.js";
13
+ import { JobEventsStreamClosed } from "./CharnelLocalTransport.js";
14
+
15
+ // tauri invoke function type
16
+ type InvokeFn = (cmd: string, args?: unknown) => Promise<unknown>;
17
+
18
+ // webkitgtk (linux) requires HTTP/HTTPS URLs for Cache API keys.
19
+ // wrap bare blobIds with a synthetic URL prefix.
20
+ function cacheKey(blobId: string): string {
21
+ return `https://blob.local/${blobId}`;
22
+ }
23
+
24
+ // tauri invoke is dynamically imported to avoid bundling in browser builds
25
+ let invoke: InvokeFn | null = null;
26
+
27
+ /**
28
+ * initialize tauri invoke function
29
+ * call this before using CharnelTransport
30
+ */
31
+ async function ensureInvoke(): Promise<InvokeFn> {
32
+ if (invoke) return invoke;
33
+ try {
34
+ const tauri = await import("@tauri-apps/api/core");
35
+ invoke = tauri.invoke as InvokeFn;
36
+ return invoke;
37
+ } catch {
38
+ throw new Error("@tauri-apps/api not available - not running in Tauri");
39
+ }
40
+ }
41
+
42
+ /**
43
+ * check if tauri is available
44
+ */
45
+ export function isCharnelAvailable(): boolean {
46
+ return typeof window !== "undefined" && "__TAURI__" in window;
47
+ }
48
+
49
+ // default cache name if none provided
50
+ const DEFAULT_CACHE_NAME = "freqhole-blobs-v1";
51
+
52
+ // in-memory url cache for revocation
53
+ const urlCache = new Map<string, string>();
54
+
55
+ /**
56
+ * decode base64 string to Uint8Array
57
+ * handles both standard and URL-safe base64 encoding
58
+ */
59
+ function base64ToBytes(base64: string): Uint8Array {
60
+ // convert URL-safe base64 to standard base64
61
+ let standardBase64 = base64.replace(/-/g, "+").replace(/_/g, "/");
62
+ // add padding if needed
63
+ const padLen = (4 - (standardBase64.length % 4)) % 4;
64
+ standardBase64 += "=".repeat(padLen);
65
+
66
+ const binaryString = atob(standardBase64);
67
+ const bytes = new Uint8Array(binaryString.length);
68
+ for (let i = 0; i < binaryString.length; i++) {
69
+ bytes[i] = binaryString.charCodeAt(i);
70
+ }
71
+ return bytes;
72
+ }
73
+
74
+ /**
75
+ * encode Uint8Array to base64 string
76
+ */
77
+ function bytesToBase64(bytes: Uint8Array): string {
78
+ let binary = "";
79
+ for (let i = 0; i < bytes.length; i++) {
80
+ binary += String.fromCharCode(bytes[i]);
81
+ }
82
+ return btoa(binary);
83
+ }
84
+
85
+ /**
86
+ * CharnelTransport - P2P transport using Tauri IPC commands
87
+ * implements Transport interface for use with FreqholeClient
88
+ */
89
+ export class CharnelTransport implements Transport {
90
+ private peerAddr: string;
91
+ private nodeId: string | null = null;
92
+ private readonly cacheName: string;
93
+
94
+ constructor(peerAddr: string, cacheName?: string) {
95
+ this.peerAddr = peerAddr;
96
+ this.cacheName = cacheName ?? DEFAULT_CACHE_NAME;
97
+ }
98
+
99
+ /**
100
+ * initialize transport - must be called before use
101
+ */
102
+ async init(): Promise<void> {
103
+ const inv = await ensureInvoke();
104
+
105
+ const available = (await inv("p2p_is_available")) as boolean;
106
+ if (!available) {
107
+ throw new Error("P2P not available - federation endpoint not running");
108
+ }
109
+
110
+ this.nodeId = (await inv("p2p_get_node_id")) as string;
111
+ }
112
+
113
+ /**
114
+ * get local node_id (returns null if not yet initialized)
115
+ */
116
+ getNodeId(): string | null {
117
+ return this.nodeId;
118
+ }
119
+
120
+ /**
121
+ * make an API request via P2P
122
+ */
123
+ async request(
124
+ method: string,
125
+ path: string,
126
+ body?: string,
127
+ ): Promise<TransportResponse> {
128
+ const inv = await ensureInvoke();
129
+
130
+ // intentionally no per-error debug log here: the underlying
131
+ // p2p_proxy_request command already traces failures on the rust
132
+ // side, and callers (auth-status, health-check, etc.) decide
133
+ // whether to surface the error. logging at this layer fires once
134
+ // per request per failed peer and drowns the console for any
135
+ // peer that's temporarily unreachable.
136
+ const result = (await inv("p2p_proxy_request", {
137
+ peerAddr: this.peerAddr,
138
+ method,
139
+ path,
140
+ body: body ?? null,
141
+ })) as { status: number; body: string };
142
+
143
+ return result;
144
+ }
145
+
146
+ /**
147
+ * upload via P2P
148
+ *
149
+ * for music uploads, returns an error directing callers to use uploadByPath()
150
+ * which uses iroh-blobs pull model (file path -> FsStore import -> remote pull).
151
+ * for non-music uploads (images), uses base64 encoding (small enough to be fine).
152
+ */
153
+ async upload(path: string, formData: FormData): Promise<TransportResponse> {
154
+ // extract file from form data
155
+ const file = formData.get("file") as File | null;
156
+ if (!file) {
157
+ return {
158
+ status: 400,
159
+ body: JSON.stringify({
160
+ success: false,
161
+ message: "no file provided",
162
+ errors: [
163
+ {
164
+ error_type: "bad_request",
165
+ title: "bad request",
166
+ detail: "no file provided",
167
+ },
168
+ ],
169
+ }),
170
+ };
171
+ }
172
+
173
+ // for music uploads, import bytes into iroh-blobs store and use the
174
+ // blake3 pull model (same as uploadByPath but from in-memory bytes).
175
+ // this supports Android where file picker returns File objects, not paths.
176
+ if (path === "/api/upload/music") {
177
+ return this.uploadMusicViaBytes(file);
178
+ }
179
+
180
+ // for non-music uploads (images etc), use base64 (small enough)
181
+ return this.uploadViaBase64(path, file, formData);
182
+ }
183
+
184
+ /**
185
+ * upload a file by filesystem path via P2P using iroh-blobs pull model
186
+ *
187
+ * 1. imports file into local FsStore (gets blake3 hash)
188
+ * 2. tells remote peer to pull the blob via iroh-blobs
189
+ * 3. remote peer downloads verified, writes to disk, creates import job
190
+ */
191
+ async uploadByPath(
192
+ path: string,
193
+ filePath: string,
194
+ metadata?: Record<string, unknown>,
195
+ ): Promise<TransportResponse> {
196
+ const inv = await ensureInvoke();
197
+
198
+ // only use iroh-blobs for music uploads
199
+ if (path === "/api/upload/music") {
200
+ console.debug("[P2P] uploadByPath: importing blob from", filePath);
201
+ // import file into local FsStore -> get blake3 hash
202
+ const blake3 = (await inv("p2p_import_blob", { filePath })) as string;
203
+ console.debug("[P2P] uploadByPath: imported blob, blake3 =", blake3);
204
+
205
+ // build request body for the remote peer
206
+ const body: Record<string, unknown> = {
207
+ blake3,
208
+ filename:
209
+ filePath.split("/").pop() || filePath.split("\\").pop() || "music",
210
+ ...metadata,
211
+ };
212
+
213
+ // tell the remote peer to pull the blob from us
214
+ return this.request(
215
+ "POST",
216
+ "/api/upload/music-by-blake3",
217
+ JSON.stringify(body),
218
+ );
219
+ }
220
+
221
+ // for non-music uploads, send path + metadata via proxy_request
222
+ const body: Record<string, unknown> = {
223
+ file_path: filePath,
224
+ ...metadata,
225
+ };
226
+ return this.request("POST", path, JSON.stringify(body));
227
+ }
228
+
229
+ /**
230
+ * fallback upload via base64 encoding
231
+ * used for non-music uploads (images are small enough)
232
+ */
233
+ private async uploadViaBase64(
234
+ path: string,
235
+ file: File,
236
+ formData: FormData,
237
+ ): Promise<TransportResponse> {
238
+ const bytes = new Uint8Array(await file.arrayBuffer());
239
+ const base64 = bytesToBase64(bytes);
240
+
241
+ const body: Record<string, unknown> = {
242
+ data: base64,
243
+ filename: file.name,
244
+ };
245
+
246
+ // include associate_with if present
247
+ const associateWithStr = formData.get("associate_with") as string | null;
248
+ if (associateWithStr) {
249
+ try {
250
+ body.associate_with = JSON.parse(associateWithStr);
251
+ } catch {
252
+ // ignore parse errors
253
+ }
254
+ }
255
+
256
+ // send via proxy_request — routes through offal dispatch on the remote peer
257
+ return this.request("POST", path, JSON.stringify(body));
258
+ }
259
+
260
+ /**
261
+ * upload music via in-memory bytes using iroh-blobs pull model
262
+ *
263
+ * streams the File to the local blobs store in bounded chunks via
264
+ * p2p_import_begin / p2p_import_chunk / p2p_import_finish, then tells the
265
+ * remote peer to pull via blake3. chunking is required on Android, where
266
+ * tauri IPC is JSON-only and a single large base64 payload OOMs the
267
+ * webview; it also keeps memory bounded on both sides (the receiver
268
+ * accumulates chunks in a temp file on disk, not in memory).
269
+ */
270
+ private async uploadMusicViaBytes(
271
+ file: File,
272
+ ): Promise<TransportResponse> {
273
+ const inv = await ensureInvoke();
274
+
275
+ console.debug("[P2P] uploadMusicViaBytes: streaming file", file.name, file.size, "bytes");
276
+
277
+ // ~4MB raw per chunk -> ~5.5MB base64 per IPC call, well within limits.
278
+ const CHUNK_SIZE = 4 * 1024 * 1024;
279
+
280
+ const uploadId = (await inv("p2p_import_begin")) as string;
281
+ try {
282
+ for (let offset = 0; offset < file.size; offset += CHUNK_SIZE) {
283
+ const slice = file.slice(offset, Math.min(offset + CHUNK_SIZE, file.size));
284
+ const chunkBytes = new Uint8Array(await slice.arrayBuffer());
285
+ const b64 = bytesToBase64(chunkBytes);
286
+ await inv("p2p_import_chunk", { uploadId, data: b64 });
287
+ }
288
+ } catch (err) {
289
+ // best-effort cleanup of the partial temp file on the receiver side.
290
+ try {
291
+ await inv("p2p_import_abort", { uploadId });
292
+ } catch {
293
+ // ignore abort failures
294
+ }
295
+ throw err;
296
+ }
297
+
298
+ // finalize: import accumulated temp file into the blobs store -> blake3.
299
+ const blake3 = (await inv("p2p_import_finish", { uploadId })) as string;
300
+ console.debug("[P2P] uploadMusicViaBytes: imported blob, blake3 =", blake3);
301
+
302
+ // tell the remote peer to pull the blob from us
303
+ const body = { blake3, filename: file.name };
304
+ return this.request(
305
+ "POST",
306
+ "/api/upload/music-by-blake3",
307
+ JSON.stringify(body),
308
+ );
309
+ }
310
+
311
+ /**
312
+ * fetch a blob via P2P
313
+ * if blake3 is provided, uses iroh-blobs verified streaming.
314
+ * for blobs without blake3 (images, waveforms, thumbnails), uses proxy_request
315
+ * to fetch base64-encoded data from the peer's /api/blobs/{id}/data endpoint.
316
+ */
317
+ async fetchBlob(blobId: string, blake3?: string): Promise<BlobData> {
318
+ const inv = await ensureInvoke();
319
+ const tauri = await import("@tauri-apps/api/core");
320
+ const onProgress = new tauri.Channel<{ bytes_downloaded: number }>();
321
+
322
+ if (blake3) {
323
+ // blake3 known — use verified iroh-blobs download
324
+ const result = (await inv("p2p_fetch_blob_verified", {
325
+ peerAddr: this.peerAddr,
326
+ blake3Hash: blake3,
327
+ onProgress,
328
+ })) as { data: string; content_type: string | null; size: number };
329
+
330
+ const bytes = base64ToBytes(result.data);
331
+ return {
332
+ data: bytes,
333
+ contentType: result.content_type ?? "audio/mpeg",
334
+ };
335
+ }
336
+
337
+ // no blake3 — try proxy_request to get blob data from database
338
+ // this is the primary path for images (waveforms, thumbnails) stored in the database
339
+ try {
340
+ const result = await this.request("GET", `/api/blobs/${blobId}/data`);
341
+ if (result.status === 200) {
342
+ const parsed = JSON.parse(result.body);
343
+ if (parsed.success && parsed.data?.data) {
344
+ const bytes = base64ToBytes(parsed.data.data);
345
+ const contentType = parsed.data.mime || "application/octet-stream";
346
+ return { data: bytes, contentType };
347
+ }
348
+ }
349
+ } catch (e) {
350
+ const errorMessage = e instanceof Error ? e.message : String(e);
351
+ console.warn(
352
+ `[CharnelTransport] proxy blob data request failed, falling back to verified download: ${errorMessage}`,
353
+ );
354
+ }
355
+
356
+ // fallback: ask the peer to compute blake3, then do verified download
357
+ const result = (await inv("p2p_fetch_blob_verified_by_id", {
358
+ peerAddr: this.peerAddr,
359
+ blobId,
360
+ onProgress,
361
+ })) as {
362
+ data: string;
363
+ content_type: string | null;
364
+ size: number;
365
+ blake3: string;
366
+ };
367
+
368
+ const bytes = base64ToBytes(result.data);
369
+ return {
370
+ data: bytes,
371
+ contentType: result.content_type ?? "application/octet-stream",
372
+ };
373
+ }
374
+
375
+ /**
376
+ * get a URL for a blob - caches in Cache API
377
+ * if blake3 provided, uses verified iroh-blobs download
378
+ */
379
+ async getBlobUrl(blobId: string, blake3?: string): Promise<string> {
380
+ // check in-memory cache first
381
+ const cached = urlCache.get(blobId);
382
+ if (cached) {
383
+ return cached;
384
+ }
385
+
386
+ // check Cache API (use HTTP URL key for webkitgtk compatibility)
387
+ const cache = await caches.open(this.cacheName);
388
+ const cachedResponse = await cache.match(cacheKey(blobId));
389
+
390
+ if (cachedResponse) {
391
+ const blob = await cachedResponse.blob();
392
+ const url = URL.createObjectURL(blob);
393
+ urlCache.set(blobId, url);
394
+ return url;
395
+ }
396
+
397
+ // fetch via P2P and cache (pass blake3 for verified download)
398
+ const blobData = await this.fetchBlob(blobId, blake3);
399
+ const blob = new Blob([blobData.data.slice().buffer], {
400
+ type: blobData.contentType,
401
+ });
402
+
403
+ // store in Cache API (HTTP URL key for webkitgtk compatibility)
404
+ const response = new Response(blob, {
405
+ headers: { "Content-Type": blobData.contentType },
406
+ });
407
+ await cache.put(cacheKey(blobId), response);
408
+
409
+ // create object URL
410
+ const url = URL.createObjectURL(blob);
411
+ urlCache.set(blobId, url);
412
+ return url;
413
+ }
414
+
415
+ /**
416
+ * revoke a blob URL
417
+ */
418
+ revokeBlobUrl(blobId: string): void {
419
+ const url = urlCache.get(blobId);
420
+ if (url) {
421
+ URL.revokeObjectURL(url);
422
+ urlCache.delete(blobId);
423
+ }
424
+ }
425
+
426
+ /**
427
+ * clear all blob URLs
428
+ */
429
+ clearBlobUrls(): void {
430
+ for (const url of urlCache.values()) {
431
+ URL.revokeObjectURL(url);
432
+ }
433
+ urlCache.clear();
434
+ }
435
+
436
+ /**
437
+ * fetch server image (public, no auth required)
438
+ * used during "add remote" flow before user is authenticated
439
+ */
440
+ async fetchHelloImage(): Promise<BlobData | null> {
441
+ const inv = await ensureInvoke();
442
+
443
+ console.debug("[P2P] fetchHelloImage: requesting from peer", this.peerAddr);
444
+ try {
445
+ const result = (await inv("p2p_fetch_hello_image", {
446
+ peerAddr: this.peerAddr,
447
+ })) as { data: string; content_type: string | null };
448
+
449
+ // decode base64 data
450
+ const bytes = base64ToBytes(result.data);
451
+
452
+ return {
453
+ data: bytes,
454
+ contentType: result.content_type ?? "image/png",
455
+ };
456
+ } catch (e) {
457
+ console.error("fetchHelloImage failed:", e);
458
+ return null;
459
+ }
460
+ }
461
+
462
+ // -----------------------------------------------------------------
463
+ // job events (remote path via freqhole-events/1 ALPN)
464
+ //
465
+ // routes through the `jobs_events_snapshot` / `jobs_events_subscribe`
466
+ // tauri commands, passing `targetPeer` so charnel dials the remote
467
+ // peer instead of the in-process broker.
468
+ // -----------------------------------------------------------------
469
+
470
+ async snapshotJobEvents(filter?: EventFilter): Promise<JobStateSnapshot[]> {
471
+ const inv = await ensureInvoke();
472
+ const out = (await inv("jobs_events_snapshot", {
473
+ filter: filter ?? null,
474
+ targetPeer: this.peerAddr,
475
+ })) as JobStateSnapshot[];
476
+ return out;
477
+ }
478
+
479
+ subscribeJobEvents(
480
+ filter?: EventFilter,
481
+ signal?: AbortSignal,
482
+ ): AsyncIterable<JobEvent> {
483
+ return charnelRemoteJobEventsIterable(this.peerAddr, filter, signal);
484
+ }
485
+ }
486
+
487
+ // frame shape emitted by the rust-side `JobsEventsFrame` — mirrors
488
+ // `CharnelLocalTransport.ts`.
489
+ type JobsEventsFrame =
490
+ | { kind: "event"; evt: JobEvent }
491
+ | { kind: "closed"; reason: CloseReason };
492
+
493
+ async function* charnelRemoteJobEventsIterable(
494
+ peerAddr: string,
495
+ filter: EventFilter | undefined,
496
+ signal: AbortSignal | undefined,
497
+ ): AsyncGenerator<JobEvent, void, void> {
498
+ const { invoke: inv, Channel } = await import("@tauri-apps/api/core");
499
+
500
+ const queue: JobsEventsFrame[] = [];
501
+ let wake: (() => void) | null = null;
502
+ let closed = false;
503
+ const waitForFrame = () =>
504
+ new Promise<void>((resolve) => {
505
+ wake = () => {
506
+ wake = null;
507
+ resolve();
508
+ };
509
+ });
510
+
511
+ const channel = new Channel<JobsEventsFrame>();
512
+ channel.onmessage = (frame: JobsEventsFrame) => {
513
+ queue.push(frame);
514
+ if (frame.kind === "closed") closed = true;
515
+ wake?.();
516
+ };
517
+
518
+ let sessionId: string;
519
+ try {
520
+ sessionId = (await inv("jobs_events_subscribe", {
521
+ filter: filter ?? null,
522
+ events: channel,
523
+ targetPeer: peerAddr,
524
+ })) as string;
525
+ } catch (err) {
526
+ throw err instanceof Error ? err : new Error(String(err));
527
+ }
528
+
529
+ const onAbort = () => {
530
+ closed = true;
531
+ inv("jobs_events_unsubscribe", { sessionId }).catch(() => {});
532
+ wake?.();
533
+ };
534
+ signal?.addEventListener("abort", onAbort, { once: true });
535
+
536
+ try {
537
+ while (true) {
538
+ if (queue.length === 0) {
539
+ if (closed) return;
540
+ if (signal?.aborted) return;
541
+ await waitForFrame();
542
+ continue;
543
+ }
544
+ const frame = queue.shift()!;
545
+ if (frame.kind === "closed") {
546
+ const reasonKind = (frame.reason as { kind: string }).kind ?? "internal";
547
+ if (reasonKind === "client_unsubscribed") return;
548
+ throw new JobEventsStreamClosed(frame.reason);
549
+ }
550
+ yield frame.evt;
551
+ }
552
+ } finally {
553
+ signal?.removeEventListener("abort", onAbort);
554
+ if (!closed) {
555
+ try {
556
+ await inv("jobs_events_unsubscribe", { sessionId });
557
+ } catch {
558
+ // session may have already been torn down; nothing to do.
559
+ }
560
+ }
561
+ }
562
+ }
563
+
564
+ // transport cache - reuse instances per peer
565
+ const transportCache = new Map<string, CharnelTransport>();
566
+
567
+ /**
568
+ * get or create a CharnelTransport for a peer (async)
569
+ * initializes transport before returning
570
+ */
571
+ export async function createCharnelTransport(
572
+ peerAddr: string,
573
+ cacheName?: string,
574
+ ): Promise<CharnelTransport> {
575
+ // include cacheName in cache key so different remotes get different transports
576
+ const cacheKey = cacheName ? `${peerAddr}:${cacheName}` : peerAddr;
577
+ const existing = transportCache.get(cacheKey);
578
+ if (existing) {
579
+ return existing;
580
+ }
581
+
582
+ const transport = new CharnelTransport(peerAddr, cacheName);
583
+ await transport.init();
584
+ transportCache.set(cacheKey, transport);
585
+ return transport;
586
+ }
587
+
588
+ /**
589
+ * get or create a CharnelTransport (alias for createCharnelTransport)
590
+ */
591
+ export async function getCharnelTransport(
592
+ peerAddr: string,
593
+ cacheName?: string,
594
+ ): Promise<CharnelTransport> {
595
+ return createCharnelTransport(peerAddr, cacheName);
596
+ }
597
+
598
+ /**
599
+ * get local node_id from tauri
600
+ */
601
+ export async function getCharnelNodeId(): Promise<string> {
602
+ const inv = await ensureInvoke();
603
+ return (await inv("p2p_get_node_id")) as string;
604
+ }
605
+
606
+ /**
607
+ * check if P2P is available in tauri
608
+ */
609
+ export async function isCharnelP2PAvailable(): Promise<boolean> {
610
+ try {
611
+ const inv = await ensureInvoke();
612
+ return (await inv("p2p_is_available")) as boolean;
613
+ } catch {
614
+ return false;
615
+ }
616
+ }