@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
package/auth-test.html ADDED
@@ -0,0 +1,651 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>webauthn test harness</title>
7
+ <style>
8
+ * {
9
+ box-sizing: border-box;
10
+ margin: 0;
11
+ padding: 0;
12
+ }
13
+
14
+ body {
15
+ font-family:
16
+ -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
17
+ sans-serif;
18
+ line-height: 1.6;
19
+ padding: 2rem;
20
+ max-width: 800px;
21
+ margin: 0 auto;
22
+ background: #000;
23
+ color: #fff;
24
+ }
25
+
26
+ h1 {
27
+ margin-bottom: 0.5rem;
28
+ color: #ff00ff;
29
+ }
30
+
31
+ .subtitle {
32
+ color: #ccc;
33
+ margin-bottom: 2rem;
34
+ font-size: 0.9rem;
35
+ }
36
+
37
+ .section {
38
+ background: #111;
39
+ padding: 1.5rem;
40
+ margin-bottom: 1.5rem;
41
+ border-radius: 8px;
42
+ border: 1px solid #333;
43
+ }
44
+
45
+ h2 {
46
+ margin-bottom: 1rem;
47
+ color: #ff00ff;
48
+ font-size: 1.2rem;
49
+ }
50
+
51
+ .form-group {
52
+ margin-bottom: 1rem;
53
+ }
54
+
55
+ label {
56
+ display: block;
57
+ margin-bottom: 0.25rem;
58
+ color: #ccc;
59
+ font-weight: 500;
60
+ font-size: 0.9rem;
61
+ }
62
+
63
+ input[type="text"],
64
+ input[type="url"] {
65
+ width: 100%;
66
+ padding: 0.5rem;
67
+ border: 1px solid #333;
68
+ border-radius: 4px;
69
+ font-size: 1rem;
70
+ background: #000;
71
+ color: #fff;
72
+ }
73
+
74
+ input[type="text"]:focus,
75
+ input[type="url"]:focus {
76
+ outline: none;
77
+ border-color: #ff00ff;
78
+ }
79
+
80
+ button {
81
+ background: #ff00ff;
82
+ color: #000;
83
+ border: none;
84
+ padding: 0.75rem 1.5rem;
85
+ border-radius: 4px;
86
+ font-size: 1rem;
87
+ cursor: pointer;
88
+ margin-right: 0.5rem;
89
+ margin-top: 0.5rem;
90
+ font-weight: 600;
91
+ }
92
+
93
+ button:hover {
94
+ background: #cc00cc;
95
+ }
96
+
97
+ button:disabled {
98
+ background: #333;
99
+ color: #666;
100
+ cursor: not-allowed;
101
+ }
102
+
103
+ button.secondary {
104
+ background: #333;
105
+ color: #fff;
106
+ }
107
+
108
+ button.secondary:hover {
109
+ background: #444;
110
+ }
111
+
112
+ .status {
113
+ margin-top: 1rem;
114
+ padding: 0.75rem;
115
+ border-radius: 4px;
116
+ font-size: 0.9rem;
117
+ }
118
+
119
+ .status.success {
120
+ background: #001a00;
121
+ color: #00ff00;
122
+ border: 1px solid #003300;
123
+ }
124
+
125
+ .status.error {
126
+ background: #1a0000;
127
+ color: #ff4444;
128
+ border: 1px solid #330000;
129
+ }
130
+
131
+ .status.info {
132
+ background: #0a0a1a;
133
+ color: #88ccff;
134
+ border: 1px solid #1a1a33;
135
+ }
136
+
137
+ .current-user {
138
+ background: #1a001a;
139
+ padding: 1rem;
140
+ border-radius: 4px;
141
+ margin-bottom: 1rem;
142
+ border: 1px solid #330033;
143
+ }
144
+
145
+ .current-user strong {
146
+ color: #ff00ff;
147
+ }
148
+
149
+ pre {
150
+ background: #0a0a0a;
151
+ padding: 1rem;
152
+ border-radius: 4px;
153
+ overflow-x: auto;
154
+ font-size: 0.85rem;
155
+ margin-top: 0.5rem;
156
+ color: #ccc;
157
+ border: 1px solid #222;
158
+ }
159
+
160
+ .hidden {
161
+ display: none;
162
+ }
163
+
164
+ .browser-info {
165
+ background: #1a1a00;
166
+ padding: 1rem;
167
+ border-radius: 4px;
168
+ margin-bottom: 1rem;
169
+ font-size: 0.9rem;
170
+ color: #ffff88;
171
+ border: 1px solid #333300;
172
+ }
173
+ </style>
174
+ </head>
175
+ <body>
176
+ <h1>webauthn test harness</h1>
177
+ <p class="subtitle">
178
+ test passkey registration and login across browsers
179
+ </p>
180
+
181
+ <div class="browser-info" id="browserInfo">
182
+ <strong>browser:</strong> <span id="browserName">detecting...</span>
183
+ </div>
184
+
185
+ <div class="section">
186
+ <h2>configuration</h2>
187
+ <div class="form-group">
188
+ <label for="baseUrl">server url</label>
189
+ <input
190
+ type="url"
191
+ id="baseUrl"
192
+ placeholder="http://localhost:8080"
193
+ />
194
+ </div>
195
+ <button onclick="testConnection()">test connection</button>
196
+ <div id="connectionStatus"></div>
197
+ </div>
198
+
199
+ <div class="section">
200
+ <h2>registration</h2>
201
+ <div class="form-group">
202
+ <label for="registerUsername">username</label>
203
+ <input type="text" id="registerUsername" placeholder="alice" />
204
+ </div>
205
+ <div class="form-group">
206
+ <label for="inviteCode">invite code</label>
207
+ <input type="text" id="inviteCode" placeholder="optional" />
208
+ </div>
209
+ <button onclick="register()">register with passkey</button>
210
+ <div id="registerStatus"></div>
211
+ </div>
212
+
213
+ <div class="section">
214
+ <h2>login</h2>
215
+ <div class="form-group">
216
+ <label for="loginUsername">username</label>
217
+ <input type="text" id="loginUsername" placeholder="alice" />
218
+ </div>
219
+ <button onclick="login()">login with passkey</button>
220
+ <div id="loginStatus"></div>
221
+ </div>
222
+
223
+ <div class="section">
224
+ <h2>session status</h2>
225
+ <div id="currentUser" class="hidden current-user">
226
+ logged in as <strong id="username"></strong>
227
+ </div>
228
+ <button onclick="checkAuth()">check auth status</button>
229
+ <button class="secondary" onclick="logout()">logout</button>
230
+ <div id="authStatus"></div>
231
+ </div>
232
+
233
+ <div class="section">
234
+ <h2>debug info</h2>
235
+ <p style="color: #ccc; font-size: 0.9rem; margin-bottom: 1rem">
236
+ last credential data (compare safari vs chrome):
237
+ </p>
238
+ <pre id="debugInfo" style="max-height: 400px; overflow-y: auto">
239
+ no credential data yet</pre
240
+ >
241
+ </div>
242
+
243
+ <script type="module">
244
+ // import api client modules from the local package
245
+ // in production this would be: import { webauthn, auth, app } from 'freqhole-api-client'
246
+ import * as webauthn from "./src/webauthn";
247
+ import * as auth from "./src/auth";
248
+ import * as app from "./src/app";
249
+
250
+ // set default base url to current origin
251
+ document.getElementById("baseUrl").value = window.location.origin;
252
+
253
+ // make functions globally available for onclick handlers
254
+ window.webauthnHelpers = webauthn;
255
+
256
+ // detect browser
257
+ function detectBrowser() {
258
+ const ua = navigator.userAgent;
259
+ let browser = "unknown";
260
+
261
+ if (ua.includes("Safari") && !ua.includes("Chrome")) {
262
+ browser = "Safari";
263
+ } else if (ua.includes("Chrome")) {
264
+ browser = "Chrome";
265
+ } else if (ua.includes("Firefox")) {
266
+ browser = "Firefox";
267
+ } else if (ua.includes("Edg")) {
268
+ browser = "Edge";
269
+ }
270
+
271
+ document.getElementById("browserName").textContent =
272
+ `${browser} (${navigator.platform})`;
273
+ }
274
+
275
+ detectBrowser();
276
+
277
+ // helper to get current base url from input
278
+ function getBaseUrl() {
279
+ return document.getElementById("baseUrl").value;
280
+ }
281
+
282
+ // show status message
283
+ function showStatus(
284
+ elementId,
285
+ message,
286
+ type = "info",
287
+ details = null,
288
+ ) {
289
+ const el = document.getElementById(elementId);
290
+ el.className = `status ${type}`;
291
+ el.innerHTML = message;
292
+
293
+ if (details) {
294
+ const pre = document.createElement("pre");
295
+ pre.textContent = JSON.stringify(details, null, 2);
296
+ el.appendChild(pre);
297
+ }
298
+
299
+ console.log(`[${type}] ${message}`, details);
300
+ }
301
+
302
+ // test connection to server
303
+ window.testConnection = async function () {
304
+ try {
305
+ const result = await app.healthCheck(getBaseUrl());
306
+ if (result.success) {
307
+ showStatus(
308
+ "connectionStatus",
309
+ "connection successful",
310
+ "success",
311
+ result.data,
312
+ );
313
+ } else {
314
+ showStatus(
315
+ "connectionStatus",
316
+ `connection failed: ${result.error.message}`,
317
+ "error",
318
+ );
319
+ }
320
+ } catch (err) {
321
+ showStatus(
322
+ "connectionStatus",
323
+ `connection failed: ${err.message}`,
324
+ "error",
325
+ );
326
+ }
327
+ };
328
+
329
+ // register with passkey
330
+ window.register = async function () {
331
+ const username =
332
+ document.getElementById("registerUsername").value;
333
+ const inviteCode = document.getElementById("inviteCode").value;
334
+
335
+ if (!username) {
336
+ showStatus(
337
+ "registerStatus",
338
+ "please enter a username",
339
+ "error",
340
+ );
341
+ return;
342
+ }
343
+
344
+ try {
345
+ showStatus(
346
+ "registerStatus",
347
+ "starting registration...",
348
+ "info",
349
+ );
350
+
351
+ // call register_start
352
+ const startParams = inviteCode
353
+ ? { username, invite_code: inviteCode }
354
+ : { username };
355
+
356
+ const startResult = await auth.registerStart(
357
+ getBaseUrl(),
358
+ startParams,
359
+ );
360
+
361
+ if (!startResult.success) {
362
+ throw new Error(
363
+ startResult.error.message ||
364
+ "register_start failed",
365
+ );
366
+ }
367
+
368
+ const challengeResponse = startResult.data;
369
+
370
+ showStatus(
371
+ "registerStatus",
372
+ "got challenge, prompting for passkey...",
373
+ "info",
374
+ challengeResponse,
375
+ );
376
+
377
+ // prepare options for browser
378
+ const options =
379
+ webauthn.prepareRegistrationOptions(challengeResponse);
380
+
381
+ // prompt user to create credential
382
+ const credential =
383
+ await navigator.credentials.create(options);
384
+
385
+ if (!credential) {
386
+ throw new Error("failed to create credential");
387
+ }
388
+
389
+ showStatus(
390
+ "registerStatus",
391
+ "credential created, finishing registration...",
392
+ "info",
393
+ );
394
+
395
+ // serialize and send to server
396
+ const serialized =
397
+ webauthn.serializeRegistrationCredential(credential);
398
+ console.log("serialized credential:", serialized);
399
+ console.log(
400
+ "credential rawId length:",
401
+ credential.rawId.byteLength,
402
+ );
403
+ console.log("credential id:", credential.id);
404
+
405
+ // show debug info
406
+ const debugInfo = {
407
+ browser:
408
+ document.getElementById("browserName").textContent,
409
+ timestamp: new Date().toISOString(),
410
+ credential: serialized,
411
+ rawIdLength: credential.rawId.byteLength,
412
+ idLength: credential.id.length,
413
+ };
414
+ document.getElementById("debugInfo").textContent =
415
+ JSON.stringify(debugInfo, null, 2);
416
+
417
+ const finishResult = await auth.registerFinish(
418
+ getBaseUrl(),
419
+ serialized,
420
+ );
421
+
422
+ if (!finishResult.success) {
423
+ console.error(
424
+ "register_finish failed:",
425
+ finishResult.error,
426
+ );
427
+ console.error(
428
+ "sent payload:",
429
+ JSON.stringify(serialized, null, 2),
430
+ );
431
+ throw new Error(
432
+ `register_finish failed: ${JSON.stringify(finishResult.error)}`,
433
+ );
434
+ }
435
+
436
+ showStatus(
437
+ "registerStatus",
438
+ "registration successful!",
439
+ "success",
440
+ finishResult.data,
441
+ );
442
+
443
+ // check auth after successful registration
444
+ setTimeout(checkAuth, 500);
445
+ } catch (err) {
446
+ const errorMsg = err.message || String(err);
447
+ showStatus(
448
+ "registerStatus",
449
+ `registration failed: ${errorMsg}`,
450
+ "error",
451
+ );
452
+ console.error("registration error:", err);
453
+ console.error(
454
+ "full error details:",
455
+ JSON.stringify(err, null, 2),
456
+ );
457
+ }
458
+ };
459
+
460
+ // login with passkey
461
+ window.login = async function () {
462
+ const username = document.getElementById("loginUsername").value;
463
+
464
+ if (!username) {
465
+ showStatus(
466
+ "loginStatus",
467
+ "please enter a username",
468
+ "error",
469
+ );
470
+ return;
471
+ }
472
+
473
+ try {
474
+ showStatus("loginStatus", "starting login...", "info");
475
+
476
+ // call login_start
477
+ const startResult = await auth.loginStart(getBaseUrl(), {
478
+ username,
479
+ });
480
+
481
+ if (!startResult.success) {
482
+ throw new Error(
483
+ startResult.error.message || "login_start failed",
484
+ );
485
+ }
486
+
487
+ const challengeResponse = startResult.data;
488
+
489
+ showStatus(
490
+ "loginStatus",
491
+ "got challenge, prompting for passkey...",
492
+ "info",
493
+ challengeResponse,
494
+ );
495
+
496
+ // prepare options for browser
497
+ const options =
498
+ webauthn.prepareAuthenticationOptions(
499
+ challengeResponse,
500
+ );
501
+
502
+ // prompt user to authenticate
503
+ const assertion = await navigator.credentials.get(options);
504
+
505
+ if (!assertion) {
506
+ throw new Error("failed to get assertion");
507
+ }
508
+
509
+ showStatus(
510
+ "loginStatus",
511
+ "assertion created, finishing login...",
512
+ "info",
513
+ );
514
+
515
+ // serialize and send to server
516
+ const serialized =
517
+ webauthn.serializeAuthenticationCredential(assertion);
518
+ console.log("serialized assertion:", serialized);
519
+ console.log(
520
+ "assertion rawId length:",
521
+ assertion.rawId.byteLength,
522
+ );
523
+ console.log("assertion id:", assertion.id);
524
+
525
+ // show debug info
526
+ const debugInfo = {
527
+ browser:
528
+ document.getElementById("browserName").textContent,
529
+ timestamp: new Date().toISOString(),
530
+ assertion: serialized,
531
+ rawIdLength: assertion.rawId.byteLength,
532
+ idLength: assertion.id.length,
533
+ };
534
+ document.getElementById("debugInfo").textContent =
535
+ JSON.stringify(debugInfo, null, 2);
536
+
537
+ const finishResult = await auth.loginFinish(
538
+ getBaseUrl(),
539
+ serialized,
540
+ );
541
+
542
+ if (!finishResult.success) {
543
+ console.error(
544
+ "login_finish failed:",
545
+ finishResult.error,
546
+ );
547
+ console.error(
548
+ "sent payload:",
549
+ JSON.stringify(serialized, null, 2),
550
+ );
551
+ throw new Error(
552
+ `login_finish failed: ${JSON.stringify(finishResult.error)}`,
553
+ );
554
+ }
555
+
556
+ showStatus(
557
+ "loginStatus",
558
+ "login successful!",
559
+ "success",
560
+ finishResult.data,
561
+ );
562
+
563
+ // check auth after successful login
564
+ setTimeout(checkAuth, 500);
565
+ } catch (err) {
566
+ showStatus(
567
+ "loginStatus",
568
+ `login failed: ${err.message}`,
569
+ "error",
570
+ );
571
+ console.error("login error:", err);
572
+ }
573
+ };
574
+
575
+ // check auth status
576
+ window.checkAuth = async function () {
577
+ try {
578
+ const result = await auth.whoami(getBaseUrl());
579
+
580
+ if (!result.success) {
581
+ throw new Error(
582
+ result.error.message || "whoami failed",
583
+ );
584
+ }
585
+
586
+ const response = result.data;
587
+
588
+ if (response.authenticated) {
589
+ document
590
+ .getElementById("currentUser")
591
+ .classList.remove("hidden");
592
+ document.getElementById("username").textContent =
593
+ response.username || "unknown";
594
+ showStatus(
595
+ "authStatus",
596
+ "authenticated",
597
+ "success",
598
+ response,
599
+ );
600
+ } else {
601
+ document
602
+ .getElementById("currentUser")
603
+ .classList.add("hidden");
604
+ showStatus(
605
+ "authStatus",
606
+ "not authenticated",
607
+ "info",
608
+ response,
609
+ );
610
+ }
611
+ } catch (err) {
612
+ document
613
+ .getElementById("currentUser")
614
+ .classList.add("hidden");
615
+ showStatus(
616
+ "authStatus",
617
+ `error checking auth: ${err.message}`,
618
+ "error",
619
+ );
620
+ }
621
+ };
622
+
623
+ // logout
624
+ window.logout = async function () {
625
+ try {
626
+ const result = await auth.logout(getBaseUrl());
627
+
628
+ if (!result.success) {
629
+ throw new Error(
630
+ result.error.message || "logout failed",
631
+ );
632
+ }
633
+
634
+ document
635
+ .getElementById("currentUser")
636
+ .classList.add("hidden");
637
+ showStatus("authStatus", "logged out", "success");
638
+ } catch (err) {
639
+ showStatus(
640
+ "authStatus",
641
+ `error logging out: ${err.message}`,
642
+ "error",
643
+ );
644
+ }
645
+ };
646
+
647
+ // check auth on page load
648
+ checkAuth();
649
+ </script>
650
+ </body>
651
+ </html>
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@freqhole/api-client",
3
+ "version": "0.1.30",
4
+ "type": "module",
5
+ "main": "./src/index.ts",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "exports": {
10
+ ".": "./src/index.ts",
11
+ "./storage": "./src/storage/index.ts",
12
+ "./playlistz": "./src/playlistz/index.ts",
13
+ "./automerge": "./src/automerge/index.ts"
14
+ },
15
+ "scripts": {
16
+ "typecheck": "tsc --noEmit",
17
+ "test": "tsx src/test.ts",
18
+ "test:coverage": "SKIP_INTEGRATION=true tsx src/test.ts",
19
+ "test:integration": "tsx src/test.ts",
20
+ "auth-test": "vite --config vite.config.js"
21
+ },
22
+ "dependencies": {
23
+ "idb": "^8.0.3",
24
+ "zod": "^4.3.5"
25
+ },
26
+ "peerDependencies": {
27
+ "@automerge/automerge-repo": "^2.5.5"
28
+ },
29
+ "peerDependenciesMeta": {
30
+ "@automerge/automerge-repo": {
31
+ "optional": true
32
+ }
33
+ },
34
+ "devDependencies": {
35
+ "@automerge/automerge-repo": "^2.5.6",
36
+ "@tauri-apps/api": "^2.10.1",
37
+ "@types/node": "^25.0.8",
38
+ "tsx": "^4.7.0",
39
+ "typescript": "^5.3.0",
40
+ "vite": "^5.4.21"
41
+ }
42
+ }