Skip to content

CLI, MCP, OAuth changes - #194

Merged
rajat1saxena merged 29 commits into
mainfrom
platform-based-rewrite
Oct 6, 2026
Merged

rajat1saxena merged 29 commits into
mainfrom
platform-based-rewrite

Conversation

@rajat1saxena

@rajat1saxena rajat1saxena commented Oct 2, 2026 •

Copy link
Copy Markdown
Member

Summary

Moves MediaLit onto CodeLit Platform and PostgreSQL, and adds the pieces that let apps, agents and people use it: a browser uploader and React component, a CLI, an MCP server with per-app OAuth, a rewritten docs site, and integration tests that gate every pull request.

MediaLit is the file backend for CourseLit, and for SendLit and FrontLit as they launch, so this PR also adds monitoring and an end-to-end PR gate.

What changes

Platform and data

  • PostgreSQL replaces MongoDB. Drizzle schema and migrations run as a separate step (db:migrate, plus an init service in Compose). bun --filter @medialit/scripts import:mongo copies existing data and keeps user ids, public ids, API key secrets and media ids. See the new Upgrade from v0.4.0 to v0.5.0 guide.
  • CodeLit Platform 0.3.0: billing, OAuth server kit, MCP server kit, observability and conformance packages, the platform manifest, and the managed conformance workflow. createApp serves the same HTTP stack in production and tests.
  • Billing: LemonSqueezy is removed, and Dodo is the only provider. With checkout off (OSS mode or no Dodo config), the billing engine is built without providers.

Auth and MCP

  • Better Auth replaces the custom OAuth server and its self-signed JWTs. BETTER_AUTH_SECRET replaces OAUTH_SIGNING_KEY.
  • Scopes are enforced. data:write is required for every write (MCP tools without readOnlyHint, and REST writes), so a token approved as read-only can no longer upload, delete or change settings. API keys keep full access, and offline_access gives MCP clients refresh tokens.
  • App picker. Users with several apps choose one after signing in. The choice goes on the token (app_id), and requests act on that app. Tokens for deleted apps are rejected.
  • New MCP tool: whoami.

New packages

  • @medialit/uploader: resumable (tus) uploads from the browser or Node.js, with progress, cancel and readable errors.
  • @medialit/react: the MediaLitUploader drop-in component and the useMediaLitUpload hook.
  • @medialit/cli: medialit login uses the same OAuth flow and app picker as MCP, registers itself with any server (self-hosted included), and adds upload, ls, get, seal, rm, whoami and logout. Uploads are sealed unless --temp is passed. MEDIALIT_API_KEY skips login for CI.
  • @medialit/integration-tests: the REST, MCP and CLI suites, plus synthetic, a quick end-to-end production check that can ping a heartbeat monitor.

SDK (medialit)

API security

  • The signature's group always wins on resumable uploads. The client's group is used only when the signature has none. Previously a client could put files into another group, which FrontLit relies on to identify a workspace.
  • The OAuth app picker escapes values written into its inline script.

Docs, CI and deployment

  • Docs: rewritten around "Storage for agents, apps and humans", with new pages for concepts, React, other frameworks, the Node.js SDK, the CLI and the dashboard. Themed with @codelitdev/design-system 0.3.0, with npm/pnpm/yarn/bun install tabs.
  • PR gate (Integration tests): boots Postgres, MinIO and Mailpit, starts the API on a fresh database and runs the REST, MCP and CLI suites. Run it locally with bun run test:integration:stack.
  • Synthetic check workflow: runs every 10 minutes once its secrets are set.
  • docker-compose.local.yml uses cgr.dev/chainguard/minio, because MinIO no longer publishes images.
  • docker-compose.yml: Postgres gets a persistent volume, and the API receives PUBLIC_API_URL, WEB_ORIGIN, CLOUD_PUBLIC_BUCKET_NAME, CLOUD_ENDPOINT_PUBLIC and DISABLE_TAGGING.

Breaking changes for self-hosters

  • MongoDB is replaced by PostgreSQL (DATABASE_URL). Run the import once; see the upgrade guide.
  • BETTER_AUTH_SECRET is required (at least 32 bytes). Set it to the previous OAUTH_SIGNING_KEY to keep existing sessions.
  • Migrations run separately (db:migrate, or the Compose init service). The API only checks that the database is reachable.
  • docker-compose.yml now requires PUBLIC_API_URL and CLOUD_PUBLIC_BUCKET_NAME.
  • Read-only OAuth tokens are now read-only. Clients that write need data:write.

