fission-worker 0.2.2 → 0.3.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.
Files changed (29) hide show
  1. package/dist/docker.js +4 -5
  2. package/dist/docker.js.map +1 -1
  3. package/package.json +4 -3
  4. package/runner/common/decorators/current-user.decorator.js +10 -0
  5. package/runner/common/decorators/public.decorator.js +7 -0
  6. package/runner/common/decorators/roles.decorator.js +7 -0
  7. package/runner/common/services/activity-log.service.js +48 -0
  8. package/runner/common/services/event-bus.service.js +38 -0
  9. package/runner/modules/github/github.controller.js +155 -0
  10. package/runner/modules/github/github.module.js +22 -0
  11. package/runner/modules/github/github.service.js +104 -0
  12. package/runner/modules/pipeline/data/api-data.service.js +140 -0
  13. package/runner/modules/pipeline/data/pipeline-data.interface.js +2 -0
  14. package/runner/modules/pipeline/data/prisma-data.service.js +149 -0
  15. package/runner/modules/pipeline/pipeline-cto.service.js +129 -0
  16. package/runner/modules/pipeline/pipeline-helpers.service.js +318 -0
  17. package/runner/modules/pipeline/pipeline-orchestrator.js +399 -0
  18. package/runner/modules/pipeline/pipeline-queue.service.js +121 -0
  19. package/runner/modules/pipeline/pipeline-techlead.service.js +127 -0
  20. package/runner/modules/pipeline/pipeline-worker.service.js +343 -0
  21. package/runner/modules/pipeline/pipeline.controller.js +310 -0
  22. package/runner/modules/pipeline/pipeline.module.js +51 -0
  23. package/runner/modules/pipeline/pipeline.service.js +706 -0
  24. package/runner/modules/worker-api/worker-api.controller.js +497 -0
  25. package/runner/modules/worker-api/worker-api.guard.js +41 -0
  26. package/runner/modules/worker-api/worker-api.module.js +25 -0
  27. package/runner/modules/worker-api/worker-dispatch.service.js +87 -0
  28. package/runner/pipeline-runner/index.js +108 -0
  29. package/runner/prisma/prisma.service.js +23 -0
