@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.
Files changed (193) 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/services.ts +11 -1
  7. package/src/commands/ship.ts +3 -1
  8. package/src/commands/tokens.ts +16 -1
  9. package/src/commands/whoami.ts +43 -8
  10. package/src/index.ts +16 -0
  11. package/src/lib/agent-files.ts +54 -4
  12. package/src/lib/agent-integration.ts +4 -166
  13. package/src/lib/claude-hooks-installer.ts +55 -0
  14. package/src/lib/control-plane.ts +78 -40
  15. package/src/lib/debug.ts +2 -1
  16. package/src/lib/deploy-upload.ts +6 -0
  17. package/src/lib/hooks.ts +3 -1
  18. package/src/lib/managed-deploy.ts +12 -9
  19. package/src/lib/project-link.ts +6 -0
  20. package/src/lib/project-operations.ts +68 -22
  21. package/src/lib/telemetry.ts +2 -0
  22. package/src/mcp/README.md +1 -1
  23. package/src/mcp/resources/index.ts +1 -16
  24. package/src/mcp/server.ts +23 -0
  25. package/src/mcp/tools/index.ts +133 -17
  26. package/src/mcp/types.ts +1 -0
  27. package/src/mcp/utils.ts +2 -1
  28. package/src/templates/index.ts +25 -73
  29. package/templates/CLAUDE.md +41 -0
  30. package/templates/ai-chat/.jack.json +10 -5
  31. package/templates/ai-chat/bun.lock +50 -1
  32. package/templates/ai-chat/package.json +5 -0
  33. package/templates/ai-chat/public/app.js +73 -0
  34. package/templates/ai-chat/public/index.html +14 -197
  35. package/templates/ai-chat/schema.sql +14 -0
  36. package/templates/ai-chat/src/index.ts +86 -102
  37. package/templates/ai-chat/wrangler.jsonc +8 -1
  38. package/templates/cron/.jack.json +66 -0
  39. package/templates/cron/bun.lock +23 -0
  40. package/templates/cron/package.json +16 -0
  41. package/templates/cron/schema.sql +24 -0
  42. package/templates/cron/src/index.ts +117 -0
  43. package/templates/cron/src/jobs.ts +139 -0
  44. package/templates/cron/src/webhooks.ts +95 -0
  45. package/templates/cron/tsconfig.json +17 -0
  46. package/templates/cron/wrangler.jsonc +11 -0
  47. package/templates/miniapp/.jack.json +1 -1
  48. package/templates/nextjs/.jack.json +1 -1
  49. package/templates/nextjs-auth/.jack.json +44 -0
  50. package/templates/nextjs-auth/app/api/auth/[...all]/route.ts +11 -0
  51. package/templates/nextjs-auth/app/dashboard/loading.tsx +53 -0
  52. package/templates/nextjs-auth/app/dashboard/page.tsx +73 -0
  53. package/templates/nextjs-auth/app/error.tsx +44 -0
  54. package/templates/nextjs-auth/app/globals.css +1 -0
  55. package/templates/nextjs-auth/app/health/route.ts +3 -0
  56. package/templates/nextjs-auth/app/layout.tsx +24 -0
  57. package/templates/nextjs-auth/app/login/page.tsx +10 -0
  58. package/templates/nextjs-auth/app/page.tsx +86 -0
  59. package/templates/nextjs-auth/app/signup/page.tsx +10 -0
  60. package/templates/nextjs-auth/bun.lock +1065 -0
  61. package/templates/nextjs-auth/cloudflare-env.d.ts +8 -0
  62. package/templates/nextjs-auth/components/auth-form.tsx +191 -0
  63. package/templates/nextjs-auth/components/header.tsx +50 -0
  64. package/templates/nextjs-auth/components/user-menu.tsx +23 -0
  65. package/templates/nextjs-auth/lib/auth-client.ts +3 -0
  66. package/templates/nextjs-auth/lib/auth.ts +43 -0
  67. package/templates/nextjs-auth/lib/utils.ts +6 -0
  68. package/templates/nextjs-auth/middleware.ts +33 -0
  69. package/templates/nextjs-auth/next.config.ts +8 -0
  70. package/templates/nextjs-auth/open-next.config.ts +6 -0
  71. package/templates/nextjs-auth/package.json +33 -0
  72. package/templates/nextjs-auth/postcss.config.mjs +8 -0
  73. package/templates/nextjs-auth/schema.sql +49 -0
  74. package/templates/nextjs-auth/tsconfig.json +28 -0
  75. package/templates/nextjs-auth/wrangler.jsonc +23 -0
  76. package/templates/nextjs-clerk/.jack.json +54 -0
  77. package/templates/nextjs-clerk/app/dashboard/page.tsx +69 -0
  78. package/templates/nextjs-clerk/app/globals.css +1 -0
  79. package/templates/nextjs-clerk/app/health/route.ts +3 -0
  80. package/templates/nextjs-clerk/app/layout.tsx +26 -0
  81. package/templates/nextjs-clerk/app/page.tsx +86 -0
  82. package/templates/nextjs-clerk/app/sign-in/[[...sign-in]]/page.tsx +9 -0
  83. package/templates/nextjs-clerk/app/sign-up/[[...sign-up]]/page.tsx +9 -0
  84. package/templates/nextjs-clerk/bun.lock +1055 -0
  85. package/templates/nextjs-clerk/cloudflare-env.d.ts +3 -0
  86. package/templates/nextjs-clerk/components/header.tsx +40 -0
  87. package/templates/nextjs-clerk/lib/utils.ts +6 -0
  88. package/templates/nextjs-clerk/middleware.ts +18 -0
  89. package/templates/nextjs-clerk/next.config.ts +8 -0
  90. package/templates/nextjs-clerk/open-next.config.ts +6 -0
  91. package/templates/nextjs-clerk/package.json +31 -0
  92. package/templates/nextjs-clerk/postcss.config.mjs +8 -0
  93. package/templates/nextjs-clerk/tsconfig.json +28 -0
  94. package/templates/nextjs-clerk/wrangler.jsonc +17 -0
  95. package/templates/nextjs-shadcn/.jack.json +34 -0
  96. package/templates/nextjs-shadcn/app/dashboard/data.json +614 -0
  97. package/templates/nextjs-shadcn/app/dashboard/page.tsx +55 -0
  98. package/templates/nextjs-shadcn/app/globals.css +126 -0
  99. package/templates/nextjs-shadcn/app/health/route.ts +3 -0
  100. package/templates/nextjs-shadcn/app/layout.tsx +24 -0
  101. package/templates/nextjs-shadcn/app/login/page.tsx +19 -0
  102. package/templates/nextjs-shadcn/app/page.tsx +180 -0
  103. package/templates/nextjs-shadcn/app/showcase.tsx +1262 -0
  104. package/templates/nextjs-shadcn/bun.lock +1789 -0
  105. package/templates/nextjs-shadcn/cloudflare-env.d.ts +4 -0
  106. package/templates/nextjs-shadcn/components/app-sidebar.tsx +175 -0
  107. package/templates/nextjs-shadcn/components/chart-area-interactive.tsx +291 -0
  108. package/templates/nextjs-shadcn/components/data-table.tsx +807 -0
  109. package/templates/nextjs-shadcn/components/login-form.tsx +95 -0
  110. package/templates/nextjs-shadcn/components/nav-documents.tsx +92 -0
  111. package/templates/nextjs-shadcn/components/nav-main.tsx +73 -0
  112. package/templates/nextjs-shadcn/components/nav-projects.tsx +89 -0
  113. package/templates/nextjs-shadcn/components/nav-secondary.tsx +42 -0
  114. package/templates/nextjs-shadcn/components/nav-user.tsx +114 -0
  115. package/templates/nextjs-shadcn/components/section-cards.tsx +102 -0
  116. package/templates/nextjs-shadcn/components/site-header.tsx +30 -0
  117. package/templates/nextjs-shadcn/components/team-switcher.tsx +91 -0
  118. package/templates/nextjs-shadcn/components/ui/accordion.tsx +66 -0
  119. package/templates/nextjs-shadcn/components/ui/alert-dialog.tsx +196 -0
  120. package/templates/nextjs-shadcn/components/ui/alert.tsx +66 -0
  121. package/templates/nextjs-shadcn/components/ui/aspect-ratio.tsx +11 -0
  122. package/templates/nextjs-shadcn/components/ui/avatar.tsx +109 -0
  123. package/templates/nextjs-shadcn/components/ui/badge.tsx +48 -0
  124. package/templates/nextjs-shadcn/components/ui/breadcrumb.tsx +109 -0
  125. package/templates/nextjs-shadcn/components/ui/button-group.tsx +83 -0
  126. package/templates/nextjs-shadcn/components/ui/button.tsx +64 -0
  127. package/templates/nextjs-shadcn/components/ui/calendar.tsx +220 -0
  128. package/templates/nextjs-shadcn/components/ui/card.tsx +92 -0
  129. package/templates/nextjs-shadcn/components/ui/carousel.tsx +241 -0
  130. package/templates/nextjs-shadcn/components/ui/chart.tsx +357 -0
  131. package/templates/nextjs-shadcn/components/ui/checkbox.tsx +32 -0
  132. package/templates/nextjs-shadcn/components/ui/collapsible.tsx +33 -0
  133. package/templates/nextjs-shadcn/components/ui/combobox.tsx +310 -0
  134. package/templates/nextjs-shadcn/components/ui/command.tsx +184 -0
  135. package/templates/nextjs-shadcn/components/ui/context-menu.tsx +252 -0
  136. package/templates/nextjs-shadcn/components/ui/dialog.tsx +158 -0
  137. package/templates/nextjs-shadcn/components/ui/direction.tsx +22 -0
  138. package/templates/nextjs-shadcn/components/ui/drawer.tsx +135 -0
  139. package/templates/nextjs-shadcn/components/ui/dropdown-menu.tsx +257 -0
  140. package/templates/nextjs-shadcn/components/ui/empty.tsx +104 -0
  141. package/templates/nextjs-shadcn/components/ui/field.tsx +248 -0
  142. package/templates/nextjs-shadcn/components/ui/form.tsx +167 -0
  143. package/templates/nextjs-shadcn/components/ui/hover-card.tsx +44 -0
  144. package/templates/nextjs-shadcn/components/ui/input-group.tsx +170 -0
  145. package/templates/nextjs-shadcn/components/ui/input-otp.tsx +77 -0
  146. package/templates/nextjs-shadcn/components/ui/input.tsx +21 -0
  147. package/templates/nextjs-shadcn/components/ui/item.tsx +193 -0
  148. package/templates/nextjs-shadcn/components/ui/kbd.tsx +28 -0
  149. package/templates/nextjs-shadcn/components/ui/label.tsx +24 -0
  150. package/templates/nextjs-shadcn/components/ui/menubar.tsx +276 -0
  151. package/templates/nextjs-shadcn/components/ui/native-select.tsx +53 -0
  152. package/templates/nextjs-shadcn/components/ui/navigation-menu.tsx +168 -0
  153. package/templates/nextjs-shadcn/components/ui/pagination.tsx +127 -0
  154. package/templates/nextjs-shadcn/components/ui/popover.tsx +89 -0
  155. package/templates/nextjs-shadcn/components/ui/progress.tsx +31 -0
  156. package/templates/nextjs-shadcn/components/ui/radio-group.tsx +45 -0
  157. package/templates/nextjs-shadcn/components/ui/resizable.tsx +53 -0
  158. package/templates/nextjs-shadcn/components/ui/scroll-area.tsx +58 -0
  159. package/templates/nextjs-shadcn/components/ui/select.tsx +190 -0
  160. package/templates/nextjs-shadcn/components/ui/separator.tsx +28 -0
  161. package/templates/nextjs-shadcn/components/ui/sheet.tsx +143 -0
  162. package/templates/nextjs-shadcn/components/ui/sidebar.tsx +726 -0
  163. package/templates/nextjs-shadcn/components/ui/skeleton.tsx +13 -0
  164. package/templates/nextjs-shadcn/components/ui/slider.tsx +63 -0
  165. package/templates/nextjs-shadcn/components/ui/sonner.tsx +40 -0
  166. package/templates/nextjs-shadcn/components/ui/spinner.tsx +16 -0
  167. package/templates/nextjs-shadcn/components/ui/switch.tsx +35 -0
  168. package/templates/nextjs-shadcn/components/ui/table.tsx +116 -0
  169. package/templates/nextjs-shadcn/components/ui/tabs.tsx +91 -0
  170. package/templates/nextjs-shadcn/components/ui/textarea.tsx +18 -0
  171. package/templates/nextjs-shadcn/components/ui/toggle-group.tsx +83 -0
  172. package/templates/nextjs-shadcn/components/ui/toggle.tsx +47 -0
  173. package/templates/nextjs-shadcn/components/ui/tooltip.tsx +57 -0
  174. package/templates/nextjs-shadcn/components.json +23 -0
  175. package/templates/nextjs-shadcn/hooks/use-mobile.ts +19 -0
  176. package/templates/nextjs-shadcn/lib/utils.ts +6 -0
  177. package/templates/nextjs-shadcn/next-env.d.ts +6 -0
  178. package/templates/nextjs-shadcn/next.config.ts +8 -0
  179. package/templates/nextjs-shadcn/open-next.config.ts +6 -0
  180. package/templates/nextjs-shadcn/package.json +55 -0
  181. package/templates/nextjs-shadcn/postcss.config.mjs +8 -0
  182. package/templates/nextjs-shadcn/tsconfig.json +28 -0
  183. package/templates/nextjs-shadcn/wrangler.jsonc +23 -0
  184. package/templates/resend/.jack.json +64 -0
  185. package/templates/resend/bun.lock +23 -0
  186. package/templates/resend/package.json +16 -0
  187. package/templates/resend/schema.sql +13 -0
  188. package/templates/resend/src/email.ts +165 -0
  189. package/templates/resend/src/index.ts +108 -0
  190. package/templates/resend/tsconfig.json +17 -0
  191. package/templates/resend/wrangler.jsonc +11 -0
  192. package/templates/saas/.jack.json +1 -1
  193. package/templates/ai-chat/public/chat.js +0 -149
