@getjack/jack 0.1.32 → 0.1.34
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/secrets.ts +3 -1
- 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/crypto.ts +84 -0
- package/src/lib/debug.ts +2 -1
- package/src/lib/deploy-upload.ts +13 -3
- package/src/lib/hooks.ts +4 -3
- package/src/lib/managed-deploy.ts +12 -9
- package/src/lib/project-link.ts +6 -0
- package/src/lib/project-operations.ts +92 -30
- package/src/lib/prompts.ts +2 -2
- 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 +62 -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 +28 -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,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "jack-template",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "next dev",
|
|
7
|
+
"build": "next build",
|
|
8
|
+
"preview": "opennextjs-cloudflare build && opennextjs-cloudflare preview",
|
|
9
|
+
"deploy": "opennextjs-cloudflare build && opennextjs-cloudflare deploy",
|
|
10
|
+
"cf-typegen": "wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@base-ui/react": "^1.1.0",
|
|
14
|
+
"@dnd-kit/core": "^6.3.1",
|
|
15
|
+
"@dnd-kit/modifiers": "^9.0.0",
|
|
16
|
+
"@dnd-kit/sortable": "^10.0.0",
|
|
17
|
+
"@dnd-kit/utilities": "^3.2.2",
|
|
18
|
+
"@hookform/resolvers": "^5.2.2",
|
|
19
|
+
"@opennextjs/cloudflare": "^1.0.0",
|
|
20
|
+
"@tabler/icons-react": "^3.36.1",
|
|
21
|
+
"@tailwindcss/postcss": "^4.1.18",
|
|
22
|
+
"@tanstack/react-table": "^8.21.3",
|
|
23
|
+
"class-variance-authority": "^0.7.1",
|
|
24
|
+
"clsx": "^2.1.1",
|
|
25
|
+
"cmdk": "^1.1.1",
|
|
26
|
+
"date-fns": "^4.1.0",
|
|
27
|
+
"embla-carousel-react": "^8.6.0",
|
|
28
|
+
"geist": "^1.7.0",
|
|
29
|
+
"input-otp": "^1.4.2",
|
|
30
|
+
"lucide-react": "^0.563.0",
|
|
31
|
+
"next": "^15.0.0",
|
|
32
|
+
"next-themes": "^0.4.6",
|
|
33
|
+
"postcss": "^8.5.6",
|
|
34
|
+
"radix-ui": "^1.4.3",
|
|
35
|
+
"react": "^19.0.0",
|
|
36
|
+
"react-day-picker": "^9.13.2",
|
|
37
|
+
"react-dom": "^19.0.0",
|
|
38
|
+
"react-hook-form": "^7.71.1",
|
|
39
|
+
"react-resizable-panels": "^4",
|
|
40
|
+
"recharts": "2.15.4",
|
|
41
|
+
"sonner": "^2.0.7",
|
|
42
|
+
"tailwind-merge": "^3.4.0",
|
|
43
|
+
"tailwindcss": "^4.1.18",
|
|
44
|
+
"vaul": "^1.1.2",
|
|
45
|
+
"zod": "^4.3.6"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@cloudflare/workers-types": "^4.20241205.0",
|
|
49
|
+
"@types/react": "^19.0.0",
|
|
50
|
+
"@types/react-dom": "^19.0.0",
|
|
51
|
+
"shadcn": "^3.8.4",
|
|
52
|
+
"tw-animate-css": "^1.4.0",
|
|
53
|
+
"typescript": "^5.6.0"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"jsx": "preserve",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"noEmit": true,
|
|
11
|
+
"incremental": true,
|
|
12
|
+
"esModuleInterop": true,
|
|
13
|
+
"resolveJsonModule": true,
|
|
14
|
+
"isolatedModules": true,
|
|
15
|
+
"paths": {
|
|
16
|
+
"@/*": ["./*"]
|
|
17
|
+
},
|
|
18
|
+
"plugins": [
|
|
19
|
+
{
|
|
20
|
+
"name": "next"
|
|
21
|
+
}
|
|
22
|
+
],
|
|
23
|
+
"types": ["@cloudflare/workers-types"],
|
|
24
|
+
"allowJs": true
|
|
25
|
+
},
|
|
26
|
+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "cloudflare-env.d.ts"],
|
|
27
|
+
"exclude": ["node_modules"]
|
|
28
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "node_modules/wrangler/config-schema.json",
|
|
3
|
+
"name": "jack-template",
|
|
4
|
+
"main": ".open-next/worker.js",
|
|
5
|
+
"compatibility_date": "2024-12-30",
|
|
6
|
+
"compatibility_flags": ["nodejs_compat"],
|
|
7
|
+
"assets": {
|
|
8
|
+
"directory": ".open-next/assets",
|
|
9
|
+
"binding": "ASSETS"
|
|
10
|
+
},
|
|
11
|
+
"d1_databases": [
|
|
12
|
+
{
|
|
13
|
+
"binding": "DB",
|
|
14
|
+
"database_name": "jack-template-db"
|
|
15
|
+
}
|
|
16
|
+
],
|
|
17
|
+
"r2_buckets": [
|
|
18
|
+
{
|
|
19
|
+
"binding": "NEXT_INC_CACHE_R2_BUCKET",
|
|
20
|
+
"bucket_name": "jack-template-cache"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "resend",
|
|
3
|
+
"description": "Transactional email API powered by Resend",
|
|
4
|
+
"secrets": ["RESEND_API_KEY"],
|
|
5
|
+
"capabilities": ["db"],
|
|
6
|
+
"requires": ["DB"],
|
|
7
|
+
"intent": {
|
|
8
|
+
"keywords": [
|
|
9
|
+
"email",
|
|
10
|
+
"resend",
|
|
11
|
+
"transactional",
|
|
12
|
+
"notification",
|
|
13
|
+
"mail",
|
|
14
|
+
"smtp"
|
|
15
|
+
],
|
|
16
|
+
"examples": [
|
|
17
|
+
"email sending API",
|
|
18
|
+
"transactional email service",
|
|
19
|
+
"notification emails"
|
|
20
|
+
]
|
|
21
|
+
},
|
|
22
|
+
"agentContext": {
|
|
23
|
+
"summary": "A transactional email API using Resend with D1-backed email logging.",
|
|
24
|
+
"full_text": "## Project Structure\n\n- `src/index.ts` - Hono API with email send and log endpoints\n- `src/email.ts` - Resend email helper with D1 logging\n- `schema.sql` - D1 schema for email_log table\n\n## Sending Email\n\n```bash\ncurl -X POST https://your-app.workers.dev/api/send \\\n -H 'Content-Type: application/json' \\\n -d '{\"to\": \"user@example.com\", \"subject\": \"Hello\", \"html\": \"<p>Welcome!</p>\"}'\n```\n\n## Email Templates\n\nBuilt-in template helpers:\n```typescript\nimport { welcomeEmail, notificationEmail } from './email';\n\n// Send welcome email\nawait sendEmail(env, db, welcomeEmail('user@example.com', 'Alice'));\n\n// Send notification\nawait sendEmail(env, db, notificationEmail('user@example.com', 'New order received', 'Order #123'));\n```\n\n## Email Log\n\nAll sent emails are logged to D1 for tracking:\n```bash\ncurl https://your-app.workers.dev/api/emails # List recent emails\ncurl https://your-app.workers.dev/api/emails/stats # Send stats\n```\n\n## Endpoints\n\n- `POST /api/send` - Send an email\n- `GET /api/emails` - List recent emails (last 50)\n- `GET /api/emails/stats` - Email send statistics\n- `GET /health` - Health check\n\n## Environment Variables\n\n- `RESEND_API_KEY` - Resend API key (re_...)\n- `FROM_EMAIL` - Default sender email (defaults to onboarding@resend.dev)\n\n## Resources\n\n- [Resend Docs](https://resend.com/docs)\n- [Resend API Reference](https://resend.com/docs/api-reference)\n- [Hono Documentation](https://hono.dev)"
|
|
25
|
+
},
|
|
26
|
+
"hooks": {
|
|
27
|
+
"preCreate": [
|
|
28
|
+
{
|
|
29
|
+
"action": "require",
|
|
30
|
+
"source": "secret",
|
|
31
|
+
"key": "RESEND_API_KEY",
|
|
32
|
+
"message": "Resend API key for transactional email",
|
|
33
|
+
"setupUrl": "https://resend.com/api-keys",
|
|
34
|
+
"onMissing": "prompt",
|
|
35
|
+
"promptMessage": "Enter your Resend API key (re_...):"
|
|
36
|
+
}
|
|
37
|
+
],
|
|
38
|
+
"postDeploy": [
|
|
39
|
+
{
|
|
40
|
+
"action": "clipboard",
|
|
41
|
+
"text": "{{url}}",
|
|
42
|
+
"message": "Deploy URL copied to clipboard"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"action": "shell",
|
|
46
|
+
"command": "curl -s {{url}}/health | head -c 200",
|
|
47
|
+
"message": "Testing worker health..."
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"action": "box",
|
|
51
|
+
"title": "Email API live: {{name}}",
|
|
52
|
+
"lines": [
|
|
53
|
+
"{{url}}",
|
|
54
|
+
"",
|
|
55
|
+
"Endpoints:",
|
|
56
|
+
" POST {{url}}/api/send — send an email",
|
|
57
|
+
" GET {{url}}/api/emails — email log",
|
|
58
|
+
"",
|
|
59
|
+
"Verify Resend domain: https://resend.com/domains"
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"lockfileVersion": 1,
|
|
3
|
+
"configVersion": 1,
|
|
4
|
+
"workspaces": {
|
|
5
|
+
"": {
|
|
6
|
+
"name": "jack-template",
|
|
7
|
+
"dependencies": {
|
|
8
|
+
"hono": "^4.6.0",
|
|
9
|
+
},
|
|
10
|
+
"devDependencies": {
|
|
11
|
+
"@cloudflare/workers-types": "^4.20241205.0",
|
|
12
|
+
"typescript": "^5.0.0",
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
"packages": {
|
|
17
|
+
"@cloudflare/workers-types": ["@cloudflare/workers-types@4.20260212.0", "", {}, "sha512-ZK+e8T/2tWBCrE8PoAi9oqTxcBen9Apq+dxbsy1R5LFVdB6M4pY+oP49OFuHTTezrvNXbyvmzbf/vjtrCPGdNg=="],
|
|
18
|
+
|
|
19
|
+
"hono": ["hono@4.11.9", "", {}, "sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ=="],
|
|
20
|
+
|
|
21
|
+
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "jack-template",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "wrangler dev",
|
|
7
|
+
"deploy": "wrangler deploy"
|
|
8
|
+
},
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"hono": "^4.6.0"
|
|
11
|
+
},
|
|
12
|
+
"devDependencies": {
|
|
13
|
+
"@cloudflare/workers-types": "^4.20241205.0",
|
|
14
|
+
"typescript": "^5.0.0"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS email_log (
|
|
2
|
+
id TEXT PRIMARY KEY,
|
|
3
|
+
to_address TEXT NOT NULL,
|
|
4
|
+
from_address TEXT NOT NULL,
|
|
5
|
+
subject TEXT NOT NULL,
|
|
6
|
+
status TEXT NOT NULL DEFAULT 'sent',
|
|
7
|
+
resend_id TEXT,
|
|
8
|
+
error TEXT,
|
|
9
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
CREATE INDEX IF NOT EXISTS idx_email_log_created ON email_log(created_at);
|
|
13
|
+
CREATE INDEX IF NOT EXISTS idx_email_log_to ON email_log(to_address);
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
export interface EmailOptions {
|
|
2
|
+
to: string;
|
|
3
|
+
subject: string;
|
|
4
|
+
html?: string;
|
|
5
|
+
text?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface SendResult {
|
|
9
|
+
success: boolean;
|
|
10
|
+
id?: string;
|
|
11
|
+
error?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface Env {
|
|
15
|
+
RESEND_API_KEY: string;
|
|
16
|
+
FROM_EMAIL?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Send an email via Resend API and log the result to D1.
|
|
21
|
+
*/
|
|
22
|
+
export async function sendEmail(
|
|
23
|
+
env: Env,
|
|
24
|
+
db: D1Database,
|
|
25
|
+
opts: EmailOptions,
|
|
26
|
+
): Promise<SendResult> {
|
|
27
|
+
const from = env.FROM_EMAIL || "onboarding@resend.dev";
|
|
28
|
+
const id = crypto.randomUUID();
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const response = await fetch("https://api.resend.com/emails", {
|
|
32
|
+
method: "POST",
|
|
33
|
+
headers: {
|
|
34
|
+
Authorization: `Bearer ${env.RESEND_API_KEY}`,
|
|
35
|
+
"Content-Type": "application/json",
|
|
36
|
+
},
|
|
37
|
+
body: JSON.stringify({
|
|
38
|
+
from,
|
|
39
|
+
to: opts.to,
|
|
40
|
+
subject: opts.subject,
|
|
41
|
+
html: opts.html,
|
|
42
|
+
text: opts.text,
|
|
43
|
+
}),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (!response.ok) {
|
|
47
|
+
const errorBody = await response.text();
|
|
48
|
+
let errorMessage: string;
|
|
49
|
+
try {
|
|
50
|
+
const parsed = JSON.parse(errorBody);
|
|
51
|
+
errorMessage = parsed.message || parsed.error || errorBody;
|
|
52
|
+
} catch {
|
|
53
|
+
errorMessage = errorBody;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
await db
|
|
57
|
+
.prepare(
|
|
58
|
+
"INSERT INTO email_log (id, to_address, from_address, subject, status, error, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
|
59
|
+
)
|
|
60
|
+
.bind(id, opts.to, from, opts.subject, "failed", errorMessage, Math.floor(Date.now() / 1000))
|
|
61
|
+
.run();
|
|
62
|
+
|
|
63
|
+
return { success: false, error: errorMessage };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const data = (await response.json()) as { id: string };
|
|
67
|
+
|
|
68
|
+
await db
|
|
69
|
+
.prepare(
|
|
70
|
+
"INSERT INTO email_log (id, to_address, from_address, subject, status, resend_id, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
|
71
|
+
)
|
|
72
|
+
.bind(id, opts.to, from, opts.subject, "sent", data.id, Math.floor(Date.now() / 1000))
|
|
73
|
+
.run();
|
|
74
|
+
|
|
75
|
+
return { success: true, id: data.id };
|
|
76
|
+
} catch (err) {
|
|
77
|
+
const errorMessage = err instanceof Error ? err.message : "Unknown error";
|
|
78
|
+
|
|
79
|
+
await db
|
|
80
|
+
.prepare(
|
|
81
|
+
"INSERT INTO email_log (id, to_address, from_address, subject, status, error, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
|
82
|
+
)
|
|
83
|
+
.bind(id, opts.to, from, opts.subject, "failed", errorMessage, Math.floor(Date.now() / 1000))
|
|
84
|
+
.run();
|
|
85
|
+
|
|
86
|
+
return { success: false, error: errorMessage };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Welcome email template.
|
|
92
|
+
*/
|
|
93
|
+
export function welcomeEmail(to: string, name: string): EmailOptions {
|
|
94
|
+
return {
|
|
95
|
+
to,
|
|
96
|
+
subject: `Welcome, ${name}!`,
|
|
97
|
+
html: `<!DOCTYPE html>
|
|
98
|
+
<html>
|
|
99
|
+
<head>
|
|
100
|
+
<meta charset="utf-8">
|
|
101
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
102
|
+
</head>
|
|
103
|
+
<body style="margin:0;padding:0;background:#f6f9fc;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;">
|
|
104
|
+
<table width="100%" cellpadding="0" cellspacing="0" style="background:#f6f9fc;padding:40px 0;">
|
|
105
|
+
<tr>
|
|
106
|
+
<td align="center">
|
|
107
|
+
<table width="560" cellpadding="0" cellspacing="0" style="background:#ffffff;border-radius:8px;padding:40px;">
|
|
108
|
+
<tr>
|
|
109
|
+
<td>
|
|
110
|
+
<h1 style="margin:0 0 16px;font-size:24px;color:#1a1a1a;">Welcome, ${name}!</h1>
|
|
111
|
+
<p style="margin:0 0 24px;font-size:16px;line-height:1.5;color:#4a4a4a;">
|
|
112
|
+
Thanks for signing up. We're glad to have you on board.
|
|
113
|
+
</p>
|
|
114
|
+
<p style="margin:0;font-size:16px;line-height:1.5;color:#4a4a4a;">
|
|
115
|
+
If you have any questions, just reply to this email.
|
|
116
|
+
</p>
|
|
117
|
+
</td>
|
|
118
|
+
</tr>
|
|
119
|
+
</table>
|
|
120
|
+
</td>
|
|
121
|
+
</tr>
|
|
122
|
+
</table>
|
|
123
|
+
</body>
|
|
124
|
+
</html>`,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Notification email template.
|
|
130
|
+
*/
|
|
131
|
+
export function notificationEmail(
|
|
132
|
+
to: string,
|
|
133
|
+
title: string,
|
|
134
|
+
body: string,
|
|
135
|
+
): EmailOptions {
|
|
136
|
+
return {
|
|
137
|
+
to,
|
|
138
|
+
subject: title,
|
|
139
|
+
html: `<!DOCTYPE html>
|
|
140
|
+
<html>
|
|
141
|
+
<head>
|
|
142
|
+
<meta charset="utf-8">
|
|
143
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
144
|
+
</head>
|
|
145
|
+
<body style="margin:0;padding:0;background:#f6f9fc;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;">
|
|
146
|
+
<table width="100%" cellpadding="0" cellspacing="0" style="background:#f6f9fc;padding:40px 0;">
|
|
147
|
+
<tr>
|
|
148
|
+
<td align="center">
|
|
149
|
+
<table width="560" cellpadding="0" cellspacing="0" style="background:#ffffff;border-radius:8px;padding:40px;">
|
|
150
|
+
<tr>
|
|
151
|
+
<td>
|
|
152
|
+
<h1 style="margin:0 0 16px;font-size:24px;color:#1a1a1a;">${title}</h1>
|
|
153
|
+
<p style="margin:0;font-size:16px;line-height:1.5;color:#4a4a4a;">
|
|
154
|
+
${body}
|
|
155
|
+
</p>
|
|
156
|
+
</td>
|
|
157
|
+
</tr>
|
|
158
|
+
</table>
|
|
159
|
+
</td>
|
|
160
|
+
</tr>
|
|
161
|
+
</table>
|
|
162
|
+
</body>
|
|
163
|
+
</html>`,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { cors } from "hono/cors";
|
|
3
|
+
import { sendEmail, welcomeEmail, notificationEmail } from "./email";
|
|
4
|
+
|
|
5
|
+
type Bindings = {
|
|
6
|
+
DB: D1Database;
|
|
7
|
+
RESEND_API_KEY: string;
|
|
8
|
+
FROM_EMAIL?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const app = new Hono<{ Bindings: Bindings }>();
|
|
12
|
+
|
|
13
|
+
app.use("/*", cors());
|
|
14
|
+
|
|
15
|
+
app.get("/", (c) => {
|
|
16
|
+
return c.json({ message: "Email API running", name: "jack-template" });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
app.get("/health", (c) => {
|
|
20
|
+
return c.json({ status: "ok", timestamp: Date.now() });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// Send an email
|
|
24
|
+
app.post("/api/send", async (c) => {
|
|
25
|
+
const db = c.env.DB;
|
|
26
|
+
|
|
27
|
+
let body: {
|
|
28
|
+
to: string;
|
|
29
|
+
subject?: string;
|
|
30
|
+
html?: string;
|
|
31
|
+
text?: string;
|
|
32
|
+
template?: "welcome" | "notification";
|
|
33
|
+
templateData?: Record<string, string>;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
body = await c.req.json();
|
|
38
|
+
} catch {
|
|
39
|
+
return c.json({ error: "Invalid JSON body" }, 400);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!body.to) {
|
|
43
|
+
return c.json({ error: "Missing required field: to" }, 400);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Build email options from template or raw fields
|
|
47
|
+
let emailOpts: { to: string; subject: string; html?: string; text?: string };
|
|
48
|
+
|
|
49
|
+
if (body.template === "welcome") {
|
|
50
|
+
const name = body.templateData?.name || "there";
|
|
51
|
+
emailOpts = welcomeEmail(body.to, name);
|
|
52
|
+
} else if (body.template === "notification") {
|
|
53
|
+
const title = body.templateData?.title || "Notification";
|
|
54
|
+
const notifBody = body.templateData?.body || "";
|
|
55
|
+
emailOpts = notificationEmail(body.to, title, notifBody);
|
|
56
|
+
} else {
|
|
57
|
+
if (!body.subject) {
|
|
58
|
+
return c.json({ error: "Missing required field: subject" }, 400);
|
|
59
|
+
}
|
|
60
|
+
if (!body.html && !body.text) {
|
|
61
|
+
return c.json(
|
|
62
|
+
{ error: "At least one of html or text is required" },
|
|
63
|
+
400,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
emailOpts = {
|
|
67
|
+
to: body.to,
|
|
68
|
+
subject: body.subject,
|
|
69
|
+
html: body.html,
|
|
70
|
+
text: body.text,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const result = await sendEmail(c.env, db, emailOpts);
|
|
75
|
+
|
|
76
|
+
if (result.success) {
|
|
77
|
+
return c.json({ success: true, id: result.id });
|
|
78
|
+
}
|
|
79
|
+
return c.json({ error: result.error }, 500);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// List recent emails
|
|
83
|
+
app.get("/api/emails", async (c) => {
|
|
84
|
+
const db = c.env.DB;
|
|
85
|
+
|
|
86
|
+
const { results } = await db
|
|
87
|
+
.prepare(
|
|
88
|
+
"SELECT * FROM email_log ORDER BY created_at DESC LIMIT 50",
|
|
89
|
+
)
|
|
90
|
+
.all();
|
|
91
|
+
|
|
92
|
+
return c.json({ emails: results });
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Email send statistics
|
|
96
|
+
app.get("/api/emails/stats", async (c) => {
|
|
97
|
+
const db = c.env.DB;
|
|
98
|
+
|
|
99
|
+
const { results } = await db
|
|
100
|
+
.prepare(
|
|
101
|
+
"SELECT status, COUNT(*) as count FROM email_log GROUP BY status",
|
|
102
|
+
)
|
|
103
|
+
.all();
|
|
104
|
+
|
|
105
|
+
return c.json({ stats: results });
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
export default app;
|
|
@@ -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
|
+
}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"examples": ["subscription app", "paid membership site", "saas product"]
|
|
15
15
|
},
|
|
16
16
|
"agentContext": {
|
|
17
|
-
"summary": "A SaaS starter with Better Auth authentication, Stripe payments, React + Vite frontend, and Hono API
|
|
17
|
+
"summary": "A SaaS starter with Better Auth authentication, Stripe payments, React + Vite frontend, and Hono API with D1 SQLite database.",
|
|
18
18
|
"full_text": "## Project Structure\n\n- `src/index.ts` - Hono API entry point with auth and payment routes\n- `src/auth.ts` - Better Auth configuration with Stripe plugin\n- `src/client/App.tsx` - React application entry point\n- `src/client/lib/auth-client.ts` - Better Auth client\n- `src/client/hooks/` - useAuth, useSubscription hooks\n- `src/client/pages/` - Page components (HomePage, DashboardPage, etc.)\n- `src/client/components/ui/` - shadcn/ui components\n- `schema.sql` - D1 database schema (user, session, account, subscription)\n- `wrangler.jsonc` - Cloudflare Workers configuration\n\n## Authentication\n\nUses Better Auth with email/password. Auth routes are handled at `/api/auth/*`.\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 } = await authClient.getSession();\n```\n\n## Payments (Stripe)\n\nUses Better Auth Stripe plugin for subscription management.\n\n### Upgrade Flow\n```tsx\nimport { authClient } from './lib/auth-client';\n\nasync function handleUpgrade(plan: 'pro' | 'enterprise') {\n const { data, error } = await authClient.subscription.upgrade({\n plan,\n successUrl: '/dashboard?upgraded=true',\n cancelUrl: '/pricing',\n });\n\n if (data?.url) {\n window.location.href = data.url; // Redirect to Stripe Checkout\n }\n}\n```\n\n### Check Subscription\n```tsx\nconst { data: subscriptions } = await authClient.subscription.list();\nconst activeSubscription = subscriptions?.find(s =>\n s.status === 'active' || s.status === 'trialing'\n);\nconst plan = activeSubscription?.plan || 'free';\n```\n\n## Webhook Setup\n\nStripe webhooks are handled at `/api/auth/stripe/webhook`. Required events:\n- `checkout.session.completed`\n- `customer.subscription.created`\n- `customer.subscription.updated`\n- `customer.subscription.deleted`\n\n## Database Schema\n\nUses Better Auth's default schema (singular table names, camelCase columns):\n- `user` - User accounts\n- `session` - Active sessions\n- `account` - OAuth/password credentials\n- `verification` - Email verification tokens\n- `subscription` - Stripe subscriptions\n\n## Environment Variables\n\n- `BETTER_AUTH_SECRET` - Random secret for auth tokens (generate with: openssl rand -base64 32)\n- `STRIPE_SECRET_KEY` - Stripe API secret key\n- `STRIPE_WEBHOOK_SECRET` - Stripe webhook signing secret (whsec_...)\n- `STRIPE_PRO_PRICE_ID` - Stripe price ID for Pro plan\n- `STRIPE_ENTERPRISE_PRICE_ID` - Stripe price ID for Enterprise plan\n\n## Resources\n\n- [Better Auth Docs](https://www.betterauth.com/docs)\n- [Better Auth Stripe Plugin](https://www.betterauth.com/docs/plugins/stripe)\n- [Stripe Subscriptions](https://docs.stripe.com/billing/subscriptions)\n- [Hono Documentation](https://hono.dev)\n- [Cloudflare D1 Docs](https://developers.cloudflare.com/d1)"
|
|
19
19
|
},
|
|
20
20
|
"hooks": {
|