@@ -0,0 +1,497 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
14
+ var WorkerApiController_1;
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.WorkerApiController = void 0;
17
+ const common_1 = require("@nestjs/common");
18
+ const throttler_1 = require("@nestjs/throttler");
19
+ const crypto_1 = require("crypto");
20
+ const prisma_service_1 = require("../../prisma/prisma.service");
21
+ const event_bus_service_1 = require("../../common/services/event-bus.service");
22
+ const activity_log_service_1 = require("../../common/services/activity-log.service");
23
+ const public_decorator_1 = require("../../common/decorators/public.decorator");
24
+ const worker_api_guard_1 = require("./worker-api.guard");
25
+ const github_service_1 = require("../github/github.service");
26
+ let WorkerApiController = WorkerApiController_1 = class WorkerApiController {
27
+ prisma;
28
+ eventBus;
29
+ activityLog;
30
+ github;
31
+ logger = new common_1.Logger(WorkerApiController_1.name);
32
+ constructor(prisma, eventBus, activityLog, github) {
33
+ this.prisma = prisma;
34
+ this.eventBus = eventBus;
35
+ this.activityLog = activityLog;
36
+ this.github = github;
37
+ }
38
+ // ── Poll for jobs ──────────────────────────────────────────────── //
39
+ async poll(req) {
40
+ const worker = req.worker;
41
+ const session = await this.prisma.session.findFirst({
42
+ where: { status: "DISPATCHED", workerId: worker.id },
43
+ include: {
44
+ project: {
45
+ select: {
46
+ id: true,
47
+ name: true,
48
+ },
49
+ },
50
+ },
51
+ orderBy: { startedAt: "asc" },
52
+ });
53
+ if (!session)
54
+ return { status: 204, data: null };
55
+ // Get repository info with GitHub installation details
56
+ const repos = await this.prisma.repository.findMany({
57
+ where: { projectId: session.projectId },
58
+ select: {
59
+ name: true,
60
+ repoUrl: true,
61
+ repoPath: true,
62
+ branch: true,
63
+ githubInstallationId: true,
64
+ githubOwner: true,
65
+ githubRepo: true,
66
+ },
67
+ });
68
+ // Generate GitHub installation tokens for repos that have them
69
+ let githubToken = null;
70
+ const repoWithInstallation = repos.find((r) => r.githubInstallationId);
71
+ if (repoWithInstallation?.githubInstallationId && this.github.isConfigured()) {
72
+ try {
73
+ githubToken = await this.github.getInstallationToken(repoWithInstallation.githubInstallationId);
74
+ }
75
+ catch (err) {
76
+ this.logger.warn(`Failed to generate GitHub token for installation ${repoWithInstallation.githubInstallationId}: ${err}`);
77
+ }
78
+ }
79
+ // Mark as ACTIVE
80
+ await this.prisma.session.update({
81
+ where: { id: session.id },
82
+ data: { status: "ACTIVE" },
83
+ });
84
+ return {
85
+ sessionId: session.id,
86
+ projectId: session.projectId,
87
+ project: {
88
+ name: session.project.name,
89
+ repos,
90
+ },
91
+ githubToken,
92
+ };
93
+ }
94
+ // ── Heartbeat ──────────────────────────────────────────────────── //
95
+ async heartbeat(req, body) {
96
+ await this.prisma.worker.update({
97
+ where: { id: req.worker.id },
98
+ data: {
99
+ lastHeartbeat: new Date(),
100
+ activeSessions: body.activeSessions,
101
+ status: "ONLINE",
102
+ },
103
+ });
104
+ return { ok: true };
105
+ }
106
+ // ── Event relay (worker → SSE → browser) ──────────────────────── //
107
+ async relayEvent(body) {
108
+ this.eventBus.emit(body.projectId, body.type, body.data);
109
+ }
110
+ // ── Tasks ──────────────────────────────────────────────────────── //
111
+ async findTasks(projectId, status, retryCountLt, take) {
112
+ const where = { projectId };
113
+ if (status) {
114
+ const statuses = status.split(",");
115
+ where.status = statuses.length === 1 ? statuses[0] : { in: statuses };
116
+ }
117
+ if (retryCountLt)
118
+ where.retryCount = { lt: parseInt(retryCountLt, 10) };
119
+ return this.prisma.task.findMany({
120
+ where,
121
+ orderBy: [{ priority: "asc" }, { createdAt: "asc" }],
122
+ take: take ? parseInt(take, 10) : undefined,
123
+ });
124
+ }
125
+ async updateTask(id, body) {
126
+ const task = await this.prisma.task.update({
127
+ where: { id },
128
+ data: body,
129
+ });
130
+ if (body.status) {
131
+ this.eventBus.emit(task.projectId, "task_updated", { id: task.id, status: task.status });
132
+ }
133
+ return task;
134
+ }
135
+ async createTask(body) {
136
+ const task = await this.prisma.task.create({
137
+ data: body,
138
+ });
139
+ this.eventBus.emit(task.projectId, "task_created", {
140
+ id: task.id,
141
+ title: task.title,
142
+ priority: task.priority,
143
+ status: task.status,
144
+ });
145
+ return task;
146
+ }
147
+ // ── Task Comments ──────────────────────────────────────────────── //
148
+ async findTaskComments(taskId, take) {
149
+ return this.prisma.taskComment.findMany({
150
+ where: { taskId },
151
+ orderBy: { createdAt: "asc" },
152
+ take: take ? parseInt(take, 10) : undefined,
153
+ });
154
+ }
155
+ async createTaskComment(taskId, body) {
156
+ await this.prisma.taskComment.create({
157
+ data: { taskId, author: body.author, content: body.content },
158
+ });
159
+ }
160
+ // ── Findings ───────────────────────────────────────────────────── //
161
+ async findFindings(projectId, status, source, take) {
162
+ const where = { projectId };
163
+ if (status) {
164
+ const statuses = status.split(",");
165
+ where.status = statuses.length === 1 ? statuses[0] : { in: statuses };
166
+ }
167
+ if (source)
168
+ where.source = source;
169
+ return this.prisma.finding.findMany({
170
+ where,
171
+ orderBy: [{ severity: "asc" }, { createdAt: "asc" }],
172
+ take: take ? parseInt(take, 10) : undefined,
173
+ });
174
+ }
175
+ async findFindingById(id) {
176
+ const finding = await this.prisma.finding.findUnique({ where: { id } });
177
+ if (!finding)
178
+ throw new common_1.NotFoundException("Finding not found");
179
+ return finding;
180
+ }
181
+ async createFinding(body) {
182
+ const finding = await this.prisma.finding.create({
183
+ data: body,
184
+ });
185
+ this.eventBus.emit(finding.projectId, "finding_created", {
186
+ id: finding.id,
187
+ title: finding.title,
188
+ severity: finding.severity,
189
+ category: finding.category,
190
+ });
191
+ return finding;
192
+ }
193
+ async updateFinding(id, body) {
194
+ await this.prisma.finding.update({
195
+ where: { id },
196
+ data: body,
197
+ });
198
+ }
199
+ async updateManyFindings(body) {
200
+ await this.prisma.finding.updateMany({
201
+ where: { id: { in: body.ids } },
202
+ data: { status: body.status },
203
+ });
204
+ }
205
+ // ── Sessions ───────────────────────────────────────────────────── //
206
+ async findSession(id) {
207
+ const session = await this.prisma.session.findUnique({
208
+ where: { id },
209
+ include: { project: { select: { name: true } } },
210
+ });
211
+ if (!session)
212
+ throw new common_1.NotFoundException("Session not found");
213
+ return session;
214
+ }
215
+ async updateSession(id, body) {
216
+ await this.prisma.session.update({
217
+ where: { id },
218
+ data: {
219
+ ...body,
220
+ endedAt: body.endedAt ? new Date(body.endedAt) : undefined,
221
+ },
222
+ });
223
+ }
224
+ async completeSession(req, id, body) {
225
+ const session = await this.prisma.session.findUnique({ where: { id } });
226
+ if (!session)
227
+ throw new common_1.NotFoundException("Session not found");
228
+ const existingMeta = session.metadata || {};
229
+ await this.prisma.session.update({
230
+ where: { id },
231
+ data: {
232
+ status: "TERMINATED",
233
+ endedAt: new Date(),
234
+ metadata: { ...existingMeta, summary: body.summary, exitCode: body.exitCode },
235
+ },
236
+ });
237
+ // Decrement worker active sessions
238
+ await this.prisma.worker.update({
239
+ where: { id: req.worker.id },
240
+ data: { activeSessions: { decrement: 1 } },
241
+ });
242
+ this.eventBus.emit(session.projectId, "pipeline_complete", { sessionId: id });
243
+ if (body.summary) {
244
+ this.eventBus.emit(session.projectId, "pipeline_summary", { sessionId: id, summary: body.summary });
245
+ }
246
+ }
247
+ // ── Activity Log ───────────────────────────────────────────────── //
248
+ async logActivity(body) {
249
+ await this.activityLog.log(body.action, body.entityType, body.entityId, body.userId, body.details);
250
+ }
251
+ // ── Worker Registration ────────────────────────────────────────── //
252
+ async register(body) {
253
+ // Look up token
254
+ const tokenRecord = await this.prisma.workerToken.findUnique({
255
+ where: { token: body.token },
256
+ });
257
+ if (!tokenRecord)
258
+ throw new common_1.NotFoundException("Invalid token");
259
+ if (tokenRecord.usedAt)
260
+ throw new common_1.NotFoundException("Token already used");
261
+ if (tokenRecord.expiresAt < new Date())
262
+ throw new common_1.NotFoundException("Token expired");
263
+ // Mark token as used
264
+ await this.prisma.workerToken.update({
265
+ where: { id: tokenRecord.id },
266
+ data: { usedAt: new Date() },
267
+ });
268
+ // Generate API key
269
+ const rawApiKey = `wk_key_${(0, crypto_1.randomBytes)(24).toString("hex")}`;
270
+ const apiKeyHash = (0, crypto_1.createHash)("sha256").update(rawApiKey).digest("hex");
271
+ // Create worker
272
+ const worker = await this.prisma.worker.create({
273
+ data: {
274
+ name: body.name,
275
+ tier: tokenRecord.tier,
276
+ ownerId: tokenRecord.ownerId,
277
+ status: "ONLINE",
278
+ apiKeyHash,
279
+ hostname: body.hostname,
280
+ specs: body.specs,
281
+ maxConcurrent: body.maxConcurrent ?? 3,
282
+ lastHeartbeat: new Date(),
283
+ },
284
+ });
285
+ return {
286
+ workerId: worker.id,
287
+ apiKey: rawApiKey,
288
+ tier: worker.tier,
289
+ name: worker.name,
290
+ };
291
+ }
292
+ // ── Token Generation ───────────────────────────────────────────── //
293
+ // Note: This endpoint uses JWT auth (not worker API key) — handled separately
294
+ // ── Worker Project Assignment ──────────────────────────────────── //
295
+ async assignProject(workerId, body) {
296
+ await this.prisma.workerProject.create({
297
+ data: { workerId, projectId: body.projectId },
298
+ });
299
+ return { ok: true };
300
+ }
301
+ async unassignProject(workerId, projectId) {
302
+ await this.prisma.workerProject.deleteMany({
303
+ where: { workerId, projectId },
304
+ });
305
+ return { ok: true };
306
+ }
307
+ };
308
+ exports.WorkerApiController = WorkerApiController;
309
+ __decorate([
310
+ (0, common_1.Get)("poll"),
311
+ (0, common_1.UseGuards)(worker_api_guard_1.WorkerApiGuard),
312
+ __param(0, (0, common_1.Req)()),
313
+ __metadata("design:type", Function),
314
+ __metadata("design:paramtypes", [Object]),
315
+ __metadata("design:returntype", Promise)
316
+ ], WorkerApiController.prototype, "poll", null);
317
+ __decorate([
318
+ (0, common_1.Post)("heartbeat"),
319
+ (0, common_1.HttpCode)(200),
320
+ (0, common_1.UseGuards)(worker_api_guard_1.WorkerApiGuard),
321
+ __param(0, (0, common_1.Req)()),
322
+ __param(1, (0, common_1.Body)()),
323
+ __metadata("design:type", Function),
324
+ __metadata("design:paramtypes", [Object, Object]),
325
+ __metadata("design:returntype", Promise)
326
+ ], WorkerApiController.prototype, "heartbeat", null);
327
+ __decorate([
328
+ (0, common_1.Post)("events"),
329
+ (0, common_1.HttpCode)(202),
330
+ (0, common_1.UseGuards)(worker_api_guard_1.WorkerApiGuard),
331
+ __param(0, (0, common_1.Body)()),
332
+ __metadata("design:type", Function),
333
+ __metadata("design:paramtypes", [Object]),
334
+ __metadata("design:returntype", Promise)
335
+ ], WorkerApiController.prototype, "relayEvent", null);
336
+ __decorate([
337
+ (0, common_1.Get)("projects/:projectId/tasks"),
338
+ (0, common_1.UseGuards)(worker_api_guard_1.WorkerApiGuard),
339
+ __param(0, (0, common_1.Param)("projectId")),
340
+ __param(1, (0, common_1.Query)("status")),
341
+ __param(2, (0, common_1.Query)("retryCountLt")),
342
+ __param(3, (0, common_1.Query)("take")),
343
+ __metadata("design:type", Function),
344
+ __metadata("design:paramtypes", [String, String, String, String]),
345
+ __metadata("design:returntype", Promise)
346
+ ], WorkerApiController.prototype, "findTasks", null);
347
+ __decorate([
348
+ (0, common_1.Patch)("tasks/:id"),
349
+ (0, common_1.UseGuards)(worker_api_guard_1.WorkerApiGuard),
350
+ __param(0, (0, common_1.Param)("id")),
351
+ __param(1, (0, common_1.Body)()),
352
+ __metadata("design:type", Function),
353
+ __metadata("design:paramtypes", [String, Object]),
354
+ __metadata("design:returntype", Promise)
355
+ ], WorkerApiController.prototype, "updateTask", null);
356
+ __decorate([
357
+ (0, common_1.Post)("tasks"),
358
+ (0, common_1.UseGuards)(worker_api_guard_1.WorkerApiGuard),
359
+ __param(0, (0, common_1.Body)()),
360
+ __metadata("design:type", Function),
361
+ __metadata("design:paramtypes", [Object]),
362
+ __metadata("design:returntype", Promise)
363
+ ], WorkerApiController.prototype, "createTask", null);
364
+ __decorate([
365
+ (0, common_1.Get)("tasks/:id/comments"),
366
+ (0, common_1.UseGuards)(worker_api_guard_1.WorkerApiGuard),
367
+ __param(0, (0, common_1.Param)("id")),
368
+ __param(1, (0, common_1.Query)("take")),
369
+ __metadata("design:type", Function),
370
+ __metadata("design:paramtypes", [String, String]),
371
+ __metadata("design:returntype", Promise)
372
+ ], WorkerApiController.prototype, "findTaskComments", null);
373
+ __decorate([
374
+ (0, common_1.Post)("tasks/:id/comments"),
375
+ (0, common_1.UseGuards)(worker_api_guard_1.WorkerApiGuard),
376
+ __param(0, (0, common_1.Param)("id")),
377
+ __param(1, (0, common_1.Body)()),
378
+ __metadata("design:type", Function),
379
+ __metadata("design:paramtypes", [String, Object]),
380
+ __metadata("design:returntype", Promise)
381
+ ], WorkerApiController.prototype, "createTaskComment", null);
382
+ __decorate([
383
+ (0, common_1.Get)("projects/:projectId/findings"),
384
+ (0, common_1.UseGuards)(worker_api_guard_1.WorkerApiGuard),
385
+ __param(0, (0, common_1.Param)("projectId")),
386
+ __param(1, (0, common_1.Query)("status")),
387
+ __param(2, (0, common_1.Query)("source")),
388
+ __param(3, (0, common_1.Query)("take")),
389
+ __metadata("design:type", Function),
390
+ __metadata("design:paramtypes", [String, String, String, String]),
391
+ __metadata("design:returntype", Promise)
392
+ ], WorkerApiController.prototype, "findFindings", null);
393
+ __decorate([
394
+ (0, common_1.Get)("findings/:id"),
395
+ (0, common_1.UseGuards)(worker_api_guard_1.WorkerApiGuard),
396
+ __param(0, (0, common_1.Param)("id")),
397
+ __metadata("design:type", Function),
398
+ __metadata("design:paramtypes", [String]),
399
+ __metadata("design:returntype", Promise)
400
+ ], WorkerApiController.prototype, "findFindingById", null);
401
+ __decorate([
402
+ (0, common_1.Post)("findings"),
403
+ (0, common_1.UseGuards)(worker_api_guard_1.WorkerApiGuard),
404
+ __param(0, (0, common_1.Body)()),
405
+ __metadata("design:type", Function),
406
+ __metadata("design:paramtypes", [Object]),
407
+ __metadata("design:returntype", Promise)
408
+ ], WorkerApiController.prototype, "createFinding", null);
409
+ __decorate([
410
+ (0, common_1.Patch)("findings/:id"),
411
+ (0, common_1.UseGuards)(worker_api_guard_1.WorkerApiGuard),
412
+ __param(0, (0, common_1.Param)("id")),
413
+ __param(1, (0, common_1.Body)()),
414
+ __metadata("design:type", Function),
415
+ __metadata("design:paramtypes", [String, Object]),
416
+ __metadata("design:returntype", Promise)
417
+ ], WorkerApiController.prototype, "updateFinding", null);
418
+ __decorate([
419
+ (0, common_1.Patch)("findings/bulk"),
420
+ (0, common_1.UseGuards)(worker_api_guard_1.WorkerApiGuard),
421
+ __param(0, (0, common_1.Body)()),
422
+ __metadata("design:type", Function),
423
+ __metadata("design:paramtypes", [Object]),
424
+ __metadata("design:returntype", Promise)
425
+ ], WorkerApiController.prototype, "updateManyFindings", null);
426
+ __decorate([
427
+ (0, common_1.Get)("sessions/:id"),
428
+ (0, common_1.UseGuards)(worker_api_guard_1.WorkerApiGuard),
429
+ __param(0, (0, common_1.Param)("id")),
430
+ __metadata("design:type", Function),
431
+ __metadata("design:paramtypes", [String]),
432
+ __metadata("design:returntype", Promise)
433
+ ], WorkerApiController.prototype, "findSession", null);
434
+ __decorate([
435
+ (0, common_1.Patch)("sessions/:id"),
436
+ (0, common_1.UseGuards)(worker_api_guard_1.WorkerApiGuard),
437
+ __param(0, (0, common_1.Param)("id")),
438
+ __param(1, (0, common_1.Body)()),
439
+ __metadata("design:type", Function),
440
+ __metadata("design:paramtypes", [String, Object]),
441
+ __metadata("design:returntype", Promise)
442
+ ], WorkerApiController.prototype, "updateSession", null);
443
+ __decorate([
444
+ (0, common_1.Post)("sessions/:id/complete"),
445
+ (0, common_1.HttpCode)(200),
446
+ (0, common_1.UseGuards)(worker_api_guard_1.WorkerApiGuard),
447
+ __param(0, (0, common_1.Req)()),
448
+ __param(1, (0, common_1.Param)("id")),
449
+ __param(2, (0, common_1.Body)()),
450
+ __metadata("design:type", Function),
451
+ __metadata("design:paramtypes", [Object, String, Object]),
452
+ __metadata("design:returntype", Promise)
453
+ ], WorkerApiController.prototype, "completeSession", null);
454
+ __decorate([
455
+ (0, common_1.Post)("activity-log"),
456
+ (0, common_1.HttpCode)(200),
457
+ (0, common_1.UseGuards)(worker_api_guard_1.WorkerApiGuard),
458
+ __param(0, (0, common_1.Body)()),
459
+ __metadata("design:type", Function),
460
+ __metadata("design:paramtypes", [Object]),
461
+ __metadata("design:returntype", Promise)
462
+ ], WorkerApiController.prototype, "logActivity", null);
463
+ __decorate([
464
+ (0, common_1.Post)("register"),
465
+ __param(0, (0, common_1.Body)()),
466
+ __metadata("design:type", Function),
467
+ __metadata("design:paramtypes", [Object]),
468
+ __metadata("design:returntype", Promise)
469
+ ], WorkerApiController.prototype, "register", null);
470
+ __decorate([
471
+ (0, common_1.Post)("workers/:workerId/projects"),
472
+ (0, common_1.UseGuards)(worker_api_guard_1.WorkerApiGuard),
473
+ __param(0, (0, common_1.Param)("workerId")),
474
+ __param(1, (0, common_1.Body)()),
475
+ __metadata("design:type", Function),
476
+ __metadata("design:paramtypes", [String, Object]),
477
+ __metadata("design:returntype", Promise)
478
+ ], WorkerApiController.prototype, "assignProject", null);
479
+ __decorate([
480
+ (0, common_1.Delete)("workers/:workerId/projects/:projectId"),
481
+ (0, common_1.UseGuards)(worker_api_guard_1.WorkerApiGuard),
482
+ __param(0, (0, common_1.Param)("workerId")),
483
+ __param(1, (0, common_1.Param)("projectId")),
484
+ __metadata("design:type", Function),
485
+ __metadata("design:paramtypes", [String, String]),
486
+ __metadata("design:returntype", Promise)
487
+ ], WorkerApiController.prototype, "unassignProject", null);
488
+ exports.WorkerApiController = WorkerApiController = WorkerApiController_1 = __decorate([
489
+ (0, common_1.Controller)("worker"),
490
+ (0, public_decorator_1.Public)() // bypass JWT auth — workers use API key
491
+ ,
492
+ (0, throttler_1.SkipThrottle)({ global: true }),
493
+ __metadata("design:paramtypes", [prisma_service_1.PrismaService,
494
+ event_bus_service_1.EventBusService,
495
+ activity_log_service_1.ActivityLogService,
496
+ github_service_1.GithubService])
497
+ ], WorkerApiController);
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.WorkerApiGuard = void 0;
13
+ const common_1 = require("@nestjs/common");
14
+ const crypto_1 = require("crypto");
15
+ const prisma_service_1 = require("../../prisma/prisma.service");
16
+ let WorkerApiGuard = class WorkerApiGuard {
17
+ prisma;
18
+ constructor(prisma) {
19
+ this.prisma = prisma;
20
+ }
21
+ async canActivate(context) {
22
+ const request = context.switchToHttp().getRequest();
23
+ const authHeader = request.headers.authorization;
24
+ if (!authHeader?.startsWith("Bearer "))
25
+ return false;
26
+ const apiKey = authHeader.slice(7);
27
+ const hash = (0, crypto_1.createHash)("sha256").update(apiKey).digest("hex");
28
+ const worker = await this.prisma.worker.findFirst({
29
+ where: { apiKeyHash: hash },
30
+ });
31
+ if (!worker)
32
+ return false;
33
+ request.worker = worker;
34
+ return true;
35
+ }
36
+ };
37
+ exports.WorkerApiGuard = WorkerApiGuard;
38
+ exports.WorkerApiGuard = WorkerApiGuard = __decorate([
39
+ (0, common_1.Injectable)(),
40
+ __metadata("design:paramtypes", [prisma_service_1.PrismaService])
41
+ ], WorkerApiGuard);
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.WorkerApiModule = void 0;
10
+ const common_1 = require("@nestjs/common");
11
+ const github_module_1 = require("../github/github.module");
12
+ const worker_api_controller_1 = require("./worker-api.controller");
13
+ const worker_api_guard_1 = require("./worker-api.guard");
14
+ const worker_dispatch_service_1 = require("./worker-dispatch.service");
15
+ let WorkerApiModule = class WorkerApiModule {
16
+ };
17
+ exports.WorkerApiModule = WorkerApiModule;
18
+ exports.WorkerApiModule = WorkerApiModule = __decorate([
19
+ (0, common_1.Module)({
20
+ imports: [github_module_1.GithubModule],
21
+ controllers: [worker_api_controller_1.WorkerApiController],
22
+ providers: [worker_api_guard_1.WorkerApiGuard, worker_dispatch_service_1.WorkerDispatchService],
23
+ exports: [worker_dispatch_service_1.WorkerDispatchService],
24
+ })
25
+ ], WorkerApiModule);
@@ -0,0 +1,87 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var WorkerDispatchService_1;
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.WorkerDispatchService = void 0;
14
+ const common_1 = require("@nestjs/common");
15
+ const prisma_service_1 = require("../../prisma/prisma.service");
16
+ let WorkerDispatchService = WorkerDispatchService_1 = class WorkerDispatchService {
17
+ prisma;
18
+ logger = new common_1.Logger(WorkerDispatchService_1.name);
19
+ constructor(prisma) {
20
+ this.prisma = prisma;
21
+ }
22
+ /**
23
+ * Find the best worker for a pipeline session.
24
+ * Returns null if no worker is available.
25
+ */
26
+ async findWorker(projectId, userId) {
27
+ // 1. Check for a DEDICATED worker assigned to this project
28
+ const dedicatedWorker = await this.prisma.$queryRaw `
29
+ SELECT w.* FROM workers w
30
+ WHERE w.tier = 'DEDICATED'
31
+ AND w.owner_id = ${userId}
32
+ AND w.status = 'ONLINE'
33
+ AND w.active_sessions < w.max_concurrent
34
+ AND w.id IN (SELECT wp.worker_id FROM worker_projects wp WHERE wp.project_id = ${projectId})
35
+ ORDER BY w.active_sessions ASC
36
+ LIMIT 1
37
+ `;
38
+ if (dedicatedWorker.length > 0)
39
+ return dedicatedWorker[0];
40
+ // 2. Check shared pool
41
+ const sharedWorker = await this.prisma.$queryRaw `
42
+ SELECT w.* FROM workers w
43
+ WHERE w.tier = 'SHARED'
44
+ AND w.status = 'ONLINE'
45
+ AND w.active_sessions < w.max_concurrent
46
+ ORDER BY w.active_sessions ASC
47
+ LIMIT 1
48
+ `;
49
+ return sharedWorker.length > 0 ? sharedWorker[0] : null;
50
+ }
51
+ /**
52
+ * Check for stale workers and re-queue orphaned sessions.
53
+ * Called periodically (e.g., every 60 seconds).
54
+ */
55
+ async checkWorkerHealth() {
56
+ const staleThreshold = new Date(Date.now() - 2 * 60 * 1000); // 2 minutes
57
+ const staleCount = await this.prisma.worker.updateMany({
58
+ where: {
59
+ status: { not: "OFFLINE" },
60
+ lastHeartbeat: { lt: staleThreshold },
61
+ },
62
+ data: { status: "OFFLINE" },
63
+ });
64
+ if (staleCount.count > 0) {
65
+ this.logger.warn(`Marked ${staleCount.count} stale worker(s) as OFFLINE`);
66
+ }
67
+ // Re-queue sessions dispatched to now-offline workers
68
+ const orphanedSessions = await this.prisma.session.findMany({
69
+ where: {
70
+ status: "DISPATCHED",
71
+ worker: { status: "OFFLINE" },
72
+ },
73
+ });
74
+ for (const session of orphanedSessions) {
75
+ await this.prisma.session.update({
76
+ where: { id: session.id },
77
+ data: { status: "QUEUED", workerId: null },
78
+ });
79
+ this.logger.warn(`Re-queued orphaned session ${session.id}`);
80
+ }
81
+ }
82
+ };
83
+ exports.WorkerDispatchService = WorkerDispatchService;
84
+ exports.WorkerDispatchService = WorkerDispatchService = WorkerDispatchService_1 = __decorate([
85
+ (0, common_1.Injectable)(),
86
+ __metadata("design:paramtypes", [prisma_service_1.PrismaService])
87
+ ], WorkerDispatchService);