@shadowob/sdk 0.3.3 → 0.4.0

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/src/client.ts DELETED
@@ -1,1570 +0,0 @@
1
- import type {
2
- ShadowApp,
3
- ShadowCartItem,
4
- ShadowCategory,
5
- ShadowChannel,
6
- ShadowContract,
7
- ShadowDmChannel,
8
- ShadowFriendship,
9
- ShadowInviteCode,
10
- ShadowListing,
11
- ShadowMember,
12
- ShadowMessage,
13
- ShadowNotification,
14
- ShadowNotificationPreferences,
15
- ShadowOAuthApp,
16
- ShadowOAuthConsent,
17
- ShadowOAuthToken,
18
- ShadowOrder,
19
- ShadowProduct,
20
- ShadowRemoteConfig,
21
- ShadowReview,
22
- ShadowServer,
23
- ShadowShop,
24
- ShadowTask,
25
- ShadowThread,
26
- ShadowTransaction,
27
- ShadowUser,
28
- ShadowWallet,
29
- } from './types'
30
-
31
- /**
32
- * Shadow REST API client.
33
- *
34
- * Provides typed HTTP methods for interacting with the Shadow server API.
35
- */
36
- export class ShadowClient {
37
- private baseUrl: string
38
-
39
- constructor(
40
- baseUrl: string,
41
- private token: string,
42
- ) {
43
- // Normalize: strip trailing /api or /api/ to prevent doubled paths
44
- this.baseUrl = baseUrl.replace(/\/api\/?$/, '')
45
- }
46
-
47
- private async request<T>(path: string, init?: RequestInit): Promise<T> {
48
- const url = `${this.baseUrl}${path}`
49
- const controller = new AbortController()
50
- const timeout = setTimeout(() => controller.abort(), 60_000)
51
- try {
52
- const res = await fetch(url, {
53
- ...init,
54
- signal: init?.signal ?? controller.signal,
55
- headers: {
56
- 'Content-Type': 'application/json',
57
- Authorization: `Bearer ${this.token}`,
58
- ...init?.headers,
59
- },
60
- })
61
- if (!res.ok) {
62
- const body = await res.text().catch(() => '')
63
- throw new Error(
64
- `Shadow API ${init?.method ?? 'GET'} ${path} failed (${res.status}): ${body}`,
65
- )
66
- }
67
- return res.json() as Promise<T>
68
- } finally {
69
- clearTimeout(timeout)
70
- }
71
- }
72
-
73
- private async requestRaw(path: string, init?: RequestInit): Promise<Response> {
74
- const url = `${this.baseUrl}${path}`
75
- const res = await fetch(url, {
76
- ...init,
77
- headers: {
78
- Authorization: `Bearer ${this.token}`,
79
- ...init?.headers,
80
- },
81
- })
82
- if (!res.ok) {
83
- const body = await res.text().catch(() => '')
84
- throw new Error(`Shadow API ${init?.method ?? 'GET'} ${path} failed (${res.status}): ${body}`)
85
- }
86
- return res
87
- }
88
-
89
- // ── Auth ──────────────────────────────────────────────────────────────
90
-
91
- async register(data: {
92
- email: string
93
- password: string
94
- username: string
95
- displayName?: string
96
- inviteCode: string
97
- }): Promise<{ token: string; user: ShadowUser }> {
98
- return this.request('/api/auth/register', {
99
- method: 'POST',
100
- body: JSON.stringify(data),
101
- })
102
- }
103
-
104
- async login(data: {
105
- email: string
106
- password: string
107
- }): Promise<{ token: string; user: ShadowUser }> {
108
- return this.request('/api/auth/login', {
109
- method: 'POST',
110
- body: JSON.stringify(data),
111
- })
112
- }
113
-
114
- async refreshToken(): Promise<{ token: string }> {
115
- return this.request('/api/auth/refresh', { method: 'POST' })
116
- }
117
-
118
- async getMe(): Promise<ShadowUser> {
119
- return this.request('/api/auth/me')
120
- }
121
-
122
- async updateProfile(data: {
123
- displayName?: string
124
- avatarUrl?: string | null
125
- }): Promise<ShadowUser> {
126
- return this.request('/api/auth/me', {
127
- method: 'PATCH',
128
- body: JSON.stringify(data),
129
- })
130
- }
131
-
132
- async disconnect(): Promise<{
133
- success: boolean
134
- }> {
135
- return this.request('/api/auth/disconnect', { method: 'POST' })
136
- }
137
-
138
- // ── Agents ────────────────────────────────────────────────────────────
139
-
140
- async listAgents(): Promise<{ id: string; name: string; status: string }[]> {
141
- return this.request('/api/agents')
142
- }
143
-
144
- async createAgent(data: {
145
- name: string
146
- displayName?: string
147
- avatarUrl?: string | null
148
- }): Promise<{ id: string; token: string; userId: string }> {
149
- return this.request('/api/agents', {
150
- method: 'POST',
151
- body: JSON.stringify(data),
152
- })
153
- }
154
-
155
- async getAgent(
156
- agentId: string,
157
- ): Promise<{ id: string; name: string; status: string; userId: string }> {
158
- return this.request(`/api/agents/${agentId}`)
159
- }
160
-
161
- async updateAgent(
162
- agentId: string,
163
- data: { name?: string; displayName?: string; avatarUrl?: string | null },
164
- ): Promise<{ id: string; name: string }> {
165
- return this.request(`/api/agents/${agentId}`, {
166
- method: 'PATCH',
167
- body: JSON.stringify(data),
168
- })
169
- }
170
-
171
- async deleteAgent(agentId: string): Promise<{ success: boolean }> {
172
- return this.request(`/api/agents/${agentId}`, { method: 'DELETE' })
173
- }
174
-
175
- async generateAgentToken(agentId: string): Promise<{ token: string }> {
176
- return this.request(`/api/agents/${agentId}/token`, { method: 'POST' })
177
- }
178
-
179
- async startAgent(agentId: string): Promise<{ ok: boolean }> {
180
- return this.request(`/api/agents/${agentId}/start`, { method: 'POST' })
181
- }
182
-
183
- async stopAgent(agentId: string): Promise<{ ok: boolean }> {
184
- return this.request(`/api/agents/${agentId}/stop`, { method: 'POST' })
185
- }
186
-
187
- async sendHeartbeat(agentId: string): Promise<{ ok: boolean }> {
188
- return this.request(`/api/agents/${agentId}/heartbeat`, {
189
- method: 'POST',
190
- body: JSON.stringify({}),
191
- })
192
- }
193
-
194
- async getAgentConfig(agentId: string): Promise<ShadowRemoteConfig> {
195
- return this.request<ShadowRemoteConfig>(`/api/agents/${agentId}/config`)
196
- }
197
-
198
- // ── Agent Policies ────────────────────────────────────────────────────
199
-
200
- async listPolicies(
201
- agentId: string,
202
- serverId: string,
203
- ): Promise<
204
- {
205
- channelId: string | null
206
- mentionOnly: boolean
207
- reply: boolean
208
- config: Record<string, unknown>
209
- }[]
210
- > {
211
- return this.request(`/api/agents/${agentId}/servers/${serverId}/policies`)
212
- }
213
-
214
- async upsertPolicy(
215
- agentId: string,
216
- serverId: string,
217
- data: {
218
- channelId?: string | null
219
- mentionOnly?: boolean
220
- reply?: boolean
221
- config?: Record<string, unknown>
222
- },
223
- ): Promise<{ channelId: string | null; mentionOnly: boolean; reply: boolean }> {
224
- return this.request(`/api/agents/${agentId}/servers/${serverId}/policies`, {
225
- method: 'PUT',
226
- body: JSON.stringify(data),
227
- })
228
- }
229
-
230
- async deletePolicy(
231
- agentId: string,
232
- serverId: string,
233
- channelId: string,
234
- ): Promise<{ success: boolean }> {
235
- return this.request(`/api/agents/${agentId}/servers/${serverId}/policies/${channelId}`, {
236
- method: 'DELETE',
237
- })
238
- }
239
-
240
- // ── Servers ───────────────────────────────────────────────────────────
241
-
242
- async discoverServers(): Promise<ShadowServer[]> {
243
- return this.request('/api/servers/discover')
244
- }
245
-
246
- async getServerByInvite(inviteCode: string): Promise<ShadowServer> {
247
- return this.request(`/api/servers/invite/${encodeURIComponent(inviteCode)}`)
248
- }
249
-
250
- async createServer(data: {
251
- name: string
252
- slug?: string
253
- description?: string
254
- isPublic?: boolean
255
- }): Promise<ShadowServer> {
256
- return this.request('/api/servers', {
257
- method: 'POST',
258
- body: JSON.stringify(data),
259
- })
260
- }
261
-
262
- async listServers(): Promise<ShadowServer[]> {
263
- return this.request('/api/servers')
264
- }
265
-
266
- async getServer(serverIdOrSlug: string): Promise<ShadowServer> {
267
- return this.request(`/api/servers/${serverIdOrSlug}`)
268
- }
269
-
270
- async updateServer(
271
- serverIdOrSlug: string,
272
- data: {
273
- name?: string
274
- description?: string | null
275
- slug?: string | null
276
- homepageHtml?: string | null
277
- isPublic?: boolean
278
- },
279
- ): Promise<ShadowServer> {
280
- return this.request(`/api/servers/${serverIdOrSlug}`, {
281
- method: 'PATCH',
282
- body: JSON.stringify(data),
283
- })
284
- }
285
-
286
- async updateServerHomepage(
287
- serverIdOrSlug: string,
288
- homepageHtml: string | null,
289
- ): Promise<ShadowServer> {
290
- return this.updateServer(serverIdOrSlug, { homepageHtml })
291
- }
292
-
293
- async deleteServer(serverId: string): Promise<{ success: boolean }> {
294
- return this.request(`/api/servers/${serverId}`, { method: 'DELETE' })
295
- }
296
-
297
- async joinServer(serverId: string, inviteCode?: string): Promise<{ success: boolean }> {
298
- return this.request(`/api/servers/${serverId}/join`, {
299
- method: 'POST',
300
- body: JSON.stringify(inviteCode ? { inviteCode } : {}),
301
- })
302
- }
303
-
304
- async leaveServer(serverId: string): Promise<{ success: boolean }> {
305
- return this.request(`/api/servers/${serverId}/leave`, { method: 'POST' })
306
- }
307
-
308
- async getMembers(serverId: string): Promise<ShadowMember[]> {
309
- return this.request(`/api/servers/${serverId}/members`)
310
- }
311
-
312
- async updateMember(
313
- serverId: string,
314
- userId: string,
315
- data: { role?: string },
316
- ): Promise<ShadowMember> {
317
- return this.request(`/api/servers/${serverId}/members/${userId}`, {
318
- method: 'PATCH',
319
- body: JSON.stringify(data),
320
- })
321
- }
322
-
323
- async kickMember(serverId: string, userId: string): Promise<{ success: boolean }> {
324
- return this.request(`/api/servers/${serverId}/members/${userId}`, { method: 'DELETE' })
325
- }
326
-
327
- async regenerateInviteCode(serverId: string): Promise<{ inviteCode: string }> {
328
- return this.request(`/api/servers/${serverId}/invite`, { method: 'POST' })
329
- }
330
-
331
- async addAgentsToServer(serverId: string, agentIds: string[]): Promise<{ added: string[] }> {
332
- return this.request(`/api/servers/${serverId}/agents`, {
333
- method: 'POST',
334
- body: JSON.stringify({ agentIds }),
335
- })
336
- }
337
-
338
- // ── Channels ──────────────────────────────────────────────────────────
339
-
340
- async getServerChannels(serverId: string): Promise<ShadowChannel[]> {
341
- return this.request<ShadowChannel[]>(`/api/servers/${serverId}/channels`)
342
- }
343
-
344
- async createChannel(
345
- serverId: string,
346
- data: { name: string; type?: string; description?: string },
347
- ): Promise<ShadowChannel> {
348
- const { description, ...rest } = data
349
- const body = { ...rest, ...(description !== undefined ? { topic: description } : {}) }
350
- const ch = await this.request<Record<string, unknown>>(`/api/servers/${serverId}/channels`, {
351
- method: 'POST',
352
- body: JSON.stringify(body),
353
- })
354
- return { ...ch, description: ch.topic } as unknown as ShadowChannel
355
- }
356
-
357
- async getChannel(channelId: string): Promise<ShadowChannel> {
358
- const ch = await this.request<Record<string, unknown>>(`/api/channels/${channelId}`)
359
- return { ...ch, description: ch.topic } as unknown as ShadowChannel
360
- }
361
-
362
- async getChannelMembers(channelId: string): Promise<ShadowMember[]> {
363
- return this.request(`/api/channels/${channelId}/members`)
364
- }
365
-
366
- async updateChannel(
367
- channelId: string,
368
- data: { name?: string; description?: string | null },
369
- ): Promise<ShadowChannel> {
370
- const { description, ...rest } = data
371
- const body = { ...rest, ...(description !== undefined ? { topic: description } : {}) }
372
- const ch = await this.request<Record<string, unknown>>(`/api/channels/${channelId}`, {
373
- method: 'PATCH',
374
- body: JSON.stringify(body),
375
- })
376
- return { ...ch, description: ch.topic } as unknown as ShadowChannel
377
- }
378
-
379
- async deleteChannel(channelId: string): Promise<{ success: boolean }> {
380
- return this.request(`/api/channels/${channelId}`, { method: 'DELETE' })
381
- }
382
-
383
- async reorderChannels(serverId: string, channelIds: string[]): Promise<{ success: boolean }> {
384
- return this.request(`/api/servers/${serverId}/channels/reorder`, {
385
- method: 'PUT',
386
- body: JSON.stringify({ channelIds }),
387
- })
388
- }
389
-
390
- async addChannelMember(channelId: string, userId: string): Promise<{ success: boolean }> {
391
- return this.request(`/api/channels/${channelId}/members`, {
392
- method: 'POST',
393
- body: JSON.stringify({ userId }),
394
- })
395
- }
396
-
397
- async removeChannelMember(channelId: string, userId: string): Promise<{ success: boolean }> {
398
- return this.request(`/api/channels/${channelId}/members/${userId}`, { method: 'DELETE' })
399
- }
400
-
401
- // ── Channel Buddy Policy ─────────────────────────────────────────────
402
-
403
- async setBuddyPolicy(
404
- channelId: string,
405
- data: { buddyUserId: string; mentionOnly?: boolean; reply?: boolean },
406
- ): Promise<{ success: boolean }> {
407
- return this.request(`/api/channels/${channelId}/buddy-policy`, {
408
- method: 'PUT',
409
- body: JSON.stringify(data),
410
- })
411
- }
412
-
413
- async getBuddyPolicy(
414
- channelId: string,
415
- ): Promise<{ buddyUserId: string | null; mentionOnly: boolean; reply: boolean } | null> {
416
- return this.request(`/api/channels/${channelId}/buddy-policy`)
417
- }
418
-
419
- // ── Messages ──────────────────────────────────────────────────────────
420
-
421
- async sendMessage(
422
- channelId: string,
423
- content: string,
424
- opts?: { threadId?: string; replyToId?: string },
425
- ): Promise<ShadowMessage> {
426
- return this.request<ShadowMessage>(`/api/channels/${channelId}/messages`, {
427
- method: 'POST',
428
- body: JSON.stringify({
429
- content,
430
- ...(opts?.threadId ? { threadId: opts.threadId } : {}),
431
- ...(opts?.replyToId ? { replyToId: opts.replyToId } : {}),
432
- }),
433
- })
434
- }
435
-
436
- async getMessages(
437
- channelId: string,
438
- limit = 50,
439
- cursor?: string,
440
- ): Promise<{ messages: ShadowMessage[]; hasMore: boolean }> {
441
- const params = new URLSearchParams({ limit: String(limit) })
442
- if (cursor) params.set('cursor', cursor)
443
- return this.request<{ messages: ShadowMessage[]; hasMore: boolean }>(
444
- `/api/channels/${channelId}/messages?${params}`,
445
- )
446
- }
447
-
448
- async getMessage(messageId: string): Promise<ShadowMessage> {
449
- return this.request(`/api/messages/${messageId}`)
450
- }
451
-
452
- async editMessage(messageId: string, content: string): Promise<ShadowMessage> {
453
- return this.request<ShadowMessage>(`/api/messages/${messageId}`, {
454
- method: 'PATCH',
455
- body: JSON.stringify({ content }),
456
- })
457
- }
458
-
459
- async deleteMessage(messageId: string): Promise<void> {
460
- await this.request<{ success: boolean }>(`/api/messages/${messageId}`, {
461
- method: 'DELETE',
462
- })
463
- }
464
-
465
- // ── Pins ──────────────────────────────────────────────────────────────
466
-
467
- async pinMessage(messageId: string, channelId?: string): Promise<{ success: boolean }> {
468
- if (channelId) {
469
- return this.request(`/api/channels/${channelId}/pins/${messageId}`, { method: 'PUT' })
470
- }
471
- return this.request(`/api/messages/${messageId}/pin`, { method: 'POST' })
472
- }
473
-
474
- async unpinMessage(messageId: string, channelId?: string): Promise<{ success: boolean }> {
475
- if (channelId) {
476
- return this.request(`/api/channels/${channelId}/pins/${messageId}`, { method: 'DELETE' })
477
- }
478
- return this.request(`/api/messages/${messageId}/pin`, { method: 'DELETE' })
479
- }
480
-
481
- async getPinnedMessages(channelId: string): Promise<ShadowMessage[]> {
482
- return this.request(`/api/channels/${channelId}/pins`)
483
- }
484
-
485
- // ── Reactions ─────────────────────────────────────────────────────────
486
-
487
- async addReaction(messageId: string, emoji: string): Promise<void> {
488
- await this.request(`/api/messages/${messageId}/reactions`, {
489
- method: 'POST',
490
- body: JSON.stringify({ emoji }),
491
- })
492
- }
493
-
494
- async removeReaction(messageId: string, emoji: string): Promise<void> {
495
- await this.request(`/api/messages/${messageId}/reactions/${encodeURIComponent(emoji)}`, {
496
- method: 'DELETE',
497
- })
498
- }
499
-
500
- async getReactions(
501
- messageId: string,
502
- ): Promise<{ emoji: string; count: number; users: string[] }[]> {
503
- return this.request(`/api/messages/${messageId}/reactions`)
504
- }
505
-
506
- // ── Threads ───────────────────────────────────────────────────────────
507
-
508
- async listThreads(channelId: string): Promise<ShadowThread[]> {
509
- return this.request(`/api/channels/${channelId}/threads`)
510
- }
511
-
512
- async createThread(
513
- channelId: string,
514
- name: string,
515
- parentMessageId: string,
516
- ): Promise<{ id: string; name: string }> {
517
- return this.request(`/api/channels/${channelId}/threads`, {
518
- method: 'POST',
519
- body: JSON.stringify({ name, parentMessageId }),
520
- })
521
- }
522
-
523
- async getThread(threadId: string): Promise<ShadowThread> {
524
- return this.request(`/api/threads/${threadId}`)
525
- }
526
-
527
- async updateThread(threadId: string, data: { name?: string }): Promise<ShadowThread> {
528
- return this.request(`/api/threads/${threadId}`, {
529
- method: 'PATCH',
530
- body: JSON.stringify(data),
531
- })
532
- }
533
-
534
- async deleteThread(threadId: string): Promise<{ success: boolean }> {
535
- return this.request(`/api/threads/${threadId}`, { method: 'DELETE' })
536
- }
537
-
538
- async getThreadMessages(threadId: string, limit = 50, cursor?: string): Promise<ShadowMessage[]> {
539
- const params = new URLSearchParams({ limit: String(limit) })
540
- if (cursor) params.set('cursor', cursor)
541
- return this.request<ShadowMessage[]>(`/api/threads/${threadId}/messages?${params}`)
542
- }
543
-
544
- async sendToThread(threadId: string, content: string): Promise<ShadowMessage> {
545
- return this.request<ShadowMessage>(`/api/threads/${threadId}/messages`, {
546
- method: 'POST',
547
- body: JSON.stringify({ content }),
548
- })
549
- }
550
-
551
- // ── DMs ───────────────────────────────────────────────────────────────
552
-
553
- async createDmChannel(userId: string): Promise<ShadowDmChannel> {
554
- return this.request('/api/dm/channels', {
555
- method: 'POST',
556
- body: JSON.stringify({ userId }),
557
- })
558
- }
559
-
560
- async listDmChannels(): Promise<ShadowDmChannel[]> {
561
- return this.request('/api/dm/channels')
562
- }
563
-
564
- async getDmMessages(channelId: string, limit = 50, cursor?: string): Promise<ShadowMessage[]> {
565
- const params = new URLSearchParams({ limit: String(limit) })
566
- if (cursor) params.set('cursor', cursor)
567
- return this.request(`/api/dm/channels/${channelId}/messages?${params}`)
568
- }
569
-
570
- async sendDmMessage(
571
- channelId: string,
572
- content: string,
573
- options?: { replyToId?: string },
574
- ): Promise<ShadowMessage> {
575
- return this.request(`/api/dm/channels/${channelId}/messages`, {
576
- method: 'POST',
577
- body: JSON.stringify({ content, replyToId: options?.replyToId }),
578
- })
579
- }
580
-
581
- // ── Notifications ─────────────────────────────────────────────────────
582
-
583
- async listNotifications(limit = 50, offset = 0): Promise<ShadowNotification[]> {
584
- const params = new URLSearchParams({ limit: String(limit), offset: String(offset) })
585
- return this.request(`/api/notifications?${params}`)
586
- }
587
-
588
- async markNotificationRead(notificationId: string): Promise<{ success: boolean }> {
589
- return this.request(`/api/notifications/${notificationId}/read`, { method: 'PATCH' })
590
- }
591
-
592
- async markAllNotificationsRead(): Promise<{ success: boolean }> {
593
- return this.request('/api/notifications/read-all', { method: 'POST' })
594
- }
595
-
596
- async getUnreadCount(): Promise<{ count: number }> {
597
- return this.request('/api/notifications/unread-count')
598
- }
599
-
600
- // ── Search ────────────────────────────────────────────────────────────
601
-
602
- async searchMessages(query: {
603
- q: string
604
- serverId?: string
605
- channelId?: string
606
- authorId?: string
607
- limit?: number
608
- offset?: number
609
- }): Promise<{ messages: ShadowMessage[]; total: number }> {
610
- const params = new URLSearchParams({ query: query.q })
611
- if (query.serverId) params.set('serverId', query.serverId)
612
- if (query.channelId) params.set('channelId', query.channelId)
613
- if (query.authorId) params.set('from', query.authorId)
614
- if (query.limit) params.set('limit', String(query.limit))
615
- if (query.offset) params.set('offset', String(query.offset))
616
- const result = await this.request<
617
- ShadowMessage[] | { messages: ShadowMessage[]; total: number }
618
- >(`/api/search/messages?${params}`)
619
- if (Array.isArray(result)) {
620
- return { messages: result, total: result.length }
621
- }
622
- return result
623
- }
624
-
625
- // ── Invites ───────────────────────────────────────────────────────────
626
-
627
- async listInvites(): Promise<ShadowInviteCode[]> {
628
- return this.request('/api/invite-codes')
629
- }
630
-
631
- async createInvites(count: number, note?: string): Promise<ShadowInviteCode[]> {
632
- return this.request('/api/invite-codes', {
633
- method: 'POST',
634
- body: JSON.stringify({ count, ...(note ? { note } : {}) }),
635
- })
636
- }
637
-
638
- async deactivateInvite(inviteId: string): Promise<ShadowInviteCode> {
639
- return this.request(`/api/invite-codes/${inviteId}/deactivate`, { method: 'PATCH' })
640
- }
641
-
642
- async deleteInvite(inviteId: string): Promise<{ success: boolean }> {
643
- return this.request(`/api/invite-codes/${inviteId}`, { method: 'DELETE' })
644
- }
645
-
646
- // ── Media ─────────────────────────────────────────────────────────────
647
-
648
- async uploadMedia(
649
- file: Blob | ArrayBuffer,
650
- filename: string,
651
- contentType: string,
652
- messageId?: string,
653
- ): Promise<{ url: string; key: string; size: number }> {
654
- const formData = new FormData()
655
- const blob = file instanceof Blob ? file : new Blob([file], { type: contentType })
656
- formData.append('file', blob, filename)
657
- if (messageId) {
658
- formData.append('messageId', messageId)
659
- }
660
-
661
- const url = `${this.baseUrl}/api/media/upload`
662
- const res = await fetch(url, {
663
- method: 'POST',
664
- headers: {
665
- Authorization: `Bearer ${this.token}`,
666
- },
667
- body: formData,
668
- })
669
- if (!res.ok) {
670
- const body = await res.text().catch(() => '')
671
- throw new Error(`Shadow API POST /api/media/upload failed (${res.status}): ${body}`)
672
- }
673
- return res.json() as Promise<{ url: string; key: string; size: number }>
674
- }
675
-
676
- /**
677
- * Download a file from a URL and upload it to the Shadow media service.
678
- * Supports local filesystem paths, file:// URLs, tilde paths, and HTTP(S) URLs.
679
- */
680
- async uploadMediaFromUrl(
681
- mediaUrl: string,
682
- messageId?: string,
683
- ): Promise<{ url: string; key: string; size: number }> {
684
- // Dynamic imports for Node.js fs/path/os
685
- // @ts-expect-error node:fs/promises is available at runtime
686
- const { readFile } = await import('node:fs/promises')
687
- // @ts-expect-error node:path is available at runtime
688
- const { basename } = await import('node:path')
689
- // @ts-expect-error node:os is available at runtime
690
- const { homedir } = await import('node:os')
691
-
692
- // Strip MEDIA: prefix used by agent tools to tag media paths
693
- let normalizedUrl = mediaUrl.replace(/^\s*MEDIA\s*:\s*/i, '')
694
-
695
- // Handle file:// URLs
696
- if (normalizedUrl.startsWith('file://')) {
697
- normalizedUrl = normalizedUrl.replace(/^file:\/\//, '')
698
- }
699
-
700
- // Expand tilde paths
701
- if (normalizedUrl.startsWith('~')) {
702
- normalizedUrl = normalizedUrl.replace(/^~/, homedir())
703
- }
704
-
705
- // Resolve relative paths
706
- if (
707
- !normalizedUrl.startsWith('/') &&
708
- !normalizedUrl.startsWith('http://') &&
709
- !normalizedUrl.startsWith('https://') &&
710
- !normalizedUrl.startsWith('//')
711
- ) {
712
- // @ts-expect-error node:fs is available at runtime
713
- const { existsSync } = await import('node:fs')
714
- // @ts-expect-error node:path is available at runtime
715
- const { resolve } = await import('node:path')
716
-
717
- const cwd = (globalThis as Record<string, unknown>).process
718
- ? ((globalThis as Record<string, unknown>).process as { cwd: () => string }).cwd()
719
- : '/'
720
- const roots = [resolve(homedir(), '.openclaw', 'workspace'), cwd]
721
- let resolved = false
722
- for (const root of roots) {
723
- const candidate = resolve(root, normalizedUrl)
724
- if (existsSync(candidate)) {
725
- normalizedUrl = candidate
726
- resolved = true
727
- break
728
- }
729
- }
730
- if (!resolved) {
731
- normalizedUrl = resolve(cwd, normalizedUrl)
732
- }
733
- }
734
-
735
- if (normalizedUrl.startsWith('/') && !normalizedUrl.startsWith('//')) {
736
- // Local filesystem path
737
- const fileBuffer = await readFile(normalizedUrl)
738
- const bytes = new Uint8Array(fileBuffer)
739
- const filename: string = basename(normalizedUrl)
740
- const ext = filename.split('.').pop()?.toLowerCase() ?? ''
741
- const mimeMap: Record<string, string> = {
742
- jpg: 'image/jpeg',
743
- jpeg: 'image/jpeg',
744
- png: 'image/png',
745
- gif: 'image/gif',
746
- webp: 'image/webp',
747
- svg: 'image/svg+xml',
748
- mp4: 'video/mp4',
749
- webm: 'video/webm',
750
- mp3: 'audio/mpeg',
751
- wav: 'audio/wav',
752
- ogg: 'audio/ogg',
753
- pdf: 'application/pdf',
754
- txt: 'text/plain',
755
- csv: 'text/csv',
756
- json: 'application/json',
757
- html: 'text/html',
758
- xml: 'application/xml',
759
- zip: 'application/zip',
760
- }
761
- const contentType = mimeMap[ext] ?? 'application/octet-stream'
762
- return this.uploadMedia(
763
- new Blob([bytes], { type: contentType }),
764
- filename,
765
- contentType,
766
- messageId,
767
- )
768
- }
769
-
770
- // HTTP/HTTPS URL
771
- const res = await fetch(normalizedUrl)
772
- if (!res.ok) {
773
- throw new Error(`Failed to download media from ${normalizedUrl}: ${res.status}`)
774
- }
775
- const blob = await res.blob()
776
- const urlPath = new URL(normalizedUrl).pathname
777
- const filename = urlPath.split('/').pop() ?? 'file'
778
- const contentType = blob.type || 'application/octet-stream'
779
- return this.uploadMedia(blob, filename, contentType, messageId)
780
- }
781
-
782
- async downloadFile(
783
- fileUrl: string,
784
- ): Promise<{ buffer: ArrayBuffer; contentType: string; filename: string }> {
785
- const headers: Record<string, string> = {}
786
- if (fileUrl.startsWith(this.baseUrl) || fileUrl.startsWith('/')) {
787
- headers.Authorization = `Bearer ${this.token}`
788
- }
789
- const fullUrl = fileUrl.startsWith('/') ? `${this.baseUrl}${fileUrl}` : fileUrl
790
- const res = await fetch(fullUrl, { headers, redirect: 'follow' })
791
- if (!res.ok) {
792
- throw new Error(`Failed to download file from ${fullUrl}: ${res.status}`)
793
- }
794
- const buffer = await res.arrayBuffer()
795
- const contentType = res.headers.get('content-type') ?? 'application/octet-stream'
796
- const urlPath = new URL(fullUrl).pathname
797
- const filename = decodeURIComponent(urlPath.split('/').pop() ?? 'file')
798
- return { buffer, contentType, filename }
799
- }
800
-
801
- // ── Workspace ─────────────────────────────────────────────────────────
802
-
803
- async getWorkspace(serverId: string): Promise<Record<string, unknown>> {
804
- return this.request(`/api/servers/${serverId}/workspace`)
805
- }
806
-
807
- async updateWorkspace(
808
- serverId: string,
809
- data: { name?: string; description?: string | null },
810
- ): Promise<Record<string, unknown>> {
811
- return this.request(`/api/servers/${serverId}/workspace`, {
812
- method: 'PATCH',
813
- body: JSON.stringify(data),
814
- })
815
- }
816
-
817
- async getWorkspaceTree(serverId: string): Promise<Record<string, unknown>> {
818
- return this.request(`/api/servers/${serverId}/workspace/tree`)
819
- }
820
-
821
- async getWorkspaceStats(serverId: string): Promise<Record<string, unknown>> {
822
- return this.request(`/api/servers/${serverId}/workspace/stats`)
823
- }
824
-
825
- async getWorkspaceChildren(
826
- serverId: string,
827
- parentId?: string | null,
828
- ): Promise<Record<string, unknown>[]> {
829
- const params = new URLSearchParams()
830
- if (parentId !== undefined && parentId !== null) params.set('parentId', parentId)
831
- const qs = params.toString()
832
- return this.request(`/api/servers/${serverId}/workspace/children${qs ? `?${qs}` : ''}`)
833
- }
834
-
835
- async batchWorkspaceChildren(
836
- serverId: string,
837
- parentIds: (string | null)[],
838
- ): Promise<Record<string, Record<string, unknown>[]>> {
839
- return this.request(`/api/servers/${serverId}/workspace/children/batch`, {
840
- method: 'POST',
841
- body: JSON.stringify({ parentIds }),
842
- })
843
- }
844
-
845
- async createWorkspaceFolder(
846
- serverId: string,
847
- data: { parentId?: string | null; name: string },
848
- ): Promise<Record<string, unknown>> {
849
- return this.request(`/api/servers/${serverId}/workspace/folders`, {
850
- method: 'POST',
851
- body: JSON.stringify(data),
852
- })
853
- }
854
-
855
- async updateWorkspaceFolder(
856
- serverId: string,
857
- folderId: string,
858
- data: { name?: string; parentId?: string | null; pos?: number },
859
- ): Promise<Record<string, unknown>> {
860
- return this.request(`/api/servers/${serverId}/workspace/folders/${folderId}`, {
861
- method: 'PATCH',
862
- body: JSON.stringify(data),
863
- })
864
- }
865
-
866
- async deleteWorkspaceFolder(serverId: string, folderId: string): Promise<{ success: boolean }> {
867
- return this.request(`/api/servers/${serverId}/workspace/folders/${folderId}`, {
868
- method: 'DELETE',
869
- })
870
- }
871
-
872
- async searchWorkspaceFolders(
873
- serverId: string,
874
- query: { searchText?: string; limit?: number },
875
- ): Promise<Record<string, unknown>[]> {
876
- const params = new URLSearchParams()
877
- if (query.searchText) params.set('searchText', query.searchText)
878
- if (query.limit) params.set('limit', String(query.limit))
879
- return this.request(`/api/servers/${serverId}/workspace/folders/search?${params}`)
880
- }
881
-
882
- async createWorkspaceFile(
883
- serverId: string,
884
- data: {
885
- parentId?: string | null
886
- name: string
887
- ext?: string | null
888
- mime?: string | null
889
- sizeBytes?: number | null
890
- contentRef?: string | null
891
- previewUrl?: string | null
892
- metadata?: Record<string, unknown> | null
893
- },
894
- ): Promise<Record<string, unknown>> {
895
- return this.request(`/api/servers/${serverId}/workspace/files`, {
896
- method: 'POST',
897
- body: JSON.stringify(data),
898
- })
899
- }
900
-
901
- async searchWorkspaceFiles(
902
- serverId: string,
903
- query: {
904
- parentId?: string
905
- searchText?: string
906
- ext?: string
907
- limit?: number
908
- offset?: number
909
- },
910
- ): Promise<Record<string, unknown>[]> {
911
- const params = new URLSearchParams()
912
- if (query.parentId) params.set('parentId', query.parentId)
913
- if (query.searchText) params.set('searchText', query.searchText)
914
- if (query.ext) params.set('ext', query.ext)
915
- if (query.limit) params.set('limit', String(query.limit))
916
- if (query.offset) params.set('offset', String(query.offset))
917
- return this.request(`/api/servers/${serverId}/workspace/files/search?${params}`)
918
- }
919
-
920
- async getWorkspaceFile(serverId: string, fileId: string): Promise<Record<string, unknown>> {
921
- return this.request(`/api/servers/${serverId}/workspace/files/${fileId}`)
922
- }
923
-
924
- async updateWorkspaceFile(
925
- serverId: string,
926
- fileId: string,
927
- data: {
928
- name?: string
929
- parentId?: string | null
930
- pos?: number
931
- ext?: string | null
932
- mime?: string | null
933
- sizeBytes?: number | null
934
- contentRef?: string | null
935
- previewUrl?: string | null
936
- metadata?: Record<string, unknown> | null
937
- },
938
- ): Promise<Record<string, unknown>> {
939
- return this.request(`/api/servers/${serverId}/workspace/files/${fileId}`, {
940
- method: 'PATCH',
941
- body: JSON.stringify(data),
942
- })
943
- }
944
-
945
- async deleteWorkspaceFile(serverId: string, fileId: string): Promise<{ success: boolean }> {
946
- return this.request(`/api/servers/${serverId}/workspace/files/${fileId}`, { method: 'DELETE' })
947
- }
948
-
949
- async cloneWorkspaceFile(serverId: string, fileId: string): Promise<Record<string, unknown>> {
950
- return this.request(`/api/servers/${serverId}/workspace/files/${fileId}/clone`, {
951
- method: 'POST',
952
- })
953
- }
954
-
955
- async pasteWorkspaceNodes(
956
- serverId: string,
957
- data: {
958
- sourceWorkspaceId: string
959
- targetParentId?: string | null
960
- nodeIds: string[]
961
- mode: 'copy' | 'cut'
962
- },
963
- ): Promise<Record<string, unknown>> {
964
- return this.request(`/api/servers/${serverId}/workspace/nodes/paste`, {
965
- method: 'POST',
966
- body: JSON.stringify(data),
967
- })
968
- }
969
-
970
- async executeWorkspaceCommands(
971
- serverId: string,
972
- commands: Record<string, unknown>[],
973
- ): Promise<Record<string, unknown>[]> {
974
- return this.request(`/api/servers/${serverId}/workspace/commands`, {
975
- method: 'POST',
976
- body: JSON.stringify({ commands }),
977
- })
978
- }
979
-
980
- async uploadWorkspaceFile(
981
- serverId: string,
982
- file: Blob,
983
- filename: string,
984
- parentId?: string,
985
- ): Promise<Record<string, unknown>> {
986
- const formData = new FormData()
987
- formData.append('file', file, filename)
988
- if (parentId) formData.append('parentId', parentId)
989
-
990
- const res = await this.requestRaw(`/api/servers/${serverId}/workspace/upload`, {
991
- method: 'POST',
992
- body: formData,
993
- })
994
- return res.json() as Promise<Record<string, unknown>>
995
- }
996
-
997
- async downloadWorkspace(serverId: string): Promise<ArrayBuffer> {
998
- const res = await this.requestRaw(`/api/servers/${serverId}/workspace/download`)
999
- return res.arrayBuffer()
1000
- }
1001
-
1002
- async downloadWorkspaceFolder(serverId: string, folderId: string): Promise<ArrayBuffer> {
1003
- const res = await this.requestRaw(
1004
- `/api/servers/${serverId}/workspace/folders/${folderId}/download`,
1005
- )
1006
- return res.arrayBuffer()
1007
- }
1008
-
1009
- // ── Auth (extended) ───────────────────────────────────────────────────
1010
-
1011
- async getUserProfile(userId: string): Promise<ShadowUser> {
1012
- return this.request(`/api/auth/users/${userId}`)
1013
- }
1014
-
1015
- async listOAuthAccounts(): Promise<
1016
- { id: string; provider: string; providerAccountId: string }[]
1017
- > {
1018
- return this.request('/api/auth/oauth/accounts')
1019
- }
1020
-
1021
- async unlinkOAuthAccount(accountId: string): Promise<{ success: boolean }> {
1022
- return this.request(`/api/auth/oauth/accounts/${accountId}`, { method: 'DELETE' })
1023
- }
1024
-
1025
- // ── Friendships ───────────────────────────────────────────────────────
1026
-
1027
- async sendFriendRequest(username: string): Promise<ShadowFriendship> {
1028
- return this.request('/api/friends/request', {
1029
- method: 'POST',
1030
- body: JSON.stringify({ username }),
1031
- })
1032
- }
1033
-
1034
- async acceptFriendRequest(requestId: string): Promise<ShadowFriendship> {
1035
- return this.request(`/api/friends/${requestId}/accept`, { method: 'POST' })
1036
- }
1037
-
1038
- async rejectFriendRequest(requestId: string): Promise<ShadowFriendship> {
1039
- return this.request(`/api/friends/${requestId}/reject`, { method: 'POST' })
1040
- }
1041
-
1042
- async removeFriend(friendshipId: string): Promise<{ success: boolean }> {
1043
- return this.request(`/api/friends/${friendshipId}`, { method: 'DELETE' })
1044
- }
1045
-
1046
- async listFriends(): Promise<ShadowFriendship[]> {
1047
- return this.request('/api/friends')
1048
- }
1049
-
1050
- async listPendingFriendRequests(): Promise<ShadowFriendship[]> {
1051
- return this.request('/api/friends/pending')
1052
- }
1053
-
1054
- async listSentFriendRequests(): Promise<ShadowFriendship[]> {
1055
- return this.request('/api/friends/sent')
1056
- }
1057
-
1058
- // ── Notifications (extended) ──────────────────────────────────────────
1059
-
1060
- async markScopeRead(scope: {
1061
- serverId?: string
1062
- channelId?: string
1063
- }): Promise<{ success: boolean }> {
1064
- return this.request('/api/notifications/read-scope', {
1065
- method: 'POST',
1066
- body: JSON.stringify(scope),
1067
- })
1068
- }
1069
-
1070
- async getScopedUnread(): Promise<Record<string, number>> {
1071
- return this.request('/api/notifications/scoped-unread')
1072
- }
1073
-
1074
- async getNotificationPreferences(): Promise<ShadowNotificationPreferences> {
1075
- return this.request('/api/notifications/preferences')
1076
- }
1077
-
1078
- async updateNotificationPreferences(
1079
- data: Partial<ShadowNotificationPreferences>,
1080
- ): Promise<ShadowNotificationPreferences> {
1081
- return this.request('/api/notifications/preferences', {
1082
- method: 'PATCH',
1083
- body: JSON.stringify(data),
1084
- })
1085
- }
1086
-
1087
- // ── OAuth Apps ────────────────────────────────────────────────────────
1088
-
1089
- async createOAuthApp(data: {
1090
- name: string
1091
- redirectUris: string[]
1092
- scopes?: string[]
1093
- }): Promise<ShadowOAuthApp> {
1094
- return this.request('/api/oauth/apps', {
1095
- method: 'POST',
1096
- body: JSON.stringify(data),
1097
- })
1098
- }
1099
-
1100
- async listOAuthApps(): Promise<ShadowOAuthApp[]> {
1101
- return this.request('/api/oauth/apps')
1102
- }
1103
-
1104
- async updateOAuthApp(
1105
- appId: string,
1106
- data: { name?: string; redirectUris?: string[]; scopes?: string[] },
1107
- ): Promise<ShadowOAuthApp> {
1108
- return this.request(`/api/oauth/apps/${appId}`, {
1109
- method: 'PATCH',
1110
- body: JSON.stringify(data),
1111
- })
1112
- }
1113
-
1114
- async deleteOAuthApp(appId: string): Promise<{ success: boolean }> {
1115
- return this.request(`/api/oauth/apps/${appId}`, { method: 'DELETE' })
1116
- }
1117
-
1118
- async resetOAuthAppSecret(appId: string): Promise<{ clientSecret: string }> {
1119
- return this.request(`/api/oauth/apps/${appId}/reset-secret`, { method: 'POST' })
1120
- }
1121
-
1122
- async getOAuthAuthorization(params: {
1123
- client_id: string
1124
- redirect_uri: string
1125
- scope?: string
1126
- state?: string
1127
- }): Promise<{ app: ShadowOAuthApp }> {
1128
- const qs = new URLSearchParams(params)
1129
- return this.request(`/api/oauth/authorize?${qs}`)
1130
- }
1131
-
1132
- async approveOAuthAuthorization(data: {
1133
- client_id: string
1134
- redirect_uri: string
1135
- scope?: string
1136
- state?: string
1137
- }): Promise<{ redirectUrl: string }> {
1138
- return this.request('/api/oauth/authorize', {
1139
- method: 'POST',
1140
- body: JSON.stringify(data),
1141
- })
1142
- }
1143
-
1144
- async exchangeOAuthToken(data: {
1145
- grant_type: 'authorization_code' | 'refresh_token'
1146
- code?: string
1147
- refresh_token?: string
1148
- client_id: string
1149
- client_secret: string
1150
- redirect_uri?: string
1151
- }): Promise<ShadowOAuthToken> {
1152
- return this.request('/api/oauth/token', {
1153
- method: 'POST',
1154
- body: JSON.stringify(data),
1155
- })
1156
- }
1157
-
1158
- async listOAuthConsents(): Promise<ShadowOAuthConsent[]> {
1159
- return this.request('/api/oauth/consents')
1160
- }
1161
-
1162
- async revokeOAuthConsent(appId: string): Promise<{ success: boolean }> {
1163
- return this.request('/api/oauth/revoke', {
1164
- method: 'POST',
1165
- body: JSON.stringify({ appId }),
1166
- })
1167
- }
1168
-
1169
- // ── Marketplace / Rentals ─────────────────────────────────────────────
1170
-
1171
- async browseListings(params?: {
1172
- search?: string
1173
- tags?: string[]
1174
- minPrice?: number
1175
- maxPrice?: number
1176
- limit?: number
1177
- offset?: number
1178
- }): Promise<{ listings: ShadowListing[]; total: number }> {
1179
- const qs = new URLSearchParams()
1180
- if (params?.search) qs.set('search', params.search)
1181
- if (params?.tags) for (const t of params.tags) qs.append('tags', t)
1182
- if (params?.minPrice != null) qs.set('minPrice', String(params.minPrice))
1183
- if (params?.maxPrice != null) qs.set('maxPrice', String(params.maxPrice))
1184
- if (params?.limit) qs.set('limit', String(params.limit))
1185
- if (params?.offset) qs.set('offset', String(params.offset))
1186
- return this.request(`/api/marketplace/listings?${qs}`)
1187
- }
1188
-
1189
- async getListing(listingId: string): Promise<ShadowListing> {
1190
- return this.request(`/api/marketplace/listings/${listingId}`)
1191
- }
1192
-
1193
- async estimateRentalCost(
1194
- listingId: string,
1195
- hours: number,
1196
- ): Promise<{ totalCost: number; currency: string }> {
1197
- const qs = new URLSearchParams({ hours: String(hours) })
1198
- return this.request(`/api/marketplace/listings/${listingId}/estimate?${qs}`)
1199
- }
1200
-
1201
- async listMyListings(): Promise<ShadowListing[]> {
1202
- return this.request('/api/marketplace/my-listings')
1203
- }
1204
-
1205
- async createListing(data: {
1206
- agentId: string
1207
- title: string
1208
- description: string
1209
- pricePerHour: number
1210
- currency?: string
1211
- tags?: string[]
1212
- }): Promise<ShadowListing> {
1213
- return this.request('/api/marketplace/listings', {
1214
- method: 'POST',
1215
- body: JSON.stringify(data),
1216
- })
1217
- }
1218
-
1219
- async updateListing(
1220
- listingId: string,
1221
- data: Partial<{ title: string; description: string; pricePerHour: number; tags: string[] }>,
1222
- ): Promise<ShadowListing> {
1223
- return this.request(`/api/marketplace/listings/${listingId}`, {
1224
- method: 'PUT',
1225
- body: JSON.stringify(data),
1226
- })
1227
- }
1228
-
1229
- async toggleListing(listingId: string): Promise<ShadowListing> {
1230
- return this.request(`/api/marketplace/listings/${listingId}/toggle`, { method: 'PUT' })
1231
- }
1232
-
1233
- async deleteListing(listingId: string): Promise<{ success: boolean }> {
1234
- return this.request(`/api/marketplace/listings/${listingId}`, { method: 'DELETE' })
1235
- }
1236
-
1237
- async signContract(data: { listingId: string; hours: number }): Promise<ShadowContract> {
1238
- return this.request('/api/marketplace/contracts', {
1239
- method: 'POST',
1240
- body: JSON.stringify(data),
1241
- })
1242
- }
1243
-
1244
- async listContracts(params?: {
1245
- role?: 'tenant' | 'owner'
1246
- status?: string
1247
- }): Promise<ShadowContract[]> {
1248
- const qs = new URLSearchParams()
1249
- if (params?.role) qs.set('role', params.role)
1250
- if (params?.status) qs.set('status', params.status)
1251
- return this.request(`/api/marketplace/contracts?${qs}`)
1252
- }
1253
-
1254
- async getContract(contractId: string): Promise<ShadowContract> {
1255
- return this.request(`/api/marketplace/contracts/${contractId}`)
1256
- }
1257
-
1258
- async terminateContract(contractId: string): Promise<ShadowContract> {
1259
- return this.request(`/api/marketplace/contracts/${contractId}/terminate`, { method: 'POST' })
1260
- }
1261
-
1262
- async recordUsageSession(
1263
- contractId: string,
1264
- data: { durationMinutes: number; description?: string },
1265
- ): Promise<{ success: boolean }> {
1266
- return this.request(`/api/marketplace/contracts/${contractId}/usage`, {
1267
- method: 'POST',
1268
- body: JSON.stringify(data),
1269
- })
1270
- }
1271
-
1272
- async reportViolation(
1273
- contractId: string,
1274
- data: { reason: string },
1275
- ): Promise<{ success: boolean }> {
1276
- return this.request(`/api/marketplace/contracts/${contractId}/violate`, {
1277
- method: 'POST',
1278
- body: JSON.stringify(data),
1279
- })
1280
- }
1281
-
1282
- // ── Shop ──────────────────────────────────────────────────────────────
1283
-
1284
- async getShop(serverId: string): Promise<ShadowShop> {
1285
- return this.request(`/api/servers/${serverId}/shop`)
1286
- }
1287
-
1288
- async updateShop(
1289
- serverId: string,
1290
- data: Partial<{ name: string; description: string | null; isEnabled: boolean }>,
1291
- ): Promise<ShadowShop> {
1292
- return this.request(`/api/servers/${serverId}/shop`, {
1293
- method: 'PUT',
1294
- body: JSON.stringify(data),
1295
- })
1296
- }
1297
-
1298
- async listCategories(serverId: string): Promise<ShadowCategory[]> {
1299
- return this.request(`/api/servers/${serverId}/shop/categories`)
1300
- }
1301
-
1302
- async createCategory(
1303
- serverId: string,
1304
- data: { name: string; description?: string },
1305
- ): Promise<ShadowCategory> {
1306
- return this.request(`/api/servers/${serverId}/shop/categories`, {
1307
- method: 'POST',
1308
- body: JSON.stringify(data),
1309
- })
1310
- }
1311
-
1312
- async updateCategory(
1313
- serverId: string,
1314
- categoryId: string,
1315
- data: Partial<{ name: string; description: string | null; position: number }>,
1316
- ): Promise<ShadowCategory> {
1317
- return this.request(`/api/servers/${serverId}/shop/categories/${categoryId}`, {
1318
- method: 'PUT',
1319
- body: JSON.stringify(data),
1320
- })
1321
- }
1322
-
1323
- async deleteCategory(serverId: string, categoryId: string): Promise<{ success: boolean }> {
1324
- return this.request(`/api/servers/${serverId}/shop/categories/${categoryId}`, {
1325
- method: 'DELETE',
1326
- })
1327
- }
1328
-
1329
- async listProducts(
1330
- serverId: string,
1331
- params?: {
1332
- status?: string
1333
- categoryId?: string
1334
- keyword?: string
1335
- limit?: number
1336
- offset?: number
1337
- },
1338
- ): Promise<{ products: ShadowProduct[]; total: number }> {
1339
- const qs = new URLSearchParams()
1340
- if (params?.status) qs.set('status', params.status)
1341
- if (params?.categoryId) qs.set('categoryId', params.categoryId)
1342
- if (params?.keyword) qs.set('keyword', params.keyword)
1343
- if (params?.limit) qs.set('limit', String(params.limit))
1344
- if (params?.offset) qs.set('offset', String(params.offset))
1345
- return this.request(`/api/servers/${serverId}/shop/products?${qs}`)
1346
- }
1347
-
1348
- async getProduct(serverId: string, productId: string): Promise<ShadowProduct> {
1349
- return this.request(`/api/servers/${serverId}/shop/products/${productId}`)
1350
- }
1351
-
1352
- async createProduct(
1353
- serverId: string,
1354
- data: {
1355
- name: string
1356
- description?: string
1357
- price: number
1358
- currency?: string
1359
- stock: number
1360
- categoryId?: string
1361
- images?: string[]
1362
- },
1363
- ): Promise<ShadowProduct> {
1364
- return this.request(`/api/servers/${serverId}/shop/products`, {
1365
- method: 'POST',
1366
- body: JSON.stringify(data),
1367
- })
1368
- }
1369
-
1370
- async updateProduct(
1371
- serverId: string,
1372
- productId: string,
1373
- data: Partial<{
1374
- name: string
1375
- description: string | null
1376
- price: number
1377
- stock: number
1378
- status: string
1379
- categoryId: string | null
1380
- images: string[]
1381
- }>,
1382
- ): Promise<ShadowProduct> {
1383
- return this.request(`/api/servers/${serverId}/shop/products/${productId}`, {
1384
- method: 'PUT',
1385
- body: JSON.stringify(data),
1386
- })
1387
- }
1388
-
1389
- async deleteProduct(serverId: string, productId: string): Promise<{ success: boolean }> {
1390
- return this.request(`/api/servers/${serverId}/shop/products/${productId}`, { method: 'DELETE' })
1391
- }
1392
-
1393
- async getCart(serverId: string): Promise<ShadowCartItem[]> {
1394
- return this.request(`/api/servers/${serverId}/shop/cart`)
1395
- }
1396
-
1397
- async addToCart(
1398
- serverId: string,
1399
- data: { productId: string; quantity: number },
1400
- ): Promise<ShadowCartItem> {
1401
- return this.request(`/api/servers/${serverId}/shop/cart`, {
1402
- method: 'POST',
1403
- body: JSON.stringify(data),
1404
- })
1405
- }
1406
-
1407
- async updateCartItem(
1408
- serverId: string,
1409
- itemId: string,
1410
- quantity: number,
1411
- ): Promise<ShadowCartItem> {
1412
- return this.request(`/api/servers/${serverId}/shop/cart/${itemId}`, {
1413
- method: 'PUT',
1414
- body: JSON.stringify({ quantity }),
1415
- })
1416
- }
1417
-
1418
- async removeCartItem(serverId: string, itemId: string): Promise<{ success: boolean }> {
1419
- return this.request(`/api/servers/${serverId}/shop/cart/${itemId}`, { method: 'DELETE' })
1420
- }
1421
-
1422
- async createOrder(
1423
- serverId: string,
1424
- data?: { items?: { productId: string; quantity: number }[] },
1425
- ): Promise<ShadowOrder> {
1426
- return this.request(`/api/servers/${serverId}/shop/orders`, {
1427
- method: 'POST',
1428
- body: JSON.stringify(data ?? {}),
1429
- })
1430
- }
1431
-
1432
- async listOrders(serverId: string): Promise<ShadowOrder[]> {
1433
- return this.request(`/api/servers/${serverId}/shop/orders`)
1434
- }
1435
-
1436
- async listShopOrders(serverId: string): Promise<ShadowOrder[]> {
1437
- return this.request(`/api/servers/${serverId}/shop/orders/manage`)
1438
- }
1439
-
1440
- async getOrder(serverId: string, orderId: string): Promise<ShadowOrder> {
1441
- return this.request(`/api/servers/${serverId}/shop/orders/${orderId}`)
1442
- }
1443
-
1444
- async updateOrderStatus(serverId: string, orderId: string, status: string): Promise<ShadowOrder> {
1445
- return this.request(`/api/servers/${serverId}/shop/orders/${orderId}/status`, {
1446
- method: 'PUT',
1447
- body: JSON.stringify({ status }),
1448
- })
1449
- }
1450
-
1451
- async cancelOrder(serverId: string, orderId: string): Promise<ShadowOrder> {
1452
- return this.request(`/api/servers/${serverId}/shop/orders/${orderId}/cancel`, {
1453
- method: 'POST',
1454
- })
1455
- }
1456
-
1457
- async getProductReviews(serverId: string, productId: string): Promise<ShadowReview[]> {
1458
- return this.request(`/api/servers/${serverId}/shop/products/${productId}/reviews`)
1459
- }
1460
-
1461
- async createReview(
1462
- serverId: string,
1463
- orderId: string,
1464
- data: { productId: string; rating: number; content: string },
1465
- ): Promise<ShadowReview> {
1466
- return this.request(`/api/servers/${serverId}/shop/orders/${orderId}/review`, {
1467
- method: 'POST',
1468
- body: JSON.stringify(data),
1469
- })
1470
- }
1471
-
1472
- async replyToReview(serverId: string, reviewId: string, reply: string): Promise<ShadowReview> {
1473
- return this.request(`/api/servers/${serverId}/shop/reviews/${reviewId}/reply`, {
1474
- method: 'PUT',
1475
- body: JSON.stringify({ reply }),
1476
- })
1477
- }
1478
-
1479
- async getWallet(): Promise<ShadowWallet> {
1480
- return this.request('/api/wallet')
1481
- }
1482
-
1483
- async topUpWallet(amount: number): Promise<ShadowWallet> {
1484
- return this.request('/api/wallet/topup', {
1485
- method: 'POST',
1486
- body: JSON.stringify({ amount }),
1487
- })
1488
- }
1489
-
1490
- async getWalletTransactions(): Promise<ShadowTransaction[]> {
1491
- return this.request('/api/wallet/transactions')
1492
- }
1493
-
1494
- async getEntitlements(serverId: string): Promise<Record<string, unknown>[]> {
1495
- return this.request(`/api/servers/${serverId}/shop/entitlements`)
1496
- }
1497
-
1498
- // ── Task Center ───────────────────────────────────────────────────────
1499
-
1500
- async getTaskCenter(): Promise<{ tasks: ShadowTask[] }> {
1501
- return this.request('/api/tasks')
1502
- }
1503
-
1504
- async claimTask(taskKey: string): Promise<{ success: boolean; reward: number }> {
1505
- return this.request(`/api/tasks/${taskKey}/claim`, { method: 'POST' })
1506
- }
1507
-
1508
- async getReferralSummary(): Promise<{ count: number; rewards: number }> {
1509
- return this.request('/api/tasks/referral-summary')
1510
- }
1511
-
1512
- async getRewardHistory(): Promise<{
1513
- rewards: { amount: number; reason: string; createdAt: string }[]
1514
- }> {
1515
- return this.request('/api/tasks/rewards')
1516
- }
1517
-
1518
- // ── Server Apps ───────────────────────────────────────────────────────
1519
-
1520
- async listApps(
1521
- serverId: string,
1522
- params?: { status?: string; limit?: number; offset?: number },
1523
- ): Promise<{ apps: ShadowApp[]; total: number }> {
1524
- const qs = new URLSearchParams()
1525
- if (params?.status) qs.set('status', params.status)
1526
- if (params?.limit) qs.set('limit', String(params.limit))
1527
- if (params?.offset) qs.set('offset', String(params.offset))
1528
- return this.request(`/api/servers/${serverId}/apps?${qs}`)
1529
- }
1530
-
1531
- async getHomepageApp(serverId: string): Promise<ShadowApp | null> {
1532
- return this.request(`/api/servers/${serverId}/apps/homepage`)
1533
- }
1534
-
1535
- async getApp(serverId: string, appId: string): Promise<ShadowApp> {
1536
- return this.request(`/api/servers/${serverId}/apps/${appId}`)
1537
- }
1538
-
1539
- async createApp(
1540
- serverId: string,
1541
- data: { name: string; slug: string; type: string; url?: string },
1542
- ): Promise<ShadowApp> {
1543
- return this.request(`/api/servers/${serverId}/apps`, {
1544
- method: 'POST',
1545
- body: JSON.stringify(data),
1546
- })
1547
- }
1548
-
1549
- async updateApp(
1550
- serverId: string,
1551
- appId: string,
1552
- data: Partial<{ name: string; slug: string; type: string; url: string; status: string }>,
1553
- ): Promise<ShadowApp> {
1554
- return this.request(`/api/servers/${serverId}/apps/${appId}`, {
1555
- method: 'PATCH',
1556
- body: JSON.stringify(data),
1557
- })
1558
- }
1559
-
1560
- async deleteApp(serverId: string, appId: string): Promise<{ success: boolean }> {
1561
- return this.request(`/api/servers/${serverId}/apps/${appId}`, { method: 'DELETE' })
1562
- }
1563
-
1564
- async publishApp(serverId: string, data: { name: string; slug: string }): Promise<ShadowApp> {
1565
- return this.request(`/api/servers/${serverId}/apps/publish`, {
1566
- method: 'POST',
1567
- body: JSON.stringify(data),
1568
- })
1569
- }
1570
- }