@@ -1,49 +1,32 @@
1
+ import { Hono } from "hono";
2
+ import { streamText, convertToCoreMessages, type Message } from "ai";
3
+ import { createWorkersAI } from "workers-ai-provider";
1
4
  import { createJackAI } from "./jack-ai";
2
5
 
3
6
  interface Env {
4
- // Direct AI binding (for local dev with wrangler)
5
7
  AI?: Ai;
6
- // Jack proxy bindings (injected in jack cloud)
7
8
  __AI_PROXY?: Fetcher;
8
9
  __JACK_PROJECT_ID?: string;
9
10
  __JACK_ORG_ID?: string;
10
- // Assets binding
11
11
  ASSETS: Fetcher;
12
+ DB: D1Database;
12
13
  }
13
14
 
14
15
  function getAI(env: Env) {
15
- // Prefer jack cloud proxy if available (for metering)
16
16
  if (env.__AI_PROXY && env.__JACK_PROJECT_ID && env.__JACK_ORG_ID) {
17
17
  return createJackAI(
18
- env as Required<Pick<Env, "__AI_PROXY" | "__JACK_PROJECT_ID" | "__JACK_ORG_ID">>,
18
+ env as Required<
19
+ Pick<Env, "__AI_PROXY" | "__JACK_PROJECT_ID" | "__JACK_ORG_ID">
20
+ >,
19
21
  );
20
22
  }
21
- // Fallback to direct binding for local dev
22
- if (env.AI) {
23
- return env.AI;
24
- }
23
+ if (env.AI) return env.AI;
25
24
  throw new Error("No AI binding available");
26
25
  }
