@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,320 @@
1
+ // integration tests - auth validation layer
2
+ //
3
+ // purpose: verify that all protected routes properly reject unauthenticated requests
4
+ //
5
+ // what this tests:
6
+ // - health check endpoint (no auth required)
7
+ // - auth rejection for all 65+ protected routes (expects 401/403)
8
+ //
9
+ // what this does NOT test:
10
+ // - successful authenticated requests (see stateful.ts)
11
+ // - entity creation/modification workflows (see stateful.ts)
12
+ // - data validation beyond auth (see stateful.ts)
13
+ //
14
+ // runs without API_KEY - uses placeholder data to trigger auth checks
15
+ import { createHttpClient } from "../FreqholeClient.js";
16
+ import {
17
+ fixtures,
18
+ PLACEHOLDER_ID,
19
+ queryParams,
20
+ } from "./fixtures.js";
21
+
22
+ const baseUrl = process.env.API_URL || "http://localhost:8080";
23
+
24
+ // client without auth - should get 401/403 on protected routes
25
+ const client = createHttpClient(baseUrl);
26
+
27
+ export async function runIntegrationTests() {
28
+ let passed = 0;
29
+ let failed = 0;
30
+
31
+ async function test(name: string, fn: () => void | Promise<void>) {
32
+ try {
33
+ await fn();
34
+ console.log(`✓ ${name}`);
35
+ passed++;
36
+ } catch (err) {
37
+ console.log(
38
+ `✗ ${name}: ${err instanceof Error ? err.message : "unknown error"}`,
39
+ );
40
+ failed++;
41
+ }
42
+ }
43
+
44
+ console.log(`running integration tests against ${baseUrl}...\n`);
45
+
46
+ // health check (no auth required)
47
+ await test("app.healthCheck - server is alive", async () => {
48
+ const result = await client.app.healthCheck();
49
+ if (!result.success) {
50
+ throw new Error(`health check failed: ${result.error.message}`);
51
+ }
52
+ if (!result.data || typeof result.data !== "object") {
53
+ throw new Error("health check returned invalid data");
54
+ }
55
+ });
56
+
57
+ // validate that protected routes reject requests without auth
58
+ console.log("\nvalidating auth requirements...\n");
59
+
60
+ const protectedRoutes = [
61
+ // auth routes
62
+ { name: "auth.whoami", fn: () => client.auth.whoami() },
63
+ { name: "auth.apiKeyStatus", fn: () => client.auth.apiKeyStatus() },
64
+ { name: "auth.regenerateApiKey", fn: () => client.auth.regenerateApiKey() },
65
+ { name: "auth.logout", fn: () => client.auth.logout() },
66
+
67
+ // songs
68
+ {
69
+ name: "music.querySongs",
70
+ fn: () => client.music.querySongs(queryParams),
71
+ },
72
+ {
73
+ name: "music.recentSongs",
74
+ fn: () => client.music.recentSongs({ limit: 10 }),
75
+ },
76
+ {
77
+ name: "music.updateSongs",
78
+ fn: () => client.music.updateSongs(fixtures.updateSongs),
79
+ },
80
+ {
81
+ name: "music.deleteSong",
82
+ fn: () => client.music.deleteSong(fixtures.deleteSong),
83
+ },
84
+
85
+ // albums
86
+ {
87
+ name: "music.queryAlbums",
88
+ fn: () => client.music.queryAlbums(fixtures.queryParams),
89
+ },
90
+ {
91
+ name: "music.getAlbum",
92
+ fn: () => client.music.getAlbum(fixtures.getAlbum),
93
+ },
94
+ {
95
+ name: "music.deleteAlbum",
96
+ fn: () => client.music.deleteAlbum(fixtures.deleteAlbum),
97
+ },
98
+
99
+ // artists
100
+ {
101
+ name: "music.queryArtists",
102
+ fn: () => client.music.queryArtists(fixtures.queryParams),
103
+ },
104
+ {
105
+ name: "music.getArtist",
106
+ fn: () => client.music.getArtist(fixtures.getArtist),
107
+ },
108
+ {
109
+ name: "music.createArtist",
110
+ fn: () => client.music.createArtist(fixtures.createArtist),
111
+ },
112
+ {
113
+ name: "music.deleteArtist",
114
+ fn: () => client.music.deleteArtist(fixtures.deleteArtist),
115
+ },
116
+
117
+ // playlists
118
+ {
119
+ name: "music.listPlaylists",
120
+ fn: () => client.music.listPlaylists(fixtures.queryParams),
121
+ },
122
+ {
123
+ name: "music.getPlaylistById",
124
+ fn: () => client.music.getPlaylistById({ id: PLACEHOLDER_ID }),
125
+ },
126
+ {
127
+ name: "music.createPlaylist",
128
+ fn: () => client.music.createPlaylist(fixtures.createPlaylist),
129
+ },
130
+ {
131
+ name: "music.updatePlaylist",
132
+ fn: () => client.music.updatePlaylist(fixtures.updatePlaylist),
133
+ },
134
+ {
135
+ name: "music.deletePlaylist",
136
+ fn: () => client.music.deletePlaylist(fixtures.deletePlaylist),
137
+ },
138
+ {
139
+ name: "music.queryPlaylistSongs",
140
+ fn: () => client.music.queryPlaylistSongs(fixtures.queryPlaylistSongs),
141
+ },
142
+ {
143
+ name: "music.addSongsToPlaylist",
144
+ fn: () => client.music.addSongsToPlaylist(fixtures.addSongsToPlaylist),
145
+ },
146
+ {
147
+ name: "music.removeSongsFromPlaylist",
148
+ fn: () =>
149
+ client.music.removeSongsFromPlaylist(fixtures.removeSongsFromPlaylist),
150
+ },
151
+ {
152
+ name: "music.reorderPlaylistSongs",
153
+ fn: () =>
154
+ client.music.reorderPlaylistSongs(fixtures.reorderPlaylistSongs),
155
+ },
156
+ {
157
+ name: "music.deleteImage",
158
+ fn: () => client.music.deleteImage(fixtures.deleteImage),
159
+ },
160
+
161
+ // genres are now exposed via the unified taxonomy api
162
+ // (queryTaxons / getTaxon with kind_slug='genre').
163
+
164
+ // favorites
165
+ {
166
+ name: "music.listFavorites",
167
+ fn: () => client.music.listFavorites(fixtures.listFavorites),
168
+ },
169
+ {
170
+ name: "music.setFavorite",
171
+ fn: () => client.music.setFavorite(fixtures.setFavorite),
172
+ },
173
+
174
+ // ratings
175
+ {
176
+ name: "music.setRating",
177
+ fn: () => client.music.setRating(fixtures.setRating),
178
+ },
179
+ {
180
+ name: "music.removeRating",
181
+ fn: () => client.music.removeRating(fixtures.removeRating),
182
+ },
183
+ {
184
+ name: "music.getRatingStats",
185
+ fn: () => client.music.getRatingStats(fixtures.getRatingStats),
186
+ },
187
+
188
+ // tags
189
+ { name: "music.listTags", fn: () => client.music.listTags() },
190
+ {
191
+ name: "music.queryTags",
192
+ fn: () => client.music.queryTags(fixtures.queryTags),
193
+ },
194
+ {
195
+ name: "music.getTag",
196
+ fn: () => client.music.getTag(fixtures.getTag),
197
+ },
198
+ {
199
+ name: "music.deleteTag",
200
+ fn: () => client.music.deleteTag(fixtures.deleteTag),
201
+ },
202
+ {
203
+ name: "music.getAlbumsTags",
204
+ fn: () => client.music.getAlbumsTags(fixtures.getAlbumTags),
205
+ },
206
+ {
207
+ name: "music.addAlbumsTags",
208
+ fn: () => client.music.addAlbumsTags(fixtures.addAlbumTags),
209
+ },
210
+ {
211
+ name: "music.removeAlbumsTags",
212
+ fn: () => client.music.removeAlbumsTags(fixtures.removeAlbumTags),
213
+ },
214
+ {
215
+ name: "music.replaceAlbumsTags",
216
+ fn: () => client.music.replaceAlbumsTags(fixtures.replaceAlbumTags),
217
+ },
218
+
219
+ // analytics
220
+ {
221
+ name: "music.recordPlay",
222
+ fn: () => client.music.recordPlay(fixtures.recordPlay),
223
+ },
224
+ {
225
+ name: "music.songAnalytics",
226
+ fn: () => client.music.songAnalytics(fixtures.songAnalytics),
227
+ },
228
+ {
229
+ name: "music.listeningHistory",
230
+ fn: () => client.music.listeningHistory(fixtures.listeningHistory),
231
+ },
232
+ {
233
+ name: "music.topSongs",
234
+ fn: () => client.music.topSongs(fixtures.topSongs),
235
+ },
236
+ {
237
+ name: "music.topArtists",
238
+ fn: () => client.music.topArtists(fixtures.topArtists),
239
+ },
240
+ {
241
+ name: "music.topAlbums",
242
+ fn: () => client.music.topAlbums(fixtures.topAlbums),
243
+ },
244
+ {
245
+ name: "music.activityFeed",
246
+ fn: () => client.music.activityFeed(fixtures.feed),
247
+ },
248
+
249
+ // musicbrainz
250
+ {
251
+ name: "music.searchMusicbrainzReleases",
252
+ fn: () =>
253
+ client.music.searchMusicbrainzReleases(fixtures.searchReleases),
254
+ },
255
+ {
256
+ name: "music.getMusicbrainzRelease",
257
+ fn: () => client.music.getMusicbrainzRelease(fixtures.getRelease),
258
+ },
259
+
260
+ // jobs
261
+ {
262
+ name: "music.listJobs",
263
+ fn: () => client.music.listJobs(fixtures.listJobs),
264
+ },
265
+ {
266
+ name: "music.getJobStatus",
267
+ fn: () => client.music.getJobStatus(fixtures.getJobsStatus),
268
+ },
269
+
270
+ // fetch
271
+ {
272
+ name: "music.createFetchJob",
273
+ fn: () => client.music.createFetchJob(fixtures.fetchMedia),
274
+ },
275
+ {
276
+ name: "music.getFetchJob",
277
+ fn: () => client.music.getFetchJob({ job_id: PLACEHOLDER_ID }),
278
+ },
279
+
280
+ // uploads - via client.upload (uses transport)
281
+ {
282
+ name: "upload.image",
283
+ fn: () => client.upload.image(new Blob(["test"])),
284
+ },
285
+ {
286
+ name: "upload.music",
287
+ fn: () => client.upload.music(new Blob(["test"])),
288
+ },
289
+ ];
290
+
291
+ for (const route of protectedRoutes) {
292
+ await test(`${route.name} - rejects without auth`, async () => {
293
+ const result = await route.fn();
294
+ if (result.success) {
295
+ throw new Error(`${route.name} should fail without auth`);
296
+ }
297
+ // extract error message from zod error
298
+ const errorMsg =
299
+ result.error.issues?.[0]?.message ||
300
+ result.error.issues?.[0]?.message ||
301
+ result.error.message ||
302
+ "";
303
+ if (!errorMsg.includes("401") && !errorMsg.includes("403")) {
304
+ throw new Error(`expected 401/403, got: ${errorMsg}`);
305
+ }
306
+ });
307
+ }
308
+
309
+ console.log("");
310
+ console.log(
311
+ "✓ auth validation complete - all routes reject without credentials\n",
312
+ );
313
+ console.log(
314
+ " (authenticated workflows tested in stateful tests - see stateful.ts)\n",
315
+ );
316
+
317
+ console.log(`\n${passed} passed, ${failed} failed\n`);
318
+
319
+ return { passed, failed };
320
+ }