@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.
- package/dist/lib/auth/auth-manager.d.ts +1 -1
- package/dist/lib/auth/auth-manager.js +2 -1
- package/dist/lib/symlinks/symlink-manager.d.ts +3 -4
- package/dist/lib/symlinks/symlink-manager.js +49 -30
- package/dist/lib/terminal/terminal-manager.d.ts +8 -5
- package/dist/lib/terminal/terminal-manager.js +29 -5
- package/dist/middleware/auth.d.ts +23 -2
- package/dist/middleware/auth.js +44 -4
- package/dist/routes/api/v1/ai/route.js +117 -4
- package/dist/routes/api/v1/api-keys/route.js +7 -1
- package/dist/routes/api/v1/jobs/[id]/route.d.ts +28 -28
- package/dist/routes/api/v1/jobs/route.d.ts +28 -28
- package/dist/routes/api/v1/knowledge/[filename]/route.js +29 -7
- package/dist/routes/api/v1/slack/route.d.ts +6 -6
- package/dist/routes/api/v1/terminal/[taskId]/create/route.js +2 -1
- package/dist/routes/api/v1/terminal/[taskId]/destroy/route.js +2 -1
- package/dist/routes/api/v1/terminal/[taskId]/resize/route.js +2 -1
- package/dist/routes/api/v1/terminal/code/route.js +2 -1
- package/dist/routes/api/v1/terminal/sessions/route.js +5 -3
- package/dist/server-with-static.js +3 -3
- package/dist/server.js +3 -3
- package/package.json +4 -4
- package/static/assets/{ActivityPage-CO2m97Hz.js → ActivityPage-CtFAFqlg.js} +1 -1
- package/static/assets/{AgentsContextEditPage-B9pcC3Y4.js → AgentsContextEditPage-CsRJvzE6.js} +1 -1
- package/static/assets/{AgentsContextPage-CWsGDD4F.js → AgentsContextPage-CWCs8CgG.js} +1 -1
- package/static/assets/{ApiKeysSettingsPage-CPckUylF.js → ApiKeysSettingsPage-B4b_fmJg.js} +1 -1
- package/static/assets/{AuthSettingsPage-CaiYl7fx.js → AuthSettingsPage-O3UdLrHC.js} +1 -1
- package/static/assets/{CallbackPage-C73ggs1e.js → CallbackPage-WTwVSxdx.js} +1 -1
- package/static/assets/{CoconutCallbackPage-Bhc_sIuJ.js → CoconutCallbackPage-LEWgUV8J.js} +1 -1
- package/static/assets/{CodePage-7zrLYTBq.js → CodePage-DpVmg_UD.js} +1 -1
- package/static/assets/{CollapsibleSection-DD6cnVaM.js → CollapsibleSection-BHag26Vf.js} +1 -1
- package/static/assets/{DashboardPage-BiVDSLAm.js → DashboardPage-BZnCikpf.js} +1 -1
- package/static/assets/{GitPage-C7n82d8O.js → GitPage-DD6dgeNu.js} +1 -1
- package/static/assets/{GitSettingsPage-D2R-zg9d.js → GitSettingsPage-CLHCZ6at.js} +1 -1
- package/static/assets/{IdentityPage-B3gUyFXr.js → IdentityPage-DtzLSTYd.js} +1 -1
- package/static/assets/{ImplementationStepsEditor-DmrYGdZB.js → ImplementationStepsEditor-Ca1aygKs.js} +1 -1
- package/static/assets/{IntegrationsSettingsPage-V6t85OD_.js → IntegrationsSettingsPage-C2tzpbuI.js} +1 -1
- package/static/assets/{JobDetailPage-BUmQGshi.js → JobDetailPage-B03z4VgG.js} +1 -1
- package/static/assets/{KnowledgeDetailPage-DzpoOs9C.js → KnowledgeDetailPage-XslT5GUv.js} +1 -1
- package/static/assets/{KnowledgeEditPage-C9zyCfr6.js → KnowledgeEditPage-5NFEnGJh.js} +1 -1
- package/static/assets/{KnowledgePage-C56_NMA9.js → KnowledgePage-Dzh409qU.js} +1 -1
- package/static/assets/{LoginPage-DZpAjZmK.js → LoginPage-IDVkR1zN.js} +1 -1
- package/static/assets/{MailInboxPage-BvOBQvF3.js → MailInboxPage-CXaBqNl2.js} +1 -1
- package/static/assets/{MailProcessingModal-CCGrVGDs.js → MailProcessingModal-BWyzTMEv.js} +1 -1
- package/static/assets/{MailReadPage-CycqG4oj.js → MailReadPage-DPGuT0gC.js} +1 -1
- package/static/assets/{MailSentPage-DpwRNrju.js → MailSentPage-Dtq37Kap.js} +1 -1
- package/static/assets/{McpSettingsPage-pJ41THOO.js → McpSettingsPage-BiCSPaAJ.js} +1 -1
- package/static/assets/{MemoryEditPage-DobGCRrl.js → MemoryEditPage-Dfa6MsnE.js} +1 -1
- package/static/assets/{MemoryPage-BrFnh8rC.js → MemoryPage-BqUngqIS.js} +1 -1
- package/static/assets/{NewKnowledgePage-CTFx_YTk.js → NewKnowledgePage-BmYwcO98.js} +1 -1
- package/static/assets/{NewSkillPage-D_FfkSKi.js → NewSkillPage-BfNaXH37.js} +1 -1
- package/static/assets/{NewTaskPage-CehAYsyJ.js → NewTaskPage-Ctmh2uiJ.js} +1 -1
- package/static/assets/{NotFoundPage-wcJQI6_Y.js → NotFoundPage-a2erdq4H.js} +1 -1
- package/static/assets/{NotificationsSettingsPage-_9Dum2JJ.js → NotificationsSettingsPage-BB9QuyUf.js} +1 -1
- package/static/assets/{PromptsSettingsPage-BBIP8PWS.js → PromptsSettingsPage-BZLwTFcd.js} +1 -1
- package/static/assets/{ResourceDetailPage-C7goHk6o.js → ResourceDetailPage-DRWNjykw.js} +1 -1
- package/static/assets/{ResourcesPage-ZH5lSPaG.js → ResourcesPage-C3Lx8hz_.js} +1 -1
- package/static/assets/{RoleEditPage-JJnQ7UXW.js → RoleEditPage-W9SV6Mwr.js} +1 -1
- package/static/assets/{RolePage-C-gQnpzL.js → RolePage-DWWo77tA.js} +1 -1
- package/static/assets/{RulesSettingsPage-Bc05Q6j8.js → RulesSettingsPage-BKsadZ7r.js} +1 -1
- package/static/assets/{RunDetailPage-BhWthPVR.js → RunDetailPage-C6miynGX.js} +1 -1
- package/static/assets/{SchedulePage-zvr5ce8N.js → SchedulePage-B6s-CkVg.js} +1 -1
- package/static/assets/{SkillDetailPage-5DhKM3NY.js → SkillDetailPage-DYkLxRjB.js} +1 -1
- package/static/assets/{SkillEditPage-1V1ADtPe.js → SkillEditPage-DIB2lUMz.js} +1 -1
- package/static/assets/{SkillsPage-6VQCj1hf.js → SkillsPage-BgR2gdCy.js} +1 -1
- package/static/assets/{SkillsSettingsPage-C7lqSbBf.js → SkillsSettingsPage-CoDOG0YQ.js} +1 -1
- package/static/assets/{SourceInput-CeltF2kQ.js → SourceInput-CS6u1yPi.js} +1 -1
- package/static/assets/{TagInput-DUR8EcF5.js → TagInput-C3WzbX6I.js} +1 -1
- package/static/assets/{TaskDetailPage-HqNWYvGE.js → TaskDetailPage-DEBBE0D8.js} +1 -1
- package/static/assets/{TaskEditPage-Cy6_oKRU.js → TaskEditPage-B26tEsUP.js} +1 -1
- package/static/assets/{TasksPage-DHdwWucP.js → TasksPage-pqQ7VlC3.js} +1 -1
- package/static/assets/{TeamEditPage-DZHs9LwL.js → TeamEditPage-C6DugOii.js} +1 -1
- package/static/assets/{TeamPage-C10th6Xi.js → TeamPage-BDqQZ6Rr.js} +1 -1
- package/static/assets/{TerminalPage-BajLCwBP.js → TerminalPage-pjss7X1h.js} +1 -1
- package/static/assets/{TerminalSessionPage-dJGlApIY.js → TerminalSessionPage-C9sJ_13t.js} +1 -1
- package/static/assets/{UserPreferencesPage-crzoMvn3.js → UserPreferencesPage-BrpKf14p.js} +1 -1
- package/static/assets/{UserSettingsPage-B4eeW2b9.js → UserSettingsPage-BbazpvdQ.js} +1 -1
- package/static/assets/{UtilitiesPage-ZnNRIlEg.js → UtilitiesPage-niclH8kb.js} +1 -1
- package/static/assets/{alert-a0XoDY__.js → alert-DBhIskRX.js} +1 -1
- package/static/assets/{arrow-down-DwCDIVHH.js → arrow-down-TpeNydxP.js} +1 -1
- package/static/assets/{arrow-left-D4GyWema.js → arrow-left-_lXkdm9Q.js} +1 -1
- package/static/assets/{arrow-up-ClZJeJXS.js → arrow-up-DaIMsmSO.js} +1 -1
- package/static/assets/{arrow-up-down-ozcTje6W.js → arrow-up-down-C7wqirJv.js} +1 -1
- package/static/assets/{badge-CgbZdX-R.js → badge-TeAh_Zh0.js} +1 -1
- package/static/assets/{browser-modal-CV9x_ILy.js → browser-modal-BpGFpZZj.js} +1 -1
- package/static/assets/{card-bSzcwVEz.js → card-CiLixwCG.js} +1 -1
- package/static/assets/{chevron-left-Dnm2Bv2g.js → chevron-left-CuMfw52D.js} +1 -1
- package/static/assets/{chevron-up-znuSUWjo.js → chevron-up-B4J4D5aE.js} +1 -1
- package/static/assets/{chevrons-up-DyHcV7KP.js → chevrons-up-Dcs-Dj4B.js} +1 -1
- package/static/assets/{circle-alert-Dn8B5NPI.js → circle-alert-nI75emvr.js} +1 -1
- package/static/assets/{circle-check-big-HUp82Tz-.js → circle-check-big-CxxxVvOw.js} +1 -1
- package/static/assets/{circle-check-2Hh5KJn8.js → circle-check-k6ER9afD.js} +1 -1
- package/static/assets/{circle-play-BG4sTHV2.js → circle-play-B4D0d7YW.js} +1 -1
- package/static/assets/{circle-x-DBrbnMPA.js → circle-x-BzPgPb47.js} +1 -1
- package/static/assets/{clipboard-B6OwO-az.js → clipboard-CLObCrC4.js} +1 -1
- package/static/assets/{clock-B36ytcOX.js → clock-BK-8XMqv.js} +1 -1
- package/static/assets/{code-CK8i3Xlg.js → code-lhIVzb1q.js} +1 -1
- package/static/assets/{download-C_C3f7cE.js → download-ChgFRTfC.js} +1 -1
- package/static/assets/{external-link-kQIXuJh_.js → external-link-D4kianlP.js} +1 -1
- package/static/assets/{eye-DTyfbeQg.js → eye-H1JnZ9E9.js} +1 -1
- package/static/assets/{folder-git-2-CKR2HcNZ.js → folder-git-2-BZGvXcyj.js} +1 -1
- package/static/assets/{globe--bHEtjre.js → globe-DAzCOUla.js} +1 -1
- package/static/assets/{index-CJBq79WW.js → index-B7UEVagI.js} +1 -1
- package/static/assets/{index-CRSpx2tQ.js → index-Bik-wLXi.js} +1 -1
- package/static/assets/{index-ObZJu-Zv.js → index-BlOxV6pf.js} +1 -1
- package/static/assets/{index-QgKSm0gN.js → index-C7dpn8dK.js} +1 -1
- package/static/assets/{index-ffbhzKxY.js → index-C9wsYS8C.js} +1 -1
- package/static/assets/{index-9Y0fmmy7.js → index-Ck9sMDuo.js} +1 -1
- package/static/assets/{index-CvrVx0kf.js → index-DMeIDO6W.js} +1 -1
- package/static/assets/{index-Bg80D0Z0.js → index-DOpjlxVe.js} +1 -1
- package/static/assets/{index-nRow2q03.js → index-DT4K0HgO.js} +1 -1
- package/static/assets/{index-DMKQFm-M.js → index-DeF-CCVg.js} +1 -1
- package/static/assets/{index-CTZ51lNe.js → index-DkvhkCo1.js} +1 -1
- package/static/assets/{index-DXkvtMiO.js → index-DuDW1LwX.js} +1 -1
- package/static/assets/{index-DOgqHj1l.js → index-Dy_-mpPq.js} +1 -1
- package/static/assets/{index-B81KYk3Z.js → index-SUQrIHd7.js} +6 -6
- package/static/assets/{index-UKy_jk8q.js → index-c-gr7Bew.js} +1 -1
- package/static/assets/{index-C_zW1Hi0.js → index-kJaEkklL.js} +1 -1
- package/static/assets/{index-XJ0Ck2RG.js → index-rslXoTnS.js} +1 -1
- package/static/assets/{index-CD6zflLk.js → index-rwsZ-IsL.js} +1 -1
- package/static/assets/{index-B52hVXHu.js → index-sBbL8_xu.js} +1 -1
- package/static/assets/{info-wSiZMXgL.js → info-CVmPHERQ.js} +1 -1
- package/static/assets/{label-CHP4q6u3.js → label-n5pqaObd.js} +1 -1
- package/static/assets/{markdown-editor-DSI9T1tt.js → markdown-editor-Cvh5pZzJ.js} +3 -3
- package/static/assets/{message-square-D1sMYX2G.js → message-square-BmnqRsu6.js} +1 -1
- package/static/assets/{paperclip-plxfUm0D.js → paperclip-BwcNeQ_C.js} +1 -1
- package/static/assets/{pause-B2JeikEI.js → pause-CUswidZW.js} +1 -1
- package/static/assets/{play-l7Mu5YPO.js → play-DyZbNGiK.js} +1 -1
- package/static/assets/{radio-group-DxYoqBDN.js → radio-group-Bzu_d7xN.js} +1 -1
- package/static/assets/{refresh-cw-BTHxZiES.js → refresh-cw-DvQ0rmk7.js} +1 -1
- package/static/assets/{search-Cpy49h7H.js → search-kc_tG8Rf.js} +1 -1
- package/static/assets/{select-CG6kgyUu.js → select-CIOYQA7a.js} +1 -1
- package/static/assets/{server-DzOL5RbP.js → server-CWIJxRf0.js} +1 -1
- package/static/assets/{switch-7YpWWyAq.js → switch-DczkR77Y.js} +1 -1
- package/static/assets/{tabs-DJk1ZrWh.js → tabs-DOEakD76.js} +1 -1
- package/static/assets/{tag-BvoXF-sG.js → tag-Dc8sXXlM.js} +1 -1
- package/static/assets/{terminal-preview-d_BZ8s8s.js → terminal-preview-D8oASUdZ.js} +1 -1
- package/static/assets/{triangle-alert-P6aKUDal.js → triangle-alert-C-5ixej6.js} +1 -1
- package/static/assets/{use-terminal-BXYq-vfY.js → use-terminal-xdcf4Z2G.js} +1 -1
- package/static/assets/{video-PACu7StA.js → video-CBneSE9B.js} +1 -1
- 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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
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
|
-
|
|
226
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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
|
-
},
|
|
29
|
+
}, 403, "json">) | (Response & import("hono").TypedResponse<{
|
|
11
30
|
error: string;
|
|
12
31
|
message: string;
|
|
13
|
-
},
|
|
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
|
package/dist/middleware/auth.js
CHANGED
|
@@ -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
|
-
//
|
|
211
|
-
//
|
|
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'
|
|
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
|
|
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(
|
|
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,
|