@donkeylabs/server 2.0.7 → 2.0.11

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.
@@ -0,0 +1,543 @@
1
+ // Core Storage Service
2
+ // File storage abstraction supporting multiple providers: S3-compatible, local filesystem, and memory
3
+
4
+ // =============================================================================
5
+ // TYPES
6
+ // =============================================================================
7
+
8
+ /** File visibility for access control */
9
+ export type StorageVisibility = "public" | "private";
10
+
11
+ /** Metadata about a stored file */
12
+ export interface StorageFile {
13
+ /** The file key/path */
14
+ key: string;
15
+ /** File size in bytes */
16
+ size: number;
17
+ /** MIME type of the file */
18
+ contentType?: string;
19
+ /** Last modified date */
20
+ lastModified: Date;
21
+ /** ETag/checksum if available */
22
+ etag?: string;
23
+ /** Custom metadata */
24
+ metadata?: Record<string, string>;
25
+ /** File visibility */
26
+ visibility?: StorageVisibility;
27
+ }
28
+
29
+ /** Options for uploading a file */
30
+ export interface UploadOptions {
31
+ /** The key/path to store the file at */
32
+ key: string;
33
+ /** The file content - Buffer, Uint8Array, string, Blob, or ReadableStream */
34
+ body: Buffer | Uint8Array | string | Blob | ReadableStream<Uint8Array>;
35
+ /** MIME type of the file */
36
+ contentType?: string;
37
+ /** File visibility (public or private) */
38
+ visibility?: StorageVisibility;
39
+ /** Custom metadata to store with the file */
40
+ metadata?: Record<string, string>;
41
+ /** Content disposition header (e.g., 'attachment; filename="file.pdf"') */
42
+ contentDisposition?: string;
43
+ /** Cache control header */
44
+ cacheControl?: string;
45
+ }
46
+
47
+ /** Result of an upload operation */
48
+ export interface UploadResult {
49
+ /** The key/path where the file was stored */
50
+ key: string;
51
+ /** File size in bytes */
52
+ size: number;
53
+ /** ETag/checksum if available */
54
+ etag?: string;
55
+ /** Public URL if the file is public */
56
+ url?: string;
57
+ }
58
+
59
+ /** Result of a download operation */
60
+ export interface DownloadResult {
61
+ /** The file content as a readable stream */
62
+ body: ReadableStream<Uint8Array>;
63
+ /** File size in bytes */
64
+ size: number;
65
+ /** MIME type of the file */
66
+ contentType?: string;
67
+ /** Last modified date */
68
+ lastModified: Date;
69
+ /** ETag/checksum if available */
70
+ etag?: string;
71
+ /** Custom metadata */
72
+ metadata?: Record<string, string>;
73
+ }
74
+
75
+ /** Options for listing files */
76
+ export interface ListOptions {
77
+ /** Prefix to filter files by (e.g., "users/123/") */
78
+ prefix?: string;
79
+ /** Maximum number of files to return */
80
+ limit?: number;
81
+ /** Cursor for pagination (from previous ListResult) */
82
+ cursor?: string;
83
+ /** Delimiter for hierarchical listing (usually "/") */
84
+ delimiter?: string;
85
+ }
86
+
87
+ /** Result of a list operation */
88
+ export interface ListResult {
89
+ /** Files matching the query */
90
+ files: StorageFile[];
91
+ /** Common prefixes (directories) when using delimiter */
92
+ prefixes: string[];
93
+ /** Cursor for next page, null if no more results */
94
+ cursor: string | null;
95
+ /** Whether there are more results */
96
+ hasMore: boolean;
97
+ }
98
+
99
+ /** Options for getting a file URL */
100
+ export interface GetUrlOptions {
101
+ /** URL expiration time in seconds (for signed URLs) */
102
+ expiresIn?: number;
103
+ /** Force download with specific filename */
104
+ download?: string | boolean;
105
+ /** Content type override */
106
+ contentType?: string;
107
+ }
108
+
109
+ /** Options for copying a file */
110
+ export interface CopyOptions {
111
+ /** Source file key */
112
+ source: string;
113
+ /** Destination file key */
114
+ destination: string;
115
+ /** Override metadata (optional) */
116
+ metadata?: Record<string, string>;
117
+ /** Override visibility (optional) */
118
+ visibility?: StorageVisibility;
119
+ }
120
+
121
+ // =============================================================================
122
+ // PROVIDER CONFIGS
123
+ // =============================================================================
124
+
125
+ /** S3-compatible provider configuration */
126
+ export interface S3ProviderConfig {
127
+ provider: "s3";
128
+ /** S3 bucket name */
129
+ bucket: string;
130
+ /** AWS region */
131
+ region: string;
132
+ /** AWS access key ID */
133
+ accessKeyId: string;
134
+ /** AWS secret access key */
135
+ secretAccessKey: string;
136
+ /** Custom endpoint URL (for R2, MinIO, DigitalOcean Spaces, etc.) */
137
+ endpoint?: string;
138
+ /** Public URL base for public files (e.g., CDN URL) */
139
+ publicUrl?: string;
140
+ /** Force path-style URLs (required for MinIO, optional for others) */
141
+ forcePathStyle?: boolean;
142
+ }
143
+
144
+ /** Local filesystem provider configuration */
145
+ export interface LocalProviderConfig {
146
+ provider: "local";
147
+ /** Base directory for file storage */
148
+ directory: string;
149
+ /** Base URL for serving files (e.g., "/storage" or "https://cdn.example.com") */
150
+ baseUrl?: string;
151
+ }
152
+
153
+ /** Memory provider configuration (for testing) */
154
+ export interface MemoryProviderConfig {
155
+ provider: "memory";
156
+ }
157
+
158
+ /** Union of all provider configurations */
159
+ export type StorageConfig = S3ProviderConfig | LocalProviderConfig | MemoryProviderConfig;
160
+
161
+ // =============================================================================
162
+ // ADAPTER INTERFACE
163
+ // =============================================================================
164
+
165
+ /** Storage adapter interface - implement this for custom providers */
166
+ export interface StorageAdapter {
167
+ /** Upload a file */
168
+ upload(options: UploadOptions): Promise<UploadResult>;
169
+ /** Download a file (returns null if not found) */
170
+ download(key: string): Promise<DownloadResult | null>;
171
+ /** Delete a file (returns true if deleted, false if not found) */
172
+ delete(key: string): Promise<boolean>;
173
+ /** Delete multiple files */
174
+ deleteMany(keys: string[]): Promise<{ deleted: string[]; errors: string[] }>;
175
+ /** List files with optional filtering and pagination */
176
+ list(options?: ListOptions): Promise<ListResult>;
177
+ /** Get file metadata without downloading (returns null if not found) */
178
+ head(key: string): Promise<StorageFile | null>;
179
+ /** Check if a file exists */
180
+ exists(key: string): Promise<boolean>;
181
+ /** Get a URL for accessing the file */
182
+ getUrl(key: string, options?: GetUrlOptions): Promise<string>;
183
+ /** Copy a file to a new location */
184
+ copy(options: CopyOptions): Promise<UploadResult>;
185
+ /** Cleanup resources (called on shutdown) */
186
+ stop(): void;
187
+ }
188
+
189
+ // =============================================================================
190
+ // PUBLIC INTERFACE
191
+ // =============================================================================
192
+
193
+ /** Storage service public interface */
194
+ export interface Storage {
195
+ /** Upload a file */
196
+ upload(options: UploadOptions): Promise<UploadResult>;
197
+ /** Download a file (returns null if not found) */
198
+ download(key: string): Promise<DownloadResult | null>;
199
+ /** Delete a file (returns true if deleted, false if not found) */
200
+ delete(key: string): Promise<boolean>;
201
+ /** Delete multiple files */
202
+ deleteMany(keys: string[]): Promise<{ deleted: string[]; errors: string[] }>;
203
+ /** List files with optional filtering and pagination */
204
+ list(options?: ListOptions): Promise<ListResult>;
205
+ /** Get file metadata without downloading (returns null if not found) */
206
+ head(key: string): Promise<StorageFile | null>;
207
+ /** Check if a file exists */
208
+ exists(key: string): Promise<boolean>;
209
+ /** Get a URL for accessing the file */
210
+ getUrl(key: string, options?: GetUrlOptions): Promise<string>;
211
+ /** Copy a file to a new location */
212
+ copy(options: CopyOptions): Promise<UploadResult>;
213
+ /** Move a file to a new location (copy + delete) */
214
+ move(source: string, destination: string): Promise<UploadResult>;
215
+ /** Cleanup resources (called on shutdown) */
216
+ stop(): void;
217
+ }
218
+
219
+ // =============================================================================
220
+ // MEMORY ADAPTER (Testing/Default)
221
+ // =============================================================================
222
+
223
+ interface MemoryFile {
224
+ body: Uint8Array;
225
+ contentType?: string;
226
+ metadata?: Record<string, string>;
227
+ visibility?: StorageVisibility;
228
+ contentDisposition?: string;
229
+ cacheControl?: string;
230
+ lastModified: Date;
231
+ }
232
+
233
+ /** In-memory storage adapter for testing and development */
234
+ export class MemoryStorageAdapter implements StorageAdapter {
235
+ private files = new Map<string, MemoryFile>();
236
+
237
+ async upload(options: UploadOptions): Promise<UploadResult> {
238
+ const body = await this.toUint8Array(options.body);
239
+
240
+ this.files.set(options.key, {
241
+ body,
242
+ contentType: options.contentType,
243
+ metadata: options.metadata,
244
+ visibility: options.visibility,
245
+ contentDisposition: options.contentDisposition,
246
+ cacheControl: options.cacheControl,
247
+ lastModified: new Date(),
248
+ });
249
+
250
+ return {
251
+ key: options.key,
252
+ size: body.byteLength,
253
+ url: options.visibility === "public" ? `memory://${options.key}` : undefined,
254
+ };
255
+ }
256
+
257
+ async download(key: string): Promise<DownloadResult | null> {
258
+ const file = this.files.get(key);
259
+ if (!file) return null;
260
+
261
+ return {
262
+ body: new ReadableStream({
263
+ start(controller) {
264
+ controller.enqueue(file.body);
265
+ controller.close();
266
+ },
267
+ }),
268
+ size: file.body.byteLength,
269
+ contentType: file.contentType,
270
+ lastModified: file.lastModified,
271
+ metadata: file.metadata,
272
+ };
273
+ }
274
+
275
+ async delete(key: string): Promise<boolean> {
276
+ return this.files.delete(key);
277
+ }
278
+
279
+ async deleteMany(keys: string[]): Promise<{ deleted: string[]; errors: string[] }> {
280
+ const deleted: string[] = [];
281
+ const errors: string[] = [];
282
+
283
+ for (const key of keys) {
284
+ if (this.files.delete(key)) {
285
+ deleted.push(key);
286
+ } else {
287
+ errors.push(key);
288
+ }
289
+ }
290
+
291
+ return { deleted, errors };
292
+ }
293
+
294
+ async list(options: ListOptions = {}): Promise<ListResult> {
295
+ const { prefix = "", limit = 1000, cursor, delimiter } = options;
296
+
297
+ let keys = Array.from(this.files.keys());
298
+
299
+ // Filter by prefix
300
+ if (prefix) {
301
+ keys = keys.filter((key) => key.startsWith(prefix));
302
+ }
303
+
304
+ // Sort for consistent pagination
305
+ keys.sort();
306
+
307
+ // Apply cursor (simple offset-based)
308
+ if (cursor) {
309
+ const cursorIndex = keys.findIndex((key) => key > cursor);
310
+ if (cursorIndex === -1) {
311
+ return { files: [], prefixes: [], cursor: null, hasMore: false };
312
+ }
313
+ keys = keys.slice(cursorIndex);
314
+ }
315
+
316
+ // Handle delimiter for hierarchical listing
317
+ const prefixes: string[] = [];
318
+ if (delimiter) {
319
+ const prefixSet = new Set<string>();
320
+ const fileKeys: string[] = [];
321
+
322
+ for (const key of keys) {
323
+ const relativePath = prefix ? key.slice(prefix.length) : key;
324
+ const delimiterIndex = relativePath.indexOf(delimiter);
325
+
326
+ if (delimiterIndex !== -1) {
327
+ // This is a "directory" - add to prefixes
328
+ const commonPrefix = prefix + relativePath.slice(0, delimiterIndex + 1);
329
+ prefixSet.add(commonPrefix);
330
+ } else {
331
+ // This is a file at this level
332
+ fileKeys.push(key);
333
+ }
334
+ }
335
+
336
+ keys = fileKeys;
337
+ prefixes.push(...Array.from(prefixSet).sort());
338
+ }
339
+
340
+ // Apply limit
341
+ const hasMore = keys.length > limit;
342
+ const resultKeys = keys.slice(0, limit);
343
+ const nextCursor = hasMore ? resultKeys[resultKeys.length - 1] : null;
344
+
345
+ // Build file list
346
+ const files: StorageFile[] = resultKeys.map((key) => {
347
+ const file = this.files.get(key)!;
348
+ return {
349
+ key,
350
+ size: file.body.byteLength,
351
+ contentType: file.contentType,
352
+ lastModified: file.lastModified,
353
+ metadata: file.metadata,
354
+ visibility: file.visibility,
355
+ };
356
+ });
357
+
358
+ return {
359
+ files,
360
+ prefixes,
361
+ cursor: nextCursor,
362
+ hasMore,
363
+ };
364
+ }
365
+
366
+ async head(key: string): Promise<StorageFile | null> {
367
+ const file = this.files.get(key);
368
+ if (!file) return null;
369
+
370
+ return {
371
+ key,
372
+ size: file.body.byteLength,
373
+ contentType: file.contentType,
374
+ lastModified: file.lastModified,
375
+ metadata: file.metadata,
376
+ visibility: file.visibility,
377
+ };
378
+ }
379
+
380
+ async exists(key: string): Promise<boolean> {
381
+ return this.files.has(key);
382
+ }
383
+
384
+ async getUrl(key: string, options: GetUrlOptions = {}): Promise<string> {
385
+ const file = this.files.get(key);
386
+ if (!file) {
387
+ throw new Error(`File not found: ${key}`);
388
+ }
389
+
390
+ // For memory adapter, just return a memory:// URL
391
+ let url = `memory://${key}`;
392
+
393
+ if (options.download) {
394
+ const filename = typeof options.download === "string" ? options.download : key.split("/").pop();
395
+ url += `?download=${encodeURIComponent(filename || "file")}`;
396
+ }
397
+
398
+ return url;
399
+ }
400
+
401
+ async copy(options: CopyOptions): Promise<UploadResult> {
402
+ const sourceFile = this.files.get(options.source);
403
+ if (!sourceFile) {
404
+ throw new Error(`Source file not found: ${options.source}`);
405
+ }
406
+
407
+ const newFile: MemoryFile = {
408
+ ...sourceFile,
409
+ metadata: options.metadata ?? sourceFile.metadata,
410
+ visibility: options.visibility ?? sourceFile.visibility,
411
+ lastModified: new Date(),
412
+ };
413
+
414
+ this.files.set(options.destination, newFile);
415
+
416
+ return {
417
+ key: options.destination,
418
+ size: newFile.body.byteLength,
419
+ url: newFile.visibility === "public" ? `memory://${options.destination}` : undefined,
420
+ };
421
+ }
422
+
423
+ stop(): void {
424
+ // Nothing to clean up for memory adapter
425
+ }
426
+
427
+ /** Helper to convert various body types to Uint8Array */
428
+ private async toUint8Array(
429
+ body: Buffer | Uint8Array | string | Blob | ReadableStream<Uint8Array>
430
+ ): Promise<Uint8Array> {
431
+ if (body instanceof Uint8Array) {
432
+ return body;
433
+ }
434
+ if (typeof body === "string") {
435
+ return new TextEncoder().encode(body);
436
+ }
437
+ if (body instanceof Blob) {
438
+ const buffer = await body.arrayBuffer();
439
+ return new Uint8Array(buffer);
440
+ }
441
+ if (body instanceof ReadableStream) {
442
+ const reader = body.getReader();
443
+ const chunks: Uint8Array[] = [];
444
+ while (true) {
445
+ const { done, value } = await reader.read();
446
+ if (done) break;
447
+ chunks.push(value);
448
+ }
449
+ const totalLength = chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0);
450
+ const result = new Uint8Array(totalLength);
451
+ let offset = 0;
452
+ for (const chunk of chunks) {
453
+ result.set(chunk, offset);
454
+ offset += chunk.byteLength;
455
+ }
456
+ return result;
457
+ }
458
+ // Buffer (Node.js)
459
+ return new Uint8Array(body);
460
+ }
461
+ }
462
+
463
+ // =============================================================================
464
+ // STORAGE IMPLEMENTATION
465
+ // =============================================================================
466
+
467
+ class StorageImpl implements Storage {
468
+ private adapter: StorageAdapter;
469
+
470
+ constructor(adapter: StorageAdapter) {
471
+ this.adapter = adapter;
472
+ }
473
+
474
+ async upload(options: UploadOptions): Promise<UploadResult> {
475
+ return this.adapter.upload(options);
476
+ }
477
+
478
+ async download(key: string): Promise<DownloadResult | null> {
479
+ return this.adapter.download(key);
480
+ }
481
+
482
+ async delete(key: string): Promise<boolean> {
483
+ return this.adapter.delete(key);
484
+ }
485
+
486
+ async deleteMany(keys: string[]): Promise<{ deleted: string[]; errors: string[] }> {
487
+ return this.adapter.deleteMany(keys);
488
+ }
489
+
490
+ async list(options?: ListOptions): Promise<ListResult> {
491
+ return this.adapter.list(options);
492
+ }
493
+
494
+ async head(key: string): Promise<StorageFile | null> {
495
+ return this.adapter.head(key);
496
+ }
497
+
498
+ async exists(key: string): Promise<boolean> {
499
+ return this.adapter.exists(key);
500
+ }
501
+
502
+ async getUrl(key: string, options?: GetUrlOptions): Promise<string> {
503
+ return this.adapter.getUrl(key, options);
504
+ }
505
+
506
+ async copy(options: CopyOptions): Promise<UploadResult> {
507
+ return this.adapter.copy(options);
508
+ }
509
+
510
+ async move(source: string, destination: string): Promise<UploadResult> {
511
+ const result = await this.adapter.copy({ source, destination });
512
+ await this.adapter.delete(source);
513
+ return result;
514
+ }
515
+
516
+ stop(): void {
517
+ this.adapter.stop();
518
+ }
519
+ }
520
+
521
+ // =============================================================================
522
+ // FACTORY
523
+ // =============================================================================
524
+
525
+ import { LocalStorageAdapter } from "./storage-adapter-local";
526
+ import { S3StorageAdapter } from "./storage-adapter-s3";
527
+
528
+ /** Create a storage service with the specified configuration */
529
+ export function createStorage(config?: StorageConfig): Storage {
530
+ let adapter: StorageAdapter;
531
+
532
+ if (!config || config.provider === "memory") {
533
+ adapter = new MemoryStorageAdapter();
534
+ } else if (config.provider === "local") {
535
+ adapter = new LocalStorageAdapter(config);
536
+ } else if (config.provider === "s3") {
537
+ adapter = new S3StorageAdapter(config);
538
+ } else {
539
+ throw new Error(`Unknown storage provider: ${(config as any).provider}`);
540
+ }
541
+
542
+ return new StorageImpl(adapter);
543
+ }
@@ -53,8 +53,10 @@ export interface WebSocketService {
53
53
  unsubscribe(clientId: string, channel: string): boolean;
54
54
  /** Register a message handler */
55
55
  onMessage(handler: WebSocketMessageHandler): void;
56
- /** Get all clients in a channel */
57
- getClients(channel?: string): string[];
56
+ /** Get all client IDs in a channel (or all if no channel) */
57
+ getClientIds(channel?: string): string[];
58
+ /** Get all clients with metadata (for admin dashboard) */
59
+ getClients(): Array<{ id: string; connectedAt: Date; channels: string[] }>;
58
60
  /** Get client count */
59
61
  getClientCount(channel?: string): number;
60
62
  /** Check if a client is connected */
@@ -195,7 +197,7 @@ class WebSocketServiceImpl implements WebSocketService {
195
197
  this.messageHandlers.push(handler);
196
198
  }
197
199
 
198
- getClients(channel?: string): string[] {
200
+ getClientIds(channel?: string): string[] {
199
201
  if (channel) {
200
202
  const channelClients = this.channels.get(channel);
201
203
  return channelClients ? Array.from(channelClients) : [];
@@ -203,6 +205,14 @@ class WebSocketServiceImpl implements WebSocketService {
203
205
  return Array.from(this.clients.keys());
204
206
  }
205
207
 
208
+ getClients(): Array<{ id: string; connectedAt: Date; channels: string[] }> {
209
+ return Array.from(this.clients.values()).map((client) => ({
210
+ id: client.id,
211
+ connectedAt: client.connectedAt,
212
+ channels: Array.from(client.channels),
213
+ }));
214
+ }
215
+
206
216
  getClientCount(channel?: string): number {
207
217
  if (channel) {
208
218
  return this.channels.get(channel)?.size ?? 0;
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import type { Kysely } from "kysely";
9
- import type { WorkflowAdapter, WorkflowInstance, WorkflowStatus, StepResult } from "./workflows";
9
+ import type { WorkflowAdapter, WorkflowInstance, WorkflowStatus, StepResult, GetAllWorkflowsOptions } from "./workflows";
10
10
 
11
11
  export interface KyselyWorkflowAdapterConfig {
12
12
  /** Auto-cleanup completed workflows older than N days (default: 30, 0 to disable) */
@@ -176,6 +176,27 @@ export class KyselyWorkflowAdapter implements WorkflowAdapter {
176
176
  return rows.map((r) => this.rowToInstance(r));
177
177
  }
178
178
 
179
+ async getAllInstances(options: GetAllWorkflowsOptions = {}): Promise<WorkflowInstance[]> {
180
+ const { status, workflowName, limit = 100, offset = 0 } = options;
181
+
182
+ let query = this.db.selectFrom("__donkeylabs_workflow_instances__").selectAll();
183
+
184
+ if (status) {
185
+ query = query.where("status", "=", status);
186
+ }
187
+ if (workflowName) {
188
+ query = query.where("workflow_name", "=", workflowName);
189
+ }
190
+
191
+ const rows = await query
192
+ .orderBy("created_at", "desc")
193
+ .limit(limit)
194
+ .offset(offset)
195
+ .execute();
196
+
197
+ return rows.map((r) => this.rowToInstance(r));
198
+ }
199
+
179
200
  private rowToInstance(row: WorkflowInstancesTable): WorkflowInstance {
180
201
  // Parse step results with proper Date handling
181
202
  const rawStepResults = JSON.parse(row.step_results);
@@ -201,6 +201,18 @@ export interface WorkflowContext {
201
201
  // Workflow Adapter (Persistence)
202
202
  // ============================================
203
203
 
204
+ /** Options for listing all workflow instances */
205
+ export interface GetAllWorkflowsOptions {
206
+ /** Filter by status */
207
+ status?: WorkflowStatus;
208
+ /** Filter by workflow name */
209
+ workflowName?: string;
210
+ /** Max number of instances to return (default: 100) */
211
+ limit?: number;
212
+ /** Skip first N instances (for pagination) */
213
+ offset?: number;
214
+ }
215
+
204
216
  export interface WorkflowAdapter {
205
217
  createInstance(instance: Omit<WorkflowInstance, "id">): Promise<WorkflowInstance>;
206
218
  getInstance(instanceId: string): Promise<WorkflowInstance | null>;
@@ -208,6 +220,8 @@ export interface WorkflowAdapter {
208
220
  deleteInstance(instanceId: string): Promise<boolean>;
209
221
  getInstancesByWorkflow(workflowName: string, status?: WorkflowStatus): Promise<WorkflowInstance[]>;
210
222
  getRunningInstances(): Promise<WorkflowInstance[]>;
223
+ /** Get all workflow instances with optional filtering (for admin dashboard) */
224
+ getAllInstances(options?: GetAllWorkflowsOptions): Promise<WorkflowInstance[]>;
211
225
  }
212
226
 
213
227
  // In-memory adapter
@@ -261,6 +275,23 @@ export class MemoryWorkflowAdapter implements WorkflowAdapter {
261
275
  }
262
276
  return results;
263
277
  }
278
+
279
+ async getAllInstances(options: GetAllWorkflowsOptions = {}): Promise<WorkflowInstance[]> {
280
+ const { status, workflowName, limit = 100, offset = 0 } = options;
281
+ const results: WorkflowInstance[] = [];
282
+
283
+ for (const instance of this.instances.values()) {
284
+ if (status && instance.status !== status) continue;
285
+ if (workflowName && instance.workflowName !== workflowName) continue;
286
+ results.push(instance);
287
+ }
288
+
289
+ // Sort by createdAt descending (newest first)
290
+ results.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
291
+
292
+ // Apply pagination
293
+ return results.slice(offset, offset + limit);
294
+ }
264
295
  }
265
296
 
266
297
  // ============================================
@@ -519,6 +550,8 @@ export interface Workflows {
519
550
  cancel(instanceId: string): Promise<boolean>;
520
551
  /** Get all instances of a workflow */
521
552
  getInstances(workflowName: string, status?: WorkflowStatus): Promise<WorkflowInstance[]>;
553
+ /** Get all workflow instances with optional filtering (for admin dashboard) */
554
+ getAllInstances(options?: GetAllWorkflowsOptions): Promise<WorkflowInstance[]>;
522
555
  /** Resume workflows after server restart */
523
556
  resume(): Promise<void>;
524
557
  /** Stop the workflow service */
@@ -624,6 +657,10 @@ class WorkflowsImpl implements Workflows {
624
657
  return this.adapter.getInstancesByWorkflow(workflowName, status);
625
658
  }
626
659
 
660
+ async getAllInstances(options?: GetAllWorkflowsOptions): Promise<WorkflowInstance[]> {
661
+ return this.adapter.getAllInstances(options);
662
+ }
663
+
627
664
  async resume(): Promise<void> {
628
665
  const running = await this.adapter.getRunningInstances();
629
666