@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
|
@@ -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
|
+
}
|