Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/code-quality.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Setup Node.js
uses: actions/setup-node@v2
uses: actions/setup-node@v4
with:
node-version: '21'
node-version: '22'

- name: Configure CI Git User
run: |
Expand All @@ -36,7 +36,7 @@ jobs:

- name: Install pnpm
run: |
npm i pnpm@latest -g
npm i pnpm@10.7.1 -g

- name: Install dependencies
run: pnpm install
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:

steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4

- name: Initialize CodeQL
uses: github/codeql-action/init@v2
Expand Down
51 changes: 22 additions & 29 deletions .github/workflows/publish-docker-image.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,56 +12,49 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v3

- name: Checkout and pull branch
run: |
LATEST_TAG=$(git describe --tags `git rev-list --tags --max-count=1`)
git checkout $LATEST_TAG
uses: actions/checkout@v4

- name: Setup buildx
uses: docker/setup-buildx-action@v1
uses: docker/setup-buildx-action@v3

- name: Login to DockerHub
uses: docker/login-action@v1
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERNAME }}
password: ${{ secrets.DOCKERTOKEN }}

- name: Docker meta for core
id: meta_core
uses: docker/metadata-action@v3
- name: Docker meta for API
id: meta_api
uses: docker/metadata-action@v5
with:
images: codelit/medialit

- name: Build and push API
id: docker_build_api
uses: docker/build-push-action@v2
uses: docker/build-push-action@v6
with:
context: .
file: ./apps/api/Dockerfile
push: true
tags: ${{ steps.meta_core.outputs.tags }}
tags: ${{ steps.meta_api.outputs.tags }}

- name: Image digest
run: echo ${{ steps.docker_build_api.outputs.digest }}

- name: Setup AWS ECR Details
uses: aws-actions/configure-aws-credentials@v4
- name: Docker meta for Web app
id: meta_web
uses: docker/metadata-action@v5
with:
aws-access-key-id: ${{ secrets.AWS_ECR_ACCESS_KEY }}
aws-secret-access-key: ${{ secrets.AWS_ECR_ACCESS_SECRET }}
aws-region: ${{ secrets.AWS_REGION }}
images: codelit/medialit-web

- name: Login to AWS ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
- name: Build and push Web App
id: docker_build_web
uses: docker/build-push-action@v6
with:
context: .
file: ./apps/web/Dockerfile
push: true
tags: ${{ steps.meta_web.outputs.tags }}

- name: Build, tag, and push docker images to Amazon ECR
env:
REGISTRY: ${{ steps.login-ecr.outputs.registry }}
REPOSITORY: ${{ secrets.AWS_ECR_REPO }}
IMAGE_TAG: latest
run: |
docker build -f apps/web/Dockerfile -t $REGISTRY/$REPOSITORY:$IMAGE_TAG .
docker push $REGISTRY/$REPOSITORY:$IMAGE_TAG
- name: Image digest
run: echo ${{ steps.docker_build_web.outputs.digest }}
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,8 @@ dist

# IDE files
.rgignore

# Agent skills (clone manually from https://github.com/addyosmani/agent-skills)
.agent-skills/
.claude/
.agents/
28 changes: 28 additions & 0 deletions .migrations/00006-mark-first-apikeys-default.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Find all unique userIds that have non-deleted API keys
const userIds = db.apikeys.distinct("userId", { deleted: { $ne: true } });

userIds.forEach((userId) => {
// Check if the user already has a default API key
const hasDefault = db.apikeys.findOne({
userId: userId,
default: true,
deleted: { $ne: true },
});
if (hasDefault) {
return; // Already has a default key, skip
}

// Find the oldest active API key for this user
const oldestKey = db.apikeys
.find({ userId: userId, deleted: { $ne: true } })
.sort({ createdAt: 1 })
.limit(1)
.toArray()[0];

if (oldestKey) {
db.apikeys.updateOne(
{ _id: oldestKey._id },
{ $set: { default: true } },
);
}
});
6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Dev Environment Tips
## Dev Environment

- Use `pnpm` as the package manager.
- This is a monorepo, so use `pnpm --filter <package-name> <command>` to run commands in specific packages.
- Use `pnpm` as the package manager
- This is a monorepo use `pnpm --filter <package-name> <command>` for specific packages
77 changes: 29 additions & 48 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,84 +1,66 @@
# Introduction

MediaLit is a Node.js based app to store, convert and optimise the media files on any AWS S3 compatible storage.
MediaLit is a platform for uploading, transforming, and storing files on any S3-compatible storage provider.

## Setting up correct access on AWS S3 bucket
Use it as cloud storage for your apps, a personal media drive, or a file system for AI agents. MediaLit provides both a REST API and an MCP server for managing files programmatically.

Before you start uploading to your bucket, make sure you have set up the correct access on your S3 bucket.
## Managing your files

### 1. Without Cloudfront
This repository contains:

#### 1. Allow public access
- The backend API (under `apps/api`)
- The frontend (under `apps/web`)

![BLock public access](./apps/api/assets/without-cloudfront.png)
### Starting the API

#### 2. Allow ACLs
In order to upload files to the platform, you need to have an app. You can interact with the service using the app's API key.

### 2. With Cloudfront

![BLock public access](./apps/api/assets/with-cloudfront.png)

