@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,468 @@
|
|
|
1
|
+
// permission helpers for route authorization
|
|
2
|
+
//
|
|
3
|
+
// these are hand-written helpers that use the generated route auth metadata
|
|
4
|
+
// to check if a user has permission to perform actions.
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
roleHierarchy,
|
|
8
|
+
routes,
|
|
9
|
+
type RouteAuth,
|
|
10
|
+
type UserRoleName,
|
|
11
|
+
} from "./codegen/routes.js";
|
|
12
|
+
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// core permission functions
|
|
15
|
+
// ============================================================================
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* check if a user role has at least the required role level.
|
|
19
|
+
* lower hierarchy number = higher privilege.
|
|
20
|
+
*
|
|
21
|
+
* @param userRole - the user's current role
|
|
22
|
+
* @param requiredRole - the minimum required role
|
|
23
|
+
* @returns true if user has sufficient privileges
|
|
24
|
+
*/
|
|
25
|
+
export function canAccessRole(
|
|
26
|
+
userRole: UserRoleName,
|
|
27
|
+
requiredRole: UserRoleName,
|
|
28
|
+
): boolean {
|
|
29
|
+
return roleHierarchy[userRole] <= roleHierarchy[requiredRole];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* check if user is the owner OR has at least the required role.
|
|
34
|
+
* used for routes with OwnerOr(Role) auth.
|
|
35
|
+
*
|
|
36
|
+
* @param userId - the current user's id
|
|
37
|
+
* @param ownerId - the resource owner's id (can be null for new resources)
|
|
38
|
+
* @param userRole - the user's current role
|
|
39
|
+
* @param requiredRole - the role that grants access regardless of ownership
|
|
40
|
+
* @returns true if user is owner or has sufficient role
|
|
41
|
+
*/
|
|
42
|
+
export function canAccessOwnerOr(
|
|
43
|
+
userId: string,
|
|
44
|
+
ownerId: string | null,
|
|
45
|
+
userRole: UserRoleName,
|
|
46
|
+
requiredRole: UserRoleName,
|
|
47
|
+
): boolean {
|
|
48
|
+
// owner always has access
|
|
49
|
+
if (ownerId !== null && userId === ownerId) {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
// otherwise check role
|
|
53
|
+
return canAccessRole(userRole, requiredRole);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* check if user is strictly the owner.
|
|
58
|
+
* used for routes with Owner auth (no admin override).
|
|
59
|
+
*
|
|
60
|
+
* @param userId - the current user's id
|
|
61
|
+
* @param ownerId - the resource owner's id
|
|
62
|
+
* @returns true if user is the owner
|
|
63
|
+
*/
|
|
64
|
+
export function canAccessOwner(
|
|
65
|
+
userId: string,
|
|
66
|
+
ownerId: string | null,
|
|
67
|
+
): boolean {
|
|
68
|
+
return ownerId !== null && userId === ownerId;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* generic route auth check.
|
|
73
|
+
* evaluates any RouteAuth against user context.
|
|
74
|
+
*
|
|
75
|
+
* @param auth - the route's auth requirement
|
|
76
|
+
* @param userRole - the user's current role (null if not authenticated)
|
|
77
|
+
* @param userId - the current user's id (null if not authenticated)
|
|
78
|
+
* @param ownerId - the resource owner's id (null if not applicable)
|
|
79
|
+
* @returns true if user can access the route
|
|
80
|
+
*/
|
|
81
|
+
export function canAccessRoute(
|
|
82
|
+
auth: RouteAuth,
|
|
83
|
+
userRole: UserRoleName | null,
|
|
84
|
+
userId: string | null,
|
|
85
|
+
ownerId: string | null,
|
|
86
|
+
): boolean {
|
|
87
|
+
switch (auth.type) {
|
|
88
|
+
case "public":
|
|
89
|
+
return true;
|
|
90
|
+
|
|
91
|
+
case "authenticated":
|
|
92
|
+
return userRole !== null;
|
|
93
|
+
|
|
94
|
+
case "role":
|
|
95
|
+
return userRole !== null && canAccessRole(userRole, auth.role);
|
|
96
|
+
|
|
97
|
+
case "owner":
|
|
98
|
+
return userId !== null && canAccessOwner(userId, ownerId);
|
|
99
|
+
|
|
100
|
+
case "owner_or":
|
|
101
|
+
return (
|
|
102
|
+
userId !== null &&
|
|
103
|
+
userRole !== null &&
|
|
104
|
+
canAccessOwnerOr(userId, ownerId, userRole, auth.role)
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ============================================================================
|
|
110
|
+
// entity-specific permission helpers
|
|
111
|
+
// ============================================================================
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* check if user can delete a playlist.
|
|
115
|
+
* playlists use OwnerOr(Admin) - owner or admin can delete.
|
|
116
|
+
*/
|
|
117
|
+
export function canDeletePlaylist(
|
|
118
|
+
userId: string,
|
|
119
|
+
playlistOwnerId: string | null,
|
|
120
|
+
userRole: UserRoleName,
|
|
121
|
+
): boolean {
|
|
122
|
+
const auth = routes.music.delete_playlist.auth;
|
|
123
|
+
if (auth.type === "owner_or") {
|
|
124
|
+
return canAccessOwnerOr(userId, playlistOwnerId, userRole, auth.role);
|
|
125
|
+
}
|
|
126
|
+
// fallback: shouldn't happen but be safe
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* check if user can update a playlist.
|
|
132
|
+
* playlists use OwnerOr(Admin) - owner or admin can update.
|
|
133
|
+
*/
|
|
134
|
+
export function canUpdatePlaylist(
|
|
135
|
+
userId: string,
|
|
136
|
+
playlistOwnerId: string | null,
|
|
137
|
+
userRole: UserRoleName,
|
|
138
|
+
): boolean {
|
|
139
|
+
const auth = routes.music.update_playlist.auth;
|
|
140
|
+
if (auth.type === "owner_or") {
|
|
141
|
+
return canAccessOwnerOr(userId, playlistOwnerId, userRole, auth.role);
|
|
142
|
+
}
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* check if user can add songs to a playlist.
|
|
148
|
+
* playlists use OwnerOr(Admin).
|
|
149
|
+
*/
|
|
150
|
+
export function canAddSongsToPlaylist(
|
|
151
|
+
userId: string,
|
|
152
|
+
playlistOwnerId: string | null,
|
|
153
|
+
userRole: UserRoleName,
|
|
154
|
+
): boolean {
|
|
155
|
+
const auth = routes.music.add_songs_to_playlist.auth;
|
|
156
|
+
if (auth.type === "owner_or") {
|
|
157
|
+
return canAccessOwnerOr(userId, playlistOwnerId, userRole, auth.role);
|
|
158
|
+
}
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* check if user can remove songs from a playlist.
|
|
164
|
+
* playlists use OwnerOr(Admin).
|
|
165
|
+
*/
|
|
166
|
+
export function canRemoveSongsFromPlaylist(
|
|
167
|
+
userId: string,
|
|
168
|
+
playlistOwnerId: string | null,
|
|
169
|
+
userRole: UserRoleName,
|
|
170
|
+
): boolean {
|
|
171
|
+
const auth = routes.music.remove_songs_from_playlist.auth;
|
|
172
|
+
if (auth.type === "owner_or") {
|
|
173
|
+
return canAccessOwnerOr(userId, playlistOwnerId, userRole, auth.role);
|
|
174
|
+
}
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* check if user can delete a listen session.
|
|
180
|
+
* listen sessions use OwnerOr(Admin) - owner or admin can delete.
|
|
181
|
+
*/
|
|
182
|
+
export function canDeleteListenSession(
|
|
183
|
+
userId: string,
|
|
184
|
+
sessionOwnerId: string | null,
|
|
185
|
+
userRole: UserRoleName,
|
|
186
|
+
): boolean {
|
|
187
|
+
// listen sessions use OwnerOr(Admin) - owner or admin can delete
|
|
188
|
+
const auth = routes.music.delete_listen_session.auth;
|
|
189
|
+
if (auth.type === "owner_or") {
|
|
190
|
+
return canAccessOwnerOr(userId, sessionOwnerId, userRole, auth.role);
|
|
191
|
+
}
|
|
192
|
+
return canAccessOwner(userId, sessionOwnerId);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* check if user can update a listen session.
|
|
197
|
+
* listen sessions use Owner - strictly owner only.
|
|
198
|
+
*/
|
|
199
|
+
export function canUpdateListenSession(
|
|
200
|
+
userId: string,
|
|
201
|
+
sessionOwnerId: string | null,
|
|
202
|
+
): boolean {
|
|
203
|
+
return canAccessOwner(userId, sessionOwnerId);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* check if user can delete a song.
|
|
208
|
+
* songs require Admin role.
|
|
209
|
+
*/
|
|
210
|
+
export function canDeleteSong(userRole: UserRoleName): boolean {
|
|
211
|
+
const auth = routes.music.delete_song.auth;
|
|
212
|
+
if (auth.type === "role") {
|
|
213
|
+
return canAccessRole(userRole, auth.role);
|
|
214
|
+
}
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* check if user can update songs.
|
|
220
|
+
* songs require Admin role.
|
|
221
|
+
*/
|
|
222
|
+
export function canUpdateSong(userRole: UserRoleName): boolean {
|
|
223
|
+
const auth = routes.music.update_songs.auth;
|
|
224
|
+
if (auth.type === "role") {
|
|
225
|
+
return canAccessRole(userRole, auth.role);
|
|
226
|
+
}
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* check if user can delete an album.
|
|
232
|
+
* albums require Admin role.
|
|
233
|
+
*/
|
|
234
|
+
export function canDeleteAlbum(userRole: UserRoleName): boolean {
|
|
235
|
+
const auth = routes.music.delete_album.auth;
|
|
236
|
+
if (auth.type === "role") {
|
|
237
|
+
return canAccessRole(userRole, auth.role);
|
|
238
|
+
}
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* check if user can update an album.
|
|
244
|
+
* albums require Admin role.
|
|
245
|
+
*/
|
|
246
|
+
export function canUpdateAlbum(userRole: UserRoleName): boolean {
|
|
247
|
+
const auth = routes.music.update_album.auth;
|
|
248
|
+
if (auth.type === "role") {
|
|
249
|
+
return canAccessRole(userRole, auth.role);
|
|
250
|
+
}
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* check if user can delete an artist.
|
|
256
|
+
* artists require Admin role.
|
|
257
|
+
*/
|
|
258
|
+
export function canDeleteArtist(userRole: UserRoleName): boolean {
|
|
259
|
+
const auth = routes.music.delete_artist.auth;
|
|
260
|
+
if (auth.type === "role") {
|
|
261
|
+
return canAccessRole(userRole, auth.role);
|
|
262
|
+
}
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* check if user can upload music.
|
|
268
|
+
* uploads require Member role.
|
|
269
|
+
*/
|
|
270
|
+
export function canUploadMusic(userRole: UserRoleName): boolean {
|
|
271
|
+
const auth = routes.music.upload_music.auth;
|
|
272
|
+
if (auth.type === "role") {
|
|
273
|
+
return canAccessRole(userRole, auth.role);
|
|
274
|
+
}
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* check if user can create a playlist.
|
|
280
|
+
* playlists require Member role to create.
|
|
281
|
+
*/
|
|
282
|
+
export function canCreatePlaylist(userRole: UserRoleName): boolean {
|
|
283
|
+
const auth = routes.music.create_playlist.auth;
|
|
284
|
+
if (auth.type === "role") {
|
|
285
|
+
return canAccessRole(userRole, auth.role);
|
|
286
|
+
}
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ============================================================================
|
|
291
|
+
// additional Member role checks
|
|
292
|
+
// ============================================================================
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* check if user can set favorites.
|
|
296
|
+
* favorites require Member role.
|
|
297
|
+
*/
|
|
298
|
+
export function canSetFavorite(userRole: UserRoleName): boolean {
|
|
299
|
+
const auth = routes.music.set_favorite.auth;
|
|
300
|
+
if (auth.type === "role") {
|
|
301
|
+
return canAccessRole(userRole, auth.role);
|
|
302
|
+
}
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* check if user can set ratings.
|
|
308
|
+
* ratings require Member role.
|
|
309
|
+
*/
|
|
310
|
+
export function canSetRating(userRole: UserRoleName): boolean {
|
|
311
|
+
const auth = routes.music.set_rating.auth;
|
|
312
|
+
if (auth.type === "role") {
|
|
313
|
+
return canAccessRole(userRole, auth.role);
|
|
314
|
+
}
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* check if user can upload images.
|
|
320
|
+
* images require Member role to upload.
|
|
321
|
+
*/
|
|
322
|
+
export function canUploadImage(userRole: UserRoleName): boolean {
|
|
323
|
+
const auth = routes.music.upload_image.auth;
|
|
324
|
+
if (auth.type === "role") {
|
|
325
|
+
return canAccessRole(userRole, auth.role);
|
|
326
|
+
}
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* check if user can create listen sessions.
|
|
332
|
+
* listen sessions require Member role.
|
|
333
|
+
*/
|
|
334
|
+
export function canCreateListenSession(userRole: UserRoleName): boolean {
|
|
335
|
+
const auth = routes.music.create_listen_session.auth;
|
|
336
|
+
if (auth.type === "role") {
|
|
337
|
+
return canAccessRole(userRole, auth.role);
|
|
338
|
+
}
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* check if user can create fetch jobs (download from URL).
|
|
344
|
+
* fetch jobs require Member role.
|
|
345
|
+
*/
|
|
346
|
+
export function canCreateFetchJob(userRole: UserRoleName): boolean {
|
|
347
|
+
const auth = routes.music.create_fetch_job.auth;
|
|
348
|
+
if (auth.type === "role") {
|
|
349
|
+
return canAccessRole(userRole, auth.role);
|
|
350
|
+
}
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// ============================================================================
|
|
355
|
+
// additional Admin role checks
|
|
356
|
+
// ============================================================================
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* check if user can create an artist.
|
|
360
|
+
* artists require Admin role to create.
|
|
361
|
+
*/
|
|
362
|
+
export function canCreateArtist(userRole: UserRoleName): boolean {
|
|
363
|
+
const auth = routes.music.create_artist.auth;
|
|
364
|
+
if (auth.type === "role") {
|
|
365
|
+
return canAccessRole(userRole, auth.role);
|
|
366
|
+
}
|
|
367
|
+
return false;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* check if user can update an artist.
|
|
372
|
+
* artists require Admin role.
|
|
373
|
+
*/
|
|
374
|
+
export function canUpdateArtist(userRole: UserRoleName): boolean {
|
|
375
|
+
const auth = routes.music.update_artist.auth;
|
|
376
|
+
if (auth.type === "role") {
|
|
377
|
+
return canAccessRole(userRole, auth.role);
|
|
378
|
+
}
|
|
379
|
+
return false;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* check if user can manage tags (add/remove/replace/delete).
|
|
384
|
+
* tags require Admin role.
|
|
385
|
+
*/
|
|
386
|
+
export function canManageTags(userRole: UserRoleName): boolean {
|
|
387
|
+
const auth = routes.music.add_albums_tags.auth;
|
|
388
|
+
if (auth.type === "role") {
|
|
389
|
+
return canAccessRole(userRole, auth.role);
|
|
390
|
+
}
|
|
391
|
+
return false;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* check if user can delete images.
|
|
396
|
+
* image deletion requires Admin role.
|
|
397
|
+
*/
|
|
398
|
+
export function canDeleteImage(userRole: UserRoleName): boolean {
|
|
399
|
+
const auth = routes.music.delete_image.auth;
|
|
400
|
+
if (auth.type === "role") {
|
|
401
|
+
return canAccessRole(userRole, auth.role);
|
|
402
|
+
}
|
|
403
|
+
return false;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* check if user can set primary image.
|
|
408
|
+
* setting primary image requires Admin role.
|
|
409
|
+
*/
|
|
410
|
+
export function canSetPrimaryImage(userRole: UserRoleName): boolean {
|
|
411
|
+
const auth = routes.music.set_primary_image.auth;
|
|
412
|
+
if (auth.type === "role") {
|
|
413
|
+
return canAccessRole(userRole, auth.role);
|
|
414
|
+
}
|
|
415
|
+
return false;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* check if user can access MusicBrainz lookup.
|
|
420
|
+
* MusicBrainz requires Admin role.
|
|
421
|
+
*/
|
|
422
|
+
export function canAccessMusicBrainz(userRole: UserRoleName): boolean {
|
|
423
|
+
const auth = routes.music.search_musicbrainz_releases.auth;
|
|
424
|
+
if (auth.type === "role") {
|
|
425
|
+
return canAccessRole(userRole, auth.role);
|
|
426
|
+
}
|
|
427
|
+
return false;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// ============================================================================
|
|
431
|
+
// playlist mutation helpers (OwnerOr)
|
|
432
|
+
// ============================================================================
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* check if user can reorder songs in a playlist.
|
|
436
|
+
* playlists use OwnerOr(Admin).
|
|
437
|
+
*/
|
|
438
|
+
export function canReorderPlaylistSongs(
|
|
439
|
+
userId: string,
|
|
440
|
+
playlistOwnerId: string | null,
|
|
441
|
+
userRole: UserRoleName,
|
|
442
|
+
): boolean {
|
|
443
|
+
const auth = routes.music.reorder_playlist_songs.auth;
|
|
444
|
+
if (auth.type === "owner_or") {
|
|
445
|
+
return canAccessOwnerOr(userId, playlistOwnerId, userRole, auth.role);
|
|
446
|
+
}
|
|
447
|
+
return false;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// ============================================================================
|
|
451
|
+
// convenience role checks (no route lookup needed)
|
|
452
|
+
// ============================================================================
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* check if user has at least Member role.
|
|
456
|
+
* use for UI elements that require any non-Viewer role.
|
|
457
|
+
*/
|
|
458
|
+
export function isMemberOrHigher(userRole: UserRoleName): boolean {
|
|
459
|
+
return canAccessRole(userRole, "member");
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* check if user has Admin role.
|
|
464
|
+
* use for UI elements that require admin access.
|
|
465
|
+
*/
|
|
466
|
+
export function isAdmin(userRole: UserRoleName): boolean {
|
|
467
|
+
return canAccessRole(userRole, "admin");
|
|
468
|
+
}
|