Skip to content

Commit 977c1e8

Browse files
authored
chore(release): promote Better Auth MCP & OAuth 2.1 Dynamic Client Registration to main (#727)
* feat(syndication): add syndicate-post workflow and dispatch runner (#708) * fix(syndication): update Hashnode tag shape to PublishPostTagInput (#710) * fix(syndication): update Hashnode coverImage input to string URL (#713) * feat(seo): add VideoObject structured data and Gemini video storyboard pipeline (#715) * fix(video): upgrade to Gemini 2.5 Pro with native Google AI and OpenRouter support (#717) * feat: complete Shorts story (/shorts shelf + kinetic captions + sync), Gemini 2.5/TTS upgrades, and TechArticle/Auto-FAQ AEO pack (#720) * chore: remove legacy apps/web Next.js directory and revert Gemini/Shorts pipeline additions (keep SEO schema) (#722) * feat(analytics): add Cloudflare Workers Analytics Engine edge telemetry, sendBeacon click tracking, and first-party PostHog /ingest proxy (#724) * feat(site): wire sponsorships and guest booking to FlipFeeds Creator OS dev/prod APIs * fix(ci): deploy latest Worker version before updating secrets in deploy-workers.yml * fix(site): defer analytics_engine_datasets binding until Analytics Engine is enabled on account * feat(auth): integrate Better Auth with Cloudflare D1 for MCP and OAuth 2.1 Dynamic Client Registration (#726)
1 parent 74d0c9b commit 977c1e8

24 files changed

Lines changed: 1724 additions & 168 deletions

‎.github/workflows/deploy-workers.yml‎

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -42,24 +42,24 @@ jobs:
4242
- name: Build site (dev)
4343
run: pnpm --filter @codingcatdev/site build
4444

45+
- name: Deploy to Cloudflare Workers (dev.codingcat.dev)
46+
run: pnpm --filter @codingcatdev/site exec wrangler deploy
47+
env:
48+
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
49+
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
50+
4551
- name: Configure Cloudflare Worker Secrets (dev)
4652
run: |
47-
echo "$SANITY_API_READ_TOKEN" | pnpm --filter @codingcatdev/site exec wrangler secret put SANITY_API_READ_TOKEN
53+
echo "$SANITY_API_READ_TOKEN" | pnpm --filter @codingcatdev/site exec wrangler secret put SANITY_API_READ_TOKEN --env=""
4854
if [ -n "$OPENROUTER_API_KEY" ]; then
49-
echo "$OPENROUTER_API_KEY" | pnpm --filter @codingcatdev/site exec wrangler secret put OPENROUTER_API_KEY
55+
echo "$OPENROUTER_API_KEY" | pnpm --filter @codingcatdev/site exec wrangler secret put OPENROUTER_API_KEY --env=""
5056
fi
5157
env:
5258
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
5359
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
5460
SANITY_API_READ_TOKEN: ${{ secrets.SANITY_API_READ_TOKEN }}
5561
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
5662

57-
- name: Deploy to Cloudflare Workers (dev.codingcat.dev)
58-
run: pnpm --filter @codingcatdev/site exec wrangler deploy
59-
env:
60-
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
61-
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
62-
6363
deploy-production:
6464
if: github.ref == 'refs/heads/main'
6565
runs-on: ubuntu-latest
@@ -88,6 +88,12 @@ jobs:
8888
- name: Build site (production)
8989
run: pnpm --filter @codingcatdev/site build:production
9090

91+
- name: Deploy to Cloudflare Workers (codingcat.dev)
92+
run: pnpm --filter @codingcatdev/site exec wrangler deploy --env production
93+
env:
94+
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
95+
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
96+
9197
- name: Configure Cloudflare Worker Secrets (production)
9298
run: |
9399
echo "$SANITY_API_READ_TOKEN" | pnpm --filter @codingcatdev/site exec wrangler secret put SANITY_API_READ_TOKEN --env production
@@ -99,9 +105,3 @@ jobs:
99105
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
100106
SANITY_API_READ_TOKEN: ${{ secrets.SANITY_API_READ_TOKEN }}
101107
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
102-
103-
- name: Deploy to Cloudflare Workers (codingcat.dev)
104-
run: pnpm --filter @codingcatdev/site exec wrangler deploy --env production
105-
env:
106-
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
107-
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
-- Migration: 0001_better_auth.sql
2+
-- Better Auth core + OAuth 2.1 Provider + MCP schema for Cloudflare D1
3+
4+
CREATE TABLE IF NOT EXISTS "user" (
5+
"id" text PRIMARY KEY NOT NULL,
6+
"name" text NOT NULL,
7+
"email" text NOT NULL UNIQUE,
8+
"emailVerified" integer NOT NULL DEFAULT 0,
9+
"image" text,
10+
"createdAt" integer NOT NULL,
11+
"updatedAt" integer NOT NULL
12+
);
13+
14+
CREATE TABLE IF NOT EXISTS "session" (
15+
"id" text PRIMARY KEY NOT NULL,
16+
"expiresAt" integer NOT NULL,
17+
"token" text NOT NULL UNIQUE,
18+
"createdAt" integer NOT NULL,
19+
"updatedAt" integer NOT NULL,
20+
"ipAddress" text,
21+
"userAgent" text,
22+
"userId" text NOT NULL REFERENCES "user"("id") ON DELETE CASCADE
23+
);
24+
25+
CREATE INDEX IF NOT EXISTS "session_userId_idx" ON "session" ("userId");
26+
27+
CREATE TABLE IF NOT EXISTS "account" (
28+
"id" text PRIMARY KEY NOT NULL,
29+
"accountId" text NOT NULL,
30+
"providerId" text NOT NULL,
31+
"userId" text NOT NULL REFERENCES "user"("id") ON DELETE CASCADE,
32+
"accessToken" text,
33+
"refreshToken" text,
34+
"idToken" text,
35+
"accessTokenExpiresAt" integer,
36+
"refreshTokenExpiresAt" integer,
37+
"scope" text,
38+
"password" text,
39+
"createdAt" integer NOT NULL,
40+
"updatedAt" integer NOT NULL
41+
);
42+
43+
CREATE INDEX IF NOT EXISTS "account_userId_idx" ON "account" ("userId");
44+
45+
CREATE TABLE IF NOT EXISTS "verification" (
46+
"id" text PRIMARY KEY NOT NULL,
47+
"identifier" text NOT NULL,
48+
"value" text NOT NULL,
49+
"expiresAt" integer NOT NULL,
50+
"createdAt" integer NOT NULL,
51+
"updatedAt" integer NOT NULL
52+
);
53+
54+
CREATE TABLE IF NOT EXISTS "jwks" (
55+
"id" text PRIMARY KEY NOT NULL,
56+
"publicKey" text NOT NULL,
57+
"privateKey" text NOT NULL,
58+
"createdAt" integer NOT NULL,
59+
"expiresAt" integer,
60+
"alg" text,
61+
"crv" text
62+
);
63+
64+
CREATE TABLE IF NOT EXISTS "oauthClient" (
65+
"id" text PRIMARY KEY NOT NULL,
66+
"clientId" text NOT NULL UNIQUE,
67+
"clientSecret" text,
68+
"clientDiscoveryId" text,
69+
"disabled" integer DEFAULT 0,
70+
"skipConsent" integer DEFAULT 0,
71+
"enableEndSession" integer DEFAULT 0,
72+
"subjectType" text,
73+
"scopes" text,
74+
"clientCredentialsScopes" text,
75+
"userId" text REFERENCES "user"("id") ON DELETE CASCADE,
76+
"createdAt" integer,
77+
"updatedAt" integer,
78+
"name" text,
79+
"uri" text,
80+
"icon" text,
81+
"contacts" text,
82+
"tos" text,
83+
"policy" text,
84+
"softwareId" text,
85+
"softwareVersion" text,
86+
"softwareStatement" text,
87+
"redirectUris" text NOT NULL,
88+
"postLogoutRedirectUris" text,
89+
"backchannelLogoutUri" text,
90+
"backchannelLogoutSessionRequired" integer,
91+
"tokenEndpointAuthMethod" text,
92+
"applicationType" text,
93+
"jwks" text,
94+
"jwksUri" text,
95+
"grantTypes" text,
96+
"responseTypes" text,
97+
"requirePKCE" integer DEFAULT 1,
98+
"dpopBoundAccessTokens" integer DEFAULT 0,
99+
"referenceId" text,
100+
"metadata" text
101+
);
102+
103+
CREATE INDEX IF NOT EXISTS "oauthClient_userId_idx" ON "oauthClient" ("userId");
104+
105+
CREATE TABLE IF NOT EXISTS "oauthResource" (
106+
"id" text PRIMARY KEY NOT NULL,
107+
"identifier" text NOT NULL UNIQUE,
108+
"name" text NOT NULL,
109+
"accessTokenTtl" integer,
110+
"refreshTokenTtl" integer,
111+
"signingAlgorithm" text,
112+
"signingKeyId" text,
113+
"allowedScopes" text,
114+
"customClaims" text,
115+
"dpopBoundAccessTokensRequired" integer DEFAULT 0,
116+
"disabled" integer DEFAULT 0,
117+
"createdAt" integer,
118+
"updatedAt" integer,
119+
"policyVersion" integer,
120+
"metadata" text
121+
);
122+
123+
CREATE TABLE IF NOT EXISTS "oauthClientResource" (
124+
"id" text PRIMARY KEY NOT NULL,
125+
"clientId" text NOT NULL REFERENCES "oauthClient"("clientId") ON DELETE CASCADE,
126+
"resourceId" text NOT NULL REFERENCES "oauthResource"("identifier") ON DELETE CASCADE,
127+
"metadata" text,
128+
"createdAt" integer
129+
);
130+
131+
CREATE UNIQUE INDEX IF NOT EXISTS "oauthClientResource_clientId_resourceId_uidx" ON "oauthClientResource" ("clientId", "resourceId");
132+
133+
CREATE TABLE IF NOT EXISTS "oauthRefreshToken" (
134+
"id" text PRIMARY KEY NOT NULL,
135+
"token" text NOT NULL UNIQUE,
136+
"clientId" text NOT NULL REFERENCES "oauthClient"("clientId") ON DELETE CASCADE,
137+
"sessionId" text REFERENCES "session"("id") ON DELETE SET NULL,
138+
"userId" text NOT NULL REFERENCES "user"("id") ON DELETE CASCADE,
139+
"referenceId" text,
140+
"authorizationCodeId" text,
141+
"resources" text,
142+
"requestedUserInfoClaims" text,
143+
"expiresAt" integer NOT NULL,
144+
"createdAt" integer NOT NULL,
145+
"revoked" integer,
146+
"rotatedAt" integer,
147+
"rotationReplayResponse" text,
148+
"rotationReplayExpiresAt" integer,
149+
"authTime" integer,
150+
"confirmation" text,
151+
"scopes" text NOT NULL
152+
);
153+
154+
CREATE INDEX IF NOT EXISTS "oauthRefreshToken_clientId_idx" ON "oauthRefreshToken" ("clientId");
155+
CREATE INDEX IF NOT EXISTS "oauthRefreshToken_userId_idx" ON "oauthRefreshToken" ("userId");
156+
CREATE INDEX IF NOT EXISTS "oauthRefreshToken_sessionId_idx" ON "oauthRefreshToken" ("sessionId");
157+
CREATE INDEX IF NOT EXISTS "oauthRefreshToken_authorizationCodeId_idx" ON "oauthRefreshToken" ("authorizationCodeId");
158+
159+
CREATE TABLE IF NOT EXISTS "oauthAccessToken" (
160+
"id" text PRIMARY KEY NOT NULL,
161+
"token" text NOT NULL UNIQUE,
162+
"clientId" text NOT NULL REFERENCES "oauthClient"("clientId") ON DELETE CASCADE,
163+
"sessionId" text REFERENCES "session"("id") ON DELETE SET NULL,
164+
"userId" text REFERENCES "user"("id") ON DELETE CASCADE,
165+
"referenceId" text,
166+
"authorizationCodeId" text,
167+
"resources" text,
168+
"requestedUserInfoClaims" text,
169+
"refreshId" text REFERENCES "oauthRefreshToken"("id") ON DELETE CASCADE,
170+
"expiresAt" integer NOT NULL,
171+
"createdAt" integer NOT NULL,
172+
"revoked" integer,
173+
"confirmation" text,
174+
"scopes" text NOT NULL
175+
);
176+
177+
CREATE INDEX IF NOT EXISTS "oauthAccessToken_clientId_idx" ON "oauthAccessToken" ("clientId");
178+
CREATE INDEX IF NOT EXISTS "oauthAccessToken_userId_idx" ON "oauthAccessToken" ("userId");
179+
CREATE INDEX IF NOT EXISTS "oauthAccessToken_sessionId_idx" ON "oauthAccessToken" ("sessionId");
180+
CREATE INDEX IF NOT EXISTS "oauthAccessToken_authorizationCodeId_idx" ON "oauthAccessToken" ("authorizationCodeId");
181+
CREATE INDEX IF NOT EXISTS "oauthAccessToken_refreshId_idx" ON "oauthAccessToken" ("refreshId");
182+
183+
CREATE TABLE IF NOT EXISTS "oauthConsent" (
184+
"id" text PRIMARY KEY NOT NULL,
185+
"clientId" text NOT NULL REFERENCES "oauthClient"("clientId") ON DELETE CASCADE,
186+
"userId" text REFERENCES "user"("id") ON DELETE CASCADE,
187+
"referenceId" text,
188+
"resources" text,
189+
"requestedUserInfoClaims" text,
190+
"scopes" text NOT NULL,
191+
"createdAt" integer NOT NULL,
192+
"updatedAt" integer NOT NULL
193+
);
194+
195+
CREATE INDEX IF NOT EXISTS "oauthConsent_clientId_idx" ON "oauthConsent" ("clientId");
196+
CREATE INDEX IF NOT EXISTS "oauthConsent_userId_idx" ON "oauthConsent" ("userId");
197+
198+
CREATE TABLE IF NOT EXISTS "oauthClientAssertion" (
199+
"id" text PRIMARY KEY NOT NULL,
200+
"expiresAt" integer NOT NULL
201+
);

‎apps/site/package.json‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
"@ai-sdk/mcp": "^2.0.72",
2222
"@astrojs/cloudflare": "^14.2.1",
2323
"@astrojs/react": "^6.0.2",
24+
"@better-auth/mcp": "^1.7.7",
25+
"@better-auth/oauth-provider": "^1.7.7",
2426
"@openrouter/ai-sdk-provider": "^3.1.0",
2527
"@portabletext/to-html": "^6.0.0",
2628
"@portabletext/types": "^4.0.2",
@@ -31,6 +33,7 @@
3133
"ai": "^7.0.135",
3234
"astro": "^7.2.2",
3335
"astro-portabletext": "^0.13.0",
36+
"better-auth": "^1.7.7",
3437
"groq": "^6.9.2",
3538
"react": "^19.2.6",
3639
"react-dom": "^19.2.6",

‎apps/site/src/lib/analytics.ts‎

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,9 @@ export function classifyActor(
114114
}
115115

116116
function extractHost(urlStr: string | null): string {
117-
if (!urlStr) return "direct";
117+
if (!urlStr) {
118+
return "direct";
119+
}
118120
try {
119121
return new URL(urlStr).hostname || "direct";
120122
} catch {
@@ -132,7 +134,9 @@ export function trackEdgeRequest(options: {
132134
detail?: string;
133135
}): void {
134136
const dataset = options.env.ANALYTICS as AnalyticsEngineDataset | undefined;
135-
if (!dataset || typeof dataset.writeDataPoint !== "function") return;
137+
if (!dataset || typeof dataset.writeDataPoint !== "function") {
138+
return;
139+
}
136140

137141
const { request, pathname, statusCode, durationMs, wantsMarkdown } = options;
138142

@@ -164,18 +168,19 @@ export function trackEdgeRequest(options: {
164168
eventType = "search_api";
165169
} else if (pathname.startsWith("/api/chat")) {
166170
eventType = "chat_api";
167-
} else if (category === "ai_bot" || category === "search_bot" || category === "social_bot") {
171+
} else if (
172+
category === "ai_bot" ||
173+
category === "search_bot" ||
174+
category === "social_bot"
175+
) {
168176
eventType = "bot_crawl";
169177
}
170178

171179
const refererHost = extractHost(request.headers.get("referer"));
172180
const cf = (request as Request & { cf?: { country?: string } }).cf;
173-
const country =
174-
cf?.country || request.headers.get("cf-ipcountry") || "XX";
181+
const country = cf?.country || request.headers.get("cf-ipcountry") || "XX";
175182
const targetOrDetail =
176-
options.detail ||
177-
new URL(request.url).searchParams.get("q") ||
178-
refererHost;
183+
options.detail || new URL(request.url).searchParams.get("q") || refererHost;
179184

180185
try {
181186
dataset.writeDataPoint({
@@ -205,7 +210,9 @@ export function trackClientBeaconEvent(options: {
205210
value?: number;
206211
}): void {
207212
const dataset = options.env.ANALYTICS as AnalyticsEngineDataset | undefined;
208-
if (!dataset || typeof dataset.writeDataPoint !== "function") return;
213+
if (!dataset || typeof dataset.writeDataPoint !== "function") {
214+
return;
215+
}
209216

210217
const refererHost = extractHost(options.request.headers.get("referer"));
211218
const cf = (options.request as Request & { cf?: { country?: string } }).cf;

‎apps/site/src/lib/auth.ts‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { env } from "cloudflare:workers";
2+
import { mcp } from "@better-auth/mcp";
3+
import { betterAuth } from "better-auth";
4+
import { jwt } from "better-auth/plugins";
5+
6+
/**
7+
* Creates or retrieves the Better Auth instance configured for
8+
* Model Context Protocol (MCP) and OAuth 2.1 authorization server
9+
* backed by Cloudflare D1.
10+
*/
11+
export function getAuth(origin: string, d1?: D1Database) {
12+
const db = d1 ?? (env as unknown as { DB?: D1Database })?.DB;
13+
14+
return betterAuth({
15+
baseURL: origin,
16+
basePath: "",
17+
...(db ? { database: db } : {}),
18+
plugins: [
19+
jwt(),
20+
mcp({
21+
resource: `${origin}/mcp`,
22+
loginPage: "/sign-in",
23+
consentPage: "/oauth2/consent",
24+
allowDynamicClientRegistration: true,
25+
allowUnauthenticatedClientRegistration: true,
26+
scopes: [
27+
"openid",
28+
"profile",
29+
"email",
30+
"offline_access",
31+
"read:content",
32+
"search",
33+
"mcp:tools",
34+
"agent",
35+
],
36+
}),
37+
],
38+
});
39+
}

0 commit comments

Comments
 (0)