From d6303e4998e266113cea8fa84e441321afadbc3d Mon Sep 17 00:00:00 2001 From: Till-JS <101404291+Till-JS@users.noreply.github.com> Date: Mon, 16 Feb 2026 15:35:18 +0100 Subject: [PATCH] feat(storage): add public endpoint support for presigned URLs When services run in Docker with internal endpoints (e.g., http://minio:9000), presigned URLs are inaccessible from browsers. This adds S3_PUBLIC_ENDPOINT support to generate presigned URLs using a publicly accessible endpoint (e.g., https://minio.mana.how) while keeping internal operations on the Docker network. Changes: - Add publicEndpoint to StorageConfig type - Create separate S3Client for presigned URL generation - Add S3_PUBLIC_ENDPOINT to factory configuration - Configure lightwrite-backend with public MinIO endpoint Co-Authored-By: Claude Opus 4.5 --- docker-compose.macmini.yml | 1 + packages/shared-storage/src/client.ts | 22 ++++++++++++++++++++-- packages/shared-storage/src/factory.ts | 7 +++++-- packages/shared-storage/src/types.ts | 7 +++++++ 4 files changed, 33 insertions(+), 4 deletions(-) diff --git a/docker-compose.macmini.yml b/docker-compose.macmini.yml index 454c37032..7267cd34b 100644 --- a/docker-compose.macmini.yml +++ b/docker-compose.macmini.yml @@ -609,6 +609,7 @@ services: MANA_CORE_AUTH_URL: http://mana-auth:3001 CORS_ORIGINS: https://lightwrite.mana.how,https://mana.how S3_ENDPOINT: http://minio:9000 + S3_PUBLIC_ENDPOINT: https://minio.mana.how S3_REGION: us-east-1 S3_ACCESS_KEY: ${MINIO_ACCESS_KEY:-minioadmin} S3_SECRET_KEY: ${MINIO_SECRET_KEY:-minioadmin} diff --git a/packages/shared-storage/src/client.ts b/packages/shared-storage/src/client.ts index 2202fbac7..d8973f70c 100644 --- a/packages/shared-storage/src/client.ts +++ b/packages/shared-storage/src/client.ts @@ -22,9 +22,11 @@ import type { */ export class StorageClient { private client: S3Client; + private presignClient: S3Client; private bucket: BucketConfig; constructor(config: StorageConfig, bucket: BucketConfig) { + // Main client for internal operations (upload, download, delete, etc.) this.client = new S3Client({ endpoint: config.endpoint, region: config.region, @@ -34,6 +36,20 @@ export class StorageClient { }, forcePathStyle: config.forcePathStyle ?? true, }); + + // Separate client for presigned URLs (uses public endpoint if available) + // This allows internal operations to use Docker network addresses + // while presigned URLs use publicly accessible endpoints + this.presignClient = new S3Client({ + endpoint: config.publicEndpoint ?? config.endpoint, + region: config.region, + credentials: { + accessKeyId: config.accessKeyId, + secretAccessKey: config.secretAccessKey, + }, + forcePathStyle: config.forcePathStyle ?? true, + }); + this.bucket = bucket; } @@ -142,6 +158,7 @@ export class StorageClient { /** * Generate a presigned URL for uploading (PUT) + * Uses the public endpoint if configured, allowing browser access */ async getUploadUrl(key: string, options: PresignedUrlOptions = {}): Promise { const command = new PutObjectCommand({ @@ -149,13 +166,14 @@ export class StorageClient { Key: key, }); - return getSignedUrl(this.client, command, { + return getSignedUrl(this.presignClient, command, { expiresIn: options.expiresIn ?? 3600, }); } /** * Generate a presigned URL for downloading (GET) + * Uses the public endpoint if configured, allowing browser access */ async getDownloadUrl(key: string, options: PresignedUrlOptions = {}): Promise { const command = new GetObjectCommand({ @@ -163,7 +181,7 @@ export class StorageClient { Key: key, }); - return getSignedUrl(this.client, command, { + return getSignedUrl(this.presignClient, command, { expiresIn: options.expiresIn ?? 3600, }); } diff --git a/packages/shared-storage/src/factory.ts b/packages/shared-storage/src/factory.ts index d3fb622a7..088a7d480 100644 --- a/packages/shared-storage/src/factory.ts +++ b/packages/shared-storage/src/factory.ts @@ -7,6 +7,7 @@ import type { StorageConfig, BucketConfig, BucketName } from './types'; */ const ENV_KEYS = { ENDPOINT: 'S3_ENDPOINT', + PUBLIC_ENDPOINT: 'S3_PUBLIC_ENDPOINT', REGION: 'S3_REGION', ACCESS_KEY: 'S3_ACCESS_KEY', SECRET_KEY: 'S3_SECRET_KEY', @@ -29,15 +30,17 @@ const MINIO_DEFAULTS: StorageConfig = { */ export function getStorageConfig(): StorageConfig { const isDev = process.env.NODE_ENV === 'development' || !process.env.NODE_ENV; + const endpoint = process.env[ENV_KEYS.ENDPOINT] ?? (isDev ? MINIO_DEFAULTS.endpoint : ''); // Use environment variables if available, otherwise use MinIO defaults return { - endpoint: process.env[ENV_KEYS.ENDPOINT] ?? (isDev ? MINIO_DEFAULTS.endpoint : ''), + endpoint, + publicEndpoint: process.env[ENV_KEYS.PUBLIC_ENDPOINT], region: process.env[ENV_KEYS.REGION] ?? MINIO_DEFAULTS.region, accessKeyId: process.env[ENV_KEYS.ACCESS_KEY] ?? (isDev ? MINIO_DEFAULTS.accessKeyId : ''), secretAccessKey: process.env[ENV_KEYS.SECRET_KEY] ?? (isDev ? MINIO_DEFAULTS.secretAccessKey : ''), - forcePathStyle: isDev || process.env[ENV_KEYS.ENDPOINT]?.includes('localhost'), + forcePathStyle: isDev || endpoint?.includes('localhost') || endpoint?.includes('minio'), }; } diff --git a/packages/shared-storage/src/types.ts b/packages/shared-storage/src/types.ts index 9a95bb143..cd7691d1b 100644 --- a/packages/shared-storage/src/types.ts +++ b/packages/shared-storage/src/types.ts @@ -12,6 +12,13 @@ export interface StorageConfig { secretAccessKey: string; /** Force path-style URLs (required for MinIO) */ forcePathStyle?: boolean; + /** + * Public endpoint for generating presigned URLs accessible from browsers. + * Use this when the internal endpoint (e.g., http://minio:9000) differs + * from the public URL (e.g., https://minio.mana.how). + * If not set, presigned URLs use the main endpoint. + */ + publicEndpoint?: string; } /**