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
17 changes: 13 additions & 4 deletions apps/api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -292,8 +292,11 @@ Summary of what's implemented:

## MCP Server

The MCP server is exposed at `POST /mcp` over JSON-RPC HTTP. Use the
`Mcp-Session-Id` header for session continuation.
The MCP server is exposed at `POST /mcp` using the TypeScript SDK 2's stateless
Streamable HTTP handler. Clients using the `2026-07-28` protocol use the modern
request pattern, while 2025-era Streamable HTTP clients use the SDK's stateless
`initialize` compatibility path. Neither path creates or retains session IDs,
and the legacy HTTP+SSE transport is not supported.

MCP clients authenticate the same way as REST clients:

Expand All @@ -306,8 +309,14 @@ OAuth-capable MCP clients can discover metadata from:
- `GET /.well-known/oauth-authorization-server`
- `GET /.well-known/openid-configuration`

OAuth client registration, authorization, token, introspection, revocation, and
userinfo endpoints are served by Better Auth under `/api/auth/oauth2/*`.
OAuth authorization, token, introspection, revocation, and userinfo endpoints
are served by Better Auth under `/api/auth/oauth2/*`. Modern clients can use
CIMD, while existing clients can use Dynamic Client Registration (DCR); static
pre-registered clients are also supported. OAuth tool access is
default-deny using the scope map in `src/mcp/policy.ts`. Team API keys have full
tool access to their fixed team, while browser session cookies are rejected.
OAuth authorization requests must include an explicit, non-empty `scope` so a
missing value cannot expand to the client's complete capability set.

MCP tools live in `src/mcp/tools/*` and cover contacts, templates, sequences,
ESP settings (both the default-ESP singleton tools and the multi-ESP
Expand Down
483 changes: 483 additions & 0 deletions apps/api/docs/mcp-2026-07-28-migration.md

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions apps/api/docs/replace-oauth-server-with-better-auth.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
**PRD: Replace Custom OAuth2 With Better Auth**

> Historical design note: its transport decisions are superseded by
> [`mcp-2026-07-28-migration.md`](./mcp-2026-07-28-migration.md). The current
> MCP implementation prefers CIMD and retains public Dynamic Client
> Registration for existing clients.

**Objective**
Replace SendLit’s custom OAuth2/auth implementation with Better Auth to support secure first-party web login, MCP OAuth, REST API authentication, and social login with Google plus Email OTP.

Expand Down
70 changes: 70 additions & 0 deletions apps/api/drizzle/0001_square_energizer.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
CREATE TABLE IF NOT EXISTS "oauth_client_assertion" (
"id" text PRIMARY KEY NOT NULL,
"expires_at" timestamp with time zone NOT NULL
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "oauth_client_resource" (
"id" text PRIMARY KEY NOT NULL,
"client_id" text NOT NULL,
"resource_id" text NOT NULL,
"metadata" jsonb,
"created_at" timestamp with time zone
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "oauth_resource" (
"id" text PRIMARY KEY NOT NULL,
"identifier" text NOT NULL,
"name" text NOT NULL,
"access_token_ttl" integer,
"refresh_token_ttl" integer,
"signing_algorithm" text,
"signing_key_id" text,
"allowed_scopes" text[],
"custom_claims" jsonb,
"dpop_bound_access_tokens_required" boolean DEFAULT false NOT NULL,
"disabled" boolean DEFAULT false NOT NULL,
"created_at" timestamp with time zone,
"updated_at" timestamp with time zone,
"policy_version" integer DEFAULT 1 NOT NULL,
"metadata" jsonb,
CONSTRAINT "oauth_resource_identifier_unique" UNIQUE("identifier")
);
--> statement-breakpoint
ALTER TABLE "oauth_access_token" ADD COLUMN "authorization_code_id" text;--> statement-breakpoint
ALTER TABLE "oauth_access_token" ADD COLUMN "resources" text[];--> statement-breakpoint
ALTER TABLE "oauth_access_token" ADD COLUMN "requested_user_info_claims" text[];--> statement-breakpoint
ALTER TABLE "oauth_access_token" ADD COLUMN "confirmation" jsonb;--> statement-breakpoint
ALTER TABLE "oauth_client" ADD COLUMN "client_discovery_id" text;--> statement-breakpoint
ALTER TABLE "oauth_client" ADD COLUMN "client_credentials_scopes" text[] DEFAULT '{}' NOT NULL;--> statement-breakpoint
ALTER TABLE "oauth_client" ADD COLUMN "backchannel_logout_uri" text;--> statement-breakpoint
ALTER TABLE "oauth_client" ADD COLUMN "backchannel_logout_session_required" boolean;--> statement-breakpoint
ALTER TABLE "oauth_client" ADD COLUMN "application_type" text;--> statement-breakpoint
ALTER TABLE "oauth_client" ADD COLUMN "jwks" text;--> statement-breakpoint
ALTER TABLE "oauth_client" ADD COLUMN "jwks_uri" text;--> statement-breakpoint
ALTER TABLE "oauth_client" ADD COLUMN "dpop_bound_access_tokens" boolean DEFAULT false;--> statement-breakpoint
ALTER TABLE "oauth_consent" ADD COLUMN "resources" text[];--> statement-breakpoint
ALTER TABLE "oauth_consent" ADD COLUMN "requested_user_info_claims" text[];--> statement-breakpoint
ALTER TABLE "oauth_refresh_token" ADD COLUMN "authorization_code_id" text;--> statement-breakpoint
ALTER TABLE "oauth_refresh_token" ADD COLUMN "resources" text[];--> statement-breakpoint
ALTER TABLE "oauth_refresh_token" ADD COLUMN "requested_user_info_claims" text[];--> statement-breakpoint
ALTER TABLE "oauth_refresh_token" ADD COLUMN "rotated_at" timestamp with time zone;--> statement-breakpoint
ALTER TABLE "oauth_refresh_token" ADD COLUMN "rotation_replay_response" text;--> statement-breakpoint
ALTER TABLE "oauth_refresh_token" ADD COLUMN "rotation_replay_expires_at" timestamp with time zone;--> statement-breakpoint
ALTER TABLE "oauth_refresh_token" ADD COLUMN "confirmation" jsonb;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "oauth_client_resource" ADD CONSTRAINT "oauth_client_resource_client_id_oauth_client_client_id_fk" FOREIGN KEY ("client_id") REFERENCES "public"."oauth_client"("client_id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "oauth_client_resource" ADD CONSTRAINT "oauth_client_resource_resource_id_oauth_resource_identifier_fk" FOREIGN KEY ("resource_id") REFERENCES "public"."oauth_resource"("identifier") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "auth_oauth_client_resource_client_id_idx" ON "oauth_client_resource" USING btree ("client_id");--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "auth_oauth_client_resource_resource_id_idx" ON "oauth_client_resource" USING btree ("resource_id");--> statement-breakpoint
CREATE UNIQUE INDEX IF NOT EXISTS "auth_oauth_client_resource_client_id_resource_id_idx" ON "oauth_client_resource" USING btree ("client_id","resource_id");--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "auth_oauth_access_token_authorization_code_id_idx" ON "oauth_access_token" USING btree ("authorization_code_id");--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "auth_oauth_refresh_token_authorization_code_id_idx" ON "oauth_refresh_token" USING btree ("authorization_code_id");
Loading
Loading