@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.
- package/auth-test.html +651 -0
- package/package.json +42 -0
- package/src/AdminClient.ts +154 -0
- package/src/CharnelLocalTransport.ts +598 -0
- package/src/CharnelTransport.ts +616 -0
- package/src/FreqholeClient.ts +261 -0
- package/src/WasmTransport.ts +1077 -0
- package/src/automerge/IrohNetworkAdapter.ts +727 -0
- package/src/automerge/index.ts +14 -0
- package/src/codegen/admin_commands.ts +61 -0
- package/src/codegen/routes.ts +213 -0
- package/src/codegen/schema.ts +6721 -0
- package/src/domains/admin.ts +97 -0
- package/src/domains/app.ts +98 -0
- package/src/domains/auth.ts +191 -0
- package/src/domains/favorites.types.ts +52 -0
- package/src/domains/jobs.ts +48 -0
- package/src/domains/music.ts +1935 -0
- package/src/domains/types.ts +19 -0
- package/src/domains/upload.ts +214 -0
- package/src/index.ts +48 -0
- package/src/permissions.ts +468 -0
- package/src/playlistz/convert.ts +267 -0
- package/src/playlistz/index.ts +6 -0
- package/src/playlistz/mutations.ts +181 -0
- package/src/playlistz/protocol.ts +222 -0
- package/src/playlistz/schema.ts +138 -0
- package/src/playlistz/shareLink.ts +76 -0
- package/src/storage/blobs.ts +272 -0
- package/src/storage/identity.ts +190 -0
- package/src/storage/index.ts +5 -0
- package/src/storage/webLocks.ts +92 -0
- package/src/test/automerge.ts +796 -0
- package/src/test/coverage.ts +105 -0
- package/src/test/fixtures.ts +460 -0
- package/src/test/integration.ts +320 -0
- package/src/test/playlistz.ts +631 -0
- package/src/test/stateful.ts +561 -0
- package/src/test/storage.ts +443 -0
- package/src/test.ts +75 -0
- package/src/transport.ts +391 -0
- package/src/utils.ts +79 -0
- package/src/webauthn.ts +228 -0
- package/tsconfig.json +22 -0
- 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
|
+
}
|