@donkeylabs/server 0.6.4 → 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 +1 -1
- package/src/core/audit.ts +370 -0
- package/src/core/index.ts +37 -0
- package/src/core/job-adapter-kysely.ts +292 -0
- package/src/core/migrations/audit/001_create_audit_log_table.ts +56 -0
- package/src/core/migrations/jobs/001_create_jobs_table.ts +65 -0
- package/src/core/migrations/processes/001_create_processes_table.ts +55 -0
- package/src/core/migrations/workflows/001_create_workflow_instances_table.ts +55 -0
- package/src/core/process-adapter-kysely.ts +260 -0
- package/src/core/websocket.ts +352 -0
- package/src/core/workflow-adapter-kysely.ts +255 -0
- package/src/core.ts +98 -1
- package/src/harness.ts +31 -4
- package/src/server.ts +55 -1
package/package.json
CHANGED
|
@@ -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";
|