@lovelybunch/api 1.0.78-alpha.3 → 1.0.78-alpha.4

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 (141) hide show
  1. package/dist/lib/auth/auth-manager.d.ts +1 -1
  2. package/dist/lib/auth/auth-manager.js +2 -1
  3. package/dist/lib/symlinks/symlink-manager.d.ts +3 -4
  4. package/dist/lib/symlinks/symlink-manager.js +49 -30
  5. package/dist/lib/terminal/terminal-manager.d.ts +8 -5
  6. package/dist/lib/terminal/terminal-manager.js +29 -5
  7. package/dist/middleware/auth.d.ts +23 -2
  8. package/dist/middleware/auth.js +44 -4
  9. package/dist/routes/api/v1/ai/route.js +117 -4
  10. package/dist/routes/api/v1/api-keys/route.js +7 -1
  11. package/dist/routes/api/v1/jobs/[id]/route.d.ts +28 -28
  12. package/dist/routes/api/v1/jobs/route.d.ts +28 -28
  13. package/dist/routes/api/v1/knowledge/[filename]/route.js +29 -7
  14. package/dist/routes/api/v1/slack/route.d.ts +6 -6
  15. package/dist/routes/api/v1/terminal/[taskId]/create/route.js +2 -1
  16. package/dist/routes/api/v1/terminal/[taskId]/destroy/route.js +2 -1
  17. package/dist/routes/api/v1/terminal/[taskId]/resize/route.js +2 -1
  18. package/dist/routes/api/v1/terminal/code/route.js +2 -1
  19. package/dist/routes/api/v1/terminal/sessions/route.js +5 -3
  20. package/dist/server-with-static.js +3 -3
  21. package/dist/server.js +3 -3
  22. package/package.json +4 -4
  23. package/static/assets/{ActivityPage-CO2m97Hz.js → ActivityPage-CtFAFqlg.js} +1 -1
  24. package/static/assets/{AgentsContextEditPage-B9pcC3Y4.js → AgentsContextEditPage-CsRJvzE6.js} +1 -1
  25. package/static/assets/{AgentsContextPage-CWsGDD4F.js → AgentsContextPage-CWCs8CgG.js} +1 -1
  26. package/static/assets/{ApiKeysSettingsPage-CPckUylF.js → ApiKeysSettingsPage-B4b_fmJg.js} +1 -1
  27. package/static/assets/{AuthSettingsPage-CaiYl7fx.js → AuthSettingsPage-O3UdLrHC.js} +1 -1
  28. package/static/assets/{CallbackPage-C73ggs1e.js → CallbackPage-WTwVSxdx.js} +1 -1
  29. package/static/assets/{CoconutCallbackPage-Bhc_sIuJ.js → CoconutCallbackPage-LEWgUV8J.js} +1 -1
  30. package/static/assets/{CodePage-7zrLYTBq.js → CodePage-DpVmg_UD.js} +1 -1
  31. package/static/assets/{CollapsibleSection-DD6cnVaM.js → CollapsibleSection-BHag26Vf.js} +1 -1
  32. package/static/assets/{DashboardPage-BiVDSLAm.js → DashboardPage-BZnCikpf.js} +1 -1
  33. package/static/assets/{GitPage-C7n82d8O.js → GitPage-DD6dgeNu.js} +1 -1
  34. package/static/assets/{GitSettingsPage-D2R-zg9d.js → GitSettingsPage-CLHCZ6at.js} +1 -1
  35. package/static/assets/{IdentityPage-B3gUyFXr.js → IdentityPage-DtzLSTYd.js} +1 -1
  36. package/static/assets/{ImplementationStepsEditor-DmrYGdZB.js → ImplementationStepsEditor-Ca1aygKs.js} +1 -1
  37. package/static/assets/{IntegrationsSettingsPage-V6t85OD_.js → IntegrationsSettingsPage-C2tzpbuI.js} +1 -1
  38. package/static/assets/{JobDetailPage-BUmQGshi.js → JobDetailPage-B03z4VgG.js} +1 -1
  39. package/static/assets/{KnowledgeDetailPage-DzpoOs9C.js → KnowledgeDetailPage-XslT5GUv.js} +1 -1
  40. package/static/assets/{KnowledgeEditPage-C9zyCfr6.js → KnowledgeEditPage-5NFEnGJh.js} +1 -1
  41. package/static/assets/{KnowledgePage-C56_NMA9.js → KnowledgePage-Dzh409qU.js} +1 -1
  42. package/static/assets/{LoginPage-DZpAjZmK.js → LoginPage-IDVkR1zN.js} +1 -1
  43. package/static/assets/{MailInboxPage-BvOBQvF3.js → MailInboxPage-CXaBqNl2.js} +1 -1
  44. package/static/assets/{MailProcessingModal-CCGrVGDs.js → MailProcessingModal-BWyzTMEv.js} +1 -1
  45. package/static/assets/{MailReadPage-CycqG4oj.js → MailReadPage-DPGuT0gC.js} +1 -1
  46. package/static/assets/{MailSentPage-DpwRNrju.js → MailSentPage-Dtq37Kap.js} +1 -1
  47. package/static/assets/{McpSettingsPage-pJ41THOO.js → McpSettingsPage-BiCSPaAJ.js} +1 -1
  48. package/static/assets/{MemoryEditPage-DobGCRrl.js → MemoryEditPage-Dfa6MsnE.js} +1 -1
  49. package/static/assets/{MemoryPage-BrFnh8rC.js → MemoryPage-BqUngqIS.js} +1 -1
  50. package/static/assets/{NewKnowledgePage-CTFx_YTk.js → NewKnowledgePage-BmYwcO98.js} +1 -1
  51. package/static/assets/{NewSkillPage-D_FfkSKi.js → NewSkillPage-BfNaXH37.js} +1 -1
  52. package/static/assets/{NewTaskPage-CehAYsyJ.js → NewTaskPage-Ctmh2uiJ.js} +1 -1
  53. package/static/assets/{NotFoundPage-wcJQI6_Y.js → NotFoundPage-a2erdq4H.js} +1 -1
  54. package/static/assets/{NotificationsSettingsPage-_9Dum2JJ.js → NotificationsSettingsPage-BB9QuyUf.js} +1 -1
  55. package/static/assets/{PromptsSettingsPage-BBIP8PWS.js → PromptsSettingsPage-BZLwTFcd.js} +1 -1
  56. package/static/assets/{ResourceDetailPage-C7goHk6o.js → ResourceDetailPage-DRWNjykw.js} +1 -1
  57. package/static/assets/{ResourcesPage-ZH5lSPaG.js → ResourcesPage-C3Lx8hz_.js} +1 -1
  58. package/static/assets/{RoleEditPage-JJnQ7UXW.js → RoleEditPage-W9SV6Mwr.js} +1 -1
  59. package/static/assets/{RolePage-C-gQnpzL.js → RolePage-DWWo77tA.js} +1 -1
  60. package/static/assets/{RulesSettingsPage-Bc05Q6j8.js → RulesSettingsPage-BKsadZ7r.js} +1 -1
  61. package/static/assets/{RunDetailPage-BhWthPVR.js → RunDetailPage-C6miynGX.js} +1 -1
  62. package/static/assets/{SchedulePage-zvr5ce8N.js → SchedulePage-B6s-CkVg.js} +1 -1
  63. package/static/assets/{SkillDetailPage-5DhKM3NY.js → SkillDetailPage-DYkLxRjB.js} +1 -1
  64. package/static/assets/{SkillEditPage-1V1ADtPe.js → SkillEditPage-DIB2lUMz.js} +1 -1
  65. package/static/assets/{SkillsPage-6VQCj1hf.js → SkillsPage-BgR2gdCy.js} +1 -1
  66. package/static/assets/{SkillsSettingsPage-C7lqSbBf.js → SkillsSettingsPage-CoDOG0YQ.js} +1 -1
  67. package/static/assets/{SourceInput-CeltF2kQ.js → SourceInput-CS6u1yPi.js} +1 -1
  68. package/static/assets/{TagInput-DUR8EcF5.js → TagInput-C3WzbX6I.js} +1 -1
  69. package/static/assets/{TaskDetailPage-HqNWYvGE.js → TaskDetailPage-DEBBE0D8.js} +1 -1
  70. package/static/assets/{TaskEditPage-Cy6_oKRU.js → TaskEditPage-B26tEsUP.js} +1 -1
  71. package/static/assets/{TasksPage-DHdwWucP.js → TasksPage-pqQ7VlC3.js} +1 -1
  72. package/static/assets/{TeamEditPage-DZHs9LwL.js → TeamEditPage-C6DugOii.js} +1 -1
  73. package/static/assets/{TeamPage-C10th6Xi.js → TeamPage-BDqQZ6Rr.js} +1 -1
  74. package/static/assets/{TerminalPage-BajLCwBP.js → TerminalPage-pjss7X1h.js} +1 -1
  75. package/static/assets/{TerminalSessionPage-dJGlApIY.js → TerminalSessionPage-C9sJ_13t.js} +1 -1
  76. package/static/assets/{UserPreferencesPage-crzoMvn3.js → UserPreferencesPage-BrpKf14p.js} +1 -1
  77. package/static/assets/{UserSettingsPage-B4eeW2b9.js → UserSettingsPage-BbazpvdQ.js} +1 -1
  78. package/static/assets/{UtilitiesPage-ZnNRIlEg.js → UtilitiesPage-niclH8kb.js} +1 -1
  79. package/static/assets/{alert-a0XoDY__.js → alert-DBhIskRX.js} +1 -1
  80. package/static/assets/{arrow-down-DwCDIVHH.js → arrow-down-TpeNydxP.js} +1 -1
  81. package/static/assets/{arrow-left-D4GyWema.js → arrow-left-_lXkdm9Q.js} +1 -1
  82. package/static/assets/{arrow-up-ClZJeJXS.js → arrow-up-DaIMsmSO.js} +1 -1
  83. package/static/assets/{arrow-up-down-ozcTje6W.js → arrow-up-down-C7wqirJv.js} +1 -1
  84. package/static/assets/{badge-CgbZdX-R.js → badge-TeAh_Zh0.js} +1 -1
  85. package/static/assets/{browser-modal-CV9x_ILy.js → browser-modal-BpGFpZZj.js} +1 -1
  86. package/static/assets/{card-bSzcwVEz.js → card-CiLixwCG.js} +1 -1
  87. package/static/assets/{chevron-left-Dnm2Bv2g.js → chevron-left-CuMfw52D.js} +1 -1
  88. package/static/assets/{chevron-up-znuSUWjo.js → chevron-up-B4J4D5aE.js} +1 -1
  89. package/static/assets/{chevrons-up-DyHcV7KP.js → chevrons-up-Dcs-Dj4B.js} +1 -1
  90. package/static/assets/{circle-alert-Dn8B5NPI.js → circle-alert-nI75emvr.js} +1 -1
  91. package/static/assets/{circle-check-big-HUp82Tz-.js → circle-check-big-CxxxVvOw.js} +1 -1
  92. package/static/assets/{circle-check-2Hh5KJn8.js → circle-check-k6ER9afD.js} +1 -1
  93. package/static/assets/{circle-play-BG4sTHV2.js → circle-play-B4D0d7YW.js} +1 -1
  94. package/static/assets/{circle-x-DBrbnMPA.js → circle-x-BzPgPb47.js} +1 -1
  95. package/static/assets/{clipboard-B6OwO-az.js → clipboard-CLObCrC4.js} +1 -1
  96. package/static/assets/{clock-B36ytcOX.js → clock-BK-8XMqv.js} +1 -1
  97. package/static/assets/{code-CK8i3Xlg.js → code-lhIVzb1q.js} +1 -1
  98. package/static/assets/{download-C_C3f7cE.js → download-ChgFRTfC.js} +1 -1
  99. package/static/assets/{external-link-kQIXuJh_.js → external-link-D4kianlP.js} +1 -1
  100. package/static/assets/{eye-DTyfbeQg.js → eye-H1JnZ9E9.js} +1 -1
  101. package/static/assets/{folder-git-2-CKR2HcNZ.js → folder-git-2-BZGvXcyj.js} +1 -1
  102. package/static/assets/{globe--bHEtjre.js → globe-DAzCOUla.js} +1 -1
  103. package/static/assets/{index-CJBq79WW.js → index-B7UEVagI.js} +1 -1
  104. package/static/assets/{index-CRSpx2tQ.js → index-Bik-wLXi.js} +1 -1
  105. package/static/assets/{index-ObZJu-Zv.js → index-BlOxV6pf.js} +1 -1
  106. package/static/assets/{index-QgKSm0gN.js → index-C7dpn8dK.js} +1 -1
  107. package/static/assets/{index-ffbhzKxY.js → index-C9wsYS8C.js} +1 -1
  108. package/static/assets/{index-9Y0fmmy7.js → index-Ck9sMDuo.js} +1 -1
  109. package/static/assets/{index-CvrVx0kf.js → index-DMeIDO6W.js} +1 -1
  110. package/static/assets/{index-Bg80D0Z0.js → index-DOpjlxVe.js} +1 -1
  111. package/static/assets/{index-nRow2q03.js → index-DT4K0HgO.js} +1 -1
  112. package/static/assets/{index-DMKQFm-M.js → index-DeF-CCVg.js} +1 -1
  113. package/static/assets/{index-CTZ51lNe.js → index-DkvhkCo1.js} +1 -1
  114. package/static/assets/{index-DXkvtMiO.js → index-DuDW1LwX.js} +1 -1
  115. package/static/assets/{index-DOgqHj1l.js → index-Dy_-mpPq.js} +1 -1
  116. package/static/assets/{index-B81KYk3Z.js → index-SUQrIHd7.js} +6 -6
  117. package/static/assets/{index-UKy_jk8q.js → index-c-gr7Bew.js} +1 -1
  118. package/static/assets/{index-C_zW1Hi0.js → index-kJaEkklL.js} +1 -1
  119. package/static/assets/{index-XJ0Ck2RG.js → index-rslXoTnS.js} +1 -1
  120. package/static/assets/{index-CD6zflLk.js → index-rwsZ-IsL.js} +1 -1
  121. package/static/assets/{index-B52hVXHu.js → index-sBbL8_xu.js} +1 -1
  122. package/static/assets/{info-wSiZMXgL.js → info-CVmPHERQ.js} +1 -1
  123. package/static/assets/{label-CHP4q6u3.js → label-n5pqaObd.js} +1 -1
  124. package/static/assets/{markdown-editor-DSI9T1tt.js → markdown-editor-Cvh5pZzJ.js} +3 -3
  125. package/static/assets/{message-square-D1sMYX2G.js → message-square-BmnqRsu6.js} +1 -1
  126. package/static/assets/{paperclip-plxfUm0D.js → paperclip-BwcNeQ_C.js} +1 -1
  127. package/static/assets/{pause-B2JeikEI.js → pause-CUswidZW.js} +1 -1
  128. package/static/assets/{play-l7Mu5YPO.js → play-DyZbNGiK.js} +1 -1
  129. package/static/assets/{radio-group-DxYoqBDN.js → radio-group-Bzu_d7xN.js} +1 -1
  130. package/static/assets/{refresh-cw-BTHxZiES.js → refresh-cw-DvQ0rmk7.js} +1 -1
  131. package/static/assets/{search-Cpy49h7H.js → search-kc_tG8Rf.js} +1 -1
  132. package/static/assets/{select-CG6kgyUu.js → select-CIOYQA7a.js} +1 -1
  133. package/static/assets/{server-DzOL5RbP.js → server-CWIJxRf0.js} +1 -1
  134. package/static/assets/{switch-7YpWWyAq.js → switch-DczkR77Y.js} +1 -1
  135. package/static/assets/{tabs-DJk1ZrWh.js → tabs-DOEakD76.js} +1 -1
  136. package/static/assets/{tag-BvoXF-sG.js → tag-Dc8sXXlM.js} +1 -1
  137. package/static/assets/{terminal-preview-d_BZ8s8s.js → terminal-preview-D8oASUdZ.js} +1 -1
  138. package/static/assets/{triangle-alert-P6aKUDal.js → triangle-alert-C-5ixej6.js} +1 -1
  139. package/static/assets/{use-terminal-BXYq-vfY.js → use-terminal-xdcf4Z2G.js} +1 -1
  140. package/static/assets/{video-PACu7StA.js → video-CBneSE9B.js} +1 -1
  141. package/static/index.html +1 -1
