@donkeylabs/server 0.6.3 → 1.0.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@donkeylabs/server",
3
- "version": "0.6.3",
3
+ "version": "1.0.0",
4
4
  "type": "module",
5
5
  "description": "Type-safe plugin system for building RPC-style APIs with Bun",
6
6
  "main": "./src/index.ts",
@@ -0,0 +1,370 @@
1
+ /**
2
+ * Audit Core Service
3
+ *
4
+ * Provides audit logging for compliance and tracking.
5
+ * Stores all audit entries in the shared database using Kysely.
6
+ */
7
+
8
+ import type { Kysely } from "kysely";
9
+
10
+ // ============================================
11
+ // Types
12
+ // ============================================
13
+
14
+ export interface AuditEntry {
15
+ id: string;
16
+ timestamp: Date;
17
+ action: string;
18
+ actor: string;
19
+ resource: string;
20
+ resourceId?: string;
21
+ metadata?: Record<string, any>;
22
+ ip?: string;
23
+ requestId?: string;
24
+ }
25
+
26
+ export interface AuditQueryFilters {
27
+ /** Filter by action */
28
+ action?: string;
29
+ /** Filter by actor (user id, email, etc.) */
30
+ actor?: string;
31
+ /** Filter by resource type */
32
+ resource?: string;
33
+ /** Filter by resource ID */
34
+ resourceId?: string;
35
+ /** Filter by date range (start) */
36
+ startDate?: Date;
37
+ /** Filter by date range (end) */
38
+ endDate?: Date;
39
+ /** Maximum number of results (default: 100) */
40
+ limit?: number;
41
+ /** Offset for pagination */
42
+ offset?: number;
43
+ }
44
+
45
+ // ============================================
46
+ // Adapter Interface
47
+ // ============================================
48
+
49
+ export interface AuditAdapter {
50
+ /** Log a new audit entry */
51
+ log(entry: Omit<AuditEntry, "id" | "timestamp">): Promise<string>;
52
+ /** Query audit entries with filters */
53
+ query(filters: AuditQueryFilters): Promise<AuditEntry[]>;
54
+ /** Get audit entries for a specific resource */
55
+ getByResource(resource: string, resourceId: string): Promise<AuditEntry[]>;
56
+ /** Get audit entries for a specific actor */
57
+ getByActor(actor: string, limit?: number): Promise<AuditEntry[]>;
58
+ /** Delete old audit entries (for retention policy) */
59
+ deleteOlderThan(date: Date): Promise<number>;
60
+ /** Stop the adapter (cleanup timers) */
61
+ stop(): void;
62
+ }
63
+
64
+ // ============================================
65
+ // Service Interface
66
+ // ============================================
67
+
68
+ export interface Audit {
69
+ /** Log an audit entry */
70
+ log(entry: Omit<AuditEntry, "id" | "timestamp">): Promise<string>;
71
+ /** Query audit entries with filters */
72
+ query(filters: AuditQueryFilters): Promise<AuditEntry[]>;
73
+ /** Get audit entries for a specific resource */
74
+ getByResource(resource: string, resourceId: string): Promise<AuditEntry[]>;
75
+ /** Get audit entries for a specific actor */
76
+ getByActor(actor: string, limit?: number): Promise<AuditEntry[]>;
77
+ /** Stop the audit service */
78
+ stop(): void;
79
+ }
80
+
81
+ // ============================================
82
+ // Kysely Adapter
83
+ // ============================================
84
+
85
+ // Table type for Kysely
86
+ interface AuditTable {
87
+ id: string;
88
+ timestamp: string;
89
+ action: string;
90
+ actor: string;
91
+ resource: string;
92
+ resource_id: string | null;
93
+ metadata: string | null;
94
+ ip: string | null;
95
+ request_id: string | null;
96
+ }
97
+
98
+ interface Database {
99
+ __donkeylabs_audit__: AuditTable;
100
+ }
101
+
102
+ export interface KyselyAuditAdapterConfig {
103
+ /** Auto-cleanup audit entries older than N days (default: 90, 0 to disable) */
104
+ retentionDays?: number;
105
+ /** Cleanup interval in ms (default: 86400000 = 24 hours) */
106
+ cleanupInterval?: number;
107
+ }
108
+
109
+ export class KyselyAuditAdapter implements AuditAdapter {
110
+ private db: Kysely<Database>;
111
+ private cleanupTimer?: ReturnType<typeof setInterval>;
112
+ private retentionDays: number;
113
+
114
+ constructor(db: Kysely<any>, config: KyselyAuditAdapterConfig = {}) {
115
+ this.db = db as Kysely<Database>;
116
+ this.retentionDays = config.retentionDays ?? 90;
117
+
118
+ // Start cleanup timer
119
+ if (this.retentionDays > 0) {
120
+ const interval = config.cleanupInterval ?? 86400000; // 24 hours
121
+ this.cleanupTimer = setInterval(() => this.runCleanup(), interval);
122
+ // Run cleanup on startup
123
+ this.runCleanup();
124
+ }
125
+ }
126
+
127
+ async log(entry: Omit<AuditEntry, "id" | "timestamp">): Promise<string> {
128
+ const id = `audit_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
129
+ const timestamp = new Date();
130
+
131
+ await this.db
132
+ .insertInto("__donkeylabs_audit__")
133
+ .values({
134
+ id,
135
+ timestamp: timestamp.toISOString(),
136
+ action: entry.action,
137
+ actor: entry.actor,
138
+ resource: entry.resource,
139
+ resource_id: entry.resourceId ?? null,
140
+ metadata: entry.metadata ? JSON.stringify(entry.metadata) : null,
141
+ ip: entry.ip ?? null,
142
+ request_id: entry.requestId ?? null,
143
+ })
144
+ .execute();
145
+
146
+ return id;
147
+ }
148
+
149
+ async query(filters: AuditQueryFilters): Promise<AuditEntry[]> {
150
+ let query = this.db.selectFrom("__donkeylabs_audit__").selectAll();
151
+
152
+ if (filters.action) {
153
+ query = query.where("action", "=", filters.action);
154
+ }
155
+ if (filters.actor) {
156
+ query = query.where("actor", "=", filters.actor);
157
+ }
158
+ if (filters.resource) {
159
+ query = query.where("resource", "=", filters.resource);
160
+ }
161
+ if (filters.resourceId) {
162
+ query = query.where("resource_id", "=", filters.resourceId);
163
+ }
164
+ if (filters.startDate) {
165
+ query = query.where("timestamp", ">=", filters.startDate.toISOString());
166
+ }
167
+ if (filters.endDate) {
168
+ query = query.where("timestamp", "<=", filters.endDate.toISOString());
169
+ }
170
+
171
+ const limit = filters.limit ?? 100;
172
+ const offset = filters.offset ?? 0;
173
+
174
+ const rows = await query
175
+ .orderBy("timestamp", "desc")
176
+ .limit(limit)
177
+ .offset(offset)
178
+ .execute();
179
+
180
+ return rows.map((r) => this.rowToEntry(r));
181
+ }
182
+
183
+ async getByResource(resource: string, resourceId: string): Promise<AuditEntry[]> {
184
+ const rows = await this.db
185
+ .selectFrom("__donkeylabs_audit__")
186
+ .selectAll()
187
+ .where("resource", "=", resource)
188
+ .where("resource_id", "=", resourceId)
189
+ .orderBy("timestamp", "desc")
190
+ .execute();
191
+
192
+ return rows.map((r) => this.rowToEntry(r));
193
+ }
194
+
195
+ async getByActor(actor: string, limit: number = 100): Promise<AuditEntry[]> {
196
+ const rows = await this.db
197
+ .selectFrom("__donkeylabs_audit__")
198
+ .selectAll()
199
+ .where("actor", "=", actor)
200
+ .orderBy("timestamp", "desc")
201
+ .limit(limit)
202
+ .execute();
203
+
204
+ return rows.map((r) => this.rowToEntry(r));
205
+ }
206
+
207
+ async deleteOlderThan(date: Date): Promise<number> {
208
+ const result = await this.db
209
+ .deleteFrom("__donkeylabs_audit__")
210
+ .where("timestamp", "<", date.toISOString())
211
+ .execute();
212
+
213
+ return Number(result[0]?.numDeletedRows ?? 0);
214
+ }
215
+
216
+ private rowToEntry(row: AuditTable): AuditEntry {
217
+ return {
218
+ id: row.id,
219
+ timestamp: new Date(row.timestamp),
220
+ action: row.action,
221
+ actor: row.actor,
222
+ resource: row.resource,
223
+ resourceId: row.resource_id ?? undefined,
224
+ metadata: row.metadata ? JSON.parse(row.metadata) : undefined,
225
+ ip: row.ip ?? undefined,
226
+ requestId: row.request_id ?? undefined,
227
+ };
228
+ }
229
+
230
+ private async runCleanup(): Promise<void> {
231
+ if (this.retentionDays <= 0) return;
232
+
233
+ try {
234
+ const cutoff = new Date();
235
+ cutoff.setDate(cutoff.getDate() - this.retentionDays);
236
+
237
+ const numDeleted = await this.deleteOlderThan(cutoff);
238
+ if (numDeleted > 0) {
239
+ console.log(`[Audit] Cleaned up ${numDeleted} old audit entries`);
240
+ }
241
+ } catch (err) {
242
+ console.error("[Audit] Cleanup error:", err);
243
+ }
244
+ }
245
+
246
+ stop(): void {
247
+ if (this.cleanupTimer) {
248
+ clearInterval(this.cleanupTimer);
249
+ this.cleanupTimer = undefined;
250
+ }
251
+ }
252
+ }
253
+
254
+ // ============================================
255
+ // In-Memory Adapter (for testing)
256
+ // ============================================
257
+
258
+ export class MemoryAuditAdapter implements AuditAdapter {
259
+ private entries = new Map<string, AuditEntry>();
260
+ private counter = 0;
261
+
262
+ async log(entry: Omit<AuditEntry, "id" | "timestamp">): Promise<string> {
263
+ const id = `audit_${++this.counter}_${Date.now()}`;
264
+ const fullEntry: AuditEntry = {
265
+ ...entry,
266
+ id,
267
+ timestamp: new Date(),
268
+ };
269
+ this.entries.set(id, fullEntry);
270
+ return id;
271
+ }
272
+
273
+ async query(filters: AuditQueryFilters): Promise<AuditEntry[]> {
274
+ let results = Array.from(this.entries.values());
275
+
276
+ if (filters.action) {
277
+ results = results.filter((e) => e.action === filters.action);
278
+ }
279
+ if (filters.actor) {
280
+ results = results.filter((e) => e.actor === filters.actor);
281
+ }
282
+ if (filters.resource) {
283
+ results = results.filter((e) => e.resource === filters.resource);
284
+ }
285
+ if (filters.resourceId) {
286
+ results = results.filter((e) => e.resourceId === filters.resourceId);
287
+ }
288
+ if (filters.startDate) {
289
+ results = results.filter((e) => e.timestamp >= filters.startDate!);
290
+ }
291
+ if (filters.endDate) {
292
+ results = results.filter((e) => e.timestamp <= filters.endDate!);
293
+ }
294
+
295
+ // Sort by timestamp descending
296
+ results.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
297
+
298
+ // Apply pagination
299
+ const offset = filters.offset ?? 0;
300
+ const limit = filters.limit ?? 100;
301
+ return results.slice(offset, offset + limit);
302
+ }
303
+
304
+ async getByResource(resource: string, resourceId: string): Promise<AuditEntry[]> {
305
+ return this.query({ resource, resourceId });
306
+ }
307
+
308
+ async getByActor(actor: string, limit: number = 100): Promise<AuditEntry[]> {
309
+ return this.query({ actor, limit });
310
+ }
311
+
312
+ async deleteOlderThan(date: Date): Promise<number> {
313
+ let deleted = 0;
314
+ for (const [id, entry] of this.entries) {
315
+ if (entry.timestamp < date) {
316
+ this.entries.delete(id);
317
+ deleted++;
318
+ }
319
+ }
320
+ return deleted;
321
+ }
322
+
323
+ stop(): void {
324
+ // No cleanup needed for in-memory adapter
325
+ }
326
+ }
327
+
328
+ // ============================================
329
+ // Service Implementation
330
+ // ============================================
331
+
332
+ export interface AuditConfig {
333
+ adapter?: AuditAdapter;
334
+ }
335
+
336
+ class AuditImpl implements Audit {
337
+ private adapter: AuditAdapter;
338
+
339
+ constructor(config: AuditConfig = {}) {
340
+ this.adapter = config.adapter ?? new MemoryAuditAdapter();
341
+ }
342
+
343
+ async log(entry: Omit<AuditEntry, "id" | "timestamp">): Promise<string> {
344
+ return this.adapter.log(entry);
345
+ }
346
+
347
+ async query(filters: AuditQueryFilters): Promise<AuditEntry[]> {
348
+ return this.adapter.query(filters);
349
+ }
350
+
351
+ async getByResource(resource: string, resourceId: string): Promise<AuditEntry[]> {
352
+ return this.adapter.getByResource(resource, resourceId);
353
+ }
354
+
355
+ async getByActor(actor: string, limit?: number): Promise<AuditEntry[]> {
356
+ return this.adapter.getByActor(actor, limit);
357
+ }
358
+
359
+ stop(): void {
360
+ this.adapter.stop();
361
+ }
362
+ }
363
+
364
+ // ============================================
365
+ // Factory Function
366
+ // ============================================
367
+
368
+ export function createAudit(config?: AuditConfig): Audit {
369
+ return new AuditImpl(config);
370
+ }
package/src/core/index.ts CHANGED
@@ -52,6 +52,11 @@ export {
52
52
  type SqliteJobAdapterConfig,
53
53
  } from "./job-adapter-sqlite";
54
54
 
55
+ export {
56
+ KyselyJobAdapter,
57
+ type KyselyJobAdapterConfig,
58
+ } from "./job-adapter-kysely";
59
+
55
60
  export {
56
61
  type ExternalJobConfig,
57
62
  type ExternalJob,
@@ -164,9 +169,41 @@ export {
164
169
  type ProcessAdapter,
165
170
  } from "./process-adapter-sqlite";
166
171
 
172
+ export {
173
+ KyselyProcessAdapter,
174
+ type KyselyProcessAdapterConfig,
175
+ } from "./process-adapter-kysely";
176
+
167
177
  export {
168
178
  type ProcessSocketServer,
169
179
  type ProcessMessage,
170
180
  type ProcessSocketConfig,
171
181
  createProcessSocketServer,
172
182
  } from "./process-socket";
183
+
184
+ export {
185
+ KyselyWorkflowAdapter,
186
+ type KyselyWorkflowAdapterConfig,
187
+ } from "./workflow-adapter-kysely";
188
+
189
+ export {
190
+ type Audit,
191
+ type AuditEntry,
192
+ type AuditQueryFilters,
193
+ type AuditAdapter,
194
+ type AuditConfig,
195
+ type KyselyAuditAdapterConfig,
196
+ KyselyAuditAdapter,
197
+ MemoryAuditAdapter,
198
+ createAudit,
199
+ } from "./audit";
200
+
201
+ export {
202
+ type WebSocketService,
203
+ type WebSocketClient,
204
+ type WebSocketData,
205
+ type WebSocketMessage,
206
+ type WebSocketMessageHandler,
207
+ type WebSocketConfig,
208
+ createWebSocket,
209
+ } from "./websocket";