-
Notifications
You must be signed in to change notification settings - Fork 1
Objectstorage outside cf #33
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
0eb4cb3
7491a16
3e8ad83
9ca0ee6
e5b43f7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| # Build stage | ||
| FROM oven/bun:1 AS builder | ||
|
|
||
| WORKDIR /app | ||
|
|
||
| # Copy package files | ||
| COPY package.json bun.lock* ./ | ||
|
|
||
| # Install all dependencies (including devDependencies needed for build) | ||
| RUN bun install --frozen-lockfile | ||
|
|
||
| # Copy source code | ||
| COPY . . | ||
|
|
||
| # Build the application | ||
| RUN bun run build | ||
|
|
||
| # Runtime stage | ||
| FROM oven/bun:1-slim | ||
|
|
||
| WORKDIR /app | ||
|
|
||
| # Copy package.json for the start script | ||
| COPY package.json ./ | ||
|
|
||
| # Copy only the dist folder from builder | ||
| COPY --from=builder /app/dist ./dist | ||
|
|
||
| # Set production environment | ||
| ENV NODE_ENV=production | ||
|
|
||
| # Expose the port (adjust if needed) | ||
| EXPOSE 3000 | ||
|
|
||
| # Start the server | ||
| CMD ["bun", "run", "start"] | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,7 +2,7 @@ | |
| * S3 Client factory for creating S3 clients from state configuration. | ||
| * Supports any S3-compatible storage provider including AWS S3, R2, MinIO, etc. | ||
| */ | ||
| import { S3Client } from "@aws-sdk/client-s3"; | ||
| import { S3Client } from "bun"; | ||
| import type { Env } from "../main.ts"; | ||
|
|
||
| /** | ||
|
|
@@ -13,21 +13,18 @@ import type { Env } from "../main.ts"; | |
| * @returns Configured S3Client instance | ||
| */ | ||
| export function createS3Client(env: Env): S3Client { | ||
| const state = env.DECO_CHAT_REQUEST_CONTEXT.state; | ||
| const state = env.DECO_REQUEST_CONTEXT.state; | ||
|
|
||
| const config: ConstructorParameters<typeof S3Client>[0] = { | ||
| region: state.region, | ||
| credentials: { | ||
| accessKeyId: state.accessKeyId, | ||
| secretAccessKey: state.secretAccessKey, | ||
| }, | ||
| accessKeyId: state.accessKeyId, | ||
| secretAccessKey: state.secretAccessKey, | ||
| bucket: state.bucketName, | ||
| }; | ||
|
|
||
| // Add custom endpoint if provided (for S3-compatible storage like R2, MinIO, etc.) | ||
| if (state.endpoint) { | ||
| config.endpoint = state.endpoint; | ||
| // For S3-compatible services, we often need to force path style | ||
| config.forcePathStyle = true; | ||
| } | ||
|
Comment on lines
18
to
28
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: Short answer: use S3Client's endpoint + virtualHostedStyle option. Set endpoint to your custom URL and set virtualHostedStyle: false (or omit virtualHostedStyle if you need path-style and Bun's default is path-style for custom endpoints), e.g.: Bun docs show using endpoint for custom/backends and the virtualHostedStyle flag to choose virtual-hosted vs path-style behavior. [1][2] Note: AWS recommends virtual-hosted style and has communicated deprecation plans for path-style access, so prefer virtualHostedStyle: true for AWS S3 (or when interoperable). [3] Sources:
🏁 Script executed: # Check for any previous implementations or history of forcePathStyle in the repo
git log --all --source --remotes -S "forcePathStyle" -- "object-storage/" 2>/dev/null | head -20Repository: decocms/mcps Length of output: 273 🏁 Script executed: # Search for any test files or documentation related to S3 configuration
fd -e test.ts -e spec.ts -e test.js -e docs.md -o -e "\.md" . object-storage/ 2>/dev/null | head -20Repository: decocms/mcps Length of output: 38 🏁 Script executed: # Check the full s3-client.ts file for context
wc -l object-storage/server/lib/s3-client.tsRepository: decocms/mcps Length of output: 97 🏁 Script executed: cat -n object-storage/server/lib/s3-client.tsRepository: decocms/mcps Length of output: 1934 🏁 Script executed: # Search for virtualHostedStyle usage in the codebase
rg "virtualHostedStyle" object-storage/Repository: decocms/mcps Length of output: 38 🏁 Script executed: # Check if there are any S3-compatible provider tests or documentation
find object-storage/ -type f \( -name "*.md" -o -name "*.test.ts" -o -name "*.spec.ts" \) | xargs cat 2>/dev/null | head -100Repository: decocms/mcps Length of output: 2914 Add Bun's S3Client does not automatically handle path-style URLs for custom endpoints—it requires explicit configuration via the Update the configuration to set if (state.endpoint) {
config.endpoint = state.endpoint;
config.virtualHostedStyle = false;
}🤖 Prompt for AI Agents |
||
|
|
||
| return new S3Client(config); | ||
|
|
@@ -48,6 +45,6 @@ export function getPresignedUrlExpiration( | |
| return overrideExpiration; | ||
| } | ||
|
|
||
| const state = env.DECO_CHAT_REQUEST_CONTEXT.state; | ||
| const state = env.DECO_REQUEST_CONTEXT.state; | ||
| return state.defaultPresignedUrlExpiration ?? 3600; // Default to 1 hour | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,6 +5,7 @@ | |
| * application at /. | ||
| */ | ||
| import { DefaultEnv, withRuntime } from "@decocms/runtime"; | ||
| import { createAssetServerFetcher } from "@decocms/runtime/bun-asset-server"; | ||
| import { | ||
| type Env as DecoEnv, | ||
| StateSchema as BaseStateSchema, | ||
|
|
@@ -54,12 +55,7 @@ export const StateSchema = BaseStateSchema.extend({ | |
| * It includes all of the generated types from your | ||
| * Deco bindings, along with the default ones. | ||
| */ | ||
| export type Env = DefaultEnv & | ||
| DecoEnv & { | ||
| ASSETS: { | ||
| fetch: (request: Request, init?: RequestInit) => Promise<Response>; | ||
| }; | ||
| }; | ||
| export type Env = DefaultEnv & DecoEnv; | ||
|
|
||
| const runtime = withRuntime<Env, typeof StateSchema>({ | ||
| oauth: { | ||
|
|
@@ -97,7 +93,10 @@ const runtime = withRuntime<Env, typeof StateSchema>({ | |
| * If you wanted to add custom api routes that dont make sense to be a tool, | ||
| * you can add them on this handler. | ||
| */ | ||
| fetch: (req, env) => env.ASSETS.fetch(req), | ||
| fetch: createAssetServerFetcher({ | ||
| env: process.env.NODE_ENV as "development" | "production" | "test", | ||
| assetsDirectory: "./dist/client", | ||
| }), | ||
|
Comment on lines
+96
to
+99
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Check NODE_ENV usage in package.json scripts, Dockerfile, and fallbacks
echo "=== Checking package.json scripts ==="
if [ -f "object-storage/package.json" ]; then
rg -n "NODE_ENV" object-storage/package.json
else
echo "package.json not found at object-storage/package.json"
fi
echo -e "\n=== Checking Dockerfile ==="
if [ -f "object-storage/Dockerfile" ]; then
rg -n "NODE_ENV" object-storage/Dockerfile
else
echo "No Dockerfile found at object-storage/Dockerfile, searching for any Dockerfile..."
find object-storage -name "Dockerfile*" -type f 2>/dev/null
fi
echo -e "\n=== Checking if there's a fallback for undefined NODE_ENV ==="
rg -n "NODE_ENV.*\|\|" object-storage/ 2>/dev/null || echo "No fallback patterns found"
echo -e "\n=== Checking main.ts around the problematic lines ==="
sed -n '90,110p' object-storage/server/main.ts 2>/dev/null || echo "File not accessible"Repository: decocms/mcps Length of output: 985 🏁 Script executed: #!/bin/bash
# Find and examine createAssetServerFetcher implementation
echo "=== Searching for createAssetServerFetcher definition ==="
rg -n "createAssetServerFetcher" object-storage/ -A 5
echo -e "\n=== Checking the import location ==="
rg -n "import.*createAssetServerFetcher" object-storage/
echo -e "\n=== Searching across entire repo ==="
rg -n "function createAssetServerFetcher\|export.*createAssetServerFetcher" --type tsRepository: decocms/mcps Length of output: 1056 Add explicit fallback for NODE_ENV to improve type safety. The code relies on Add an explicit fallback: This ensures type safety and handles edge cases where NODE_ENV might not be set. 🤖 Prompt for AI Agents |
||
| }); | ||
|
|
||
| export default runtime; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,15 +7,6 @@ | |
| * - Generating presigned URLs for GET and PUT operations | ||
| * - Deleting objects (single and batch) | ||
| */ | ||
| import { | ||
| DeleteObjectCommand, | ||
| DeleteObjectsCommand, | ||
| GetObjectCommand, | ||
| HeadObjectCommand, | ||
| ListObjectsV2Command, | ||
| PutObjectCommand, | ||
| } from "@aws-sdk/client-s3"; | ||
| import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; | ||
| import { createPrivateTool } from "@decocms/runtime/mastra"; | ||
| import { z } from "zod"; | ||
| import type { Env } from "../main.ts"; | ||
|
|
@@ -66,26 +57,28 @@ export const createListObjectsTool = (env: Env) => | |
| execute: async (ctx: any) => { | ||
| const { prefix, maxKeys, continuationToken } = ctx; | ||
| const s3Client = createS3Client(env); | ||
| const state = env.DECO_CHAT_REQUEST_CONTEXT.state; | ||
|
|
||
| const command = new ListObjectsV2Command({ | ||
| Bucket: state.bucketName, | ||
| Prefix: prefix, | ||
| MaxKeys: maxKeys, | ||
| ContinuationToken: continuationToken, | ||
| const response = await s3Client.list({ | ||
| prefix, | ||
| maxKeys, | ||
| startAfter: continuationToken, | ||
| }); | ||
|
|
||
| const response = await s3Client.send(command); | ||
|
|
||
| return { | ||
| objects: (response.Contents || []).map((obj) => ({ | ||
| key: obj.Key!, | ||
| size: obj.Size!, | ||
| lastModified: obj.LastModified!.toISOString(), | ||
| etag: obj.ETag!, | ||
| objects: (response.contents || []).map((obj) => ({ | ||
| key: obj.key, | ||
| size: obj.size ?? 0, | ||
| lastModified: obj.lastModified | ||
| ? typeof obj.lastModified === "object" | ||
| ? (obj.lastModified as Date).toISOString() | ||
| : String(obj.lastModified) | ||
| : "", | ||
| etag: obj.eTag ?? "", | ||
| })), | ||
| nextContinuationToken: response.NextContinuationToken, | ||
| isTruncated: response.IsTruncated ?? false, | ||
| nextContinuationToken: response.isTruncated | ||
| ? response.contents?.at(-1)?.key | ||
| : undefined, | ||
| isTruncated: response.isTruncated ?? false, | ||
| }; | ||
| }, | ||
| }); | ||
|
|
@@ -114,21 +107,15 @@ export const createGetObjectMetadataTool = (env: Env) => | |
| execute: async (ctx: any) => { | ||
| const { key } = ctx; | ||
| const s3Client = createS3Client(env); | ||
| const state = env.DECO_CHAT_REQUEST_CONTEXT.state; | ||
|
|
||
| const command = new HeadObjectCommand({ | ||
| Bucket: state.bucketName, | ||
| Key: key, | ||
| }); | ||
|
|
||
| const response = await s3Client.send(command); | ||
| const stat = await s3Client.file(key).stat(); | ||
|
|
||
| return { | ||
| contentType: response.ContentType, | ||
| contentLength: response.ContentLength!, | ||
| lastModified: response.LastModified!.toISOString(), | ||
| etag: response.ETag!, | ||
| metadata: response.Metadata, | ||
| contentType: stat.type, | ||
| contentLength: stat.size, | ||
| lastModified: stat.lastModified.toISOString(), | ||
| etag: stat.etag, | ||
| metadata: undefined, // Bun's stat doesn't include custom metadata | ||
| }; | ||
| }, | ||
| }); | ||
|
|
@@ -159,15 +146,10 @@ export const createGetPresignedUrlTool = (env: Env) => | |
| execute: async (ctx: any) => { | ||
| const { key, expiresIn } = ctx; | ||
| const s3Client = createS3Client(env); | ||
| const state = env.DECO_CHAT_REQUEST_CONTEXT.state; | ||
| const expirationSeconds = getPresignedUrlExpiration(env, expiresIn); | ||
|
|
||
| const command = new GetObjectCommand({ | ||
| Bucket: state.bucketName, | ||
| Key: key, | ||
| }); | ||
|
|
||
| const url = await getSignedUrl(s3Client, command, { | ||
| const url = s3Client.file(key).presign({ | ||
| method: "GET", | ||
| expiresIn: expirationSeconds, | ||
| }); | ||
|
|
||
|
|
@@ -208,17 +190,12 @@ export const createPutPresignedUrlTool = (env: Env) => | |
| execute: async (ctx: any) => { | ||
| const { key, expiresIn, contentType } = ctx; | ||
| const s3Client = createS3Client(env); | ||
| const state = env.DECO_CHAT_REQUEST_CONTEXT.state; | ||
| const expirationSeconds = getPresignedUrlExpiration(env, expiresIn); | ||
|
|
||
| const command = new PutObjectCommand({ | ||
| Bucket: state.bucketName, | ||
| Key: key, | ||
| ContentType: contentType, | ||
| }); | ||
|
|
||
| const url = await getSignedUrl(s3Client, command, { | ||
| const url = s3Client.file(key).presign({ | ||
| method: "PUT", | ||
| expiresIn: expirationSeconds, | ||
| type: contentType, | ||
| }); | ||
|
|
||
| return { | ||
|
|
@@ -245,14 +222,8 @@ export const createDeleteObjectTool = (env: Env) => | |
| execute: async (ctx: any) => { | ||
| const { key } = ctx; | ||
| const s3Client = createS3Client(env); | ||
| const state = env.DECO_CHAT_REQUEST_CONTEXT.state; | ||
|
|
||
| const command = new DeleteObjectCommand({ | ||
| Bucket: state.bucketName, | ||
| Key: key, | ||
| }); | ||
|
|
||
| await s3Client.send(command); | ||
| await s3Client.file(key).delete(); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: wc -l object-storage/server/tools/storage.tsRepository: decocms/mcps Length of output: 98 🏁 Script executed: sed -n '210,240p' object-storage/server/tools/storage.tsRepository: decocms/mcps Length of output: 872 🏁 Script executed: rg -n "createDeleteObjectTool" object-storage/server/tools/storage.ts -A 20Repository: decocms/mcps Length of output: 847 🏁 Script executed: rg -n "\.delete\(\)" object-storage/server/tools/storage.ts -B 5 -A 5Repository: decocms/mcps Length of output: 751 🏁 Script executed: rg -n "createDeleteObjectTool\|DELETE_OBJECT" object-storage/server/tools/storage.ts -B 2 -A 2Repository: decocms/mcps Length of output: 38 🏁 Script executed: rg -n "DELETE_OBJECT" --type=ts -B 3 -A 3 | head -50Repository: decocms/mcps Length of output: 2708 🏁 Script executed: grep -r "\.delete\(\)" object-storage --type=ts -B 3 -A 3Repository: decocms/mcps Length of output: 176 🏁 Script executed: sed -n '250,280p' object-storage/server/tools/storage.tsRepository: decocms/mcps Length of output: 1045 🏁 Script executed: rg -n "Promise.allSettled" object-storage --type=ts -B 5 -A 10Repository: decocms/mcps Length of output: 1359 🏁 Script executed: rg -n "execute.*async" object-storage/server/tools/storage.ts -A 15Repository: decocms/mcps Length of output: 3027 Add error handling to match the batch delete pattern. The single delete operation lacks error handling, while the batch delete implementation (line 267–277) correctly uses 🤖 Prompt for AI Agents |
||
|
|
||
| return { | ||
| success: true, | ||
|
|
@@ -291,23 +262,29 @@ export const createDeleteObjectsTool = (env: Env) => | |
| execute: async (ctx: any) => { | ||
| const { keys } = ctx; | ||
| const s3Client = createS3Client(env); | ||
| const state = env.DECO_CHAT_REQUEST_CONTEXT.state; | ||
|
|
||
| const command = new DeleteObjectsCommand({ | ||
| Bucket: state.bucketName, | ||
| Delete: { | ||
| Objects: keys.map((key: string) => ({ Key: key })), | ||
| }, | ||
| }); | ||
| // Bun doesn't have batch delete, so we use Promise.allSettled for parallel deletes | ||
| const results = await Promise.allSettled( | ||
| keys.map((key: string) => s3Client.file(key).delete()), | ||
| ); | ||
|
|
||
| const response = await s3Client.send(command); | ||
| const deleted: string[] = []; | ||
| const errors: Array<{ key: string; message: string }> = []; | ||
|
|
||
| results.forEach((result, index) => { | ||
| if (result.status === "fulfilled") { | ||
| deleted.push(keys[index]); | ||
| } else { | ||
| errors.push({ | ||
| key: keys[index], | ||
| message: result.reason?.message || "Unknown error", | ||
| }); | ||
| } | ||
| }); | ||
|
|
||
| return { | ||
| deleted: (response.Deleted || []).map((obj) => obj.Key!), | ||
| errors: (response.Errors || []).map((err) => ({ | ||
| key: err.Key!, | ||
| message: err.Message || "Unknown error", | ||
| })), | ||
| deleted, | ||
| errors, | ||
| }; | ||
| }, | ||
| }); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,42 +1,16 @@ | ||
| import { defineConfig } from "vite"; | ||
| import react from "@vitejs/plugin-react"; | ||
| import tailwindcss from "@tailwindcss/vite"; | ||
| import { cloudflare } from "@cloudflare/vite-plugin"; | ||
| import deco from "@decocms/mcps-shared/vite-plugin"; | ||
| import deco from "@decocms/vite-plugin"; | ||
|
|
||
| import path from "path"; | ||
|
|
||
| const VITE_SERVER_ENVIRONMENT_NAME = "server"; | ||
|
|
||
| // https://vitejs.dev/config/ | ||
| export default defineConfig({ | ||
| plugins: [ | ||
| react(), | ||
| cloudflare({ | ||
| configPath: "wrangler.toml", | ||
| viteEnvironment: { | ||
| name: VITE_SERVER_ENVIRONMENT_NAME, | ||
| }, | ||
| }), | ||
| tailwindcss(), | ||
| deco(), | ||
| ], | ||
| plugins: [react(), tailwindcss(), deco({ target: "bun" })], | ||
| resolve: { | ||
| alias: { | ||
| "@": path.resolve(__dirname, "./view/src"), | ||
| }, | ||
| }, | ||
|
|
||
| define: { | ||
| // Ensure proper module definitions for Cloudflare Workers context | ||
| "process.env.NODE_ENV": JSON.stringify( | ||
| process.env.NODE_ENV || "development", | ||
| ), | ||
| global: "globalThis", | ||
| // '__filename': '""', | ||
| // '__dirname': '""', | ||
| }, | ||
|
|
||
| // Clear cache more aggressively | ||
| cacheDir: "node_modules/.vite", | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: decocms/mcps
Length of output: 292
🏁 Script executed:
Repository: decocms/mcps
Length of output: 600
🏁 Script executed:
#!/bin/bash cat object-storage/DockerfileRepository: decocms/mcps
Length of output: 689
🏁 Script executed:
Repository: decocms/mcps
Length of output: 946
Verify the vite build output configuration and document the expected build structure.
The build scripts orchestrate client and server builds, but there's a critical configuration gap:
vite.config.tsdoes not explicitly setbuild.outDir, sobun run build:clientwill output todist/by default rather thandist/client. This creates a mixed directory structure where client assets and the server bundle (dist/server/main.js) coexist at the root ofdist/.To match the apparent intent (separate client and server builds):
build: { outDir: 'dist/client' }tovite.config.tsdist/The Dockerfile copies all of
dist/without issues, but the build structure should be explicitly defined to avoid confusion and prevent accidental asset collisions.🤖 Prompt for AI Agents