Skip to content

Commit 335b880

Browse files
authored
Merge pull request #111 from better-stack-ai/fix/media-plugin-local-adapter-export
chore: media plugin improvements
2 parents 4d16d11 + 719dbc0 commit 335b880

9 files changed

Lines changed: 48 additions & 19 deletions

File tree

packages/stack/build.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ export default defineBuildConfig({
117117
"./src/plugins/comments/query-keys.ts",
118118
// media plugin entries
119119
"./src/plugins/media/api/index.ts",
120+
"./src/plugins/media/api/adapters/local.ts",
120121
"./src/plugins/media/api/adapters/s3.ts",
121122
"./src/plugins/media/api/adapters/vercel-blob.ts",
122123
"./src/plugins/media/client/index.ts",

packages/stack/package.json

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@btst/stack",
3-
"version": "2.11.0",
3+
"version": "2.11.1",
44
"description": "A composable, plugin-based library for building full-stack applications.",
55
"repository": {
66
"type": "git",
@@ -424,6 +424,16 @@
424424
"default": "./dist/plugins/media/api/index.cjs"
425425
}
426426
},
427+
"./plugins/media/api/adapters/local": {
428+
"import": {
429+
"types": "./dist/plugins/media/api/adapters/local.d.ts",
430+
"default": "./dist/plugins/media/api/adapters/local.mjs"
431+
},
432+
"require": {
433+
"types": "./dist/plugins/media/api/adapters/local.d.cts",
434+
"default": "./dist/plugins/media/api/adapters/local.cjs"
435+
}
436+
},
427437
"./plugins/media/api/adapters/s3": {
428438
"import": {
429439
"types": "./dist/plugins/media/api/adapters/s3.d.ts",
@@ -684,6 +694,9 @@
684694
"plugins/media/api": [
685695
"./dist/plugins/media/api/index.d.ts"
686696
],
697+
"plugins/media/api/adapters/local": [
698+
"./dist/plugins/media/api/adapters/local.d.ts"
699+
],
687700
"plugins/media/api/adapters/s3": [
688701
"./dist/plugins/media/api/adapters/s3.d.ts"
689702
],

packages/stack/src/plugins/media/__tests__/storage-adapters.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,17 @@ describe("localAdapter", () => {
145145
const adapter = localAdapter();
146146
expect(adapter.type).toBe("local");
147147
});
148+
149+
it("throws when a URL contains a path traversal sequence", async () => {
150+
const uploadDir = await makeTmpDir();
151+
const adapter = localAdapter({ uploadDir, publicPath: "/uploads" });
152+
153+
// Simulate a tampered URL that would resolve outside uploadDir after decoding.
154+
const maliciousUrl = "/uploads/..%2F..%2Fetc%2Fpasswd";
155+
await expect(adapter.delete(maliciousUrl)).rejects.toThrow(
156+
"Refusing to delete file outside upload directory",
157+
);
158+
});
148159
});
149160

150161
// ── S3 adapter ────────────────────────────────────────────────────────────────

packages/stack/src/plugins/media/api/adapters/local.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,16 @@ export function localAdapter(
6666
if (!encodedFilename) return;
6767
const filename = decodeURIComponent(encodedFilename);
6868

69-
const filePath = path.join(uploadDir, filename);
69+
const resolvedUploadDir = path.resolve(uploadDir);
70+
const filePath = path.join(resolvedUploadDir, filename);
71+
72+
// Guard against path traversal: reject any path that escapes uploadDir.
73+
if (!filePath.startsWith(resolvedUploadDir + path.sep)) {
74+
throw new Error(
75+
`Refusing to delete file outside upload directory: ${filePath}`,
76+
);
77+
}
78+
7079
try {
7180
await fs.unlink(filePath);
7281
} catch (err: unknown) {

packages/stack/src/plugins/media/api/index.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,6 @@ export { serializeAsset, serializeFolder } from "./serializers";
2626

2727
export { MEDIA_QUERY_KEYS, assetListDiscriminator } from "./query-key-defs";
2828

29-
export {
30-
localAdapter,
31-
type LocalStorageAdapterOptions,
32-
} from "./adapters/local";
33-
3429
export type {
3530
StorageAdapter,
3631
DirectStorageAdapter,

packages/stack/src/plugins/media/api/plugin.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,10 @@ export const mediaBackendPlugin = (config: MediaBackendConfig) =>
240240
listAssets: (params?: Parameters<typeof listAssets>[1]) =>
241241
listAssets(adapter, params),
242242
getAssetById: (id: string) => getAssetById(adapter, id),
243+
createAsset: (input: Parameters<typeof createAsset>[1]) =>
244+
createAsset(adapter, input),
245+
updateAsset: (id: string, input: Parameters<typeof updateAsset>[2]) =>
246+
updateAsset(adapter, id, input),
243247
listFolders: (params?: Parameters<typeof listFolders>[1]) =>
244248
listFolders(adapter, params),
245249
getFolderById: (id: string) => getFolderById(adapter, id),
@@ -248,6 +252,8 @@ export const mediaBackendPlugin = (config: MediaBackendConfig) =>
248252
parentId?: string | null,
249253
tenantId?: string,
250254
) => getFolderByName(adapter, name, parentId, tenantId),
255+
createFolder: (input: Parameters<typeof createFolder>[1]) =>
256+
createFolder(adapter, input),
251257
}),
252258

253259
routes: (adapter: Adapter) => {

scripts/codegen/files/nextjs/lib/stack.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,8 @@ import { formBuilderBackendPlugin } from "@btst/stack/plugins/form-builder/api";
1111
import { openApiBackendPlugin } from "@btst/stack/plugins/open-api/api";
1212
import { kanbanBackendPlugin } from "@btst/stack/plugins/kanban/api";
1313
import { commentsBackendPlugin } from "@btst/stack/plugins/comments/api";
14-
import {
15-
mediaBackendPlugin,
16-
localAdapter,
17-
} from "@btst/stack/plugins/media/api";
14+
import { mediaBackendPlugin } from "@btst/stack/plugins/media/api";
15+
import { localAdapter } from "@btst/stack/plugins/media/api/adapters/local";
1816
import { UI_BUILDER_CONTENT_TYPE } from "@btst/stack/plugins/ui-builder";
1917
import { openai } from "@ai-sdk/openai";
2018
import { tool } from "ai";

scripts/codegen/files/react-router/app/lib/stack.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,8 @@ import { formBuilderBackendPlugin } from "@btst/stack/plugins/form-builder/api";
1111
import { openApiBackendPlugin } from "@btst/stack/plugins/open-api/api";
1212
import { kanbanBackendPlugin } from "@btst/stack/plugins/kanban/api";
1313
import { commentsBackendPlugin } from "@btst/stack/plugins/comments/api";
14-
import {
15-
mediaBackendPlugin,
16-
localAdapter,
17-
} from "@btst/stack/plugins/media/api";
14+
import { mediaBackendPlugin } from "@btst/stack/plugins/media/api";
15+
import { localAdapter } from "@btst/stack/plugins/media/api/adapters/local";
1816
import { UI_BUILDER_CONTENT_TYPE } from "@btst/stack/plugins/ui-builder";
1917
import { openai } from "@ai-sdk/openai";
2018
import { tool } from "ai";

scripts/codegen/files/tanstack/src/lib/stack.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,8 @@ import { formBuilderBackendPlugin } from "@btst/stack/plugins/form-builder/api";
1111
import { openApiBackendPlugin } from "@btst/stack/plugins/open-api/api";
1212
import { kanbanBackendPlugin } from "@btst/stack/plugins/kanban/api";
1313
import { commentsBackendPlugin } from "@btst/stack/plugins/comments/api";
14-
import {
15-
mediaBackendPlugin,
16-
localAdapter,
17-
} from "@btst/stack/plugins/media/api";
14+
import { mediaBackendPlugin } from "@btst/stack/plugins/media/api";
15+
import { localAdapter } from "@btst/stack/plugins/media/api/adapters/local";
1816
import { UI_BUILDER_CONTENT_TYPE } from "@btst/stack/plugins/ui-builder";
1917
import { openai } from "@ai-sdk/openai";
2018
import { tool } from "ai";

0 commit comments

Comments
 (0)