Testing

  • Unit tests: API 81, SDK 15, uploader 10, React 3, CLI 6.
  • The REST, MCP and CLI integration suites pass against a fresh local stack (bun run test:integration:stack) and in the Integration tests workflow.
  • platform-cli doctor, lint and Prettier pass.
  • Checked by hand:
    • CLI login, refresh and logout against a local API, through a real browser;
    • an uploader component upload against QA;
    • a forged group sent on a resumable upload is ignored.

@rajat1saxena rajat1saxena self-assigned this Oct 2, 2026
Comment thread apps/api/src/auth/legacy-oauth.ts Fixed
Comment thread apps/api/src/auth/legacy-oauth.ts Fixed
Comment thread apps/api/src/auth/legacy-oauth.ts Fixed
Comment thread apps/api/src/auth/legacy-oauth.ts Fixed
Comment thread apps/api/src/auth/legacy-oauth.ts Fixed

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

if (
!process.env.CLOUD_BUCKET_NAME ||
!process.env.CLOUD_PUBLIC_BUCKET_NAME
) {

P1 Badge Configure the public bucket in the Compose deployment

The startup validation now requires CLOUD_PUBLIC_BUCKET_NAME, but the production docker-compose.yml only passes CLOUD_BUCKET_NAME to the API and has no way to forward the public bucket variable. Consequently, the documented Compose deployment exits during checkConfig() before serving requests, even when all of its currently required environment variables are supplied.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread docker-compose.yml Outdated
Comment thread apps/api/src/index.ts
Comment on lines +146 to +155
const publicApiUrl = (
process.env.PUBLIC_API_URL ||
process.env.API_SERVER ||
`http://127.0.0.1:${port}`
).replace(/\/$/, "");
const webOrigin = (
process.env.WEB_ORIGIN ||
process.env.WEB_CLIENT ||
"http://localhost:3000"
).replace(/\/$/, "");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Pass external auth origins to the Compose API

In the production Compose deployment, SITE_ADDRESS is supplied only to Caddy; neither PUBLIC_API_URL/API_SERVER nor WEB_ORIGIN/WEB_CLIENT reaches the API. The new auth initialization therefore advertises http://127.0.0.1:8000 as its issuer and seeds redirects to http://localhost:3000, so OAuth and dashboard sign-in redirects on an externally served Compose instance point at the visitor's local machine instead of the deployed site.

Useful? React with 👍 / 👎.

Lemon had no subscriptions. OSS mode and an empty Dodo configuration now build an engine with no providers, offers, or catalog revision. Paid checkout still requires the full cloud catalog.
Root lint was parsing every JavaScript file with next/babel, which does not resolve outside the Next app. The legacy token and revoke routes are now rate-limited, refresh tokens are checked before new tokens are issued, and userinfo reads the bearer credential without a backtracking expression.
CodeQL types better-auth's handler property as undefined, and a typeof guard does not change that. An optional call is skipped by that query and still returns 500 when the handler is missing.
Comment thread apps/api/src/auth/legacy-oauth.ts Fixed
CodeQL treats better-auth's handler property as undefined, including through a typeof guard and optional chaining. Reflect.apply keeps the called value a known function.
Comment thread apps/api/src/auth/legacy-oauth.ts Fixed
rajat1saxena and others added 15 commits October 3, 2026 14:07
CodeQL was reporting new Request as a call of undefined. The type-only Express import used the same name as the fetch constructor. The legacy forward path calls better-auth's handler directly again.
- Remove the scripts package’s duplicate database layer and migration files
- Require the API to apply migrations before running the Mongo import
- Add Drizzle migration config and metadata
- Run migrations via a dedicated command and Compose init service
- Document the migration workflow
- Adopt the Platform packages at 0.2.0 (billing, mcp-server-kit,
  oauth-server-kit, observability, platform, platform-conformance), the
  platform manifest, the managed conformance workflow, and Dependabot.
- Add `createApp` so production and tests serve the same HTTP stack, and a
  `test:conformance` suite that runs Platform MCP OAuth discovery against
  the real app on PGlite.
- Remove the legacy OAuth endpoints and self-signed JWTs, drop
  `legacy_revoked_tokens`, and replace `OAUTH_SIGNING_KEY` with a required
  `BETTER_AUTH_SECRET`. Deployments must set BETTER_AUTH_SECRET to the
  previous OAUTH_SIGNING_KEY value to keep existing sessions.
- Handle the `malformed` credential selection from @codelitdev/platform.
- Upgrade the AWS SDK so S3 XML parsing no longer depends on the
  overridden fast-xml-parser, which broke media sealing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Better Auth 1.7.7 no longer uses account.issuer, which 0003 only relaxed.
Migration 0005 drops it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every MediaLit MongoDB database already ran the `.migrations` scripts
before moving to Postgres. The dual-bucket guide described upgrading a
MongoDB install and pointed at a script that no longer exists.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The docs app pinned lucide-react ^0.487.0 while fumadocs-openapi and
fumadocs-ui need ^0.577.0, so Bun installed two fumadocs-core copies and
the OpenAPI loader plugin no longer typechecked. CircleHelp is renamed
to CircleQuestionMark in the newer lucide-react.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The single data:read scope was never checked, so a token the user
approved as read-only could upload, delete, and change settings. Add
data:write, require it for every MCP tool not annotated readOnlyHint and
for REST writes, and keep API keys at full access. Advertise
offline_access to MCP clients so they receive a refresh token.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After sign-in, users with more than one app choose which app the client
manages. The choice becomes the consent reference and an app_id claim on
the access token, and OAuth requests act on that app instead of the
default one. A token whose app was deleted is rejected.

MCP tools read a typed auth context instead of the API key and user id
overloaded into token and clientId, and register through a wrapper that
always calls them as (args, extra). This fixes tools without input
crashing in the scope check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread apps/api/src/auth/oauth-app-pages.ts Fixed
Comment thread apps/api/src/auth/oauth-app-pages.ts Fixed
Comment thread apps/api/src/auth/oauth-app-pages.ts Fixed
Comment thread apps/api/src/auth/oauth-app-pages.ts Fixed
New packages:
- @medialit/uploader: resumable (tus) uploads from the browser or a
  Node.js file stream, with progress, cancel and readable errors.
- @medialit/react: MediaLitUploader drop-in component and
  useMediaLitUpload hook, with an optional stylesheet.
- @medialit/cli: `medialit login` signs in through the browser with
  the same OAuth flow and app picker as MCP, registers itself with
  each server (works with self-hosted instances), and provides
  upload, ls, get, seal, rm, whoami and logout. Uploads are sealed
  unless --temp is passed. MEDIALIT_API_KEY skips login for CI.

SDK (medialit):
- Add createSignatureHandler for Fetch API signature routes.
- Accept an OAuth accessToken instead of an API key.
- Fix upload(): it sent "[object FormData]" instead of the file with
  Node's fetch. Use native FormData and add fileName/mimeType options.
- Fix list(): send paging and filters in the body, where the API
  reads them. They were ignored.

API:
- Resumable uploads always keep the group bound to the upload
  signature; the client's group is used only when the signature has
  none. Previously a client could override it.

Docs:
- Restructure around "Storage for agents, apps and humans": new
  concepts, React, other frameworks, Node.js SDK, CLI and dashboard
  pages; rewrite quick start, Next.js, Express, MCP, FAQs and Why
  MediaLit; correct the MCP upload limit and OAuth app selection.
- Theme with @codelitdev/design-system (MediaLit accent, fonts, logo).
- Show install commands as npm/pnpm/yarn/bun tabs.

Example: the Next.js app uses MediaLitUploader and
createSignatureHandler.
Comment thread packages/cli/src/oauth.ts
revocationEndpoint: string,
input: { clientId: string; token: string },
): Promise<void> {
await fetch(revocationEndpoint, {
Comment thread packages/cli/src/oauth.ts
tokenEndpoint: string,
params: Record<string, string>,
): Promise<Tokens> {
const response = await fetch(tokenEndpoint, {
Comment thread packages/cli/src/oauth.ts
}

async function getJson(url: string) {
const response = await fetch(url);
  Integration tests:
  - Move the REST and MCP integration tests from packages/scripts into a
    new @medialit/integration-tests package, and add a CLI suite that
    runs
    the built `medialit` command: command inventory, errors, upload
    --temp, seal, get and download, multi-file public uploads, ls
    filters
    and paging, rm, and an opt-in login check.
  - Cover the MCP whoami tool, which the MCP suite was missing.
  - `bun run test:integration` runs all three suites against one server.

  Pull request gate:
  - run-with-local-stack.sh starts Postgres, MinIO and Mailpit from
    docker-compose.local.yml under its own project and ports, migrates a
    fresh database, starts the API, reads the admin API key from its log
    and runs every suite. `bun run test:integration:stack` runs it
    locally.
  - The integration-tests workflow runs it on pull requests and pushes
    to
    main.

  Synthetic check:
  - `medialit-integration-tests synthetic` checks /ready, then uploads,
    seals, downloads and deletes a tiny file, cleans up leftovers from
    interrupted runs, and pings a heartbeat URL (or <url>/fail).
  - The synthetic workflow runs it every 10 minutes from a checkout once
    its secrets are set.
  - Publish the package to npm.

  Other:
  - Switch docker-compose.local.yml to cgr.dev/chainguard/minio: MinIO
    no
    longer publishes images on quay.io or Docker Hub.
  - Document the gate in the README and monitoring in the self-hosting
    docs; point the CLI and MCP maintenance rules at the new tests.
Comment thread packages/integration-tests/src/synthetic.ts Fixed
Rajat added 3 commits October 5, 2026 12:26
  - Docs: upgrade @codelitdev/design-system to 0.3.0, the Platform
    preset's recommended version. 0.1.0-alpha.6 failed `platform-cli
    doctor` (below_minimum_secure, unsupported). Refresh the docs icon
    from the new logo.
  - CLI: build medialit and @medialit/uploader before running the CLI's
    tests. They import those packages' dist output, which CI doesn't
    build.
  - docker-compose.yml: store Postgres in a named volume so recreating
    the container keeps the data; pass CLOUD_PUBLIC_BUCKET_NAME
    (required at startup), CLOUD_ENDPOINT_PUBLIC and DISABLE_TAGGING;
    require PUBLIC_API_URL and pass WEB_ORIGIN so OAuth and sign-in
    redirects use the public URLs instead of localhost.
  - OAuth app picker: escape "<" and line separators in strings written
    into its inline script.
  - Synthetic check: trim trailing slashes from the heartbeat URL with a
    loop instead of a regex that is slow on many slashes.
  get(), seal() and delete() put the media ID into the request URL, so
  an ID such as "../signature/create" from a user reached a different
  API endpoint with the app's API key. Fixes code scanning alerts #4,
  #5 and #53.
@rajat1saxena rajat1saxena changed the title CodeLit's Platform based migration CLI, MCP, OAuth changes Oct 5, 2026
Rajat added 2 commits October 6, 2026 08:12
docker-compose.yml now starts everything a self-hoster needs: the API,
the dashboard, PostgreSQL, MinIO with its two buckets, Mailpit for
sign-in emails, and Caddy as a plain-HTTP reverse proxy on port 80.
`cp .env.example .env && docker compose up` works without edits.

- Caddy routes three hostnames to the API, dashboard and MinIO. It does
  not do TLS; self-hosters put their own HTTPS proxy in front.
- The proxy's network aliases let the API reach its own public URL and
  MinIO from inside the stack. The API fetches its JWKS from its public
  URL to verify OAuth tokens, so MCP and CLI sign-in failed without
  them.
- .env.example uses localtest.me hostnames. Bun never resolves localhost
  or *.localhost through DNS, so those names point the API at itself.
- NODE_ENV comes from .env and defaults to production. The local trial
  sets development, because the OAuth kit rejects http:// URLs in
  production.
- WEB_ORIGIN is required, so sign-in redirects no longer fall back to
  http://localhost:3000.
- PostgreSQL is published on 127.0.0.1 only.

Dashboard: drop the hardcoded image hosts, which listed CodeLit's own
buckets and CDNs and blocked thumbnails from any other storage. Images
are now unoptimized and load straight from the storage URL.

API: reject uploads that express-fileupload truncated at its size limit.
It only flags them, so an oversized file was stored incomplete with a
200 response. The partial temp file is removed.

Docs: add Docker Compose setup, a test upload and seal, and server
steps to the self-hosting guide. Point the v0.4.0 upgrade guide at this
repository's Compose file instead of the internal single-server-setup
repo, and drop the SSH tunnel steps. The importer's .env is now created
by hand, because the scripts package no longer ships an .env.example.
Comment thread apps/api/src/media/storage-middleware.ts Fixed
Rajat and others added 2 commits October 6, 2026 13:44
removeTempFile now resolves the path and skips anything outside the
upload temp folder, which fixes CodeQL alert #152 (js/path-injection).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@rajat1saxena
rajat1saxena merged commit 435e80c into main Oct 6, 2026
5 checks passed
@rajat1saxena rajat1saxena mentioned this pull request Oct 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants