Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
fc2c85b
feat(syndication): add syndicate-post workflow and dispatch runner (#…
codercatdev Oct 10, 2026
6b80878
fix(syndication): update Hashnode tag shape to PublishPostTagInput (#…
codercatdev Oct 10, 2026
f01fad6
chore: merge origin/main into dev
codercatdev Oct 10, 2026
9f3e1d9
fix(syndication): update Hashnode coverImage input to string URL (#713)
codercatdev Oct 10, 2026
3c95e63
feat(seo): add VideoObject structured data and Gemini video storyboar…
codercatdev Oct 10, 2026
bf33f14
fix(video): upgrade to Gemini 2.5 Pro with native Google AI and OpenR…
codercatdev Oct 10, 2026
ab7c63e
chore: sync dev with main
codercatdev Oct 10, 2026
2b95c86
feat: complete Shorts story (/shorts shelf + kinetic captions + sync)…
codercatdev Oct 10, 2026
08a9794
chore: sync dev with main
codercatdev Oct 10, 2026
bc06604
Merge remote-tracking branch 'origin/main' into dev
codercatdev Oct 10, 2026
a145450
chore: remove legacy apps/web Next.js directory and revert Gemini/Sho…
codercatdev Oct 10, 2026
a24576d
Merge remote-tracking branch 'origin/main' into dev
codercatdev Oct 10, 2026
41938e2
feat(analytics): add Cloudflare Workers Analytics Engine edge telemet…
codercatdev Oct 10, 2026
a472065
Merge remote-tracking branch 'origin/main' into dev
codercatdev Oct 10, 2026
908eca9
feat(site): wire sponsorships and guest booking to FlipFeeds Creator …
codercatdev Oct 12, 2026
91c3aa0
fix(ci): deploy latest Worker version before updating secrets in depl…
codercatdev Oct 12, 2026
514245d
fix(site): defer analytics_engine_datasets binding until Analytics En…
codercatdev Oct 12, 2026
862120f
feat(auth): integrate Better Auth with Cloudflare D1 for MCP and OAut…
codercatdev Oct 13, 2026
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
28 changes: 14 additions & 14 deletions .github/workflows/deploy-workers.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,24 +42,24 @@ jobs:
- name: Build site (dev)
run: pnpm --filter @codingcatdev/site build

- name: Deploy to Cloudflare Workers (dev.codingcat.dev)
run: pnpm --filter @codingcatdev/site exec wrangler deploy
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}

- name: Configure Cloudflare Worker Secrets (dev)
run: |
echo "$SANITY_API_READ_TOKEN" | pnpm --filter @codingcatdev/site exec wrangler secret put SANITY_API_READ_TOKEN
echo "$SANITY_API_READ_TOKEN" | pnpm --filter @codingcatdev/site exec wrangler secret put SANITY_API_READ_TOKEN --env=""
if [ -n "$OPENROUTER_API_KEY" ]; then
echo "$OPENROUTER_API_KEY" | pnpm --filter @codingcatdev/site exec wrangler secret put OPENROUTER_API_KEY
echo "$OPENROUTER_API_KEY" | pnpm --filter @codingcatdev/site exec wrangler secret put OPENROUTER_API_KEY --env=""
fi
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
SANITY_API_READ_TOKEN: ${{ secrets.SANITY_API_READ_TOKEN }}
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}

- name: Deploy to Cloudflare Workers (dev.codingcat.dev)
run: pnpm --filter @codingcatdev/site exec wrangler deploy
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}

deploy-production:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
Expand Down Expand Up @@ -88,6 +88,12 @@ jobs:
- name: Build site (production)
run: pnpm --filter @codingcatdev/site build:production

- name: Deploy to Cloudflare Workers (codingcat.dev)
run: pnpm --filter @codingcatdev/site exec wrangler deploy --env production
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}

- name: Configure Cloudflare Worker Secrets (production)
run: |
echo "$SANITY_API_READ_TOKEN" | pnpm --filter @codingcatdev/site exec wrangler secret put SANITY_API_READ_TOKEN --env production
Expand All @@ -99,9 +105,3 @@ jobs:
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
SANITY_API_READ_TOKEN: ${{ secrets.SANITY_API_READ_TOKEN }}
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}