27
26
 
28
- interface ChatMessage {
29
- role: "user" | "assistant" | "system";
30
- content: string;
31
- }
32
-
33
- // System prompt - customize this to change the AI's personality
34
- const SYSTEM_PROMPT = `You are a helpful AI assistant built with jack (getjack.sh).
35
-
36
- jack helps developers ship ideas fast - from "what if" to a live URL in seconds. You're running on Cloudflare's edge network, close to users worldwide.
37
-
38
- Be concise, friendly, and helpful. If asked about jack:
39
- - jack new creates projects from templates
40
- - jack ship deploys to production
41
- - jack open opens your app in browser
42
- - Docs: https://docs.getjack.sh
27
+ const SYSTEM_PROMPT = `You are a helpful AI assistant. Be concise, friendly, and helpful. Keep responses short unless detail is needed.`;
43
28
 
44
- Focus on being useful. Keep responses short unless detail is needed.`;
45
-
46
- // Rate limiting: 10 requests per minute per IP
29
+ // Rate limiting
47
30
  const RATE_LIMIT = 10;
48
31
  const RATE_WINDOW_MS = 60_000;
49
32
  const rateLimitMap = new Map<string, { count: number; resetAt: number }>();
@@ -51,91 +34,92 @@ const rateLimitMap = new Map<string, { count: number; resetAt: number }>();
51
34
  function checkRateLimit(ip: string): boolean {
52
35
  const now = Date.now();
53
36
  const entry = rateLimitMap.get(ip);
54
-
55
37
  if (!entry || now >= entry.resetAt) {
56
38
  rateLimitMap.set(ip, { count: 1, resetAt: now + RATE_WINDOW_MS });
57
39
  return true;
58
40
  }
59
-
60
- if (entry.count >= RATE_LIMIT) {
61
- return false;
62
- }
63
-
41
+ if (entry.count >= RATE_LIMIT) return false;
64
42
  entry.count++;
65
43
  return true;
66
44
  }