## Using Cloudfront

If you need to use a Cloudfront CDN, you can enable it in the app, by setting up the following values in your .env file.
To create one, set up the following variable in your `.env` file:

```sh
ACCESS_PRIVATE_BUCKET_VIA_CLOUDFRONT=true
CDN_ENDPOINT=CLOUDFRONT_DISTRIBUTION_NAME
CLOUDFRONT_PRIVATE_KEY="PRIVATE_KEY"
CLOUDFRONT_KEY_PAIR_ID=KEY_PAIR_ID
EMAIL=email@yourdomain.com
```

We assume that since you are using Cloudfront, you have locked down your bucket from public access. Therefore, all the files uploaded to the bucket will have ACL set to `private` i.e. they will require signed URLs in order to access them.
Then, start the API:

### Generating a key pair

Use the following commands to generate a key pair to be used above.

```sh
openssl genrsa -out private_key.pem 2048
openssl rsa -pubout -in private_key.pem -out public_key.pem
```bash
pnpm --filter @medialit/api dev
```

## Enable trust proxy
When the API starts for the very first time, a user with the provided email will be generated, and their subscription will be renewed for 10 years.

This app is based on [Express](https://expressjs.com/) which cannot work reliably when it is behind a proxy. For example, it cannot detect if it behind a proxy.
Additionally, a default app will be generated for the user and its API key will be printed in the application logs. The log containing the API key will look something like the following:

Hence, we need to enable it on our own. To do that, set the following environment variable.

```
ENABLE_TRUST_PROXY=true
```sh
{"level":30,"time":1781683124417,"pid":20848,"hostname":"hostname","apiKey":"kwtwsoMX3Xs_sDNxklMfz","msg":"Admin user created"}
```

## Creating a User
> CAUTION: Keep the generated API key confidential, as anyone could use it to store files on your instance.

In order to interact with the service, you need to have a user. You can interact with the service using the user's API key.
### Starting the frontend

To create one, set up the following variable in your `.env` file:
The frontend is optional if you simply want to store, transform, and manage your files.

```sh
EMAIL=email@yourdomain.com
```
Use the frontend if you want to:

When the app starts for the very first time, a user with the provided email will be generated, and their subscription will be renewed for 10 years.
- Manage files through a user interface
- Organize your files across multiple apps instead of putting everything in the default app

Additionally, an API key will be generated for the user and printed in the application logs. The log containing the API key will look something like the following:
To start the frontend:

```sh
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@ API key: testcktI8Sa71QUgYtest @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
pnpm --filter @medialit/web dev
```

> CAUTION: Keep the generated API key confidential, as anyone could use it to store files on your instance.
Then log in using the same email you provided above while booting up the API.

## API documentation

In order to interact with the service, you have to use the REST API. Our official Postman collection is available [here](https://www.postman.com/dark-rocket-625879/codelit/collection/5b8hfkr/medialit).
To interact with the service, you can use the REST API. Our API is documented [here](https://docs.medialit.cloud/api/createUploadSignature).

## Development

We build on Linux based systems. Hence the instructions are for those system only. If you are on Windows, we recommend using WSL.
We build on Linux-based systems. Hence, these instructions are for those systems only. If you are on Windows, we recommend using WSL.

### Install the utilities

Expand All @@ -95,8 +77,7 @@ pnpm install
### Build packages

```bash
pnpm --filter=@medialit/images build
pnpm --filter=@medialit/thumbnail build
pnpm -r build
```

### Run the service
Expand Down
11 changes: 11 additions & 0 deletions apps/api/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# --- OAuth 2.0 (Phase 3.5 — restart-safety hardening) ---
# Signing key for access + refresh tokens. MUST be at least 32 bytes.
# Generate with: openssl rand -base64 48
#
# For key rotation, set a comma-separated list — the first key signs
# new tokens, all listed keys are accepted for verification.
OAUTH_SIGNING_KEY=

# Access token lifetime in seconds. Keep short; refresh tokens are used to
# continue sessions and are revoked on logout.
TOKEN_TTL_SECONDS=900
42 changes: 42 additions & 0 deletions apps/api/__tests__/mcp/media-tool.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Constants } from "@medialit/models";
import test, { afterEach, describe, mock } from "node:test";
import assert from "node:assert";
import { handleGetTotalStorageTool } from "../../src/mcp/tools/media";
import { maxStorageAllowedSubscribed } from "../../src/config/constants";

describe("MCP get_total_storage", () => {
afterEach(() => {
mock.restoreAll();
});

test("uses the authenticated user id and returns the account storage limit", async () => {
const user = {
id: "string-user-id",
_id: "object-user-id",
subscriptionStatus: Constants.SubscriptionStatus.SUBSCRIBED,
};

let queriedUserId: unknown;
const response = await handleGetTotalStorageTool(
{
authInfo: {
clientId: user.id,
token: "test-api-key",
user,
},
},
{
getTotalSpace: async ({ userId }: any) => {
queriedUserId = userId;
return 2103931;
},
},
);

assert.equal(queriedUserId, user._id);
assert.deepEqual((response as any).structuredContent, {
storage: 2103931,
maxStorage: maxStorageAllowedSubscribed,
});
});
});
Loading
Loading