@@ -98,7 +98,7 @@ export declare class AuthManager {
98
98
  /**
99
99
  * Create API key
100
100
  */
101
- createApiKey(name: string, createdBy: string, scopes: string[], expiresIn?: string): Promise<{
101
+ createApiKey(name: string, createdBy: string, scopes: string[], expiresIn?: string, createdByRole?: UserRole): Promise<{
102
102
  apiKey: ApiKey;
103
103
  rawKey: string;
104
104
  }>;
@@ -340,7 +340,7 @@ export class AuthManager {
340
340
  /**
341
341
  * Create API key
342
342
  */
343
- async createApiKey(name, createdBy, scopes, expiresIn) {
343
+ async createApiKey(name, createdBy, scopes, expiresIn, createdByRole) {
344
344
  const config = await this.loadAuthConfig();
345
345
  const rawKey = this.generateApiKey();
346
346
  const hashedKey = await bcrypt.hash(rawKey, SALT_ROUNDS);
@@ -350,6 +350,7 @@ export class AuthManager {
350
350
  key: hashedKey,
351
351
  keyPreview: rawKey.slice(-4),
352
352
  createdBy,
353
+ createdByRole,
353
354
  createdAt: new Date(),
354
355
  expiresAt: expiresIn ? this.calculateExpiry(expiresIn) : undefined,
355
356
  scopes,
@@ -5,6 +5,7 @@ import { SymlinkConfig, SymlinkOperationResult } from './types.js';
5
5
  export declare class SymlinkManager {
6
6
  private configPath;
7
7
  private projectRoot;
8
+ private resolvedProjectRoot;
8
9
  private state;
9
10
  constructor(projectRoot: string);
10
11
  /**
@@ -39,10 +40,8 @@ export declare class SymlinkManager {
39
40
  * Toggle a symlink on/off
40
41
  */
41
42
  toggleSymlink(id: string): Promise<SymlinkOperationResult>;
42
- /**
43
- * Expand tilde in path to actual home directory
44
- */
45
- private expandTilde;
43
+ private validateConfiguredPaths;
44
+ private resolveProjectPath;
46
45
  /**
47
46
  * Create a symlink on the filesystem
48
47
  */
@@ -1,16 +1,17 @@
1
1
  import { promises as fs } from 'fs';
2
2
  import path from 'path';
3
- import os from 'os';
4
3
  /**
5
4
  * SymlinkManager handles all symlink operations and persists state
6
5
  */
7
6
  export class SymlinkManager {
8
7
  configPath;
9
8
  projectRoot;
9
+ resolvedProjectRoot;
10
10
  state;
11
11
  constructor(projectRoot) {
12
12
  this.projectRoot = projectRoot;
13
- this.configPath = path.join(projectRoot, '.nut', 'symlinks.json');
13
+ this.resolvedProjectRoot = path.resolve(projectRoot);
14
+ this.configPath = path.join(this.resolvedProjectRoot, '.nut', 'symlinks.json');
14
15
  this.state = {
15
16
  version: '1.0.0',
16
17
  symlinks: []
@@ -24,7 +25,7 @@ export class SymlinkManager {
24
25
  console.log(`[SymlinkManager] Initializing with project root: ${this.projectRoot}`);
25
26
  console.log(`[SymlinkManager] Config path: ${this.configPath}`);
26
27
  // Ensure .nut directory exists
27
- const nutDir = path.join(this.projectRoot, '.nut');
28
+ const nutDir = path.join(this.resolvedProjectRoot, '.nut');
28
29
  await fs.mkdir(nutDir, { recursive: true });
29
30
  // Try to load existing configuration
30
31
  try {
@@ -83,11 +84,17 @@ export class SymlinkManager {
83
84
  */
84
85
  async validateSymlinks() {
85
86
  for (const symlink of this.state.symlinks) {
86
- // Expand tilde and resolve path
87
- const expandedLinkPath = this.expandTilde(symlink.linkPath);
88
- const fullLinkPath = path.isAbsolute(expandedLinkPath)
89
- ? expandedLinkPath
90
- : path.join(this.projectRoot, expandedLinkPath);
87
+ let fullLinkPath;
88
+ try {
89
+ fullLinkPath = this.resolveProjectPath(symlink.linkPath);
90
+ }
91
+ catch {
92
+ if (symlink.isActive) {
93
+ symlink.isActive = false;
94
+ symlink.updatedAt = new Date().toISOString();
95
+ }
96
+ continue;
97
+ }
91
98
  try {
92
99
  const stats = await fs.lstat(fullLinkPath);
93
100
  const actuallyActive = stats.isSymbolicLink();
@@ -146,6 +153,7 @@ export class SymlinkManager {
146
153
  */
147
154
  async addSymlink(config) {
148
155
  try {
156
+ this.validateConfiguredPaths(config.linkPath, config.targetPath);
149
157
  // Check if link path already exists
150
158
  if (this.state.symlinks.some(s => s.linkPath === config.linkPath)) {
151
159
  return {
@@ -201,14 +209,26 @@ export class SymlinkManager {
201
209
  return await this.createSymlink(id);
202
210
  }
203
211
  }
204
- /**
205
- * Expand tilde in path to actual home directory
206
- */
207
- expandTilde(filepath) {
208
- if (filepath.startsWith('~/')) {
209
- return path.join(os.homedir(), filepath.slice(2));
212
+ validateConfiguredPaths(linkPath, targetPath) {
213
+ this.resolveProjectPath(linkPath);
214
+ this.resolveProjectPath(targetPath);
215
+ }
216
+ resolveProjectPath(configuredPath) {
217
+ if (!configuredPath || typeof configuredPath !== 'string') {
218
+ throw new Error('Path is required');
219
+ }
220
+ if (configuredPath.includes('\0')) {
221
+ throw new Error('Path contains invalid characters');
210
222
  }
211
- return filepath;
223
+ if (path.isAbsolute(configuredPath) || configuredPath === '~' || configuredPath.startsWith('~/')) {
224
+ throw new Error('Symlink paths must be relative to the project root');
225
+ }
226
+ const resolved = path.resolve(this.resolvedProjectRoot, configuredPath);
227
+ const relative = path.relative(this.resolvedProjectRoot, resolved);
228
+ if (relative === '' || relative.startsWith('..') || path.isAbsolute(relative)) {
229
+ throw new Error('Symlink paths must stay inside the project root');
230
+ }
231
+ return resolved;
212
232
  }
213
233
  /**
214
234
  * Create a symlink on the filesystem
@@ -222,16 +242,8 @@ export class SymlinkManager {
222
242
  };
223
243
  }
224
244
  try {
225
- // Expand tilde in paths and resolve them
226
- const expandedLinkPath = this.expandTilde(symlink.linkPath);
227
- const expandedTargetPath = this.expandTilde(symlink.targetPath);
228
- // If paths are relative (not starting with / or ~), make them relative to project root
229
- const fullLinkPath = path.isAbsolute(expandedLinkPath)
230
- ? expandedLinkPath
231
- : path.join(this.projectRoot, expandedLinkPath);
232
- const fullTargetPath = path.isAbsolute(expandedTargetPath)
233
- ? expandedTargetPath
234
- : path.join(this.projectRoot, expandedTargetPath);
245
+ const fullLinkPath = this.resolveProjectPath(symlink.linkPath);
246
+ const fullTargetPath = this.resolveProjectPath(symlink.targetPath);
235
247
  // Ensure target exists
236
248
  try {
237
249
  await fs.access(fullTargetPath);
@@ -249,6 +261,13 @@ Created: ${new Date().toISOString()}
249
261
  }
250
262
  // Remove existing file/symlink if it exists
251
263
  try {
264
+ const existingStats = await fs.lstat(fullLinkPath);
265
+ if (!existingStats.isSymbolicLink()) {
266
+ return {
267
+ success: false,
268
+ message: `Cannot replace non-symlink path ${symlink.linkPath}`,
269
+ };
270
+ }
252
271
  await fs.unlink(fullLinkPath);
253
272
  }
254
273
  catch (error) {
@@ -256,6 +275,7 @@ Created: ${new Date().toISOString()}
256
275
  throw error;
257
276
  }
258
277
  // Create the symlink (use relative path for portability)
278
+ await fs.mkdir(path.dirname(fullLinkPath), { recursive: true });
259
279
  const relativePath = path.relative(path.dirname(fullLinkPath), fullTargetPath);
260
280
  await fs.symlink(relativePath, fullLinkPath);
261
281
  symlink.isActive = true;
@@ -287,11 +307,7 @@ Created: ${new Date().toISOString()}
287
307
  };
288
308
  }
289
309
  try {
290
- // Expand tilde and resolve path
291
- const expandedLinkPath = this.expandTilde(symlink.linkPath);
292
- const fullLinkPath = path.isAbsolute(expandedLinkPath)
293
- ? expandedLinkPath
294
- : path.join(this.projectRoot, expandedLinkPath);
310
+ const fullLinkPath = this.resolveProjectPath(symlink.linkPath);
295
311
  try {
296
312
  const stats = await fs.lstat(fullLinkPath);
297
313
  if (stats.isSymbolicLink()) {
@@ -354,6 +370,9 @@ Created: ${new Date().toISOString()}
354
370
  };
355
371
  }
356
372
  try {
373
+ const nextLinkPath = updates.linkPath ?? symlink.linkPath;
374
+ const nextTargetPath = updates.targetPath ?? symlink.targetPath;
375
+ this.validateConfiguredPaths(nextLinkPath, nextTargetPath);
357
376
  // If changing paths and symlink is active, need to recreate
358
377
  if (symlink.isActive && (updates.linkPath || updates.targetPath)) {
359
378
  await this.removeSymlink(id);
@@ -3,6 +3,7 @@ import { ShellType } from './shell-utils.js';
3
3
  export interface TerminalSession {
4
4
  id: string;
5
5
  taskId: string;
6
+ ownerId?: string;
6
7
  pty: any;
7
8
  websocket?: WebSocket;
8
9
  createdAt: Date;
@@ -19,15 +20,17 @@ export declare class TerminalManager {
19
20
  private cleanupInterval;
20
21
  private static readonly MAX_BACKLOG_BYTES;
21
22
  constructor();
22
- createSession(taskId: string, shellPreference?: ShellType, startupCommand?: string): Promise<TerminalSession>;
23
+ createSession(taskId: string, shellPreference?: ShellType, startupCommand?: string, ownerId?: string): Promise<TerminalSession>;
23
24
  getSession(sessionId: string): TerminalSession | undefined;
24
25
  getSessionsByTask(taskId: string): TerminalSession[];
25
- attachWebSocket(sessionId: string, ws: WebSocket): boolean;
26
- destroySession(sessionId: string): boolean;
27
- resizeSession(sessionId: string, cols: number, rows: number): boolean;
26
+ getSessionsForOwner(ownerId: string): TerminalSession[];
27
+ canAccessSession(session: TerminalSession | undefined, ownerId: string): boolean;
28
+ attachWebSocket(sessionId: string, ws: WebSocket, ownerId: string): boolean;
29
+ destroySession(sessionId: string, ownerId?: string): boolean;
30
+ resizeSession(sessionId: string, cols: number, rows: number, ownerId?: string): boolean;
28
31
  private cleanupInactiveSessions;
29
32
  getAllSessions(): TerminalSession[];
30
33
  destroy(): void;
31
34
  private enqueuePreviewBroadcast;
32
- attachPreviewWebSocket(sessionId: string, ws: WebSocket): boolean;
35
+ attachPreviewWebSocket(sessionId: string, ws: WebSocket, ownerId: string): boolean;
33
36
  }
@@ -44,7 +44,7 @@ export class TerminalManager {
44
44
  this.cleanupInactiveSessions();
45
45
  }, 5 * 60 * 1000);
46
46
  }
47
- async createSession(taskId, shellPreference = 'bash', startupCommand) {
47
+ async createSession(taskId, shellPreference = 'bash', startupCommand, ownerId) {
48
48
  if (!spawnHelperFixed) {
49
49
  ensureSpawnHelperPermissions();
50
50
  spawnHelperFixed = true;
@@ -133,6 +133,7 @@ export class TerminalManager {
133
133
  const session = {
134
134
  id: sessionId,
135
135
  taskId,
136
+ ownerId,
136
137
  pty: ptyProcess,
137
138
  createdAt: new Date(),
138
139
  lastActivity: new Date(),
@@ -244,8 +245,23 @@ export class TerminalManager {
244
245
  getSessionsByTask(taskId) {
245
246
  return Array.from(this.sessions.values()).filter(session => session.taskId === taskId);
246
247
  }
247
- attachWebSocket(sessionId, ws) {
248
+ getSessionsForOwner(ownerId) {
249
+ return Array.from(this.sessions.values()).filter(session => this.canAccessSession(session, ownerId));
250
+ }
251
+ canAccessSession(session, ownerId) {
252
+ if (!session) {
253
+ return false;
254
+ }
255
+ if (ownerId === 'localhost:localhost' || ownerId === 'anonymous:anonymous') {
256
+ return true;
257
+ }
258
+ return session.ownerId === ownerId;
259
+ }
260
+ attachWebSocket(sessionId, ws, ownerId) {
248
261
  const session = this.sessions.get(sessionId);
262
+ if (!this.canAccessSession(session, ownerId)) {
263
+ return false;
264
+ }
249
265
  if (!session) {
250
266
  return false;
251
267
  }
@@ -318,11 +334,14 @@ export class TerminalManager {
318
334
  });
319
335
  return true;
320
336
  }
321
- destroySession(sessionId) {
337
+ destroySession(sessionId, ownerId) {
322
338
  const session = this.sessions.get(sessionId);
323
339
  if (!session) {
324
340
  return false;
325
341
  }
342
+ if (ownerId && !this.canAccessSession(session, ownerId)) {
343
+ return false;
344
+ }
326
345
  // Clear main WebSocket ping interval
327
346
  if (session.pingInterval) {
328
347
  clearInterval(session.pingInterval);
@@ -360,11 +379,14 @@ export class TerminalManager {
360
379
  this.sessions.delete(sessionId);
361
380
  return true;
362
381
  }
363
- resizeSession(sessionId, cols, rows) {
382
+ resizeSession(sessionId, cols, rows, ownerId) {
364
383
  const session = this.sessions.get(sessionId);
365
384
  if (!session) {
366
385
  return false;
367
386
  }
387
+ if (ownerId && !this.canAccessSession(session, ownerId)) {
388
+ return false;
389
+ }
368
390
  try {
369
391
  session.pty.resize(cols, rows);
370
392
  session.lastActivity = new Date();
@@ -441,8 +463,10 @@ export class TerminalManager {
441
463
  }
442
464
  catch { }
443
465
  }
444
- attachPreviewWebSocket(sessionId, ws) {
466
+ attachPreviewWebSocket(sessionId, ws, ownerId) {
445
467
  const session = this.sessions.get(sessionId);
468
+ if (!this.canAccessSession(session, ownerId))
469
+ return false;
446
470
  if (!session)
447
471
  return false;
448
472
  if (!session.previewSockets)
@@ -1,5 +1,24 @@
1
1
  import { Context, Next } from 'hono';
2
2
  import { AuthSession } from '@lovelybunch/types';
3
+ import type { ApiKey } from '@lovelybunch/types';
4
+ export type AuthPrincipal = {
5
+ type: 'session';
6
+ id: string;
7
+ role: AuthSession['role'];
8
+ session: AuthSession;
9
+ } | {
10
+ type: 'apiKey';
11
+ id: string;
12
+ role?: AuthSession['role'];
13
+ apiKey: ApiKey;
14
+ } | {
15
+ type: 'localhost';
16
+ id: 'localhost';
17
+ role: 'admin';
18
+ } | {
19
+ type: 'anonymous';
20
+ id: 'anonymous';
21
+ };
3
22
  /**
4
23
  * Authentication middleware
5
24
  * Checks for valid JWT token in cookie or Authorization header
@@ -7,10 +26,10 @@ import { AuthSession } from '@lovelybunch/types';
7
26
  export declare function authMiddleware(c: Context, next: Next): Promise<void | (Response & import("hono").TypedResponse<{
8
27
  error: string;
9
28
  message: string;
10
- }, 401, "json">) | (Response & import("hono").TypedResponse<{
29
+ }, 403, "json">) | (Response & import("hono").TypedResponse<{
11
30
  error: string;
12
31
  message: string;
13
- }, 403, "json">)>;
32
+ }, 401, "json">)>;
14
33
  /**
15
34
  * Helper to get current session from context
16
35
  */
@@ -25,6 +44,8 @@ export declare function isAdmin(c: Context): boolean;
25
44
  * Also accepts API key authentication
26
45
  */
27
46
  export declare function requireAuth(c: Context): AuthSession | null;
47
+ export declare function getAuthPrincipal(c: Context): AuthPrincipal;
48
+ export declare function getAuthPrincipalKey(c: Context): string;
28
49
  /**
29
50
  * Helper to require admin access (throws if not admin)
30
51
  * Returns null if auth is disabled
@@ -54,6 +54,9 @@ function isPublicRoute(path) {
54
54
  function isAdminRoute(path) {
55
55
  return ADMIN_ROUTES.some((route) => path.startsWith(route));
56
56
  }
57
+ function apiKeyHasAdminAccess(apiKey) {
58
+ return apiKey.createdByRole === 'admin' && apiKey.scopes.some((scope) => (scope === 'admin' || scope === 'admin:all' || scope.startsWith('admin:')));
59
+ }
57
60
  /**
58
61
  * Authentication middleware
59
62
  * Checks for valid JWT token in cookie or Authorization header
@@ -103,6 +106,9 @@ export async function authMiddleware(c, next) {
103
106
  if (apiKey) {
104
107
  const validApiKey = await authManager.verifyApiKey(apiKey);
105
108
  if (validApiKey) {
109
+ if (isAdminRoute(path) && !apiKeyHasAdminAccess(validApiKey)) {
110
+ return c.json({ error: 'Forbidden', message: 'Admin access required' }, 403);
111
+ }
106
112
  // Store API key info in context
107
113
  c.set('apiKey', validApiKey);
108
114
  c.set('authType', 'apikey');
@@ -197,6 +203,34 @@ export function requireAuth(c) {
197
203
  }
198
204
  throw new Error('Authentication required');
199
205
  }
206
+ export function getAuthPrincipal(c) {
207
+ const authEnabled = c.get('authEnabled');
208
+ if (authEnabled === false) {
209
+ return { type: 'anonymous', id: 'anonymous' };
210
+ }
211
+ const session = getSession(c);
212
+ if (session) {
213
+ return { type: 'session', id: session.userId, role: session.role, session };
214
+ }
215
+ const authType = c.get('authType');
216
+ if (authType === 'apikey') {
217
+ const apiKey = c.get('apiKey');
218
+ if (apiKey) {
219
+ return { type: 'apiKey', id: apiKey.createdBy || apiKey.id, role: apiKey.createdByRole, apiKey };
220
+ }
221
+ }
222
+ if (authType === 'localhost') {
223
+ return { type: 'localhost', id: 'localhost', role: 'admin' };
224
+ }
225
+ return { type: 'anonymous', id: 'anonymous' };
226
+ }
227
+ export function getAuthPrincipalKey(c) {
228
+ const principal = getAuthPrincipal(c);
229
+ if (principal.type === 'session' || principal.type === 'apiKey') {
230
+ return `user:${principal.id}`;
231
+ }
232
+ return `${principal.type}:${principal.id}`;
233
+ }
200
234
  /**
201
235
  * Helper to require admin access (throws if not admin)
202
236
  * Returns null if auth is disabled
@@ -207,13 +241,19 @@ export function requireAdmin(c) {
207
241
  if (authEnabled === false) {
208
242
  return null;
209
243
  }
210
- // Trusted non-session authenticators (direct localhost connection, valid API
211
- // key) are authoritative: they've already passed auth at the middleware
212
- // layer, so admin-gated routes accept them without a session.role check.
244
+ // Direct localhost connections are trusted because the middleware only marks
245
+ // raw, unproxied loopback sockets this way.
213
246
  const authType = c.get('authType');
214
- if (authType === 'localhost' || authType === 'apikey') {
247
+ if (authType === 'localhost') {
215
248
  return null;
216
249
  }
250
+ const apiKey = c.get('apiKey');
251
+ if (authType === 'apikey' && apiKey && apiKeyHasAdminAccess(apiKey)) {
252
+ return null;
253
+ }
254
+ if (authType === 'apikey') {
255
+ throw new Error('Admin access required');
256
+ }
217
257
  const session = requireAuth(c);
218
258
  if (!session) {
219
259
  throw new Error('Authentication required');
@@ -1,4 +1,6 @@
1
1
  import { createHash } from 'node:crypto';
2
+ import { lookup } from 'node:dns/promises';
3
+ import { isIP } from 'node:net';
2
4
  import { homedir } from 'os';
3
5
  import { join, resolve as pathResolve, basename } from 'path';
4
6
  import { existsSync, readFileSync, promises as fs, createReadStream } from 'fs';
@@ -780,6 +782,118 @@ function getInternalApiBase() {
780
782
  const port = process.env.PORT ? parseInt(process.env.PORT) : 3001;
781
783
  return `http://127.0.0.1:${port}`;
782
784
  }
785
+ const MAX_EXTERNAL_RESOURCE_BYTES = 20 * 1024 * 1024;
786
+ const EXTERNAL_FETCH_TIMEOUT_MS = 10_000;
787
+ const MAX_EXTERNAL_REDIRECTS = 3;
788
+ function isPrivateIpAddress(address) {
789
+ const normalized = address.toLowerCase().split('%')[0];
790
+ const ipv4Mapped = normalized.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/);
791
+ const ip = ipv4Mapped ? ipv4Mapped[1] : normalized;
792
+ const version = isIP(ip);
793
+ if (version === 4) {
794
+ const octets = ip.split('.').map(Number);
795
+ const [a, b] = octets;
796
+ return (a === 0 ||
797
+ a === 10 ||
798
+ a === 127 ||
799
+ (a === 100 && b >= 64 && b <= 127) ||
800
+ (a === 169 && b === 254) ||
801
+ (a === 172 && b >= 16 && b <= 31) ||
802
+ (a === 192 && b === 168) ||
803
+ (a === 198 && (b === 18 || b === 19)) ||
804
+ a >= 224);
805
+ }
806
+ if (version === 6) {
807
+ return (ip === '::' ||
808
+ ip === '::1' ||
809
+ ip.startsWith('fc') ||
810
+ ip.startsWith('fd') ||
811
+ ip.startsWith('fe80') ||
812
+ ip.startsWith('ff'));
813
+ }
814
+ return true;
815
+ }
816
+ function parseExternalUrl(rawUrl) {
817
+ const parsed = new URL(rawUrl);
818
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
819
+ throw new Error('Only http and https URLs are allowed');
820
+ }
821
+ if (parsed.username || parsed.password) {
822
+ throw new Error('URLs with embedded credentials are not allowed');
823
+ }
824
+ if (!parsed.hostname) {
825
+ throw new Error('URL host is required');
826
+ }
827
+ return parsed;
828
+ }
829
+ async function assertPublicUrlHost(parsed) {
830
+ const host = parsed.hostname.replace(/^\[|\]$/g, '');
831
+ if (isIP(host)) {
832
+ if (isPrivateIpAddress(host)) {
833
+ throw new Error('URL host resolves to a blocked network address');
834
+ }
835
+ return;
836
+ }
837
+ const records = await lookup(host, { all: true, verbatim: false });
838
+ if (records.length === 0 || records.some(record => isPrivateIpAddress(record.address))) {
839
+ throw new Error('URL host resolves to a blocked network address');
840
+ }
841
+ }
842
+ async function readResponseWithLimit(response) {
843
+ const contentLength = response.headers.get('content-length');
844
+ if (contentLength && Number(contentLength) > MAX_EXTERNAL_RESOURCE_BYTES) {
845
+ throw new Error('Downloaded resource is too large');
846
+ }
847
+ if (!response.body) {
848
+ const fallback = Buffer.from(await response.arrayBuffer());
849
+ if (fallback.length > MAX_EXTERNAL_RESOURCE_BYTES) {
850
+ throw new Error('Downloaded resource is too large');
851
+ }
852
+ return fallback;
853
+ }
854
+ const reader = response.body.getReader();
855
+ const chunks = [];
856
+ let total = 0;
857
+ while (true) {
858
+ const { done, value } = await reader.read();
859
+ if (done)
860
+ break;
861
+ const chunk = Buffer.from(value);
862
+ total += chunk.length;
863
+ if (total > MAX_EXTERNAL_RESOURCE_BYTES) {
864
+ try {
865
+ await reader.cancel();
866
+ }
867
+ catch {
868
+ // Ignore cancel errors.
869
+ }
870
+ throw new Error('Downloaded resource is too large');
871
+ }
872
+ chunks.push(chunk);
873
+ }
874
+ return Buffer.concat(chunks, total);
875
+ }
876
+ async function fetchExternalResource(rawUrl) {
877
+ let currentUrl = parseExternalUrl(rawUrl);
878
+ for (let redirects = 0; redirects <= MAX_EXTERNAL_REDIRECTS; redirects += 1) {
879
+ await assertPublicUrlHost(currentUrl);
880
+ const response = await fetch(currentUrl, {
881
+ redirect: 'manual',
882
+ signal: AbortSignal.timeout(EXTERNAL_FETCH_TIMEOUT_MS),
883
+ });
884
+ if (response.status >= 300 && response.status < 400) {
885
+ const location = response.headers.get('location');
886
+ if (!location) {
887
+ throw new Error('Redirect response did not include a location');
888
+ }
889
+ currentUrl = parseExternalUrl(new URL(location, currentUrl).toString());
890
+ continue;
891
+ }
892
+ const buffer = await readResponseWithLimit(response);
893
+ return { response, buffer, finalUrl: currentUrl.toString() };
894
+ }
895
+ throw new Error('Too many redirects while downloading URL');
896
+ }
783
897
  async function executeResourcesToolDirect(args) {
784
898
  const { operation, query, type_filter, resource_id, prompt, model, aspect_ratio, text, voice, duration, resolution, url, tags, description } = args;
785
899
  const apiBase = getInternalApiBase();
@@ -939,17 +1053,16 @@ async function executeResourcesToolDirect(args) {
939
1053
  if (!url) {
940
1054
  return { success: false, error: 'url is required for add_from_url operation' };
941
1055
  }
942
- // Download the URL
943
- const dlResponse = await fetch(url);
1056
+ // Download the URL after validating the host and redirect chain.
1057
+ const { response: dlResponse, buffer, finalUrl } = await fetchExternalResource(url);
944
1058
  if (!dlResponse.ok) {
945
1059
  return { success: false, error: `Failed to download URL: ${dlResponse.statusText}` };
946
1060
  }
947
- const buffer = Buffer.from(await dlResponse.arrayBuffer());
948
1061
  const contentType = dlResponse.headers.get('content-type') || 'application/octet-stream';
949
1062
  // Derive filename from URL path
950
1063
  let fileName;
951
1064
  try {
952
- const urlPath = new URL(url).pathname;
1065
+ const urlPath = new URL(finalUrl).pathname;
953
1066
  fileName = basename(urlPath);
954
1067
  if (!fileName || fileName === '/') {
955
1068
  fileName = `downloaded-${Date.now()}`;
@@ -29,6 +29,7 @@ apiKeys.get('/', async (c) => {
29
29
  name: k.name,
30
30
  keyPreview: k.keyPreview,
31
31
  createdBy: k.createdBy,
32
+ createdByRole: k.createdByRole,
32
33
  createdAt: k.createdAt,
33
34
  expiresAt: k.expiresAt,
34
35
  lastUsedAt: k.lastUsedAt,
@@ -61,8 +62,12 @@ apiKeys.post('/', async (c) => {
61
62
  if (!scopes || scopes.length === 0) {
62
63
  return c.json({ success: false, error: 'At least one scope is required' }, 400);
63
64
  }
65
+ const requestsAdminScope = scopes.some((scope) => (scope === 'admin' || scope === 'admin:all' || scope.startsWith('admin:')));
66
+ if (requestsAdminScope && session.role !== 'admin') {
67
+ return c.json({ success: false, error: 'Admin access is required to create admin-scoped API keys' }, 403);
68
+ }
64
69
  // Create API key
65
- const { apiKey, rawKey } = await authManager.createApiKey(name, session.userId, scopes, expiresIn);
70
+ const { apiKey, rawKey } = await authManager.createApiKey(name, session.userId, scopes, expiresIn, session.role);
66
71
  return c.json({
67
72
  success: true,
68
73
  data: {
@@ -71,6 +76,7 @@ apiKeys.post('/', async (c) => {
71
76
  name: apiKey.name,
72
77
  keyPreview: apiKey.keyPreview,
73
78
  createdBy: apiKey.createdBy,
79
+ createdByRole: apiKey.createdByRole,
74
80
  createdAt: apiKey.createdAt,
75
81
  expiresAt: apiKey.expiresAt,
76
82
  scopes: apiKey.scopes,