- name: Deploy to Cloudflare Workers (codingcat.dev)
run: pnpm --filter @codingcatdev/site exec wrangler deploy --env production
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
201 changes: 201 additions & 0 deletions apps/site/migrations/0001_better_auth.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
-- Migration: 0001_better_auth.sql
-- Better Auth core + OAuth 2.1 Provider + MCP schema for Cloudflare D1

CREATE TABLE IF NOT EXISTS "user" (
"id" text PRIMARY KEY NOT NULL,
"name" text NOT NULL,
"email" text NOT NULL UNIQUE,
"emailVerified" integer NOT NULL DEFAULT 0,
"image" text,
"createdAt" integer NOT NULL,
"updatedAt" integer NOT NULL
);

CREATE TABLE IF NOT EXISTS "session" (
"id" text PRIMARY KEY NOT NULL,
"expiresAt" integer NOT NULL,
"token" text NOT NULL UNIQUE,
"createdAt" integer NOT NULL,
"updatedAt" integer NOT NULL,
"ipAddress" text,
"userAgent" text,
"userId" text NOT NULL REFERENCES "user"("id") ON DELETE CASCADE
);

CREATE INDEX IF NOT EXISTS "session_userId_idx" ON "session" ("userId");

CREATE TABLE IF NOT EXISTS "account" (
"id" text PRIMARY KEY NOT NULL,
"accountId" text NOT NULL,
"providerId" text NOT NULL,
"userId" text NOT NULL REFERENCES "user"("id") ON DELETE CASCADE,
"accessToken" text,
"refreshToken" text,
"idToken" text,
"accessTokenExpiresAt" integer,
"refreshTokenExpiresAt" integer,
"scope" text,
"password" text,
"createdAt" integer NOT NULL,
"updatedAt" integer NOT NULL
);

CREATE INDEX IF NOT EXISTS "account_userId_idx" ON "account" ("userId");

CREATE TABLE IF NOT EXISTS "verification" (
"id" text PRIMARY KEY NOT NULL,
"identifier" text NOT NULL,
"value" text NOT NULL,
"expiresAt" integer NOT NULL,
"createdAt" integer NOT NULL,
"updatedAt" integer NOT NULL
);

CREATE TABLE IF NOT EXISTS "jwks" (
"id" text PRIMARY KEY NOT NULL,
"publicKey" text NOT NULL,
"privateKey" text NOT NULL,
"createdAt" integer NOT NULL,
"expiresAt" integer,
"alg" text,
"crv" text
);

CREATE TABLE IF NOT EXISTS "oauthClient" (
"id" text PRIMARY KEY NOT NULL,
"clientId" text NOT NULL UNIQUE,
"clientSecret" text,
"clientDiscoveryId" text,
"disabled" integer DEFAULT 0,
"skipConsent" integer DEFAULT 0,
"enableEndSession" integer DEFAULT 0,
"subjectType" text,
"scopes" text,
"clientCredentialsScopes" text,
"userId" text REFERENCES "user"("id") ON DELETE CASCADE,
"createdAt" integer,
"updatedAt" integer,
"name" text,
"uri" text,
"icon" text,
"contacts" text,
"tos" text,
"policy" text,
"softwareId" text,
"softwareVersion" text,
"softwareStatement" text,
"redirectUris" text NOT NULL,
"postLogoutRedirectUris" text,
"backchannelLogoutUri" text,
"backchannelLogoutSessionRequired" integer,
"tokenEndpointAuthMethod" text,
"applicationType" text,
"jwks" text,
"jwksUri" text,
"grantTypes" text,
"responseTypes" text,
"requirePKCE" integer DEFAULT 1,
"dpopBoundAccessTokens" integer DEFAULT 0,
"referenceId" text,
"metadata" text
);

CREATE INDEX IF NOT EXISTS "oauthClient_userId_idx" ON "oauthClient" ("userId");

CREATE TABLE IF NOT EXISTS "oauthResource" (
"id" text PRIMARY KEY NOT NULL,
"identifier" text NOT NULL UNIQUE,
"name" text NOT NULL,
"accessTokenTtl" integer,
"refreshTokenTtl" integer,
"signingAlgorithm" text,
"signingKeyId" text,
"allowedScopes" text,
"customClaims" text,
"dpopBoundAccessTokensRequired" integer DEFAULT 0,
"disabled" integer DEFAULT 0,
"createdAt" integer,
"updatedAt" integer,
"policyVersion" integer,
"metadata" text
);

