crewx 0.8.8-rc.2 → 0.8.8-rc.21

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 (105) hide show
  1. package/bin/cli-commands.js +1 -1
  2. package/dist/assets/{MarketPage-Bh1UfiC2.js → MarketPage-B5JbqK4T.js} +9 -14
  3. package/dist/assets/{PromptTab-DYmYVBKo.js → PromptTab-_97mSbLk.js} +7 -7
  4. package/dist/assets/{arc-C14FbhzP.js → arc-3FDm7TxG.js} +1 -1
  5. package/dist/assets/architectureDiagram-UYN6MBPD-D8X56UlC.js +36 -0
  6. package/dist/assets/blockDiagram-ZHA2E4KO-B6jUnEVc.js +121 -0
  7. package/dist/assets/{c4Diagram-6F5ED5ID-DLSfQS-Z.js → c4Diagram-6F5ED5ID-Cy8x6Ims.js} +1 -1
  8. package/dist/assets/channel-Bs0J8jjN.js +1 -0
  9. package/dist/assets/{chunk-5HRBRIJM-j7YcJ7Bx.js → chunk-5HRBRIJM-Bh819Q5A.js} +1 -1
  10. package/dist/assets/{chunk-7U56Z5CX-Baqt1cj3.js → chunk-7U56Z5CX-CfNtPGa8.js} +1 -1
  11. package/dist/assets/{chunk-ASOPGD6M-F1YJ2mzH.js → chunk-ASOPGD6M-CMR1ElQ6.js} +1 -1
  12. package/dist/assets/{chunk-KFBOBJHC-Dvhzl6G9.js → chunk-KFBOBJHC-Dm2I1dHM.js} +1 -1
  13. package/dist/assets/{chunk-T2TOU4HS-BDczCXP6.js → chunk-T2TOU4HS-0tf-aNtk.js} +1 -1
  14. package/dist/assets/{chunk-TMUBEWPD-4BhwBH_F.js → chunk-TMUBEWPD-b5ZrpKJw.js} +1 -1
  15. package/dist/assets/classDiagram-LNE6IOMH-B1-6qC07.js +1 -0
  16. package/dist/assets/classDiagram-v2-MQ7JQ4JX-B1-6qC07.js +1 -0
  17. package/dist/assets/dagre-4EVJKHTY-zYmFBlwR.js +4 -0
  18. package/dist/assets/diagram-QW4FP2JN-DkZngH-Z.js +24 -0
  19. package/dist/assets/{erDiagram-6RL3IURR-DRhNKkH0.js → erDiagram-6RL3IURR-8TfuIQ6D.js} +2 -2
  20. package/dist/assets/{flowDiagram-7ASYPVHJ-BDJZsUJS.js → flowDiagram-7ASYPVHJ-BnhSLZTo.js} +1 -1
  21. package/dist/assets/{ganttDiagram-NTVNEXSI-BYBeyULV.js → ganttDiagram-NTVNEXSI--jDGPnYB.js} +3 -3
  22. package/dist/assets/gitGraph-YCYPL57B-DkCOt6bF.js +133 -0
  23. package/dist/assets/gitGraphDiagram-NRZ2UAAF-D4p17Lzw.js +65 -0
  24. package/dist/assets/graph-BlwPajkw.js +1 -0
  25. package/dist/assets/infoDiagram-A4XQUW5V-COnYi0Q8.js +2 -0
  26. package/dist/assets/{journeyDiagram-G5WM74LC-BhSSQ4Od.js → journeyDiagram-G5WM74LC-CTZcqDJf.js} +1 -1
  27. package/dist/assets/{kanban-definition-QRCXZQQD-CZOkitn-.js → kanban-definition-QRCXZQQD-CezLYsK3.js} +1 -1
  28. package/dist/assets/layout-Bvwu4dCi.js +1 -0
  29. package/dist/assets/{linear-BRR4YPhd.js → linear-BcYBjD0V.js} +1 -1
  30. package/dist/assets/main-CgT_Atre.js +1210 -0
  31. package/dist/assets/main-kJ_DZpZU.css +10 -0
  32. package/dist/assets/{mindmap-definition-GWI6TPTV-CiMg1rSL.js → mindmap-definition-GWI6TPTV-NM7qIPkp.js} +1 -1
  33. package/dist/assets/pieDiagram-YF2LJOPJ-Ck2UBMrZ.js +30 -0
  34. package/dist/assets/{quadrantDiagram-OS5C2QUG-cYMjsA0L.js → quadrantDiagram-OS5C2QUG-0OKnDbeW.js} +1 -1
  35. package/dist/assets/{requirementDiagram-MIRIMTAZ-D8piGWQV.js → requirementDiagram-MIRIMTAZ-Nq_mStSk.js} +2 -2
  36. package/dist/assets/{sankeyDiagram-Y46BX6SQ-DfNocUti.js → sankeyDiagram-Y46BX6SQ-ZPgHNk3C.js} +1 -1
  37. package/dist/assets/{sequenceDiagram-G6AWOVSC-Xu2X2iUX.js → sequenceDiagram-G6AWOVSC-BRzYMlx2.js} +1 -1
  38. package/dist/assets/stateDiagram-MAYHULR4-CCNAeCLE.js +1 -0
  39. package/dist/assets/stateDiagram-v2-4JROLMXI-Bz1kxCVf.js +1 -0
  40. package/dist/assets/{timeline-definition-U7ZMHBDA-CNra78s7.js → timeline-definition-U7ZMHBDA-BSw5vmnf.js} +1 -1
  41. package/dist/assets/{xychartDiagram-6QU3TZC5-BSwY5sKC.js → xychartDiagram-6QU3TZC5-BuqrahPf.js} +2 -2
  42. package/dist/index.html +2 -2
  43. package/dist-server/app.module.js +4 -0
  44. package/dist-server/bootstrap/crewx-server.js +6 -3
  45. package/dist-server/common/limits/defaults.js +2 -0
  46. package/dist-server/domain/auth/auth.module.js +2 -1
  47. package/dist-server/domain/auth/guards/base-auth.guard.js +36 -14
  48. package/dist-server/domain/doc/doc.controller.js +21 -0
  49. package/dist-server/domain/doc/doc.service.js +36 -6
  50. package/dist-server/domain/doc/image-constants.js +11 -0
  51. package/dist-server/domain/events/events.module.js +31 -0
  52. package/dist-server/domain/events/server-conversation.plugin.js +45 -0
  53. package/dist-server/domain/events/workspace-event.bus.js +32 -0
  54. package/dist-server/domain/events/workspace-event.types.js +12 -0
  55. package/dist-server/domain/events/workspace-events-test.controller.js +83 -0
  56. package/dist-server/domain/events/workspace-events.controller.js +47 -0
  57. package/dist-server/domain/mcp/browser-session.store.js +50 -0
  58. package/dist-server/domain/mcp/chromex-blacklist.js +26 -0
  59. package/dist-server/domain/mcp/chromex-negotiate.controller.js +77 -0
  60. package/dist-server/domain/mcp/chromex-negotiate.dto.js +95 -0
  61. package/dist-server/domain/mcp/chromex-negotiate.service.js +249 -0
  62. package/dist-server/domain/mcp/mcp-auth.guard.js +40 -26
  63. package/dist-server/domain/mcp/mcp.constants.js +4 -1
  64. package/dist-server/domain/mcp/mcp.controller.js +5 -4
  65. package/dist-server/domain/mcp/mcp.module.js +6 -2
  66. package/dist-server/domain/mcp/mcp.service.js +62 -5
  67. package/dist-server/domain/message/dto/send-message.dto.js +48 -1
  68. package/dist-server/domain/message/message.controller.js +40 -1
  69. package/dist-server/domain/message/message.service.js +50 -5
  70. package/dist-server/domain/task/task.service.js +34 -13
  71. package/dist-server/domain/thread/thread.controller.js +16 -0
  72. package/dist-server/domain/thread/thread.module.js +2 -0
  73. package/dist-server/domain/thread/thread.service.js +80 -11
  74. package/dist-server/domain/token/token.controller.js +73 -0
  75. package/dist-server/domain/token/token.dto.js +56 -0
  76. package/dist-server/domain/token/token.module.js +22 -0
  77. package/dist-server/domain/token/token.service.js +228 -0
  78. package/dist-server/main.js +23 -2
  79. package/dist-server/modules/crewx-pool.service.js +129 -5
  80. package/dist-server/modules/crewx.module.js +6 -0
  81. package/package.json +17 -14
  82. package/packages/cli/dist/builtin.js +2 -0
  83. package/packages/cli/dist/commands/init.js +5 -5
  84. package/packages/cli/dist/commands/registry.js +1 -1
  85. package/packages/cli/package.json +2 -1
  86. package/dist/assets/_baseUniq-B5kIqYhv.js +0 -1
  87. package/dist/assets/architectureDiagram-UYN6MBPD-Bvk4uHjQ.js +0 -36
  88. package/dist/assets/blockDiagram-ZHA2E4KO-C9-iioqe.js +0 -121
  89. package/dist/assets/channel-B1S1YSXQ.js +0 -1
  90. package/dist/assets/classDiagram-LNE6IOMH-BMiPdBl0.js +0 -1
  91. package/dist/assets/classDiagram-v2-MQ7JQ4JX-BMiPdBl0.js +0 -1
  92. package/dist/assets/clone-DkRtovKq.js +0 -1
  93. package/dist/assets/dagre-4EVJKHTY-LsgK-pqb.js +0 -4
  94. package/dist/assets/diagram-QW4FP2JN-uvhH5Eln.js +0 -24
  95. package/dist/assets/gitGraph-YCYPL57B-D9Hkzkze.js +0 -133
  96. package/dist/assets/gitGraphDiagram-NRZ2UAAF-oWiYlrjZ.js +0 -65
  97. package/dist/assets/graph-D66Q_If6.js +0 -1
  98. package/dist/assets/infoDiagram-A4XQUW5V-aJ6i9Snw.js +0 -2
  99. package/dist/assets/layout-CaxkerSC.js +0 -1
  100. package/dist/assets/main-3IYf82MG.css +0 -10
  101. package/dist/assets/main-DCHJFonK.js +0 -1167
  102. package/dist/assets/min-uKYaLZUT.js +0 -1
  103. package/dist/assets/pieDiagram-YF2LJOPJ-BFJ_DAm-.js +0 -30
  104. package/dist/assets/stateDiagram-MAYHULR4-WmJVTQsF.js +0 -1
  105. package/dist/assets/stateDiagram-v2-4JROLMXI-DNAOV6w0.js +0 -1
