diff --git a/package-lock.json b/package-lock.json index f61f23982..6981e5308 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6270,29 +6270,6 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/core": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", - "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" - } - }, - "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", - "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@rolldown/binding-win32-arm64-msvc": { "version": "1.0.0-rc.18", "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.18.tgz", diff --git a/src/backend/controllers/fs/LegacyFSController.ts b/src/backend/controllers/fs/LegacyFSController.ts index 2be4b528f..43846dc9c 100644 --- a/src/backend/controllers/fs/LegacyFSController.ts +++ b/src/backend/controllers/fs/LegacyFSController.ts @@ -1054,6 +1054,18 @@ export class LegacyFSController extends PuterController { const operation = typeof query.operation === 'string' ? query.operation : 'write'; + // A valid write signature authorises overwriting the file's bytes, + // not structural changes. Restrict copy/move/mkdir/rename/delete/trash + // to a caller authenticated as the owner — otherwise a recipient of a + // write-share could relocate or destroy the source via this endpoint. + if (operation !== 'write' && req.actor?.user?.id !== userId) { + throw new HttpError( + 403, + `'${operation}' via signed URL requires owner authentication`, + { legacyCode: 'forbidden' }, + ); + } + // `write` — multipart upload, streamed directly to the v2 write path. if (operation === 'write') { const body = asRecord(req.body); @@ -1087,8 +1099,19 @@ export class LegacyFSController extends PuterController { } // Non-write operations: route to existing service methods and sign the result. + // The signature alone authorises only byte writes to `targetEntry`. Structural + // ops (mkdir/rename/copy/move/delete) require a caller actor with explicit + // ACL on the affected paths — mirroring the unsigned counterparts above. const record = asRecord(req.body); + const callerActor = this.#requireActor(req); if (operation === 'mkdir') { + await assertAccess( + this.services.acl, + this.services.fs, + callerActor, + targetEntry.path, + 'write', + ); const folderName = typeof record.name === 'string' ? record.name @@ -1110,12 +1133,26 @@ export class LegacyFSController extends PuterController { throw new HttpError(400, '`new_name` required', { legacyCode: 'bad_request', }); + await assertAccess( + this.services.acl, + this.services.fs, + callerActor, + targetEntry.path, + 'write', + ); const renamed = await this.services.fs.rename(targetEntry, newName); await this.#emitGuiEvent('outer.gui.item.updated', renamed); res.json({ ...signEntry(renamed, signingCfg), path: renamed.path }); return; } if (operation === 'delete' || operation === 'trash') { + await assertAccess( + this.services.acl, + this.services.fs, + callerActor, + targetEntry.path, + 'write', + ); // Treat trash == delete (recursive). Most clients just call delete // directly; if a trash folder becomes important we can revisit. await this.services.fs.remove(userId, { @@ -1139,6 +1176,20 @@ export class LegacyFSController extends PuterController { this.stores.fsEntry, destRef, ); + await assertAccess( + this.services.acl, + this.services.fs, + callerActor, + targetEntry.path, + operation === 'copy' ? 'read' : 'write', + ); + await assertAccess( + this.services.acl, + this.services.fs, + callerActor, + destinationParent.path, + 'write', + ); const method = operation === 'copy' ? 'copy' : 'move'; const result = await this.services.fs[method](userId, { source: targetEntry, diff --git a/src/backend/drivers/ai-chat/providers/openrouter/OpenRouterProvider.ts b/src/backend/drivers/ai-chat/providers/openrouter/OpenRouterProvider.ts index 05e2d7eb5..4cd31d656 100644 --- a/src/backend/drivers/ai-chat/providers/openrouter/OpenRouterProvider.ts +++ b/src/backend/drivers/ai-chat/providers/openrouter/OpenRouterProvider.ts @@ -230,7 +230,7 @@ export class OpenRouterProvider implements IChatProvider { }); models = resp.data.data; - kv.set('openrouterChat:models', models); + kv.set('openrouterChat:models', models, { EX: 15 * 60 }); // cache for 15 minutes } catch (e) { console.log(e); } diff --git a/src/backend/drivers/ai-chat/providers/together/TogetherAIProvider.ts b/src/backend/drivers/ai-chat/providers/together/TogetherAIProvider.ts index 7917189b8..fe379483e 100644 --- a/src/backend/drivers/ai-chat/providers/together/TogetherAIProvider.ts +++ b/src/backend/drivers/ai-chat/providers/together/TogetherAIProvider.ts @@ -96,7 +96,7 @@ export class TogetherAIProvider implements IChatProvider { }, max_tokens: 1000, }); - kv.set(this.#kvKey, models, { EX: 5 * 60 }); + kv.set(this.#kvKey, models, { EX: 15 * 60 }); return models; } diff --git a/src/backend/drivers/ai-chat/types.ts b/src/backend/drivers/ai-chat/types.ts index bed92b742..1af4c8972 100644 --- a/src/backend/drivers/ai-chat/types.ts +++ b/src/backend/drivers/ai-chat/types.ts @@ -125,4 +125,5 @@ export interface IChatProvider { list(): string[] | Promise; getDefaultModel(): string; complete(arg: ICompleteArguments): Promise; + checkModeration(text: string): { flagged: boolean; categories: string[] }; } diff --git a/src/gui/webpack/BaseConfig.cjs b/src/gui/webpack/BaseConfig.cjs index 59880b850..624b15429 100644 --- a/src/gui/webpack/BaseConfig.cjs +++ b/src/gui/webpack/BaseConfig.cjs @@ -34,7 +34,7 @@ module.exports = async (options = {}) => { const entries = []; for ( const extensionsDir of extension_directories ) { - // console.log(`Reading extensions from ${extensionsDir}`); + if ( ! fs.existsSync(extensionsDir) ) continue; // Read and process extension entries from the extensions directory const readdir_entries = fs.readdirSync(extensionsDir, { withFileTypes: true }); for ( const entry of readdir_entries ) {