CREATE TABLE IF NOT EXISTS "oauthClientResource" (
"id" text PRIMARY KEY NOT NULL,
"clientId" text NOT NULL REFERENCES "oauthClient"("clientId") ON DELETE CASCADE,
"resourceId" text NOT NULL REFERENCES "oauthResource"("identifier") ON DELETE CASCADE,
"metadata" text,
"createdAt" integer
);

CREATE UNIQUE INDEX IF NOT EXISTS "oauthClientResource_clientId_resourceId_uidx" ON "oauthClientResource" ("clientId", "resourceId");

CREATE TABLE IF NOT EXISTS "oauthRefreshToken" (
"id" text PRIMARY KEY NOT NULL,
"token" text NOT NULL UNIQUE,
"clientId" text NOT NULL REFERENCES "oauthClient"("clientId") ON DELETE CASCADE,
"sessionId" text REFERENCES "session"("id") ON DELETE SET NULL,
"userId" text NOT NULL REFERENCES "user"("id") ON DELETE CASCADE,
"referenceId" text,
"authorizationCodeId" text,
"resources" text,
"requestedUserInfoClaims" text,
"expiresAt" integer NOT NULL,
"createdAt" integer NOT NULL,
"revoked" integer,
"rotatedAt" integer,
"rotationReplayResponse" text,
"rotationReplayExpiresAt" integer,
"authTime" integer,
"confirmation" text,
"scopes" text NOT NULL
);

CREATE INDEX IF NOT EXISTS "oauthRefreshToken_clientId_idx" ON "oauthRefreshToken" ("clientId");
CREATE INDEX IF NOT EXISTS "oauthRefreshToken_userId_idx" ON "oauthRefreshToken" ("userId");
CREATE INDEX IF NOT EXISTS "oauthRefreshToken_sessionId_idx" ON "oauthRefreshToken" ("sessionId");
CREATE INDEX IF NOT EXISTS "oauthRefreshToken_authorizationCodeId_idx" ON "oauthRefreshToken" ("authorizationCodeId");

CREATE TABLE IF NOT EXISTS "oauthAccessToken" (
"id" text PRIMARY KEY NOT NULL,
"token" text NOT NULL UNIQUE,
"clientId" text NOT NULL REFERENCES "oauthClient"("clientId") ON DELETE CASCADE,
"sessionId" text REFERENCES "session"("id") ON DELETE SET NULL,
"userId" text REFERENCES "user"("id") ON DELETE CASCADE,
"referenceId" text,
"authorizationCodeId" text,
"resources" text,
"requestedUserInfoClaims" text,
"refreshId" text REFERENCES "oauthRefreshToken"("id") ON DELETE CASCADE,
"expiresAt" integer NOT NULL,
"createdAt" integer NOT NULL,
"revoked" integer,
"confirmation" text,
"scopes" text NOT NULL
);

CREATE INDEX IF NOT EXISTS "oauthAccessToken_clientId_idx" ON "oauthAccessToken" ("clientId");
CREATE INDEX IF NOT EXISTS "oauthAccessToken_userId_idx" ON "oauthAccessToken" ("userId");
CREATE INDEX IF NOT EXISTS "oauthAccessToken_sessionId_idx" ON "oauthAccessToken" ("sessionId");
CREATE INDEX IF NOT EXISTS "oauthAccessToken_authorizationCodeId_idx" ON "oauthAccessToken" ("authorizationCodeId");
CREATE INDEX IF NOT EXISTS "oauthAccessToken_refreshId_idx" ON "oauthAccessToken" ("refreshId");

CREATE TABLE IF NOT EXISTS "oauthConsent" (
"id" text PRIMARY KEY NOT NULL,
"clientId" text NOT NULL REFERENCES "oauthClient"("clientId") ON DELETE CASCADE,
"userId" text REFERENCES "user"("id") ON DELETE CASCADE,
"referenceId" text,
"resources" text,
"requestedUserInfoClaims" text,
"scopes" text NOT NULL,
"createdAt" integer NOT NULL,
"updatedAt" integer NOT NULL
);

CREATE INDEX IF NOT EXISTS "oauthConsent_clientId_idx" ON "oauthConsent" ("clientId");
CREATE INDEX IF NOT EXISTS "oauthConsent_userId_idx" ON "oauthConsent" ("userId");