@@ -5,9 +5,11 @@ exports.FREE_TIER_LIMITS = {
5
5
  tier: 'FREE',
6
6
  workspaces: 3,
7
7
  agentsPerWorkspace: 10,
8
+ apiTokens: 3,
8
9
  };
9
10
  exports.UNLIMITED_LIMITS = {
10
11
  tier: 'ENTERPRISE',
11
12
  workspaces: -1,
12
13
  agentsPerWorkspace: -1,
14
+ apiTokens: -1,
13
15
  };
@@ -15,12 +15,13 @@ const mcp_constants_js_1 = require("../mcp/mcp.constants.js");
15
15
  const auth_constants_js_1 = require("./auth.constants.js");
16
16
  const auth_controller_js_1 = require("./auth.controller.js");
17
17
  const base_auth_guard_js_1 = require("./guards/base-auth.guard.js");
18
+ const token_module_js_1 = require("../token/token.module.js");
18
19
  let AuthModule = class AuthModule {
19
20
  };
20
21
  exports.AuthModule = AuthModule;
21
22
  exports.AuthModule = AuthModule = __decorate([
22
23
  (0, common_1.Module)({
23
- imports: [mcp_module_js_1.McpModule],
24
+ imports: [mcp_module_js_1.McpModule, token_module_js_1.TokenModule],
24
25
  controllers: [auth_controller_js_1.AuthController],
25
26
  providers: [
26
27
  {
@@ -15,18 +15,18 @@ Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.BaseAuthGuard = void 0;
16
16
  const common_1 = require("@nestjs/common");
17
17
  const core_1 = require("@nestjs/core");
18
- const mcp_constants_js_1 = require("../../mcp/mcp.constants.js");
19
18
  const public_decorator_js_1 = require("../decorators/public.decorator.js");
20
19
  const ip_utils_js_1 = require("../utils/ip.utils.js");
21
20
  const session_store_js_1 = require("../session-store.js");
21
+ const token_service_js_1 = require("../../token/token.service.js");
22
22
  let BaseAuthGuard = class BaseAuthGuard {
23
23
  reflector;
24
- mcpToken;
25
- constructor(reflector, mcpToken) {
24
+ tokenService;
25
+ constructor(reflector, tokenService) {
26
26
  this.reflector = reflector;
27
- this.mcpToken = mcpToken;
27
+ this.tokenService = tokenService;
28
28
  }
29
- canActivate(context) {
29
+ async canActivate(context) {
30
30
  const isPublic = this.reflector.getAllAndOverride(public_decorator_js_1.IS_PUBLIC_KEY, [
31
31
  context.getHandler(),
32
32
  context.getClass(),
@@ -34,33 +34,55 @@ let BaseAuthGuard = class BaseAuthGuard {
34
34
  if (isPublic)
35
35
  return true;
36
36
  const req = context.switchToHttp().getRequest();
37
+ // Bearer header present → validate with TokenService (no localhost exception)
38
+ const rawToken = (0, ip_utils_js_1.extractBearerToken)(req);
39
+ if (rawToken) {
40
+ const claims = await this.tokenService.validateToken(rawToken);
41
+ if (!claims)
42
+ throw new common_1.UnauthorizedException('Invalid token');
43
+ req.tokenClaims = claims;
44
+ return true;
45
+ }
46
+ // localhost bypass (DNS rebinding defense: Host header must also be local)
47
+ // CSRF not required for loopback — cross-origin CSRF to localhost is blocked by the browser.
37
48
  if (this.isLocalRequest(req))
38
49
  return true;
39
- return this.validateToken(req);
50
+ // External access → session first (throws 401 if missing/invalid)
51
+ this.validateWebSession(req);
52
+ // CSRF Double Submit only on unsafe methods (state-changing)
53
+ if (this.requiresCsrf(req)) {
54
+ const csrfHeader = req.headers['x-csrf-token'];
55
+ const csrfCookie = req.cookies?.['crewx_csrf'];
56
+ if (!csrfHeader || csrfHeader !== csrfCookie) {
57
+ throw new common_1.ForbiddenException('CSRF token mismatch');
58
+ }
59
+ }
60
+ return true;
40
61
  }
41
62
  isLocalRequest(req) {
42
63
  const ip = req.socket?.remoteAddress;
43
64
  if (!(0, ip_utils_js_1.isLoopback)(ip))
44
65
  return false;
45
- // DNS rebinding defense: Host header must also be local
46
66
  const host = (req.headers['host'] ?? '').split(':')[0];
47
67
  return host === 'localhost' || host === '127.0.0.1' || host === '[::1]';
48
68
  }
49
- validateToken(req) {
69
+ requiresCsrf(req) {
70
+ const method = (req.method ?? 'GET').toUpperCase();
71
+ return !['GET', 'HEAD', 'OPTIONS'].includes(method);
72
+ }
73
+ validateWebSession(req) {
50
74
  const cookieSessionId = req.cookies?.['crewx_token'];
51
- const bearerToken = (0, ip_utils_js_1.extractBearerToken)(req);
52
75
  if (cookieSessionId && (0, session_store_js_1.validateSession)(cookieSessionId)) {
53
76
  return true;
54
77
  }
55
- if (bearerToken && (0, ip_utils_js_1.safeCompare)(bearerToken, this.mcpToken.token)) {
56
- return true;
57
- }
58
78
  throw new common_1.UnauthorizedException('Authentication required');
59
79
  }
60
80
  };
61
81
  exports.BaseAuthGuard = BaseAuthGuard;
62
82
  exports.BaseAuthGuard = BaseAuthGuard = __decorate([
63
83
  (0, common_1.Injectable)(),
64
- __param(1, (0, common_1.Inject)(mcp_constants_js_1.MCP_TOKEN)),
65
- __metadata("design:paramtypes", [core_1.Reflector, Object])
84
+ __param(0, (0, common_1.Inject)(core_1.Reflector)),
85
+ __param(1, (0, common_1.Inject)(token_service_js_1.TokenService)),
86
+ __metadata("design:paramtypes", [core_1.Reflector,
87
+ token_service_js_1.TokenService])
66
88
  ], BaseAuthGuard);
@@ -15,6 +15,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.DocController = void 0;
16
16
  const common_1 = require("@nestjs/common");
17
17
  const swagger_1 = require("@nestjs/swagger");
18
+ const fs_1 = require("fs");
18
19
  const doc_service_js_1 = require("./doc.service.js");
19
20
  const doc_dto_js_1 = require("./dto/doc.dto.js");
20
21
  let DocController = class DocController {
@@ -42,6 +43,17 @@ let DocController = class DocController {
42
43
  const data = this.docService.getImplStatus();
43
44
  return { success: true, data };
44
45
  }
46
+ // DOC.IMAGE: Serve document image file
47
+ serveImage(filePath, res) {
48
+ const { absPath, mime } = this.docService.getDocImagePath(filePath);
49
+ const stat = (0, fs_1.statSync)(absPath);
50
+ res.set({
51
+ 'Content-Type': mime,
52
+ 'Content-Length': stat.size,
53
+ 'Cache-Control': 'public, max-age=3600',
54
+ });
55
+ (0, fs_1.createReadStream)(absPath).pipe(res);
56
+ }
45
57
  // DOC.SAVE: Save document content
46
58
  async saveContent(body) {
47
59
  const data = await this.docService.saveContent(body.path, body.content, body.mtime);
@@ -80,6 +92,15 @@ __decorate([
80
92
  __metadata("design:paramtypes", []),
81
93
  __metadata("design:returntype", void 0)
82
94
  ], DocController.prototype, "getImplStatus", null);
95
+ __decorate([
96
+ (0, swagger_1.ApiOperation)({ operationId: 'DOC.IMAGE', summary: 'Serve a document image (png/jpg/jpeg/gif/webp)' }),
97
+ (0, common_1.Get)('image'),
98
+ __param(0, (0, common_1.Query)('path')),
99
+ __param(1, (0, common_1.Res)()),
100
+ __metadata("design:type", Function),
101
+ __metadata("design:paramtypes", [String, Object]),
102
+ __metadata("design:returntype", void 0)
103
+ ], DocController.prototype, "serveImage", null);
83
104
  __decorate([
84
105
  (0, swagger_1.ApiOperation)({ operationId: 'DOC.SAVE', summary: 'Save document content' }),
85
106
  (0, common_1.Put)('content'),
@@ -14,6 +14,7 @@ const node_crypto_1 = require("node:crypto");
14
14
  const doc_1 = require("@crewx/doc");
15
15
  const knowledge_core_1 = require("@crewx/knowledge-core");
16
16
  const workspace_context_store_js_1 = require("../../common/workspace-context.store.js");
17
+ const image_constants_js_1 = require("./image-constants.js");
17
18
  // Screen ID → page directory mapping for code existence check
18
19
  const SCREEN_CODE_PATHS = {
19
20
  NAVUI: 'src/ui/components/layout',
@@ -94,8 +95,11 @@ let DocService = class DocService {
94
95
  children.push(child);
95
96
  }
96
97
  }
97
- else if (entry.isFile() && entry.name.endsWith('.md')) {
98
- children.push(this.fileNode(entryAbs));
98
+ else if (entry.isFile()) {
99
+ const ext = entry.name.split('.').pop()?.toLowerCase() ?? '';
100
+ if (entry.name.endsWith('.md') || image_constants_js_1.IMAGE_EXTENSIONS.has(ext)) {
101
+ children.push(this.fileNode(entryAbs));
102
+ }
99
103
  }
100
104
  }
101
105
  return { name, path: rel, type: 'dir', children };
@@ -301,10 +305,11 @@ let DocService = class DocService {
301
305
  }
302
306
  // ── Path validation ───────────────────────────────────────
303
307
  /**
304
- * Prevent path traversal attacks by rejecting paths containing '..'.
305
- * Also ensures the resolved path stays within the project root.
308
+ * Validates traversal and boundary only does NOT check existence.
309
+ * Fix: use `rootCmp + '/'` prefix (or exact match) to reject sibling dirs
310
+ * like /home/u/proj-secret that share the root as a string prefix.
306
311
  */
307
- validatePath(filePath) {
312
+ validatePathBoundary(filePath) {
308
313
  if (filePath.includes('..')) {
309
314
  throw new common_1.BadRequestException('Path traversal is not allowed');
310
315
  }
@@ -314,13 +319,38 @@ let DocService = class DocService {
314
319
  // projectRoot may already be normalized (c:/…). Naive startsWith fails.
315
320
  const absCmp = abs.replace(/\\/g, '/').toLowerCase();
316
321
  const rootCmp = this.projectRoot.replace(/\\/g, '/').toLowerCase();
317
- if (!absCmp.startsWith(rootCmp)) {
322
+ if (absCmp !== rootCmp && !absCmp.startsWith(rootCmp + '/')) {
318
323
  throw new common_1.BadRequestException('Path is outside project root');
319
324
  }
325
+ }
326
+ /**
327
+ * Prevent path traversal attacks by rejecting paths containing '..'.
328
+ * Also ensures the resolved path stays within the project root.
329
+ */
330
+ validatePath(filePath) {
331
+ this.validatePathBoundary(filePath);
332
+ const abs = (0, path_1.resolve)(this.projectRoot, filePath);
320
333
  if (!(0, fs_1.existsSync)(abs)) {
321
334
  throw new common_1.BadRequestException(`File not found: ${filePath}`);
322
335
  }
323
336
  }
337
+ /**
338
+ * DOC.IMAGE: Validate an image path and return its absolute filesystem path.
339
+ * Enforces extension whitelist before existence check to prevent info leakage.
340
+ * Missing images throw 404 (not 400) to match spec and prevent probing.
341
+ */
342
+ getDocImagePath(filePath) {
343
+ const ext = filePath.split('.').pop()?.toLowerCase() ?? '';
344
+ if (!image_constants_js_1.IMAGE_EXTENSIONS.has(ext)) {
345
+ throw new common_1.NotFoundException('Not found');
346
+ }
347
+ this.validatePathBoundary(filePath);
348
+ const abs = (0, path_1.resolve)(this.projectRoot, filePath);
349
+ if (!(0, fs_1.existsSync)(abs)) {
350
+ throw new common_1.NotFoundException('Not found');
351
+ }
352
+ return { absPath: abs, mime: image_constants_js_1.EXT_MIME[ext] };
353
+ }
324
354
  validateEditable(filePath) {
325
355
  const lower = filePath.toLowerCase();
326
356
  if (!lower.endsWith('.md')) {
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EXT_MIME = exports.IMAGE_EXTENSIONS = void 0;
4
+ exports.IMAGE_EXTENSIONS = new Set(['png', 'jpg', 'jpeg', 'gif', 'webp']);
5
+ exports.EXT_MIME = {
6
+ png: 'image/png',
7
+ jpg: 'image/jpeg',
8
+ jpeg: 'image/jpeg',
9
+ gif: 'image/gif',
10
+ webp: 'image/webp',
11
+ };
@@ -0,0 +1,31 @@
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.EventsModule = void 0;
10
+ const common_1 = require("@nestjs/common");
11
+ const workspace_events_controller_js_1 = require("./workspace-events.controller.js");
12
+ const workspace_events_test_controller_js_1 = require("./workspace-events-test.controller.js");
13
+ const workspace_event_bus_js_1 = require("./workspace-event.bus.js");
14
+ /**
15
+ * EventsModule — workspace SSE event bus + streaming endpoint.
16
+ *
17
+ * WorkspaceEventBus is exported so CrewxModule can import it and wire
18
+ * SDK event subscriptions without a circular dependency:
19
+ * EventsModule → (no CrewxModule dependency)
20
+ * CrewxModule → imports EventsModule for WorkspaceEventBus
21
+ */
22
+ let EventsModule = class EventsModule {
23
+ };
24
+ exports.EventsModule = EventsModule;
25
+ exports.EventsModule = EventsModule = __decorate([
26
+ (0, common_1.Module)({
27
+ controllers: [workspace_events_controller_js_1.WorkspaceEventsController, workspace_events_test_controller_js_1.WorkspaceEventsTestController],
28
+ providers: [workspace_event_bus_js_1.WorkspaceEventBus],
29
+ exports: [workspace_event_bus_js_1.WorkspaceEventBus],
30
+ })
31
+ ], EventsModule);
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ServerConversationPlugin = void 0;
4
+ const plugins_1 = require("@crewx/sdk/plugins");
5
+ /**
6
+ * ServerConversationPlugin — extends ConversationPlugin to emit workspace SSE events
7
+ * immediately after conversation persistence (afterUserMessage / afterAssistantMessage hooks).
8
+ *
9
+ * wsId strategy: captured from crewx.workspaceId in attach() (option a).
10
+ * TaskEndEvent carries no workspaceId, so we must capture it at attach time.
11
+ * Each plugin instance is bound to exactly one Crewx instance, so wsId is stable.
12
+ *
13
+ * Phase 1 note: messageId from hook = threadId (thread PK, not per-message row).
14
+ * SSE payload uses taskId = e.traceId for per-task identity. Per-message IDs are Phase 2.
15
+ */
16
+ class ServerConversationPlugin extends plugins_1.ConversationPlugin {
17
+ bus;
18
+ wsId = null;
19
+ constructor(bus) {
20
+ super();
21
+ this.bus = bus;
22
+ }
23
+ attach(crewx) {
24
+ this.wsId = crewx.workspaceId;
25
+ super.attach(crewx);
26
+ }
27
+ async afterUserMessage(threadId, _messageId, created, e) {
28
+ const wsId = e.workspaceId;
29
+ if (!wsId)
30
+ return;
31
+ if (created) {
32
+ this.bus.emit(wsId, { type: 'thread.created', data: { threadId } });
33
+ }
34
+ this.bus.emit(wsId, { type: 'message.created', data: { threadId, taskId: e.traceId } });
35
+ this.bus.emit(wsId, { type: 'thread.updated', data: { threadId, updatedAt: e.timestamp.toISOString() } });
36
+ }
37
+ async afterAssistantMessage(threadId, _messageId, e) {
38
+ const wsId = this.wsId;
39
+ if (!wsId)
40
+ return;
41
+ this.bus.emit(wsId, { type: 'message.created', data: { threadId, taskId: e.traceId } });
42
+ this.bus.emit(wsId, { type: 'thread.updated', data: { threadId, updatedAt: e.timestamp.toISOString() } });
43
+ }
44
+ }
45
+ exports.ServerConversationPlugin = ServerConversationPlugin;
@@ -0,0 +1,32 @@
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.WorkspaceEventBus = void 0;
10
+ const common_1 = require("@nestjs/common");
11
+ const rxjs_1 = require("rxjs");
12
+ const operators_1 = require("rxjs/operators");
13
+ /**
14
+ * WorkspaceEventBus — singleton RxJS Subject that fan-outs workspace events.
15
+ *
16
+ * All workspace SSE events flow through this single Subject.
17
+ * Subscribers use for(wsId) to receive only their workspace's events.
18
+ */
19
+ let WorkspaceEventBus = class WorkspaceEventBus {
20
+ subject = new rxjs_1.Subject();
21
+ emit(wsId, event) {
22
+ this.subject.next({ wsId, event });
23
+ }
24
+ /** Returns an Observable filtered to a single workspace's events. */
25
+ for(wsId) {
26
+ return this.subject.pipe((0, operators_1.filter)((envelope) => envelope.wsId === wsId), (0, operators_1.map)((envelope) => envelope.event));
27
+ }
28
+ };
29
+ exports.WorkspaceEventBus = WorkspaceEventBus;
30
+ exports.WorkspaceEventBus = WorkspaceEventBus = __decorate([
31
+ (0, common_1.Injectable)()
32
+ ], WorkspaceEventBus);
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WORKSPACE_EVENT_TYPES = void 0;
4
+ /** SSOT: all workspace SSE event types. Value = SSE event name = addEventListener key. */
5
+ exports.WORKSPACE_EVENT_TYPES = [
6
+ 'task.created',
7
+ 'task.updated',
8
+ 'task.logged',
9
+ 'thread.created',
10
+ 'thread.updated',
11
+ 'message.created',
12
+ ];
@@ -0,0 +1,83 @@
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
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.WorkspaceEventsTestController = void 0;
16
+ const common_1 = require("@nestjs/common");
17
+ const class_validator_1 = require("class-validator");
18
+ const workspace_decorator_js_1 = require("../../common/decorators/workspace.decorator.js");
19
+ const workspace_event_bus_js_1 = require("./workspace-event.bus.js");
20
+ const workspace_event_types_js_1 = require("./workspace-event.types.js");
21
+ function makeDefaultData(type) {
22
+ const now = new Date().toISOString();
23
+ const taskId = `tsk_test_${Date.now()}`;
24
+ const threadId = `thr_test_${Date.now()}`;
25
+ switch (type) {
26
+ case 'task.created':
27
+ return { taskId, threadId, agent: 'test_agent', status: 'running', updatedAt: now };
28
+ case 'task.updated':
29
+ return { taskId, threadId, agent: 'test_agent', status: 'success', costUsd: 0.001, updatedAt: now };
30
+ case 'task.logged':
31
+ return { taskId, threadId };
32
+ case 'thread.created':
33
+ return { threadId };
34
+ case 'thread.updated':
35
+ return { threadId, updatedAt: now };
36
+ case 'message.created':
37
+ return { threadId, taskId };
38
+ }
39
+ }
40
+ class TestEmitBody {
41
+ type;
42
+ data;
43
+ }
44
+ __decorate([
45
+ (0, class_validator_1.IsString)(),
46
+ (0, class_validator_1.IsIn)(workspace_event_types_js_1.WORKSPACE_EVENT_TYPES),
47
+ __metadata("design:type", String)
48
+ ], TestEmitBody.prototype, "type", void 0);
49
+ __decorate([
50
+ (0, class_validator_1.IsOptional)(),
51
+ (0, class_validator_1.IsObject)(),
52
+ __metadata("design:type", Object)
53
+ ], TestEmitBody.prototype, "data", void 0);
54
+ /** POST /api/v1/ws/:slug/test/emit — manually push a test event into the workspace SSE bus. Dev only. */
55
+ let WorkspaceEventsTestController = class WorkspaceEventsTestController {
56
+ bus;
57
+ constructor(bus) {
58
+ this.bus = bus;
59
+ }
60
+ emit(body, ws) {
61
+ if (!workspace_event_types_js_1.WORKSPACE_EVENT_TYPES.includes(body.type)) {
62
+ return { success: false, error: `Unknown event type: ${body.type}` };
63
+ }
64
+ const data = body.data ?? makeDefaultData(body.type);
65
+ const event = { type: body.type, data };
66
+ this.bus.emit(ws.id, event);
67
+ return { success: true, wsId: ws.id, event };
68
+ }
69
+ };
70
+ exports.WorkspaceEventsTestController = WorkspaceEventsTestController;
71
+ __decorate([
72
+ (0, common_1.Post)('emit'),
73
+ (0, common_1.HttpCode)(200),
74
+ __param(0, (0, common_1.Body)()),
75
+ __param(1, (0, workspace_decorator_js_1.Workspace)()),
76
+ __metadata("design:type", Function),
77
+ __metadata("design:paramtypes", [TestEmitBody, Object]),
78
+ __metadata("design:returntype", void 0)
79
+ ], WorkspaceEventsTestController.prototype, "emit", null);
80
+ exports.WorkspaceEventsTestController = WorkspaceEventsTestController = __decorate([
81
+ (0, common_1.Controller)('ws/:slug/test'),
82
+ __metadata("design:paramtypes", [workspace_event_bus_js_1.WorkspaceEventBus])
83
+ ], WorkspaceEventsTestController);
@@ -0,0 +1,47 @@
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
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.WorkspaceEventsController = void 0;
16
+ const common_1 = require("@nestjs/common");
17
+ const throttler_1 = require("@nestjs/throttler");
18
+ const rxjs_1 = require("rxjs");
19
+ const operators_1 = require("rxjs/operators");
20
+ const workspace_event_bus_js_1 = require("./workspace-event.bus.js");
21
+ const workspace_decorator_js_1 = require("../../common/decorators/workspace.decorator.js");
22
+ const KEEPALIVE_INTERVAL_MS = 15_000;
23
+ let WorkspaceEventsController = class WorkspaceEventsController {
24
+ bus;
25
+ constructor(bus) {
26
+ this.bus = bus;
27
+ }
28
+ /** GET /api/v1/ws/:slug/events — workspace-scoped SSE stream. */
29
+ stream(ws) {
30
+ const events$ = this.bus.for(ws.id).pipe((0, operators_1.map)((event) => ({ type: event.type, data: event.data })));
31
+ const ping$ = (0, rxjs_1.interval)(KEEPALIVE_INTERVAL_MS).pipe((0, operators_1.map)(() => ({ type: 'ping', data: {} })));
32
+ return (0, rxjs_1.merge)(events$, ping$);
33
+ }
34
+ };
35
+ exports.WorkspaceEventsController = WorkspaceEventsController;
36
+ __decorate([
37
+ (0, throttler_1.SkipThrottle)(),
38
+ (0, common_1.Sse)(),
39
+ __param(0, (0, workspace_decorator_js_1.Workspace)()),
40
+ __metadata("design:type", Function),
41
+ __metadata("design:paramtypes", [Object]),
42
+ __metadata("design:returntype", rxjs_1.Observable)
43
+ ], WorkspaceEventsController.prototype, "stream", null);
44
+ exports.WorkspaceEventsController = WorkspaceEventsController = __decorate([
45
+ (0, common_1.Controller)('ws/:slug/events'),
46
+ __metadata("design:paramtypes", [workspace_event_bus_js_1.WorkspaceEventBus])
47
+ ], WorkspaceEventsController);
@@ -23,6 +23,7 @@ let BrowserSessionStore = BrowserSessionStore_1 = class BrowserSessionStore {
23
23
  logger = new common_1.Logger(BrowserSessionStore_1.name);
24
24
  sessions = new Map();
25
25
  sweepTimer;
26
+ sseDisconnectListeners = new Map();
26
27
  constructor() {
27
28
  this.sweepTimer = setInterval(() => this.sweep(), SWEEP_INTERVAL_MS);
28
29
  this.sweepTimer.unref();
@@ -33,6 +34,7 @@ let BrowserSessionStore = BrowserSessionStore_1 = class BrowserSessionStore {
33
34
  this.destroySession(session);
34
35
  }
35
36
  this.sessions.clear();
37
+ this.sseDisconnectListeners.clear();
36
38
  }
37
39
  create(clientType) {
38
40
  const id = SESSION_PREFIX + (0, crypto_1.randomBytes)(SESSION_ID_BYTES).toString('hex');
@@ -100,6 +102,7 @@ let BrowserSessionStore = BrowserSessionStore_1 = class BrowserSessionStore {
100
102
  this.logger.debug(`SSE disconnected for session: ${sessionId}`);
101
103
  this.cleanupSse(session);
102
104
  this.rejectAllPending(session, new Error('SSE connection closed'));
105
+ this.fireSseDisconnectListeners(sessionId);
103
106
  });
104
107
  return true;
105
108
  }
@@ -117,6 +120,41 @@ let BrowserSessionStore = BrowserSessionStore_1 = class BrowserSessionStore {
117
120
  this.logger.debug(`Tools registered (${mode}) for session ${sessionId}: ${tools.map((t) => t.name).join(', ')}`);
118
121
  return true;
119
122
  }
123
+ /** Returns the most recently created browser session with an active SSE stream. */
124
+ getLatestBrowserSession() {
125
+ let latest;
126
+ for (const session of this.sessions.values()) {
127
+ if (session.clientType === 'browser' &&
128
+ session.sseResponse &&
129
+ !session.sseResponse.writableEnded) {
130
+ if (!latest || session.createdAt > latest.createdAt) {
131
+ latest = session;
132
+ }
133
+ }
134
+ }
135
+ return latest;
136
+ }
137
+ /** Push a fire-and-forget JSON-RPC notification to a session's SSE stream. */
138
+ pushNotification(sessionId, payload) {
139
+ const session = this.sessions.get(sessionId);
140
+ if (!session?.sseResponse || session.sseResponse.writableEnded)
141
+ return false;
142
+ session.sseResponse.write(`data: ${JSON.stringify(payload)}\n\n`);
143
+ session.lastActivity = Date.now();
144
+ return true;
145
+ }
146
+ /** Register a callback that fires when the SSE connection for sessionId closes. Returns an unregister fn. */
147
+ onSseDisconnect(sessionId, cb) {
148
+ let listeners = this.sseDisconnectListeners.get(sessionId);
149
+ if (!listeners) {
150
+ listeners = new Set();
151
+ this.sseDisconnectListeners.set(sessionId, listeners);
152
+ }
153
+ listeners.add(cb);
154
+ return () => {
155
+ this.sseDisconnectListeners.get(sessionId)?.delete(cb);
156
+ };
157
+ }
120
158
  getRegisteredTools(sessionId) {
121
159
  const session = this.sessions.get(sessionId);
122
160
  if (!session)
@@ -204,6 +242,18 @@ let BrowserSessionStore = BrowserSessionStore_1 = class BrowserSessionStore {
204
242
  pending.reject(error);
205
243
  return true;
206
244
  }
245
+ fireSseDisconnectListeners(sessionId) {
246
+ const listeners = this.sseDisconnectListeners.get(sessionId);
247
+ if (!listeners)
248
+ return;
249
+ for (const cb of listeners) {
250
+ try {
251
+ cb();
252
+ }
253
+ catch { }
254
+ }
255
+ this.sseDisconnectListeners.delete(sessionId);
256
+ }
207
257
  cleanupSse(session) {
208
258
  if (session.keepaliveTimer) {
209
259
  clearInterval(session.keepaliveTimer);
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CHROMEX_BLACKLIST_DOMAINS = void 0;
4
+ exports.isBlacklisted = isBlacklisted;
5
+ exports.CHROMEX_BLACKLIST_DOMAINS = [
6
+ // 금융
7
+ 'kbanknow.com', 'mybank.wooribank.com', 'banking.shinhan.com',
8
+ 'mybanking.kbstar.com', 'ibs.kbstar.com', 'ib.hana.co.kr',
9
+ // 이메일
10
+ 'mail.google.com', 'outlook.live.com', 'mail.naver.com',
11
+ // 인증
12
+ 'accounts.google.com', 'nid.naver.com', 'auth.kakao.com',
13
+ ];
14
+ function isBlacklisted(url) {
15
+ try {
16
+ let hostname = new URL(url).hostname.toLowerCase();
17
+ if (hostname.endsWith('.'))
18
+ hostname = hostname.slice(0, -1);
19
+ // normalize via URL constructor to handle IDN/punycode bypass attempts
20
+ hostname = new URL(`https://${hostname}`).hostname;
21
+ return exports.CHROMEX_BLACKLIST_DOMAINS.some((domain) => hostname === domain || hostname.endsWith(`.${domain}`));
22
+ }
23
+ catch {
24
+ return true; // fail-close on parse error
25
+ }
26
+ }