@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.
Files changed (196) hide show
  1. package/package.json +1 -1
  2. package/src/commands/deploys.ts +95 -0
  3. package/src/commands/link.ts +8 -0
  4. package/src/commands/mcp.ts +179 -4
  5. package/src/commands/rollback.ts +53 -0
  6. package/src/commands/secrets.ts +3 -1
  7. package/src/commands/services.ts +11 -1
  8. package/src/commands/ship.ts +3 -1
  9. package/src/commands/tokens.ts +16 -1
  10. package/src/commands/whoami.ts +43 -8
  11. package/src/index.ts +16 -0
  12. package/src/lib/agent-files.ts +54 -4
  13. package/src/lib/agent-integration.ts +4 -166
  14. package/src/lib/claude-hooks-installer.ts +55 -0
  15. package/src/lib/control-plane.ts +78 -40
  16. package/src/lib/crypto.ts +84 -0
  17. package/src/lib/debug.ts +2 -1
  18. package/src/lib/deploy-upload.ts +13 -3
  19. package/src/lib/hooks.ts +4 -3
  20. package/src/lib/managed-deploy.ts +12 -9
  21. package/src/lib/project-link.ts +6 -0
  22. package/src/lib/project-operations.ts +92 -30
  23. package/src/lib/prompts.ts +2 -2
  24. package/src/lib/telemetry.ts +2 -0
  25. package/src/mcp/README.md +1 -1
  26. package/src/mcp/resources/index.ts +1 -16
  27. package/src/mcp/server.ts +23 -0
  28. package/src/mcp/tools/index.ts +133 -17
  29. package/src/mcp/types.ts +1 -0
  30. package/src/mcp/utils.ts +2 -1
  31. package/src/templates/index.ts +25 -73
  32. package/templates/CLAUDE.md +62 -0
  33. package/templates/ai-chat/.jack.json +10 -5
  34. package/templates/ai-chat/bun.lock +50 -1
  35. package/templates/ai-chat/package.json +5 -0
  36. package/templates/ai-chat/public/app.js +73 -0
  37. package/templates/ai-chat/public/index.html +14 -197
  38. package/templates/ai-chat/schema.sql +14 -0
  39. package/templates/ai-chat/src/index.ts +86 -102
  40. package/templates/ai-chat/wrangler.jsonc +8 -1
  41. package/templates/cron/.jack.json +66 -0
  42. package/templates/cron/bun.lock +23 -0
  43. package/templates/cron/package.json +16 -0
  44. package/templates/cron/schema.sql +24 -0
  45. package/templates/cron/src/index.ts +117 -0
  46. package/templates/cron/src/jobs.ts +139 -0
  47. package/templates/cron/src/webhooks.ts +95 -0
  48. package/templates/cron/tsconfig.json +17 -0
  49. package/templates/cron/wrangler.jsonc +11 -0
  50. package/templates/miniapp/.jack.json +1 -1
  51. package/templates/nextjs/.jack.json +1 -1
  52. package/templates/nextjs-auth/.jack.json +44 -0
  53. package/templates/nextjs-auth/app/api/auth/[...all]/route.ts +11 -0
  54. package/templates/nextjs-auth/app/dashboard/loading.tsx +53 -0
  55. package/templates/nextjs-auth/app/dashboard/page.tsx +73 -0
  56. package/templates/nextjs-auth/app/error.tsx +44 -0
  57. package/templates/nextjs-auth/app/globals.css +1 -0
  58. package/templates/nextjs-auth/app/health/route.ts +3 -0
  59. package/templates/nextjs-auth/app/layout.tsx +24 -0
  60. package/templates/nextjs-auth/app/login/page.tsx +10 -0
  61. package/templates/nextjs-auth/app/page.tsx +86 -0
  62. package/templates/nextjs-auth/app/signup/page.tsx +10 -0
  63. package/templates/nextjs-auth/bun.lock +1065 -0
  64. package/templates/nextjs-auth/cloudflare-env.d.ts +8 -0
  65. package/templates/nextjs-auth/components/auth-form.tsx +191 -0
  66. package/templates/nextjs-auth/components/header.tsx +50 -0
  67. package/templates/nextjs-auth/components/user-menu.tsx +23 -0
  68. package/templates/nextjs-auth/lib/auth-client.ts +3 -0
  69. package/templates/nextjs-auth/lib/auth.ts +43 -0
  70. package/templates/nextjs-auth/lib/utils.ts +6 -0
  71. package/templates/nextjs-auth/middleware.ts +33 -0
  72. package/templates/nextjs-auth/next.config.ts +8 -0
  73. package/templates/nextjs-auth/open-next.config.ts +6 -0
  74. package/templates/nextjs-auth/package.json +33 -0
  75. package/templates/nextjs-auth/postcss.config.mjs +8 -0
  76. package/templates/nextjs-auth/schema.sql +49 -0
  77. package/templates/nextjs-auth/tsconfig.json +28 -0
  78. package/templates/nextjs-auth/wrangler.jsonc +23 -0
  79. package/templates/nextjs-clerk/.jack.json +54 -0
  80. package/templates/nextjs-clerk/app/dashboard/page.tsx +69 -0
  81. package/templates/nextjs-clerk/app/globals.css +1 -0
  82. package/templates/nextjs-clerk/app/health/route.ts +3 -0
  83. package/templates/nextjs-clerk/app/layout.tsx +28 -0
  84. package/templates/nextjs-clerk/app/page.tsx +86 -0
  85. package/templates/nextjs-clerk/app/sign-in/[[...sign-in]]/page.tsx +9 -0
  86. package/templates/nextjs-clerk/app/sign-up/[[...sign-up]]/page.tsx +9 -0
  87. package/templates/nextjs-clerk/bun.lock +1055 -0
  88. package/templates/nextjs-clerk/cloudflare-env.d.ts +3 -0
  89. package/templates/nextjs-clerk/components/header.tsx +40 -0
  90. package/templates/nextjs-clerk/lib/utils.ts +6 -0
  91. package/templates/nextjs-clerk/middleware.ts +18 -0
  92. package/templates/nextjs-clerk/next.config.ts +8 -0
  93. package/templates/nextjs-clerk/open-next.config.ts +6 -0
  94. package/templates/nextjs-clerk/package.json +31 -0
  95. package/templates/nextjs-clerk/postcss.config.mjs +8 -0
  96. package/templates/nextjs-clerk/tsconfig.json +28 -0
  97. package/templates/nextjs-clerk/wrangler.jsonc +17 -0
  98. package/templates/nextjs-shadcn/.jack.json +34 -0
  99. package/templates/nextjs-shadcn/app/dashboard/data.json +614 -0
  100. package/templates/nextjs-shadcn/app/dashboard/page.tsx +55 -0
  101. package/templates/nextjs-shadcn/app/globals.css +126 -0
  102. package/templates/nextjs-shadcn/app/health/route.ts +3 -0
  103. package/templates/nextjs-shadcn/app/layout.tsx +24 -0
  104. package/templates/nextjs-shadcn/app/login/page.tsx +19 -0
  105. package/templates/nextjs-shadcn/app/page.tsx +180 -0
  106. package/templates/nextjs-shadcn/app/showcase.tsx +1262 -0
  107. package/templates/nextjs-shadcn/bun.lock +1789 -0
  108. package/templates/nextjs-shadcn/cloudflare-env.d.ts +4 -0
  109. package/templates/nextjs-shadcn/components/app-sidebar.tsx +175 -0
  110. package/templates/nextjs-shadcn/components/chart-area-interactive.tsx +291 -0
  111. package/templates/nextjs-shadcn/components/data-table.tsx +807 -0
  112. package/templates/nextjs-shadcn/components/login-form.tsx +95 -0
  113. package/templates/nextjs-shadcn/components/nav-documents.tsx +92 -0
  114. package/templates/nextjs-shadcn/components/nav-main.tsx +73 -0
  115. package/templates/nextjs-shadcn/components/nav-projects.tsx +89 -0
  116. package/templates/nextjs-shadcn/components/nav-secondary.tsx +42 -0
  117. package/templates/nextjs-shadcn/components/nav-user.tsx +114 -0
  118. package/templates/nextjs-shadcn/components/section-cards.tsx +102 -0
  119. package/templates/nextjs-shadcn/components/site-header.tsx +30 -0
  120. package/templates/nextjs-shadcn/components/team-switcher.tsx +91 -0
  121. package/templates/nextjs-shadcn/components/ui/accordion.tsx +66 -0
  122. package/templates/nextjs-shadcn/components/ui/alert-dialog.tsx +196 -0
  123. package/templates/nextjs-shadcn/components/ui/alert.tsx +66 -0
  124. package/templates/nextjs-shadcn/components/ui/aspect-ratio.tsx +11 -0
  125. package/templates/nextjs-shadcn/components/ui/avatar.tsx +109 -0
  126. package/templates/nextjs-shadcn/components/ui/badge.tsx +48 -0
  127. package/templates/nextjs-shadcn/components/ui/breadcrumb.tsx +109 -0
  128. package/templates/nextjs-shadcn/components/ui/button-group.tsx +83 -0
  129. package/templates/nextjs-shadcn/components/ui/button.tsx +64 -0
  130. package/templates/nextjs-shadcn/components/ui/calendar.tsx +220 -0
  131. package/templates/nextjs-shadcn/components/ui/card.tsx +92 -0
  132. package/templates/nextjs-shadcn/components/ui/carousel.tsx +241 -0
  133. package/templates/nextjs-shadcn/components/ui/chart.tsx +357 -0
  134. package/templates/nextjs-shadcn/components/ui/checkbox.tsx +32 -0
  135. package/templates/nextjs-shadcn/components/ui/collapsible.tsx +33 -0
  136. package/templates/nextjs-shadcn/components/ui/combobox.tsx +310 -0
  137. package/templates/nextjs-shadcn/components/ui/command.tsx +184 -0
  138. package/templates/nextjs-shadcn/components/ui/context-menu.tsx +252 -0
  139. package/templates/nextjs-shadcn/components/ui/dialog.tsx +158 -0
  140. package/templates/nextjs-shadcn/components/ui/direction.tsx +22 -0
  141. package/templates/nextjs-shadcn/components/ui/drawer.tsx +135 -0
  142. package/templates/nextjs-shadcn/components/ui/dropdown-menu.tsx +257 -0
  143. package/templates/nextjs-shadcn/components/ui/empty.tsx +104 -0
  144. package/templates/nextjs-shadcn/components/ui/field.tsx +248 -0
  145. package/templates/nextjs-shadcn/components/ui/form.tsx +167 -0
  146. package/templates/nextjs-shadcn/components/ui/hover-card.tsx +44 -0
  147. package/templates/nextjs-shadcn/components/ui/input-group.tsx +170 -0
  148. package/templates/nextjs-shadcn/components/ui/input-otp.tsx +77 -0
  149. package/templates/nextjs-shadcn/components/ui/input.tsx +21 -0
  150. package/templates/nextjs-shadcn/components/ui/item.tsx +193 -0
  151. package/templates/nextjs-shadcn/components/ui/kbd.tsx +28 -0
  152. package/templates/nextjs-shadcn/components/ui/label.tsx +24 -0
  153. package/templates/nextjs-shadcn/components/ui/menubar.tsx +276 -0
  154. package/templates/nextjs-shadcn/components/ui/native-select.tsx +53 -0
  155. package/templates/nextjs-shadcn/components/ui/navigation-menu.tsx +168 -0
  156. package/templates/nextjs-shadcn/components/ui/pagination.tsx +127 -0
  157. package/templates/nextjs-shadcn/components/ui/popover.tsx +89 -0
  158. package/templates/nextjs-shadcn/components/ui/progress.tsx +31 -0
  159. package/templates/nextjs-shadcn/components/ui/radio-group.tsx +45 -0
  160. package/templates/nextjs-shadcn/components/ui/resizable.tsx +53 -0
  161. package/templates/nextjs-shadcn/components/ui/scroll-area.tsx +58 -0
  162. package/templates/nextjs-shadcn/components/ui/select.tsx +190 -0
  163. package/templates/nextjs-shadcn/components/ui/separator.tsx +28 -0
  164. package/templates/nextjs-shadcn/components/ui/sheet.tsx +143 -0
  165. package/templates/nextjs-shadcn/components/ui/sidebar.tsx +726 -0
  166. package/templates/nextjs-shadcn/components/ui/skeleton.tsx +13 -0
  167. package/templates/nextjs-shadcn/components/ui/slider.tsx +63 -0
  168. package/templates/nextjs-shadcn/components/ui/sonner.tsx +40 -0
  169. package/templates/nextjs-shadcn/components/ui/spinner.tsx +16 -0
  170. package/templates/nextjs-shadcn/components/ui/switch.tsx +35 -0
  171. package/templates/nextjs-shadcn/components/ui/table.tsx +116 -0
  172. package/templates/nextjs-shadcn/components/ui/tabs.tsx +91 -0
  173. package/templates/nextjs-shadcn/components/ui/textarea.tsx +18 -0
  174. package/templates/nextjs-shadcn/components/ui/toggle-group.tsx +83 -0
  175. package/templates/nextjs-shadcn/components/ui/toggle.tsx +47 -0
  176. package/templates/nextjs-shadcn/components/ui/tooltip.tsx +57 -0
  177. package/templates/nextjs-shadcn/components.json +23 -0
  178. package/templates/nextjs-shadcn/hooks/use-mobile.ts +19 -0
  179. package/templates/nextjs-shadcn/lib/utils.ts +6 -0
  180. package/templates/nextjs-shadcn/next-env.d.ts +6 -0
  181. package/templates/nextjs-shadcn/next.config.ts +8 -0
  182. package/templates/nextjs-shadcn/open-next.config.ts +6 -0
  183. package/templates/nextjs-shadcn/package.json +55 -0
  184. package/templates/nextjs-shadcn/postcss.config.mjs +8 -0
  185. package/templates/nextjs-shadcn/tsconfig.json +28 -0
  186. package/templates/nextjs-shadcn/wrangler.jsonc +23 -0
  187. package/templates/resend/.jack.json +64 -0
  188. package/templates/resend/bun.lock +23 -0
  189. package/templates/resend/package.json +16 -0
  190. package/templates/resend/schema.sql +13 -0
  191. package/templates/resend/src/email.ts +165 -0
  192. package/templates/resend/src/index.ts +108 -0
  193. package/templates/resend/tsconfig.json +17 -0
  194. package/templates/resend/wrangler.jsonc +11 -0
  195. package/templates/saas/.jack.json +1 -1
  196. package/templates/ai-chat/public/chat.js +0 -149