CREATE TABLE IF NOT EXISTS "oauthClientAssertion" (
"id" text PRIMARY KEY NOT NULL,
"expiresAt" integer NOT NULL
);
3 changes: 3 additions & 0 deletions apps/site/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
"@ai-sdk/mcp": "^2.0.72",
"@astrojs/cloudflare": "^14.2.1",
"@astrojs/react": "^6.0.2",
"@better-auth/mcp": "^1.7.7",
"@better-auth/oauth-provider": "^1.7.7",
"@openrouter/ai-sdk-provider": "^3.1.0",
"@portabletext/to-html": "^6.0.0",
"@portabletext/types": "^4.0.2",
Expand All @@ -31,6 +33,7 @@
"ai": "^7.0.135",
"astro": "^7.2.2",
"astro-portabletext": "^0.13.0",
"better-auth": "^1.7.7",
"groq": "^6.9.2",
"react": "^19.2.6",
"react-dom": "^19.2.6",
Expand Down
25 changes: 16 additions & 9 deletions apps/site/src/lib/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,9 @@ export function classifyActor(
}

function extractHost(urlStr: string | null): string {
if (!urlStr) return "direct";
if (!urlStr) {
return "direct";
}
try {
return new URL(urlStr).hostname || "direct";
} catch {
Expand All @@ -132,7 +134,9 @@ export function trackEdgeRequest(options: {
detail?: string;
}): void {
const dataset = options.env.ANALYTICS as AnalyticsEngineDataset | undefined;
if (!dataset || typeof dataset.writeDataPoint !== "function") return;
if (!dataset || typeof dataset.writeDataPoint !== "function") {
return;
}

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

Expand Down Expand Up @@ -164,18 +168,19 @@ export function trackEdgeRequest(options: {
eventType = "search_api";
} else if (pathname.startsWith("/api/chat")) {
eventType = "chat_api";
} else if (category === "ai_bot" || category === "search_bot" || category === "social_bot") {
} else if (
category === "ai_bot" ||
category === "search_bot" ||
category === "social_bot"
) {
eventType = "bot_crawl";
}

const refererHost = extractHost(request.headers.get("referer"));
const cf = (request as Request & { cf?: { country?: string } }).cf;
const country =
cf?.country || request.headers.get("cf-ipcountry") || "XX";
const country = cf?.country || request.headers.get("cf-ipcountry") || "XX";
const targetOrDetail =
options.detail ||
new URL(request.url).searchParams.get("q") ||
refererHost;
options.detail || new URL(request.url).searchParams.get("q") || refererHost;

try {
dataset.writeDataPoint({
Expand Down Expand Up @@ -205,7 +210,9 @@ export function trackClientBeaconEvent(options: {
value?: number;
}): void {
const dataset = options.env.ANALYTICS as AnalyticsEngineDataset | undefined;
if (!dataset || typeof dataset.writeDataPoint !== "function") return;
if (!dataset || typeof dataset.writeDataPoint !== "function") {
return;
}

const refererHost = extractHost(options.request.headers.get("referer"));
const cf = (options.request as Request & { cf?: { country?: string } }).cf;
Expand Down
39 changes: 39 additions & 0 deletions apps/site/src/lib/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { env } from "cloudflare:workers";
import { mcp } from "@better-auth/mcp";
import { betterAuth } from "better-auth";
import { jwt } from "better-auth/plugins";

/**
* Creates or retrieves the Better Auth instance configured for
* Model Context Protocol (MCP) and OAuth 2.1 authorization server
* backed by Cloudflare D1.
*/
export function getAuth(origin: string, d1?: D1Database) {
const db = d1 ?? (env as unknown as { DB?: D1Database })?.DB;

return betterAuth({
baseURL: origin,
basePath: "",
...(db ? { database: db } : {}),
plugins: [
jwt(),
mcp({
resource: `${origin}/mcp`,
loginPage: "/sign-in",
consentPage: "/oauth2/consent",
allowDynamicClientRegistration: true,
allowUnauthenticatedClientRegistration: true,
scopes: [
"openid",
"profile",
"email",
"offline_access",
"read:content",
"search",
"mcp:tools",
"agent",
],
}),
],
});
}
Loading
Loading