67
45
 
68
- // Clean up old entries periodically to prevent memory leaks
69
- function cleanupRateLimitMap(): void {
70
- const now = Date.now();
71
- for (const [ip, entry] of rateLimitMap) {
72
- if (now >= entry.resetAt) {
73
- rateLimitMap.delete(ip);
74
- }
46
+ const app = new Hono<{ Bindings: Env }>();
47
+
48
+ // Create new chat
49
+ app.post("/api/chat/new", async (c) => {
50
+ const id = crypto.randomUUID();
51
+ await c.env.DB.prepare("INSERT INTO chats (id) VALUES (?)").bind(id).run();
52
+ return c.json({ id });
53
+ });
54
+
55
+ // Load chat history
56
+ app.get("/api/chat/:id", async (c) => {
57
+ const chatId = c.req.param("id");
58
+ const { results } = await c.env.DB.prepare(
59
+ "SELECT id, role, content, created_at FROM messages WHERE chat_id = ? ORDER BY created_at ASC",
60
+ )
61
+ .bind(chatId)
62
+ .all();
63
+ return c.json({ messages: results || [] });
64
+ });
65
+
66
+ // Chat endpoint with streaming
67
+ app.post("/api/chat", async (c) => {
68
+ const ip = c.req.header("cf-connecting-ip") || "unknown";
69
+ if (!checkRateLimit(ip)) {
70
+ return c.json({ error: "Too many requests. Please wait a moment." }, 429);
75
71
  }
76
- }
77
72
 
78
- export default {
79
- async fetch(request: Request, env: Env): Promise<Response> {
80
- const url = new URL(request.url);
81
-
82
- // Serve static assets for non-API routes
83
- if (request.method === "GET" && !url.pathname.startsWith("/api")) {
84
- return env.ASSETS.fetch(request);
85
- }
86
-
87
- // POST /api/chat - Streaming chat endpoint
88
- if (request.method === "POST" && url.pathname === "/api/chat") {
89
- const ip = request.headers.get("cf-connecting-ip") || "unknown";
90
-
91
- // Check rate limit
92
- if (!checkRateLimit(ip)) {
93
- // Cleanup old entries occasionally
94
- cleanupRateLimitMap();
95
- return Response.json(
96
- { error: "Too many requests. Please wait a moment and try again." },
97
- { status: 429 },
98
- );
99
- }
73
+ const { messages, chatId } = await c.req.json<{
74
+ messages: Message[];
75
+ chatId?: string;
76
+ }>();
77
+ if (!messages || !Array.isArray(messages)) {
78
+ return c.json({ error: "Invalid request." }, 400);
79
+ }
100
80
 
101
- try {
102
- const body = (await request.json()) as { messages?: ChatMessage[] };
103
- let messages = body.messages;
104
-
105
- if (!messages || !Array.isArray(messages)) {
106
- return Response.json(
107
- { error: "Invalid request. Please provide a messages array." },
108
- { status: 400 },
109
- );
110
- }
111
-
112
- // Prepend system prompt if not already present
113
- if (messages.length === 0 || messages[0].role !== "system") {
114
- messages = [{ role: "system", content: SYSTEM_PROMPT }, ...messages];
115
- }
116
-
117
- // Stream response using Llama 3.2 1B - cheapest model with good quality
118
- // See: https://developers.cloudflare.com/workers-ai/models/
119
- const ai = getAI(env);
120
- const stream = await ai.run("@cf/meta/llama-3.2-1b-instruct", {
121
- messages,
122
- stream: true,
123
- max_tokens: 1024,
124
- });
125
-
126
- return new Response(stream, {
127
- headers: {
128
- "Content-Type": "text/event-stream",
129
- "Cache-Control": "no-cache",
130
- Connection: "keep-alive",
131
- },
132
- });
133
- } catch (err) {
134
- console.error("Chat error:", err);
135
- return Response.json({ error: "Something went wrong. Please try again." }, { status: 500 });
81
+ // Save user message to DB if we have a chatId
82
+ const lastUserMsg = messages.findLast((m: Message) => m.role === "user");
83
+ if (chatId && lastUserMsg) {
84
+ await c.env.DB.prepare(
85
+ "INSERT INTO messages (id, chat_id, role, content) VALUES (?, ?, ?, ?)",
86
+ )
87
+ .bind(
88
+ crypto.randomUUID(),
89
+ chatId,
90
+ "user",
91
+ typeof lastUserMsg.content === "string"
92
+ ? lastUserMsg.content
93
+ : JSON.stringify(lastUserMsg.content),
94
+ )
95
+ .run();
96
+ }
97
+
98
+ const ai = getAI(c.env);
99
+ const provider = createWorkersAI({ binding: ai as Ai });
100
+
101
+ const result = streamText({
102
+ model: provider("@cf/meta/llama-3.3-70b-instruct-fp8-fast"),
103
+ system: SYSTEM_PROMPT,
104
+ messages: convertToCoreMessages(messages),
105
+ onFinish: async ({ text }) => {
106
+ // Save assistant response to DB
107
+ if (chatId && text) {
108
+ await c.env.DB.prepare(
109
+ "INSERT INTO messages (id, chat_id, role, content) VALUES (?, ?, ?, ?)",
110
+ )
111
+ .bind(crypto.randomUUID(), chatId, "assistant", text)
112
+ .run();
136
113
  }
137
- }
114
+ },
115
+ });
116
+
117
+ return result.toDataStreamResponse();
118
+ });
119
+
120
+ // Serve static assets for non-API routes
121
+ app.get("*", async (c) => {
122
+ return c.env.ASSETS.fetch(c.req.raw);
123
+ });
138
124
 
139
- return Response.json({ error: "Not found" }, { status: 404 });
140
- },
141
- };
125
+ export default app;
@@ -2,11 +2,18 @@
2
2
  "name": "jack-template",
3
3
  "main": "src/index.ts",
4
4
  "compatibility_date": "2024-12-01",
5
+ "compatibility_flags": ["nodejs_compat"],
5
6
  "ai": {
6
7
  "binding": "AI"
7
8
  },
8
9
  "assets": {
9
10
  "directory": "public",
10
11
  "binding": "ASSETS"
11
- }
12
+ },
13
+ "d1_databases": [
14
+ {
15
+ "binding": "DB",
16
+ "database_name": "jack-template-db"
17
+ }
18
+ ]
12
19
  }
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "cron",
3
+ "description": "Background tasks with cron scheduling and webhook ingestion",
4
+ "secrets": [],
5
+ "capabilities": ["db"],
6
+ "requires": ["DB", "CRON"],
7
+ "intent": {
8
+ "keywords": [
9
+ "cron",
10
+ "background",
11
+ "jobs",
12
+ "webhook",
13
+ "scheduled",
14
+ "queue",
15
+ "worker",
16
+ "tasks"
17
+ ],
18
+ "examples": [
19
+ "background job processor",
20
+ "scheduled tasks",
21
+ "webhook handler",
22
+ "job queue"
23
+ ]
24
+ },
25
+ "agentContext": {
26
+ "summary": "A background task worker with cron scheduling, D1 job queue, and webhook ingestion.",
27
+ "full_text": "## Project Structure\n\n- `src/index.ts` - Hono API with cron handler, webhook endpoint, and job status routes\n- `src/jobs.ts` - Job queue: create, process, retry with D1 backend\n- `src/webhooks.ts` - Webhook ingestion with HMAC-SHA256 signature verification\n- `schema.sql` - D1 schema (jobs, webhook_events)\n\n## Cron\n\nThe `POST /__scheduled` route runs on a cron schedule (default: every 5 minutes). It processes pending jobs and retries failed ones.\n\n## Jobs\n\n```typescript\n// Create a job\nawait createJob(db, { type: 'process-order', payload: { orderId: '123' } });\n\n// Jobs are processed automatically by cron\n// Failed jobs retry up to 3 times with exponential backoff\n```\n\n## Webhooks\n\n```\nPOST /webhook\nX-Signature: sha256=abc123...\nContent-Type: application/json\n\n{ \"event\": \"order.completed\", \"data\": { ... } }\n```\n\nWebhooks are verified using HMAC-SHA256, logged to D1, and can create jobs for async processing.\n\n## Endpoints\n\n- `POST /__scheduled` - Cron handler (called by scheduler)\n- `POST /webhook` - Inbound webhook receiver with signature verification\n- `GET /jobs` - List recent jobs with status\n- `GET /health` - Health check\n\n## Environment Variables\n\n- `WEBHOOK_SECRET` - HMAC signing secret for webhook verification (auto-generated)\n\n## Resources\n\n- [Hono Documentation](https://hono.dev)"
28
+ },
29
+ "hooks": {
30
+ "preCreate": [
31
+ {
32
+ "action": "require",
33
+ "source": "secret",
34
+ "key": "WEBHOOK_SECRET",
35
+ "message": "Generating webhook signing secret...",
36
+ "onMissing": "generate",
37
+ "generateCommand": "openssl rand -hex 32"
38
+ }
39
+ ],
40
+ "postDeploy": [
41
+ {
42
+ "action": "clipboard",
43
+ "text": "{{url}}",
44
+ "message": "Deploy URL copied to clipboard"
45
+ },
46
+ {
47
+ "action": "shell",
48
+ "command": "curl -s {{url}}/health | head -c 200",
49
+ "message": "Testing worker health..."
50
+ },
51
+ {
52
+ "action": "box",
53
+ "title": "Background worker live: {{name}}",
54
+ "lines": [
55
+ "{{url}}",
56
+ "",
57
+ "Endpoints:",
58
+ " POST {{url}}/webhook — inbound webhook receiver",
59
+ " GET {{url}}/jobs — job queue status",
60
+ "",
61
+ "Cron: runs every 5 minutes"
62
+ ]
63
+ }
64
+ ]
65
+ }
66
+ }
@@ -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,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
+ }