@@ -0,0 +1,24 @@
1
+ CREATE TABLE IF NOT EXISTS jobs (
2
+ id TEXT PRIMARY KEY,
3
+ type TEXT NOT NULL,
4
+ payload TEXT NOT NULL DEFAULT '{}',
5
+ status TEXT NOT NULL DEFAULT 'pending',
6
+ attempts INTEGER NOT NULL DEFAULT 0,
7
+ max_attempts INTEGER NOT NULL DEFAULT 3,
8
+ last_error TEXT,
9
+ run_at INTEGER NOT NULL DEFAULT (unixepoch()),
10
+ created_at INTEGER NOT NULL DEFAULT (unixepoch()),
11
+ updated_at INTEGER NOT NULL DEFAULT (unixepoch())
12
+ );
13
+
14
+ CREATE TABLE IF NOT EXISTS webhook_events (
15
+ id TEXT PRIMARY KEY,
16
+ source TEXT NOT NULL DEFAULT 'unknown',
17
+ event_type TEXT,
18
+ payload TEXT NOT NULL,
19
+ status TEXT NOT NULL DEFAULT 'received',
20
+ created_at INTEGER NOT NULL DEFAULT (unixepoch())
21
+ );
22
+
23
+ CREATE INDEX IF NOT EXISTS idx_jobs_status_run_at ON jobs(status, run_at);
24
+ CREATE INDEX IF NOT EXISTS idx_webhook_events_created ON webhook_events(created_at);
@@ -0,0 +1,117 @@
1
+ import { Hono } from "hono";
2
+ import { cors } from "hono/cors";
3
+ import { createJob, getPendingJobs, processJob, retryFailedJobs } from "./jobs";
4
+ import { logWebhookEvent, verifyWebhookSignature } from "./webhooks";
5
+
6
+ type Bindings = {
7
+ DB: D1Database;
8
+ WEBHOOK_SECRET: string;
9
+ };
10
+
11
+ const app = new Hono<{ Bindings: Bindings }>();
12
+
13
+ app.use("/*", cors());
14
+
15
+ app.get("/", (c) => {
16
+ return c.json({
17
+ message: "Background worker running",
18
+ name: "jack-template",
19
+ });
20
+ });
21
+
22
+ app.get("/health", (c) => {
23
+ return c.json({ status: "ok", timestamp: Date.now() });
24
+ });
25
+
26
+ // Cron handler - called by scheduler on POST /__scheduled
27
+ app.post("/__scheduled", async (c) => {
28
+ const db = c.env.DB;
29
+
30
+ // Retry failed jobs that haven't exceeded max attempts
31
+ const retried = await retryFailedJobs(db);
32
+
33
+ // Get pending jobs that are ready to run
34
+ const jobs = await getPendingJobs(db, 10);
35
+ let processed = 0;
36
+
37
+ for (const job of jobs) {
38
+ await processJob(db, job);
39
+ processed++;
40
+ }
41
+
42
+ return c.json({ processed, retried });
43
+ });
44
+
45
+ // Webhook ingestion endpoint
46
+ app.post("/webhook", async (c) => {
47
+ const db = c.env.DB;
48
+ const body = await c.req.text();
49
+ const signature = c.req.header("X-Signature") || "";
50
+
51
+ // Verify webhook signature
52
+ const valid = await verifyWebhookSignature(
53
+ body,
54
+ signature,
55
+ c.env.WEBHOOK_SECRET,
56
+ );
57
+ if (!valid) {
58
+ return c.json({ error: "Invalid signature" }, 401);
59
+ }
60
+
61
+ // Parse and log the event
62
+ let parsed: Record<string, unknown>;
63
+ try {
64
+ parsed = JSON.parse(body);
65
+ } catch {
66
+ return c.json({ error: "Invalid JSON body" }, 400);
67
+ }
68
+
69
+ const eventType = (parsed.event as string) || "unknown";
70
+ const source = (parsed.source as string) || "unknown";
71
+
72
+ const eventId = await logWebhookEvent(db, {
73
+ source,
74
+ eventType,
75
+ payload: body,
76
+ });
77
+
78
+ // Create a job from the webhook event for async processing
79
+ await createJob(db, {
80
+ type: `webhook.${eventType}`,
81
+ payload: { webhookEventId: eventId, data: parsed.data || {} },
82
+ });
83
+
84
+ return c.json({ received: true, id: eventId });
85
+ });
86
+
87
+ // List recent jobs
88
+ app.get("/jobs", async (c) => {
89
+ const db = c.env.DB;
90
+
91
+ const { results } = await db
92
+ .prepare(
93
+ "SELECT * FROM jobs ORDER BY created_at DESC LIMIT 50",
94
+ )
95
+ .all();
96
+
97
+ return c.json({ jobs: results });
98
+ });
99
+
100
+ // Get a single job by ID
101
+ app.get("/jobs/:id", async (c) => {
102
+ const db = c.env.DB;
103
+ const id = c.req.param("id");
104
+
105
+ const job = await db
106
+ .prepare("SELECT * FROM jobs WHERE id = ?")
107
+ .bind(id)
108
+ .first();
109
+
110
+ if (!job) {
111
+ return c.json({ error: "Job not found" }, 404);
112
+ }
113
+
114
+ return c.json({ job });
115
+ });
116
+
117
+ export default app;
@@ -0,0 +1,139 @@
1
+ export interface Job {
2
+ id: string;
3
+ type: string;
4
+ payload: string;
5
+ status: string;
6
+ attempts: number;
7
+ max_attempts: number;
8
+ last_error: string | null;
9
+ run_at: number;
10
+ created_at: number;
11
+ updated_at: number;
12
+ }
13
+
14
+ export interface CreateJobInput {
15
+ type: string;
16
+ payload?: Record<string, unknown>;
17
+ runAt?: number;
18
+ }
19
+
20
+ // Create a new job in the queue
21
+ export async function createJob(
22
+ db: D1Database,
23
+ input: CreateJobInput,
24
+ ): Promise<string> {
25
+ const id = crypto.randomUUID();
26
+ const now = Math.floor(Date.now() / 1000);
27
+ const runAt = input.runAt || now;
28
+ const payload = JSON.stringify(input.payload || {});
29
+
30
+ await db
31
+ .prepare(
32
+ "INSERT INTO jobs (id, type, payload, status, attempts, max_attempts, run_at, created_at, updated_at) VALUES (?, ?, ?, 'pending', 0, 3, ?, ?, ?)",
33
+ )
34
+ .bind(id, input.type, payload, runAt, now, now)
35
+ .run();
36
+
37
+ return id;
38
+ }
39
+
40
+ // Get pending jobs that are ready to run
41
+ export async function getPendingJobs(
42
+ db: D1Database,
43
+ limit = 10,
44
+ ): Promise<Job[]> {
45
+ const now = Math.floor(Date.now() / 1000);
46
+
47
+ const { results } = await db
48
+ .prepare(
49
+ "SELECT * FROM jobs WHERE status = 'pending' AND run_at <= ? ORDER BY run_at ASC LIMIT ?",
50
+ )
51
+ .bind(now, limit)
52
+ .all<Job>();
53
+
54
+ return results;
55
+ }
56
+
57
+ // Process a single job
58
+ // Add your own processing logic in the switch statement below
59
+ export async function processJob(db: D1Database, job: Job): Promise<void> {
60
+ const now = Math.floor(Date.now() / 1000);
61
+
62
+ try {
63
+ // Mark as running
64
+ await db
65
+ .prepare(
66
+ "UPDATE jobs SET status = 'running', attempts = attempts + 1, updated_at = ? WHERE id = ?",
67
+ )
68
+ .bind(now, job.id)
69
+ .run();
70
+
71
+ // Process based on job type
72
+ const payload = JSON.parse(job.payload);
73
+
74
+ switch (job.type) {
75
+ case "example-task":
76
+ // Replace with your own logic
77
+ console.log(`Processing example task: ${JSON.stringify(payload)}`);
78
+ break;
79
+
80
+ default:
81
+ // Webhook-created jobs and other types
82
+ if (job.type.startsWith("webhook.")) {
83
+ console.log(`Processing webhook job: ${job.type}`, payload);
84
+ } else {
85
+ console.log(`Unknown job type: ${job.type}`);
86
+ }
87
+ break;
88
+ }
89
+
90
+ // Mark as completed
91
+ await db
92
+ .prepare(
93
+ "UPDATE jobs SET status = 'completed', updated_at = ? WHERE id = ?",
94
+ )
95
+ .bind(Math.floor(Date.now() / 1000), job.id)
96
+ .run();
97
+ } catch (err) {
98
+ const errorMessage = err instanceof Error ? err.message : String(err);
99
+
100
+ // Mark as failed
101
+ await db
102
+ .prepare(
103
+ "UPDATE jobs SET status = 'failed', last_error = ?, updated_at = ? WHERE id = ?",
104
+ )
105
+ .bind(errorMessage, Math.floor(Date.now() / 1000), job.id)
106
+ .run();
107
+ }
108
+ }
109
+
110
+ // Retry failed jobs that haven't exceeded max attempts
111
+ // Uses exponential backoff: 2^attempts * 60 seconds
112
+ export async function retryFailedJobs(db: D1Database): Promise<number> {
113
+ const now = Math.floor(Date.now() / 1000);
114
+
115
+ const { results } = await db
116
+ .prepare(
117
+ "SELECT id, attempts FROM jobs WHERE status = 'failed' AND attempts < max_attempts",
118
+ )
119
+ .all<{ id: string; attempts: number }>();
120
+
121
+ let retried = 0;
122
+
123
+ for (const job of results) {
124
+ // Exponential backoff: 2^attempts * 60 seconds
125
+ const backoffSeconds = Math.pow(2, job.attempts) * 60;
126
+ const nextRunAt = now + backoffSeconds;
127
+
128
+ await db
129
+ .prepare(
130
+ "UPDATE jobs SET status = 'pending', run_at = ?, updated_at = ? WHERE id = ?",
131
+ )
132
+ .bind(nextRunAt, now, job.id)
133
+ .run();
134
+
135
+ retried++;
136
+ }
137
+
138
+ return retried;
139
+ }
@@ -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
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "jack-template",
3
+ "main": "src/index.ts",
4
+ "compatibility_date": "2024-12-01",
5
+ "d1_databases": [
6
+ {
7
+ "binding": "DB",
8
+ "database_name": "jack-template-db"
9
+ }
10
+ ]
11
+ }
@@ -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 on Cloudflare Workers, with D1 SQLite database and AI capabilities.",
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,3 @@
1
+ export function GET() {
2
+ return Response.json({ status: "ok", timestamp: Date.now() });
3
+ }