@getjack/jack 0.1.32 → 0.1.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/commands/deploys.ts +95 -0
- package/src/commands/link.ts +8 -0
- package/src/commands/mcp.ts +179 -4
- package/src/commands/rollback.ts +53 -0
- package/src/commands/services.ts +11 -1
- package/src/commands/ship.ts +3 -1
- package/src/commands/tokens.ts +16 -1
- package/src/commands/whoami.ts +43 -8
- package/src/index.ts +16 -0
- package/src/lib/agent-files.ts +54 -4
- package/src/lib/agent-integration.ts +4 -166
- package/src/lib/claude-hooks-installer.ts +55 -0
- package/src/lib/control-plane.ts +78 -40
- package/src/lib/debug.ts +2 -1
- package/src/lib/deploy-upload.ts +6 -0
- package/src/lib/hooks.ts +3 -1
- package/src/lib/managed-deploy.ts +12 -9
- package/src/lib/project-link.ts +6 -0
- package/src/lib/project-operations.ts +68 -22
- package/src/lib/telemetry.ts +2 -0
- package/src/mcp/README.md +1 -1
- package/src/mcp/resources/index.ts +1 -16
- package/src/mcp/server.ts +23 -0
- package/src/mcp/tools/index.ts +133 -17
- package/src/mcp/types.ts +1 -0
- package/src/mcp/utils.ts +2 -1
- package/src/templates/index.ts +25 -73
- package/templates/CLAUDE.md +41 -0
- package/templates/ai-chat/.jack.json +10 -5
- package/templates/ai-chat/bun.lock +50 -1
- package/templates/ai-chat/package.json +5 -0
- package/templates/ai-chat/public/app.js +73 -0
- package/templates/ai-chat/public/index.html +14 -197
- package/templates/ai-chat/schema.sql +14 -0
- package/templates/ai-chat/src/index.ts +86 -102
- package/templates/ai-chat/wrangler.jsonc +8 -1
- package/templates/cron/.jack.json +66 -0
- package/templates/cron/bun.lock +23 -0
- package/templates/cron/package.json +16 -0
- package/templates/cron/schema.sql +24 -0
- package/templates/cron/src/index.ts +117 -0
- package/templates/cron/src/jobs.ts +139 -0
- package/templates/cron/src/webhooks.ts +95 -0
- package/templates/cron/tsconfig.json +17 -0
- package/templates/cron/wrangler.jsonc +11 -0
- package/templates/miniapp/.jack.json +1 -1
- package/templates/nextjs/.jack.json +1 -1
- package/templates/nextjs-auth/.jack.json +44 -0
- package/templates/nextjs-auth/app/api/auth/[...all]/route.ts +11 -0
- package/templates/nextjs-auth/app/dashboard/loading.tsx +53 -0
- package/templates/nextjs-auth/app/dashboard/page.tsx +73 -0
- package/templates/nextjs-auth/app/error.tsx +44 -0
- package/templates/nextjs-auth/app/globals.css +1 -0
- package/templates/nextjs-auth/app/health/route.ts +3 -0
- package/templates/nextjs-auth/app/layout.tsx +24 -0
- package/templates/nextjs-auth/app/login/page.tsx +10 -0
- package/templates/nextjs-auth/app/page.tsx +86 -0
- package/templates/nextjs-auth/app/signup/page.tsx +10 -0
- package/templates/nextjs-auth/bun.lock +1065 -0
- package/templates/nextjs-auth/cloudflare-env.d.ts +8 -0
- package/templates/nextjs-auth/components/auth-form.tsx +191 -0
- package/templates/nextjs-auth/components/header.tsx +50 -0
- package/templates/nextjs-auth/components/user-menu.tsx +23 -0
- package/templates/nextjs-auth/lib/auth-client.ts +3 -0
- package/templates/nextjs-auth/lib/auth.ts +43 -0
- package/templates/nextjs-auth/lib/utils.ts +6 -0
- package/templates/nextjs-auth/middleware.ts +33 -0
- package/templates/nextjs-auth/next.config.ts +8 -0
- package/templates/nextjs-auth/open-next.config.ts +6 -0
- package/templates/nextjs-auth/package.json +33 -0
- package/templates/nextjs-auth/postcss.config.mjs +8 -0
- package/templates/nextjs-auth/schema.sql +49 -0
- package/templates/nextjs-auth/tsconfig.json +28 -0
- package/templates/nextjs-auth/wrangler.jsonc +23 -0
- package/templates/nextjs-clerk/.jack.json +54 -0
- package/templates/nextjs-clerk/app/dashboard/page.tsx +69 -0
- package/templates/nextjs-clerk/app/globals.css +1 -0
- package/templates/nextjs-clerk/app/health/route.ts +3 -0
- package/templates/nextjs-clerk/app/layout.tsx +26 -0
- package/templates/nextjs-clerk/app/page.tsx +86 -0
- package/templates/nextjs-clerk/app/sign-in/[[...sign-in]]/page.tsx +9 -0
- package/templates/nextjs-clerk/app/sign-up/[[...sign-up]]/page.tsx +9 -0
- package/templates/nextjs-clerk/bun.lock +1055 -0
- package/templates/nextjs-clerk/cloudflare-env.d.ts +3 -0
- package/templates/nextjs-clerk/components/header.tsx +40 -0
- package/templates/nextjs-clerk/lib/utils.ts +6 -0
- package/templates/nextjs-clerk/middleware.ts +18 -0
- package/templates/nextjs-clerk/next.config.ts +8 -0
- package/templates/nextjs-clerk/open-next.config.ts +6 -0
- package/templates/nextjs-clerk/package.json +31 -0
- package/templates/nextjs-clerk/postcss.config.mjs +8 -0
- package/templates/nextjs-clerk/tsconfig.json +28 -0
- package/templates/nextjs-clerk/wrangler.jsonc +17 -0
- package/templates/nextjs-shadcn/.jack.json +34 -0
- package/templates/nextjs-shadcn/app/dashboard/data.json +614 -0
- package/templates/nextjs-shadcn/app/dashboard/page.tsx +55 -0
- package/templates/nextjs-shadcn/app/globals.css +126 -0
- package/templates/nextjs-shadcn/app/health/route.ts +3 -0
- package/templates/nextjs-shadcn/app/layout.tsx +24 -0
- package/templates/nextjs-shadcn/app/login/page.tsx +19 -0
- package/templates/nextjs-shadcn/app/page.tsx +180 -0
- package/templates/nextjs-shadcn/app/showcase.tsx +1262 -0
- package/templates/nextjs-shadcn/bun.lock +1789 -0
- package/templates/nextjs-shadcn/cloudflare-env.d.ts +4 -0
- package/templates/nextjs-shadcn/components/app-sidebar.tsx +175 -0
- package/templates/nextjs-shadcn/components/chart-area-interactive.tsx +291 -0
- package/templates/nextjs-shadcn/components/data-table.tsx +807 -0
- package/templates/nextjs-shadcn/components/login-form.tsx +95 -0
- package/templates/nextjs-shadcn/components/nav-documents.tsx +92 -0
- package/templates/nextjs-shadcn/components/nav-main.tsx +73 -0
- package/templates/nextjs-shadcn/components/nav-projects.tsx +89 -0
- package/templates/nextjs-shadcn/components/nav-secondary.tsx +42 -0
- package/templates/nextjs-shadcn/components/nav-user.tsx +114 -0
- package/templates/nextjs-shadcn/components/section-cards.tsx +102 -0
- package/templates/nextjs-shadcn/components/site-header.tsx +30 -0
- package/templates/nextjs-shadcn/components/team-switcher.tsx +91 -0
- package/templates/nextjs-shadcn/components/ui/accordion.tsx +66 -0
- package/templates/nextjs-shadcn/components/ui/alert-dialog.tsx +196 -0
- package/templates/nextjs-shadcn/components/ui/alert.tsx +66 -0
- package/templates/nextjs-shadcn/components/ui/aspect-ratio.tsx +11 -0
- package/templates/nextjs-shadcn/components/ui/avatar.tsx +109 -0
- package/templates/nextjs-shadcn/components/ui/badge.tsx +48 -0
- package/templates/nextjs-shadcn/components/ui/breadcrumb.tsx +109 -0
- package/templates/nextjs-shadcn/components/ui/button-group.tsx +83 -0
- package/templates/nextjs-shadcn/components/ui/button.tsx +64 -0
- package/templates/nextjs-shadcn/components/ui/calendar.tsx +220 -0
- package/templates/nextjs-shadcn/components/ui/card.tsx +92 -0
- package/templates/nextjs-shadcn/components/ui/carousel.tsx +241 -0
- package/templates/nextjs-shadcn/components/ui/chart.tsx +357 -0
- package/templates/nextjs-shadcn/components/ui/checkbox.tsx +32 -0
- package/templates/nextjs-shadcn/components/ui/collapsible.tsx +33 -0
- package/templates/nextjs-shadcn/components/ui/combobox.tsx +310 -0
- package/templates/nextjs-shadcn/components/ui/command.tsx +184 -0
- package/templates/nextjs-shadcn/components/ui/context-menu.tsx +252 -0
- package/templates/nextjs-shadcn/components/ui/dialog.tsx +158 -0
- package/templates/nextjs-shadcn/components/ui/direction.tsx +22 -0
- package/templates/nextjs-shadcn/components/ui/drawer.tsx +135 -0
- package/templates/nextjs-shadcn/components/ui/dropdown-menu.tsx +257 -0
- package/templates/nextjs-shadcn/components/ui/empty.tsx +104 -0
- package/templates/nextjs-shadcn/components/ui/field.tsx +248 -0
- package/templates/nextjs-shadcn/components/ui/form.tsx +167 -0
- package/templates/nextjs-shadcn/components/ui/hover-card.tsx +44 -0
- package/templates/nextjs-shadcn/components/ui/input-group.tsx +170 -0
- package/templates/nextjs-shadcn/components/ui/input-otp.tsx +77 -0
- package/templates/nextjs-shadcn/components/ui/input.tsx +21 -0
- package/templates/nextjs-shadcn/components/ui/item.tsx +193 -0
- package/templates/nextjs-shadcn/components/ui/kbd.tsx +28 -0
- package/templates/nextjs-shadcn/components/ui/label.tsx +24 -0
- package/templates/nextjs-shadcn/components/ui/menubar.tsx +276 -0
- package/templates/nextjs-shadcn/components/ui/native-select.tsx +53 -0
- package/templates/nextjs-shadcn/components/ui/navigation-menu.tsx +168 -0
- package/templates/nextjs-shadcn/components/ui/pagination.tsx +127 -0
- package/templates/nextjs-shadcn/components/ui/popover.tsx +89 -0
- package/templates/nextjs-shadcn/components/ui/progress.tsx +31 -0
- package/templates/nextjs-shadcn/components/ui/radio-group.tsx +45 -0
- package/templates/nextjs-shadcn/components/ui/resizable.tsx +53 -0
- package/templates/nextjs-shadcn/components/ui/scroll-area.tsx +58 -0
- package/templates/nextjs-shadcn/components/ui/select.tsx +190 -0
- package/templates/nextjs-shadcn/components/ui/separator.tsx +28 -0
- package/templates/nextjs-shadcn/components/ui/sheet.tsx +143 -0
- package/templates/nextjs-shadcn/components/ui/sidebar.tsx +726 -0
- package/templates/nextjs-shadcn/components/ui/skeleton.tsx +13 -0
- package/templates/nextjs-shadcn/components/ui/slider.tsx +63 -0
- package/templates/nextjs-shadcn/components/ui/sonner.tsx +40 -0
- package/templates/nextjs-shadcn/components/ui/spinner.tsx +16 -0
- package/templates/nextjs-shadcn/components/ui/switch.tsx +35 -0
- package/templates/nextjs-shadcn/components/ui/table.tsx +116 -0
- package/templates/nextjs-shadcn/components/ui/tabs.tsx +91 -0
- package/templates/nextjs-shadcn/components/ui/textarea.tsx +18 -0
- package/templates/nextjs-shadcn/components/ui/toggle-group.tsx +83 -0
- package/templates/nextjs-shadcn/components/ui/toggle.tsx +47 -0
- package/templates/nextjs-shadcn/components/ui/tooltip.tsx +57 -0
- package/templates/nextjs-shadcn/components.json +23 -0
- package/templates/nextjs-shadcn/hooks/use-mobile.ts +19 -0
- package/templates/nextjs-shadcn/lib/utils.ts +6 -0
- package/templates/nextjs-shadcn/next-env.d.ts +6 -0
- package/templates/nextjs-shadcn/next.config.ts +8 -0
- package/templates/nextjs-shadcn/open-next.config.ts +6 -0
- package/templates/nextjs-shadcn/package.json +55 -0
- package/templates/nextjs-shadcn/postcss.config.mjs +8 -0
- package/templates/nextjs-shadcn/tsconfig.json +28 -0
- package/templates/nextjs-shadcn/wrangler.jsonc +23 -0
- package/templates/resend/.jack.json +64 -0
- package/templates/resend/bun.lock +23 -0
- package/templates/resend/package.json +16 -0
- package/templates/resend/schema.sql +13 -0
- package/templates/resend/src/email.ts +165 -0
- package/templates/resend/src/index.ts +108 -0
- package/templates/resend/tsconfig.json +17 -0
- package/templates/resend/wrangler.jsonc +11 -0
- package/templates/saas/.jack.json +1 -1
- package/templates/ai-chat/public/chat.js +0 -149
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Convert an ArrayBuffer to hex string
|
|
2
|
+
function bufferToHex(buffer: ArrayBuffer): string {
|
|
3
|
+
const bytes = new Uint8Array(buffer);
|
|
4
|
+
const hexChars: string[] = [];
|
|
5
|
+
for (const byte of bytes) {
|
|
6
|
+
hexChars.push(byte.toString(16).padStart(2, "0"));
|
|
7
|
+
}
|
|
8
|
+
return hexChars.join("");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Verify HMAC-SHA256 webhook signature
|
|
12
|
+
// Expected header format: sha256=<hex-encoded-signature>
|
|
13
|
+
export async function verifyWebhookSignature(
|
|
14
|
+
payload: string,
|
|
15
|
+
signatureHeader: string,
|
|
16
|
+
secret: string,
|
|
17
|
+
): Promise<boolean> {
|
|
18
|
+
if (!signatureHeader || !secret) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Parse the signature header
|
|
23
|
+
const parts = signatureHeader.split("=");
|
|
24
|
+
if (parts.length !== 2 || parts[0] !== "sha256") {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const receivedSignature = parts[1];
|
|
29
|
+
if (!receivedSignature) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Import the secret as a CryptoKey
|
|
34
|
+
const encoder = new TextEncoder();
|
|
35
|
+
const key = await crypto.subtle.importKey(
|
|
36
|
+
"raw",
|
|
37
|
+
encoder.encode(secret),
|
|
38
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
39
|
+
false,
|
|
40
|
+
["sign"],
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
// Sign the payload
|
|
44
|
+
const signatureBuffer = await crypto.subtle.sign(
|
|
45
|
+
"HMAC",
|
|
46
|
+
key,
|
|
47
|
+
encoder.encode(payload),
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
// Convert to hex and compare
|
|
51
|
+
const expectedSignature = bufferToHex(signatureBuffer);
|
|
52
|
+
|
|
53
|
+
// Constant-time comparison
|
|
54
|
+
if (receivedSignature.length !== expectedSignature.length) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let mismatch = 0;
|
|
59
|
+
for (let i = 0; i < receivedSignature.length; i++) {
|
|
60
|
+
mismatch |=
|
|
61
|
+
receivedSignature.charCodeAt(i) ^ expectedSignature.charCodeAt(i);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return mismatch === 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface WebhookEventInput {
|
|
68
|
+
source?: string;
|
|
69
|
+
eventType?: string;
|
|
70
|
+
payload: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Log a webhook event to D1
|
|
74
|
+
export async function logWebhookEvent(
|
|
75
|
+
db: D1Database,
|
|
76
|
+
input: WebhookEventInput,
|
|
77
|
+
): Promise<string> {
|
|
78
|
+
const id = crypto.randomUUID();
|
|
79
|
+
const now = Math.floor(Date.now() / 1000);
|
|
80
|
+
|
|
81
|
+
await db
|
|
82
|
+
.prepare(
|
|
83
|
+
"INSERT INTO webhook_events (id, source, event_type, payload, status, created_at) VALUES (?, ?, ?, ?, 'received', ?)",
|
|
84
|
+
)
|
|
85
|
+
.bind(
|
|
86
|
+
id,
|
|
87
|
+
input.source || "unknown",
|
|
88
|
+
input.eventType || null,
|
|
89
|
+
input.payload,
|
|
90
|
+
now,
|
|
91
|
+
)
|
|
92
|
+
.run();
|
|
93
|
+
|
|
94
|
+
return id;
|
|
95
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"lib": ["ESNext"],
|
|
7
|
+
"types": ["@cloudflare/workers-types"],
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"isolatedModules": true
|
|
14
|
+
},
|
|
15
|
+
"include": ["src/**/*.ts"],
|
|
16
|
+
"exclude": ["node_modules"]
|
|
17
|
+
}
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"examples": ["simple dashboard", "farcaster miniapp"]
|
|
17
17
|
},
|
|
18
18
|
"agentContext": {
|
|
19
|
-
"summary": "A Farcaster miniapp using React + Vite frontend, Hono API
|
|
19
|
+
"summary": "A Farcaster miniapp using React + Vite frontend, Hono API backend, with D1 SQLite database and AI capabilities.",
|
|
20
20
|
"full_text": "## Project Structure\n\n- `src/App.tsx` - React application entry point\n- `src/worker.ts` - Hono API routes for backend\n- `src/hooks/useAI.ts` - AI generation hook\n- `src/components/` - React components\n- `schema.sql` - D1 database schema\n- `wrangler.jsonc` - Cloudflare Workers configuration\n\n## Conventions\n\n- API routes are defined with Hono in `src/worker.ts`\n- Frontend uses Vite for bundling and is served as static assets\n- Database uses D1 prepared statements for queries\n- Secrets are managed via jack and pushed to Cloudflare\n- Wrangler is installed globally by jack, not in project dependencies\n\n## AI Feature\n\nThe template includes a `useAI()` hook for AI text generation.\n\n### Usage\n```tsx\nconst { generate, isLoading, error } = useAI();\nconst result = await generate('Your prompt here');\n// result.result = AI response text\n// result.provider = 'openai' | 'workers-ai'\n```\n\n### Providers\n- **Workers AI** (default): Free, uses Llama 3.1 8B. No API key needed.\n- **OpenAI** (optional): Better quality with gpt-4o-mini. Requires OPENAI_API_KEY.\n\n### Costs\n- **Workers AI**: Free (included in Cloudflare Workers)\n- **OpenAI gpt-4o-mini**: ~$0.0001 per request (~$0.15/1M input + $0.60/1M output tokens)\n- Typical usage (10 requests/day): ~$0.03/month\n\n### Rate Limiting\n- 10 requests per minute per IP address\n- Returns 429 with Retry-After header when exceeded\n- Rate limit headers: X-RateLimit-Limit, X-RateLimit-Remaining\n\n## Resources\n\n- [Farcaster Miniapp Docs](https://miniapps.farcaster.xyz)\n- [Hono Documentation](https://hono.dev)\n- [Cloudflare D1 Docs](https://developers.cloudflare.com/d1)\n- [Workers AI Models](https://developers.cloudflare.com/workers-ai/models/)"
|
|
21
21
|
},
|
|
22
22
|
"hooks": {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
"agentContext": {
|
|
11
11
|
"summary": "A Next.js app deployed with jack. Supports SSR, SSG, and React Server Components.",
|
|
12
|
-
"full_text": "## Project Structure\n\n- `app/` - Next.js App Router pages and layouts\n- `public/` - Static assets\n- `open-next.config.ts` - OpenNext configuration\n- `wrangler.jsonc` - Worker configuration\n\n## Commands\n\n- `bun run dev` - Local development\n- `jack ship` - Deploy to production\n- `bun run preview` - Preview production build locally\n\n## Conventions\n\n- Uses App Router (not Pages Router)\n- SSR and SSG work out of the box\n- Server Components supported\n\n## Resources\n\n- [OpenNext Docs](https://opennext.js.org/cloudflare)\n- [Next.js App Router](https://nextjs.org/docs/app)"
|
|
12
|
+
"full_text": "## Project Structure\n\n- `app/` - Next.js App Router pages and layouts\n- `public/` - Static assets\n- `open-next.config.ts` - OpenNext configuration\n- `wrangler.jsonc` - Worker configuration\n\n## Commands\n\n- `bun run dev` - Local development\n- `jack ship` - Deploy to production\n- `bun run preview` - Preview production build locally\n\n## Conventions\n\n- Uses App Router (not Pages Router)\n- SSR and SSG work out of the box\n- Server Components supported\n\n## OpenNext on Cloudflare — Important Rules\n\nThis app runs via OpenNext on Cloudflare Workers. Follow these rules to avoid runtime errors:\n\n### Use window.location.href instead of router.push() for full-page transitions\nOpenNext has a broken webpack chunk URL resolver. Client-side navigation via `router.push()` to pages whose chunks aren't already loaded fails with `ChunkLoadError`. Use `window.location.href` for navigations that change auth state or cross major app sections. `<Link>` components work fine because Next.js prefetches their chunks.\n\n```tsx\n// BAD — may cause ChunkLoadError\nrouter.push('/dashboard');\nrouter.refresh();\n\n// GOOD — full reload, always works\nwindow.location.href = '/dashboard';\n```\n\n### Add `export const dynamic = 'force-dynamic'` to pages using getCloudflareContext()\nWithout this, Next.js tries to statically prerender the page at build time, which fails because `getCloudflareContext()` is only available at request time.\n\n### Edge middleware cannot use Node.js APIs\nNext.js middleware runs in the edge runtime. Do not import or call anything that requires Node.js built-in modules (e.g., `perf_hooks`, `fs`, `crypto` beyond Web Crypto).\n\n### nodejs_compat flag is required\nThe `wrangler.jsonc` must include `\"compatibility_flags\": [\"nodejs_compat\"]` and a recent `compatibility_date`.\n\n## Resources\n\n- [OpenNext Docs](https://opennext.js.org/cloudflare)\n- [Next.js App Router](https://nextjs.org/docs/app)"
|
|
13
13
|
},
|
|
14
14
|
"hooks": {
|
|
15
15
|
"postDeploy": [
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nextjs-auth",
|
|
3
|
+
"description": "Next.js + Better Auth (self-hosted auth with D1)",
|
|
4
|
+
"secrets": ["BETTER_AUTH_SECRET"],
|
|
5
|
+
"capabilities": ["db"],
|
|
6
|
+
"requires": ["DB"],
|
|
7
|
+
"intent": {
|
|
8
|
+
"keywords": ["auth", "login", "signup", "better-auth", "authentication", "session", "nextjs"],
|
|
9
|
+
"examples": ["app with self-hosted auth", "login system", "authenticated app"]
|
|
10
|
+
},
|
|
11
|
+
"agentContext": {
|
|
12
|
+
"summary": "A Next.js app with Better Auth self-hosted authentication, email/password login, optional GitHub/Google OAuth, and D1 database.",
|
|
13
|
+
"full_text": "## Project Structure\n\n- `app/layout.tsx` - Root layout\n- `app/page.tsx` - Landing page with sign-in CTA\n- `app/login/page.tsx` - Login page with email/password form\n- `app/signup/page.tsx` - Sign up page\n- `app/dashboard/page.tsx` - Protected dashboard\n- `app/api/auth/[...all]/route.ts` - Better Auth API handler\n- `middleware.ts` - Edge-safe cookie-based auth middleware\n- `lib/auth.ts` - Better Auth server configuration\n- `lib/auth-client.ts` - Better Auth client for client components\n- `schema.sql` - D1 database schema\n\n## Authentication\n\nUses Better Auth with email/password. Auth routes are handled at `/api/auth/*`.\nWorks immediately with zero external dependencies — just email/password.\n\n### Client-side\n```tsx\nimport { authClient } from '@/lib/auth-client';\n\n// Sign up\nawait authClient.signUp.email({ email, password, name });\n\n// Sign in\nawait authClient.signIn.email({ email, password });\n\n// Get session\nconst { data: session } = authClient.useSession();\n\n// Sign out\nawait authClient.signOut();\n```\n\n### Server-side\n```tsx\nimport { getAuth } from '@/lib/auth';\nimport { headers } from 'next/headers';\n\nconst auth = await getAuth();\nconst session = await auth.api.getSession({ headers: await headers() });\n```\n\n## Optional Social Login (Add Later)\n\nSocial providers are pre-wired but disabled by default. Add keys via `jack secrets` to enable:\n\n**GitHub** (easiest — 30 second setup):\n1. Go to github.com/settings/developers → New OAuth App\n2. Set callback URL to `https://your-app.workers.dev/api/auth/callback/github`\n3. Run: `jack secrets` and add `GITHUB_CLIENT_ID` + `GITHUB_CLIENT_SECRET`\n\n**Google**:\n1. Go to console.cloud.google.com/apis/credentials\n2. Create OAuth 2.0 Client ID\n3. Run: `jack secrets` and add `GOOGLE_CLIENT_ID` + `GOOGLE_CLIENT_SECRET`\n\nThe login form automatically shows buttons for configured providers.\n\n## Database\n\nD1 SQLite via Kysely. Tables: user, session, account, verification.\n\n## Environment Variables\n\n- `BETTER_AUTH_SECRET` - Auth token signing secret (auto-generated)\n- `GITHUB_CLIENT_ID` - GitHub OAuth client ID (optional)\n- `GITHUB_CLIENT_SECRET` - GitHub OAuth client secret (optional)\n- `GOOGLE_CLIENT_ID` - Google OAuth client ID (optional)\n- `GOOGLE_CLIENT_SECRET` - Google OAuth client secret (optional)\n\n## Cloudflare Workers Compatibility Notes\n\nBetter Auth has known compatibility issues with Cloudflare Workers. These are already handled in this template, but be aware when modifying:\n\n### NEVER call auth.api.getSession() in middleware\nNext.js edge middleware cannot use Node.js `perf_hooks` module. The middleware.ts uses cookie-only checks instead. Full session validation happens in server components.\n\n### Auth must be created per-request, not as a singleton\nD1 bindings are only available inside request handlers. The `getAuth()` helper in lib/auth.ts calls `getCloudflareContext()` on each invocation to get the D1 binding. Do NOT try to create a top-level auth instance.\n\n### Required wrangler.jsonc flags\n- `nodejs_compat` compatibility flag is required\n- Use a recent `compatibility_date` (2024-12-30 or later)\n\n### Version pinning\nBetter Auth v1.4+ had `createRequire` and `runWithRequestState` errors on Workers. If you hit module errors, try pinning to the latest working version or using `better-auth@beta`.\n\n### Do NOT use Drizzle adapter directly\nThis template uses Kysely + kysely-d1 for the database adapter. The Drizzle ORM adapter (`drizzleAdapter`) requires `drizzle-orm/d1` which has build-time initialization issues with OpenNext. Kysely works reliably with D1 in this setup.\n\n## OpenNext Navigation Rules\n\nOpenNext on Cloudflare Workers has a broken webpack chunk URL resolver (`r.u` is empty). This means dynamic chunk loading during client-side navigation can fail with `ChunkLoadError: Loading chunk X failed`.\n\n### NEVER use router.push() after auth state changes\nAfter sign-in, sign-up, or sign-out, always use `window.location.href` instead of `router.push()` + `router.refresh()`. Auth state changes need a full page reload so middleware and server components re-evaluate with the new session.\n\n```tsx\n// BAD - causes ChunkLoadError on OpenNext\nawait authClient.signOut();\nrouter.push('/');\nrouter.refresh();\n\n// GOOD - full reload ensures clean auth state\nawait authClient.signOut();\nwindow.location.href = '/';\n```\n\n### Use <Link> for in-app navigation\nNext.js `<Link>` components work because they prefetch chunks via `<script>` tags in the HTML. Only `router.push()` to pages whose chunks aren't already loaded will fail.\n\n### Add `export const dynamic = 'force-dynamic'` to pages using getCloudflareContext\nWithout this, Next.js tries to statically prerender the page at build time, which fails because `getCloudflareContext()` is only available at runtime.\n\n## Resources\n\n- [Better Auth Docs](https://www.betterauth.com/docs)\n- [Better Auth Cloudflare package](https://github.com/zpg6/better-auth-cloudflare)\n- [Hono + Better Auth on Cloudflare](https://hono.dev/examples/better-auth-on-cloudflare)\n- [OpenNext Docs](https://opennext.js.org/cloudflare)"
|
|
14
|
+
},
|
|
15
|
+
"hooks": {
|
|
16
|
+
"preCreate": [
|
|
17
|
+
{
|
|
18
|
+
"action": "require",
|
|
19
|
+
"source": "secret",
|
|
20
|
+
"key": "BETTER_AUTH_SECRET",
|
|
21
|
+
"message": "Generating authentication secret...",
|
|
22
|
+
"onMissing": "generate",
|
|
23
|
+
"generateCommand": "openssl rand -base64 32"
|
|
24
|
+
}
|
|
25
|
+
],
|
|
26
|
+
"postDeploy": [
|
|
27
|
+
{
|
|
28
|
+
"action": "clipboard",
|
|
29
|
+
"text": "{{url}}",
|
|
30
|
+
"message": "Deploy URL copied to clipboard"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"action": "box",
|
|
34
|
+
"title": "Auth ready: {{name}}",
|
|
35
|
+
"lines": [
|
|
36
|
+
"{{url}}",
|
|
37
|
+
"",
|
|
38
|
+
"Email/password login works out of the box.",
|
|
39
|
+
"Social login requires OAuth credentials (run jack secrets to add later)."
|
|
40
|
+
]
|
|
41
|
+
}
|
|
42
|
+
]
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { getAuth } from "@/lib/auth";
|
|
2
|
+
|
|
3
|
+
export const dynamic = "force-dynamic";
|
|
4
|
+
|
|
5
|
+
async function handler(request: Request) {
|
|
6
|
+
const auth = await getAuth();
|
|
7
|
+
return auth.handler(request);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const GET = handler;
|
|
11
|
+
export const POST = handler;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export default function DashboardLoading() {
|
|
2
|
+
return (
|
|
3
|
+
<div className="min-h-screen">
|
|
4
|
+
<header className="border-b border-gray-200 bg-white">
|
|
5
|
+
<div className="mx-auto flex h-14 max-w-3xl items-center justify-between px-6">
|
|
6
|
+
<div className="h-5 w-28 animate-pulse rounded bg-gray-200" />
|
|
7
|
+
<div className="h-8 w-20 animate-pulse rounded bg-gray-200" />
|
|
8
|
+
</div>
|
|
9
|
+
</header>
|
|
10
|
+
|
|
11
|
+
<main className="mx-auto max-w-3xl px-6 py-12">
|
|
12
|
+
<div className="mb-8">
|
|
13
|
+
<div className="h-8 w-40 animate-pulse rounded bg-gray-200" />
|
|
14
|
+
<div className="mt-2 h-5 w-56 animate-pulse rounded bg-gray-200" />
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<div className="grid gap-6 sm:grid-cols-2">
|
|
18
|
+
<div className="rounded-xl border border-gray-200 bg-white p-6">
|
|
19
|
+
<div className="h-5 w-16 animate-pulse rounded bg-gray-200" />
|
|
20
|
+
<div className="mt-4 space-y-3">
|
|
21
|
+
<div>
|
|
22
|
+
<div className="h-3 w-12 animate-pulse rounded bg-gray-200" />
|
|
23
|
+
<div className="mt-1.5 h-5 w-32 animate-pulse rounded bg-gray-200" />
|
|
24
|
+
</div>
|
|
25
|
+
<div>
|
|
26
|
+
<div className="h-3 w-12 animate-pulse rounded bg-gray-200" />
|
|
27
|
+
<div className="mt-1.5 h-5 w-44 animate-pulse rounded bg-gray-200" />
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<div className="rounded-xl border border-gray-200 bg-white p-6">
|
|
33
|
+
<div className="h-5 w-16 animate-pulse rounded bg-gray-200" />
|
|
34
|
+
<div className="mt-4 space-y-3">
|
|
35
|
+
<div>
|
|
36
|
+
<div className="h-3 w-20 animate-pulse rounded bg-gray-200" />
|
|
37
|
+
<div className="mt-1.5 h-5 w-48 animate-pulse rounded bg-gray-200" />
|
|
38
|
+
</div>
|
|
39
|
+
<div>
|
|
40
|
+
<div className="h-3 w-16 animate-pulse rounded bg-gray-200" />
|
|
41
|
+
<div className="mt-1.5 h-5 w-36 animate-pulse rounded bg-gray-200" />
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<div className="mt-8">
|
|
48
|
+
<div className="h-10 w-28 animate-pulse rounded-lg bg-gray-200" />
|
|
49
|
+
</div>
|
|
50
|
+
</main>
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Header } from "@/components/header";
|
|
2
|
+
import { UserMenu } from "@/components/user-menu";
|
|
3
|
+
import { getAuth } from "@/lib/auth";
|
|
4
|
+
import { headers } from "next/headers";
|
|
5
|
+
import { redirect } from "next/navigation";
|
|
6
|
+
|
|
7
|
+
export const dynamic = "force-dynamic";
|
|
8
|
+
|
|
9
|
+
export default async function DashboardPage() {
|
|
10
|
+
const auth = await getAuth();
|
|
11
|
+
const session = await auth.api.getSession({
|
|
12
|
+
headers: await headers(),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
if (!session) {
|
|
16
|
+
redirect("/login");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div className="min-h-screen">
|
|
21
|
+
<Header user={session.user} />
|
|
22
|
+
|
|
23
|
+
<main className="mx-auto max-w-3xl px-6 py-12">
|
|
24
|
+
<div className="mb-8">
|
|
25
|
+
<h1 className="text-2xl font-bold">Dashboard</h1>
|
|
26
|
+
<p className="mt-1 text-gray-500">
|
|
27
|
+
Welcome back, {session.user.name || session.user.email}
|
|
28
|
+
</p>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<div className="grid gap-6 sm:grid-cols-2">
|
|
32
|
+
<div className="rounded-xl border border-gray-200 bg-white p-6">
|
|
33
|
+
<h2 className="font-semibold">Profile</h2>
|
|
34
|
+
<div className="mt-4 space-y-3">
|
|
35
|
+
<div>
|
|
36
|
+
<p className="text-xs font-medium uppercase tracking-wide text-gray-400">Name</p>
|
|
37
|
+
<p className="mt-0.5">{session.user.name || "Not set"}</p>
|
|
38
|
+
</div>
|
|
39
|
+
<div>
|
|
40
|
+
<p className="text-xs font-medium uppercase tracking-wide text-gray-400">Email</p>
|
|
41
|
+
<p className="mt-0.5">{session.user.email}</p>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<div className="rounded-xl border border-gray-200 bg-white p-6">
|
|
47
|
+
<h2 className="font-semibold">Session</h2>
|
|
48
|
+
<div className="mt-4 space-y-3">
|
|
49
|
+
<div>
|
|
50
|
+
<p className="text-xs font-medium uppercase tracking-wide text-gray-400">
|
|
51
|
+
Session ID
|
|
52
|
+
</p>
|
|
53
|
+
<p className="mt-0.5 truncate font-mono text-sm text-gray-600">
|
|
54
|
+
{session.session.id}
|
|
55
|
+
</p>
|
|
56
|
+
</div>
|
|
57
|
+
<div>
|
|
58
|
+
<p className="text-xs font-medium uppercase tracking-wide text-gray-400">Expires</p>
|
|
59
|
+
<p className="mt-0.5 text-sm text-gray-600">
|
|
60
|
+
{new Date(session.session.expiresAt).toLocaleString()}
|
|
61
|
+
</p>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<div className="mt-8">
|
|
68
|
+
<UserMenu />
|
|
69
|
+
</div>
|
|
70
|
+
</main>
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { AlertCircle, RotateCcw } from "lucide-react";
|
|
4
|
+
import Link from "next/link";
|
|
5
|
+
|
|
6
|
+
// biome-ignore lint/suspicious/noShadowRestrictedNames: Next.js convention for error boundaries
|
|
7
|
+
export default function Error({
|
|
8
|
+
error,
|
|
9
|
+
reset,
|
|
10
|
+
}: {
|
|
11
|
+
error: Error & { digest?: string };
|
|
12
|
+
reset: () => void;
|
|
13
|
+
}) {
|
|
14
|
+
return (
|
|
15
|
+
<div className="flex min-h-screen items-center justify-center px-4">
|
|
16
|
+
<div className="w-full max-w-sm text-center">
|
|
17
|
+
<div className="mx-auto mb-4 flex size-12 items-center justify-center rounded-full bg-red-100">
|
|
18
|
+
<AlertCircle className="size-6 text-red-600" />
|
|
19
|
+
</div>
|
|
20
|
+
<h1 className="text-xl font-bold">Something went wrong</h1>
|
|
21
|
+
<p className="mt-2 text-sm text-gray-500">
|
|
22
|
+
{error.message || "An unexpected error occurred."}
|
|
23
|
+
</p>
|
|
24
|
+
{error.digest && <p className="mt-1 text-xs text-gray-400">Error ID: {error.digest}</p>}
|
|
25
|
+
<div className="mt-6 flex items-center justify-center gap-3">
|
|
26
|
+
<button
|
|
27
|
+
type="button"
|
|
28
|
+
onClick={reset}
|
|
29
|
+
className="inline-flex items-center gap-2 rounded-lg bg-gray-900 px-4 py-2.5 text-sm font-medium text-white transition hover:bg-gray-800"
|
|
30
|
+
>
|
|
31
|
+
<RotateCcw className="size-4" />
|
|
32
|
+
Try again
|
|
33
|
+
</button>
|
|
34
|
+
<Link
|
|
35
|
+
href="/"
|
|
36
|
+
className="rounded-lg border border-gray-200 bg-white px-4 py-2.5 text-sm font-medium text-gray-700 transition hover:bg-gray-50"
|
|
37
|
+
>
|
|
38
|
+
Go home
|
|
39
|
+
</Link>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import { Inter } from "next/font/google";
|
|
3
|
+
import "./globals.css";
|
|
4
|
+
|
|
5
|
+
const inter = Inter({ subsets: ["latin"] });
|
|
6
|
+
|
|
7
|
+
export const metadata: Metadata = {
|
|
8
|
+
title: "jack-template",
|
|
9
|
+
description: "Next.js app with self-hosted authentication",
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export default function RootLayout({
|
|
13
|
+
children,
|
|
14
|
+
}: {
|
|
15
|
+
children: React.ReactNode;
|
|
16
|
+
}) {
|
|
17
|
+
return (
|
|
18
|
+
<html lang="en">
|
|
19
|
+
<body className={`${inter.className} min-h-screen bg-gray-50 text-gray-900 antialiased`}>
|
|
20
|
+
{children}
|
|
21
|
+
</body>
|
|
22
|
+
</html>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { Header } from "@/components/header";
|
|
2
|
+
import { LogIn, Shield, UserPlus } from "lucide-react";
|
|
3
|
+
import Link from "next/link";
|
|
4
|
+
|
|
5
|
+
export default function Home() {
|
|
6
|
+
return (
|
|
7
|
+
<div className="min-h-screen">
|
|
8
|
+
<Header />
|
|
9
|
+
|
|
10
|
+
<main className="mx-auto max-w-3xl px-6 py-20 text-center">
|
|
11
|
+
<div className="mb-6 inline-flex items-center gap-2 rounded-full border border-gray-200 bg-white px-4 py-1.5 text-sm text-gray-600">
|
|
12
|
+
<Shield className="size-4" />
|
|
13
|
+
Self-hosted authentication
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<h1 className="text-4xl font-bold tracking-tight sm:text-5xl">
|
|
17
|
+
Authentication
|
|
18
|
+
<br />
|
|
19
|
+
<span className="text-gray-400">ready to go</span>
|
|
20
|
+
</h1>
|
|
21
|
+
|
|
22
|
+
<p className="mx-auto mt-4 max-w-lg text-lg text-gray-500">
|
|
23
|
+
Email/password login, session management, and optional social login. Your auth data stays
|
|
24
|
+
in your database.
|
|
25
|
+
</p>
|
|
26
|
+
|
|
27
|
+
<div className="mt-10 flex items-center justify-center gap-4">
|
|
28
|
+
<Link
|
|
29
|
+
href="/signup"
|
|
30
|
+
className="inline-flex items-center gap-2 rounded-lg bg-gray-900 px-5 py-2.5 text-sm font-medium text-white transition hover:bg-gray-800"
|
|
31
|
+
>
|
|
32
|
+
<UserPlus className="size-4" />
|
|
33
|
+
Sign Up
|
|
34
|
+
</Link>
|
|
35
|
+
<Link
|
|
36
|
+
href="/login"
|
|
37
|
+
className="inline-flex items-center gap-2 rounded-lg border border-gray-200 bg-white px-5 py-2.5 text-sm font-medium text-gray-700 transition hover:bg-gray-50"
|
|
38
|
+
>
|
|
39
|
+
<LogIn className="size-4" />
|
|
40
|
+
Sign In
|
|
41
|
+
</Link>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<div className="mt-20 grid gap-6 sm:grid-cols-3">
|
|
45
|
+
<div className="rounded-xl border border-gray-200 bg-white p-6 text-left">
|
|
46
|
+
<div className="mb-3 flex size-10 items-center justify-center rounded-lg bg-gray-100">
|
|
47
|
+
<Shield className="size-5 text-gray-600" />
|
|
48
|
+
</div>
|
|
49
|
+
<h3 className="font-semibold">Self-hosted</h3>
|
|
50
|
+
<p className="mt-1 text-sm text-gray-500">
|
|
51
|
+
Your auth data lives in your D1 database. No third-party dependency.
|
|
52
|
+
</p>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<div className="rounded-xl border border-gray-200 bg-white p-6 text-left">
|
|
56
|
+
<div className="mb-3 flex size-10 items-center justify-center rounded-lg bg-gray-100">
|
|
57
|
+
<LogIn className="size-5 text-gray-600" />
|
|
58
|
+
</div>
|
|
59
|
+
<h3 className="font-semibold">Email + Social</h3>
|
|
60
|
+
<p className="mt-1 text-sm text-gray-500">
|
|
61
|
+
Email/password out of the box. Add GitHub or Google OAuth with two environment
|
|
62
|
+
variables.
|
|
63
|
+
</p>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<div className="rounded-xl border border-gray-200 bg-white p-6 text-left">
|
|
67
|
+
<div className="mb-3 flex size-10 items-center justify-center rounded-lg bg-gray-100">
|
|
68
|
+
<UserPlus className="size-5 text-gray-600" />
|
|
69
|
+
</div>
|
|
70
|
+
<h3 className="font-semibold">Extensible</h3>
|
|
71
|
+
<p className="mt-1 text-sm text-gray-500">
|
|
72
|
+
Add 2FA, magic links, passkeys, and organizations via Better Auth plugins.
|
|
73
|
+
</p>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
</main>
|
|
77
|
+
|
|
78
|
+
<footer className="border-t border-gray-200 py-6">
|
|
79
|
+
<p className="text-center text-sm text-gray-400">
|
|
80
|
+
Built with jack. Deploy with{" "}
|
|
81
|
+
<code className="rounded bg-gray-100 px-1 py-0.5 text-gray-600">jack ship</code>
|
|
82
|
+
</p>
|
|
83
|
+
</footer>
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
}
|