@rune-kit/rune 2.8.0 → 2.11.0
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/LICENSE +21 -21
- package/README.md +68 -34
- package/agents/adversary.md +27 -0
- package/agents/architect.md +19 -29
- package/agents/asset-creator.md +18 -4
- package/agents/audit.md +25 -4
- package/agents/autopsy.md +19 -4
- package/agents/ba.md +35 -0
- package/agents/brainstorm.md +31 -4
- package/agents/browser-pilot.md +21 -4
- package/agents/coder.md +21 -29
- package/agents/completion-gate.md +20 -4
- package/agents/constraint-check.md +18 -4
- package/agents/context-engine.md +22 -4
- package/agents/context-pack.md +32 -0
- package/agents/cook.md +41 -4
- package/agents/db.md +19 -4
- package/agents/debug.md +33 -4
- package/agents/dependency-doctor.md +20 -4
- package/agents/deploy.md +27 -4
- package/agents/design.md +22 -4
- package/agents/doc-processor.md +27 -0
- package/agents/docs-seeker.md +19 -4
- package/agents/docs.md +31 -0
- package/agents/fix.md +37 -4
- package/agents/git.md +29 -0
- package/agents/hallucination-guard.md +20 -4
- package/agents/incident.md +21 -4
- package/agents/integrity-check.md +18 -4
- package/agents/journal.md +19 -4
- package/agents/launch.md +32 -4
- package/agents/logic-guardian.md +26 -11
- package/agents/marketing.md +23 -4
- package/agents/mcp-builder.md +26 -0
- package/agents/neural-memory.md +30 -0
- package/agents/onboard.md +22 -4
- package/agents/perf.md +21 -4
- package/agents/plan.md +29 -4
- package/agents/preflight.md +22 -4
- package/agents/problem-solver.md +20 -4
- package/agents/rescue.md +23 -4
- package/agents/research.md +19 -4
- package/agents/researcher.md +19 -29
- package/agents/retro.md +32 -0
- package/agents/review-intake.md +20 -4
- package/agents/review.md +32 -4
- package/agents/reviewer.md +20 -28
- package/agents/safeguard.md +19 -4
- package/agents/sast.md +18 -4
- package/agents/scaffold.md +41 -0
- package/agents/scanner.md +19 -28
- package/agents/scope-guard.md +18 -4
- package/agents/scout.md +23 -4
- package/agents/sentinel-env.md +26 -0
- package/agents/sentinel.md +33 -4
- package/agents/sequential-thinking.md +20 -4
- package/agents/session-bridge.md +24 -4
- package/agents/skill-forge.md +22 -4
- package/agents/skill-router.md +26 -4
- package/agents/slides.md +24 -0
- package/agents/surgeon.md +19 -4
- package/agents/team.md +30 -4
- package/agents/test.md +36 -4
- package/agents/trend-scout.md +17 -4
- package/agents/verification.md +20 -4
- package/agents/video-creator.md +20 -4
- package/agents/watchdog.md +19 -4
- package/agents/worktree.md +17 -4
- package/commands/rune.md +168 -168
- package/compiler/__tests__/analytics.test.js +370 -0
- package/compiler/adapters/openclaw.js +2 -2
- package/compiler/analytics.js +385 -0
- package/compiler/bin/rune.js +68 -2
- package/compiler/dashboard.js +883 -0
- package/compiler/transforms/branding.js +1 -1
- package/contexts/dev.md +34 -34
- package/contexts/research.md +43 -43
- package/contexts/review.md +55 -55
- package/extensions/ai-ml/PACK.md +88 -88
- package/extensions/ai-ml/skills/ai-agents.md +172 -172
- package/extensions/ai-ml/skills/code-sandbox.md +187 -187
- package/extensions/ai-ml/skills/deep-research.md +146 -146
- package/extensions/ai-ml/skills/embedding-search.md +66 -66
- package/extensions/ai-ml/skills/fine-tuning-guide.md +74 -74
- package/extensions/ai-ml/skills/llm-architect.md +125 -125
- package/extensions/ai-ml/skills/llm-integration.md +64 -64
- package/extensions/ai-ml/skills/prompt-patterns.md +72 -72
- package/extensions/ai-ml/skills/rag-patterns.md +66 -66
- package/extensions/ai-ml/skills/web-extraction.md +114 -114
- package/extensions/analytics/PACK.md +92 -92
- package/extensions/analytics/skills/ab-testing.md +72 -72
- package/extensions/analytics/skills/dashboard-patterns.md +83 -83
- package/extensions/analytics/skills/data-validation.md +68 -68
- package/extensions/analytics/skills/funnel-analysis.md +81 -81
- package/extensions/analytics/skills/sql-patterns.md +57 -57
- package/extensions/analytics/skills/statistical-analysis.md +79 -79
- package/extensions/analytics/skills/tracking-setup.md +71 -71
- package/extensions/backend/PACK.md +104 -104
- package/extensions/backend/skills/api-patterns.md +84 -84
- package/extensions/backend/skills/async-pipeline.md +193 -193
- package/extensions/backend/skills/auth-patterns.md +97 -97
- package/extensions/backend/skills/background-jobs.md +133 -133
- package/extensions/backend/skills/caching-patterns.md +108 -108
- package/extensions/backend/skills/cli-generation.md +133 -133
- package/extensions/backend/skills/database-patterns.md +87 -87
- package/extensions/backend/skills/middleware-patterns.md +104 -104
- package/extensions/chrome-ext/PACK.md +93 -93
- package/extensions/chrome-ext/skills/cws-preflight.md +143 -143
- package/extensions/chrome-ext/skills/cws-publish.md +104 -104
- package/extensions/chrome-ext/skills/ext-ai-integration.md +251 -251
- package/extensions/chrome-ext/skills/ext-messaging.md +139 -139
- package/extensions/chrome-ext/skills/ext-storage.md +133 -133
- package/extensions/chrome-ext/skills/mv3-scaffold.md +164 -164
- package/extensions/content/PACK.md +96 -96
- package/extensions/content/skills/blog-patterns.md +88 -88
- package/extensions/content/skills/cms-integration.md +131 -131
- package/extensions/content/skills/content-scoring.md +107 -107
- package/extensions/content/skills/i18n.md +83 -83
- package/extensions/content/skills/mdx-authoring.md +137 -137
- package/extensions/content/skills/reference.md +1014 -1014
- package/extensions/content/skills/seo-patterns.md +67 -67
- package/extensions/content/skills/video-repurpose.md +153 -153
- package/extensions/devops/PACK.md +101 -101
- package/extensions/devops/skills/chaos-testing.md +67 -67
- package/extensions/devops/skills/ci-cd.md +75 -75
- package/extensions/devops/skills/docker.md +58 -58
- package/extensions/devops/skills/edge-serverless.md +163 -163
- package/extensions/devops/skills/infra-as-code.md +158 -158
- package/extensions/devops/skills/kubernetes.md +110 -110
- package/extensions/devops/skills/monitoring.md +57 -57
- package/extensions/devops/skills/server-setup.md +64 -64
- package/extensions/devops/skills/ssl-domain.md +42 -42
- package/extensions/ecommerce/PACK.md +116 -116
- package/extensions/ecommerce/skills/cart-system.md +79 -79
- package/extensions/ecommerce/skills/inventory-mgmt.md +102 -102
- package/extensions/ecommerce/skills/order-management.md +126 -126
- package/extensions/ecommerce/skills/payment-integration.md +472 -472
- package/extensions/ecommerce/skills/shopify-dev.md +69 -69
- package/extensions/ecommerce/skills/subscription-billing.md +93 -93
- package/extensions/ecommerce/skills/tax-compliance.md +117 -117
- package/extensions/gamedev/PACK.md +142 -142
- package/extensions/gamedev/skills/asset-pipeline.md +74 -74
- package/extensions/gamedev/skills/audio-system.md +129 -129
- package/extensions/gamedev/skills/camera-system.md +87 -87
- package/extensions/gamedev/skills/ecs.md +98 -98
- package/extensions/gamedev/skills/game-loops.md +72 -72
- package/extensions/gamedev/skills/input-system.md +199 -199
- package/extensions/gamedev/skills/multiplayer.md +180 -180
- package/extensions/gamedev/skills/particles.md +105 -105
- package/extensions/gamedev/skills/physics-engine.md +89 -89
- package/extensions/gamedev/skills/scene-management.md +146 -146
- package/extensions/gamedev/skills/threejs-patterns.md +90 -90
- package/extensions/gamedev/skills/webgl.md +71 -71
- package/extensions/mobile/PACK.md +106 -106
- package/extensions/mobile/skills/app-store-connect.md +152 -152
- package/extensions/mobile/skills/app-store-prep.md +66 -66
- package/extensions/mobile/skills/deep-linking.md +109 -109
- package/extensions/mobile/skills/flutter.md +60 -60
- package/extensions/mobile/skills/ios-build-pipeline.md +142 -142
- package/extensions/mobile/skills/native-bridge.md +66 -66
- package/extensions/mobile/skills/ota-updates.md +97 -97
- package/extensions/mobile/skills/push-notifications.md +111 -111
- package/extensions/mobile/skills/react-native.md +82 -82
- package/extensions/saas/PACK.md +116 -116
- package/extensions/saas/skills/billing-integration.md +200 -200
- package/extensions/saas/skills/feature-flags.md +130 -130
- package/extensions/saas/skills/multi-tenant.md +103 -103
- package/extensions/saas/skills/onboarding-flow.md +139 -139
- package/extensions/saas/skills/subscription-flow.md +95 -95
- package/extensions/saas/skills/team-management.md +144 -144
- package/extensions/security/PACK.md +99 -99
- package/extensions/security/skills/api-security.md +140 -140
- package/extensions/security/skills/compliance.md +68 -68
- package/extensions/security/skills/owasp-audit.md +64 -64
- package/extensions/security/skills/pentest-patterns.md +77 -77
- package/extensions/security/skills/secret-mgmt.md +65 -65
- package/extensions/security/skills/supply-chain.md +65 -65
- package/extensions/trading/PACK.md +80 -80
- package/extensions/trading/skills/chart-components.md +55 -55
- package/extensions/trading/skills/experiment-loop.md +125 -125
- package/extensions/trading/skills/fintech-patterns.md +47 -47
- package/extensions/trading/skills/indicator-library.md +58 -58
- package/extensions/trading/skills/quant-analysis.md +111 -111
- package/extensions/trading/skills/realtime-data.md +58 -58
- package/extensions/trading/skills/trade-logic.md +104 -104
- package/extensions/ui/PACK.md +130 -130
- package/extensions/ui/skills/a11y-audit.md +91 -91
- package/extensions/ui/skills/animation-patterns.md +127 -106
- package/extensions/ui/skills/component-patterns.md +100 -75
- package/extensions/ui/skills/design-decision.md +108 -108
- package/extensions/ui/skills/design-system.md +68 -68
- package/extensions/ui/skills/landing-patterns.md +155 -155
- package/extensions/ui/skills/palette-picker.md +173 -173
- package/extensions/ui/skills/react-health.md +90 -90
- package/extensions/ui/skills/type-system.md +125 -125
- package/extensions/ui/skills/web-vitals.md +153 -153
- package/extensions/zalo/PACK.md +145 -145
- package/extensions/zalo/skills/zalo-oa-mcp.md +317 -317
- package/extensions/zalo/skills/zalo-oa-messaging.md +429 -429
- package/extensions/zalo/skills/zalo-oa-setup.md +236 -236
- package/extensions/zalo/skills/zalo-oa-webhook.md +189 -189
- package/extensions/zalo/skills/zalo-personal-messaging.md +194 -194
- package/extensions/zalo/skills/zalo-personal-setup.md +153 -153
- package/extensions/zalo/skills/zalo-rate-guard.md +219 -219
- package/hooks/auto-format/index.cjs +48 -48
- package/hooks/context-watch/index.cjs +95 -68
- package/hooks/hooks.json +111 -111
- package/hooks/metrics-collector/index.cjs +86 -42
- package/hooks/post-session-reflect/index.cjs +189 -153
- package/hooks/pre-compact/index.cjs +95 -95
- package/hooks/run-hook.cmd +1 -1
- package/hooks/secrets-scan/index.cjs +100 -100
- package/hooks/session-start/index.cjs +71 -65
- package/hooks/typecheck/index.cjs +65 -65
- package/package.json +63 -63
- package/references/ui-pro-max-data/LICENSE-UI-PRO-MAX +21 -21
- package/references/ui-pro-max-data/charts.csv +26 -26
- package/references/ui-pro-max-data/colors.csv +161 -161
- package/references/ui-pro-max-data/styles.csv +68 -68
- package/references/ui-pro-max-data/typography.csv +74 -74
- package/references/ui-pro-max-data/ui-reasoning.csv +162 -162
- package/references/ui-pro-max-data/ux-guidelines.csv +99 -99
- package/skills/adversary/SKILL.md +283 -283
- package/skills/asset-creator/SKILL.md +157 -157
- package/skills/audit/SKILL.md +148 -2
- package/skills/autopsy/SKILL.md +335 -259
- package/skills/autopsy/references/repo-analysis-patterns.md +113 -0
- package/skills/ba/SKILL.md +72 -2
- package/skills/brainstorm/SKILL.md +342 -341
- package/skills/browser-pilot/SKILL.md +168 -168
- package/skills/constraint-check/SKILL.md +165 -165
- package/skills/context-engine/SKILL.md +404 -404
- package/skills/cook/SKILL.md +917 -834
- package/skills/cook/references/output-format.md +33 -0
- package/skills/db/SKILL.md +273 -272
- package/skills/debug/SKILL.md +465 -443
- package/skills/dependency-doctor/SKILL.md +265 -235
- package/skills/deploy/SKILL.md +274 -231
- package/skills/design/DESIGN-REFERENCE.md +365 -365
- package/skills/design/SKILL.md +589 -482
- package/skills/doc-processor/SKILL.md +254 -254
- package/skills/docs/SKILL.md +374 -373
- package/skills/docs-seeker/SKILL.md +177 -177
- package/skills/fix/SKILL.md +330 -308
- package/skills/git/SKILL.md +339 -339
- package/skills/graft/SKILL.md +352 -0
- package/skills/graft/references/challenge-framework.md +98 -0
- package/skills/graft/references/mode-decision.md +44 -0
- package/skills/hallucination-guard/SKILL.md +219 -219
- package/skills/incident/SKILL.md +254 -251
- package/skills/integrity-check/SKILL.md +169 -169
- package/skills/journal/SKILL.md +240 -238
- package/skills/launch/SKILL.md +344 -342
- package/skills/logic-guardian/SKILL.md +251 -251
- package/skills/marketing/SKILL.md +290 -245
- package/skills/mcp-builder/SKILL.md +425 -423
- package/skills/mcp-builder/references/auto-discovery-pattern.md +169 -0
- package/skills/neural-memory/SKILL.md +362 -362
- package/skills/onboard/SKILL.md +404 -403
- package/skills/perf/SKILL.md +346 -346
- package/skills/plan/SKILL.md +433 -370
- package/skills/plan/references/feature-map.md +84 -0
- package/skills/preflight/SKILL.md +415 -396
- package/skills/problem-solver/SKILL.md +380 -284
- package/skills/rescue/SKILL.md +474 -450
- package/skills/retro/SKILL.md +5 -1
- package/skills/review/SKILL.md +612 -535
- package/skills/review-intake/SKILL.md +249 -249
- package/skills/safeguard/SKILL.md +200 -200
- package/skills/sast/SKILL.md +190 -190
- package/skills/scaffold/SKILL.md +328 -286
- package/skills/scope-guard/SKILL.md +180 -162
- package/skills/scout/SKILL.md +263 -263
- package/skills/sentinel/SKILL.md +382 -353
- package/skills/sentinel-env/SKILL.md +254 -254
- package/skills/sequential-thinking/SKILL.md +234 -234
- package/skills/session-bridge/SKILL.md +543 -397
- package/skills/skill-forge/SKILL.md +581 -539
- package/skills/skill-router/{skill.md → SKILL.md} +30 -2
- package/skills/surgeon/SKILL.md +215 -215
- package/skills/team/SKILL.md +556 -514
- package/skills/test/SKILL.md +614 -587
- package/skills/trend-scout/SKILL.md +145 -145
- package/skills/verification/SKILL.md +326 -325
- package/skills/video-creator/SKILL.md +201 -201
- package/skills/watchdog/SKILL.md +168 -168
- package/skills/worktree/SKILL.md +140 -140
|
@@ -1,189 +1,189 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: zalo-oa-webhook
|
|
3
|
-
pack: "@rune/zalo"
|
|
4
|
-
description: "Set up and handle Zalo OA webhook server — signature verification, event routing, idempotency, and tunnel for local development."
|
|
5
|
-
model: sonnet
|
|
6
|
-
tools: "Read, Glob, Grep, Bash, Write, Edit"
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# zalo-oa-webhook
|
|
10
|
-
|
|
11
|
-
Set up and handle Zalo OA webhook server — signature verification, event routing, idempotency, and tunnel for local development.
|
|
12
|
-
|
|
13
|
-
#### Workflow
|
|
14
|
-
|
|
15
|
-
**Step 1 — Register webhook at Zalo Developer Portal**
|
|
16
|
-
Go to [developers.zalo.me](https://developers.zalo.me) → select App → **App Settings → Webhook**. Enter your HTTPS endpoint URL (e.g., `https://your-domain.com/webhook/zalo`). Zalo sends `POST` requests to this URL for every OA event. The URL must be HTTPS — no plain HTTP. For local dev, use ngrok: `ngrok http 3000` and paste the `https://` tunnel URL. Remember to update the URL when the tunnel restarts.
|
|
17
|
-
|
|
18
|
-
**Step 2 — Verify signature on every request (CRITICAL)**
|
|
19
|
-
Every request from Zalo includes `X-ZEvent-Signature` header — HMAC-SHA256 of the raw request body, signed with your **OA Secret Key** (not the App Secret — different keys). Verify before processing. Use `crypto.timingSafeEqual` to prevent timing attacks. Reject with 403 if invalid.
|
|
20
|
-
|
|
21
|
-
```typescript
|
|
22
|
-
import crypto from 'crypto'
|
|
23
|
-
|
|
24
|
-
function verifyWebhookSignature(
|
|
25
|
-
body: string,
|
|
26
|
-
signature: string,
|
|
27
|
-
oaSecretKey: string
|
|
28
|
-
): boolean {
|
|
29
|
-
const computed = crypto
|
|
30
|
-
.createHmac('sha256', oaSecretKey)
|
|
31
|
-
.update(body)
|
|
32
|
-
.digest('hex')
|
|
33
|
-
return crypto.timingSafeEqual(
|
|
34
|
-
Buffer.from(computed, 'hex'),
|
|
35
|
-
Buffer.from(signature, 'hex')
|
|
36
|
-
)
|
|
37
|
-
}
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
NEVER skip verification — even in development. NEVER use `===` string compare (timing leak).
|
|
41
|
-
|
|
42
|
-
**Step 3 — Respond within 5 seconds**
|
|
43
|
-
Zalo expects `200 OK` within 5 seconds or it marks the delivery failed and retries up to 3 times. Acknowledge immediately, then process asynchronously:
|
|
44
|
-
|
|
45
|
-
```typescript
|
|
46
|
-
// Return 200 first, then process
|
|
47
|
-
return c.json({ received: true }) // respond immediately
|
|
48
|
-
await queue.push(event) // async processing
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
**Step 4 — Implement idempotency**
|
|
52
|
-
Retries cause duplicate events. Use `msg_id` (present on message events) to deduplicate. Check before processing, mark as processed after:
|
|
53
|
-
|
|
54
|
-
```typescript
|
|
55
|
-
const processedIds = new Set<string>() // or Redis for production
|
|
56
|
-
|
|
57
|
-
async function idempotentHandle(event: ZaloEvent): Promise<void> {
|
|
58
|
-
const id = event.message?.msg_id ?? `${event.event_name}:${event.timestamp}`
|
|
59
|
-
if (processedIds.has(id)) return
|
|
60
|
-
processedIds.add(id)
|
|
61
|
-
await routeEvent(event)
|
|
62
|
-
}
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
**Step 5 — Route events by event_name**
|
|
66
|
-
|
|
67
|
-
| event_name | Trigger | Key payload fields |
|
|
68
|
-
|---|---|---|
|
|
69
|
-
| `user_send_text` | User sends text | `sender.id`, `message.text`, `message.msg_id` |
|
|
70
|
-
| `user_send_image` | User sends image | `sender.id`, `message.attachments[].payload.url` |
|
|
71
|
-
| `user_send_file` | User sends file | `sender.id`, `message.attachments[]` |
|
|
72
|
-
| `user_send_sticker` | User sends sticker | `sender.id`, `message.attachments[]` |
|
|
73
|
-
| `user_send_location` | User sends location | `sender.id`, `message.attachments[].payload.coordinates` |
|
|
74
|
-
| `follow` | User follows OA | `follower.id` |
|
|
75
|
-
| `unfollow` | User unfollows OA | `follower.id` |
|
|
76
|
-
| `user_click_button` | User clicks button | `sender.id`, `message.text` (button payload) |
|
|
77
|
-
| `oa_send_text` | OA message delivered | — |
|
|
78
|
-
|
|
79
|
-
Note: naming is inconsistent — messages use `user_send_*` prefix, follow/unfollow do not.
|
|
80
|
-
|
|
81
|
-
#### Server Implementations
|
|
82
|
-
|
|
83
|
-
**Hono (recommended — edge-ready)**
|
|
84
|
-
|
|
85
|
-
```typescript
|
|
86
|
-
import { Hono } from 'hono'
|
|
87
|
-
import { serve } from '@hono/node-server'
|
|
88
|
-
import crypto from 'crypto'
|
|
89
|
-
|
|
90
|
-
const OA_SECRET_KEY = process.env.ZALO_OA_SECRET_KEY!
|
|
91
|
-
const app = new Hono()
|
|
92
|
-
|
|
93
|
-
app.post('/webhook/zalo', async (c) => {
|
|
94
|
-
const signature = c.req.header('X-ZEvent-Signature') ?? ''
|
|
95
|
-
const body = await c.req.text() // raw body — MUST use text(), not json()
|
|
96
|
-
|
|
97
|
-
if (!verifyWebhookSignature(body, signature, OA_SECRET_KEY)) {
|
|
98
|
-
return c.json({ error: 'Invalid signature' }, 403)
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const event: ZaloEvent = JSON.parse(body)
|
|
102
|
-
c.executionCtx?.waitUntil(idempotentHandle(event)) // non-blocking
|
|
103
|
-
return c.json({ received: true })
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
async function routeEvent(event: ZaloEvent): Promise<void> {
|
|
107
|
-
switch (event.event_name) {
|
|
108
|
-
case 'user_send_text': return handleTextMessage(event)
|
|
109
|
-
case 'user_send_image': return handleImageMessage(event)
|
|
110
|
-
case 'user_send_file': return handleFileMessage(event)
|
|
111
|
-
case 'user_send_location': return handleLocation(event)
|
|
112
|
-
case 'follow': return handleFollow(event)
|
|
113
|
-
case 'unfollow': return handleUnfollow(event)
|
|
114
|
-
case 'user_click_button': return handleButtonClick(event)
|
|
115
|
-
default: console.warn('Unhandled Zalo event:', event.event_name)
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
serve({ fetch: app.fetch, port: 3000 })
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
**Express**
|
|
123
|
-
|
|
124
|
-
```typescript
|
|
125
|
-
import express from 'express'
|
|
126
|
-
|
|
127
|
-
const app = express()
|
|
128
|
-
|
|
129
|
-
// MUST use raw body parser — not express.json() — to preserve signature input
|
|
130
|
-
app.post('/webhook/zalo', express.raw({ type: 'application/json' }), async (req, res) => {
|
|
131
|
-
const signature = req.headers['x-zevent-signature'] as string ?? ''
|
|
132
|
-
const body = req.body.toString()
|
|
133
|
-
|
|
134
|
-
if (!verifyWebhookSignature(body, signature, OA_SECRET_KEY)) {
|
|
135
|
-
return res.status(403).json({ error: 'Invalid signature' })
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const event: ZaloEvent = JSON.parse(body)
|
|
139
|
-
res.json({ received: true }) // respond first
|
|
140
|
-
idempotentHandle(event).catch(console.error) // then process
|
|
141
|
-
})
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
**Fastify**
|
|
145
|
-
|
|
146
|
-
```typescript
|
|
147
|
-
import Fastify from 'fastify'
|
|
148
|
-
|
|
149
|
-
const fastify = Fastify()
|
|
150
|
-
|
|
151
|
-
fastify.addContentTypeParser('application/json', { parseAs: 'string' }, (req, body, done) => {
|
|
152
|
-
done(null, body) // keep raw string for signature verification
|
|
153
|
-
})
|
|
154
|
-
|
|
155
|
-
fastify.post('/webhook/zalo', async (request, reply) => {
|
|
156
|
-
const signature = request.headers['x-zevent-signature'] as string ?? ''
|
|
157
|
-
const body = request.body as string
|
|
158
|
-
|
|
159
|
-
if (!verifyWebhookSignature(body, signature, OA_SECRET_KEY)) {
|
|
160
|
-
return reply.status(403).send({ error: 'Invalid signature' })
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
const event: ZaloEvent = JSON.parse(body)
|
|
164
|
-
reply.send({ received: true })
|
|
165
|
-
idempotentHandle(event).catch(console.error)
|
|
166
|
-
})
|
|
167
|
-
```
|
|
168
|
-
|
|
169
|
-
#### Local Development Tunnel
|
|
170
|
-
|
|
171
|
-
```bash
|
|
172
|
-
# ngrok (most common)
|
|
173
|
-
ngrok http 3000
|
|
174
|
-
# → copy https://xxxx.ngrok.io → paste to Zalo Developer Portal
|
|
175
|
-
|
|
176
|
-
# cloudflared (free, no account needed for temp tunnels)
|
|
177
|
-
cloudflare tunnel --url http://localhost:3000
|
|
178
|
-
```
|
|
179
|
-
|
|
180
|
-
Update webhook URL in Zalo portal every time the tunnel restarts. Use a stable subdomain (`ngrok http --subdomain=myapp 3000`) with a paid ngrok account to avoid this.
|
|
181
|
-
|
|
182
|
-
#### Sharp Edges
|
|
183
|
-
|
|
184
|
-
- **5-second timeout**: If your handler takes longer, Zalo marks it failed and retries. Always return 200 immediately, process async.
|
|
185
|
-
- **Wrong secret key**: Signature uses **OA Secret Key** from OA Management → Settings, NOT the App Secret Key from Developer Portal. Different keys, same name confusion.
|
|
186
|
-
- **Raw body required**: Parse body as raw string before verification. Using `express.json()` or Hono's `.json()` before verification will break the HMAC because the body gets re-serialized.
|
|
187
|
-
- **Inconsistent event naming**: `user_send_text` but just `follow` — not `user_follow`. Handle both patterns in your router.
|
|
188
|
-
- **HTTPS required**: Zalo rejects plain HTTP webhook URLs. ngrok/cloudflared tunnels provide HTTPS automatically.
|
|
189
|
-
- **msg_id deduplication is mandatory in production**: Zalo retries on non-200 (up to 3x), and network issues can cause duplicate deliveries. A Redis-backed `SETNX msg_id EX 86400` is the production-safe pattern.
|
|
1
|
+
---
|
|
2
|
+
name: zalo-oa-webhook
|
|
3
|
+
pack: "@rune/zalo"
|
|
4
|
+
description: "Set up and handle Zalo OA webhook server — signature verification, event routing, idempotency, and tunnel for local development."
|
|
5
|
+
model: sonnet
|
|
6
|
+
tools: "Read, Glob, Grep, Bash, Write, Edit"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# zalo-oa-webhook
|
|
10
|
+
|
|
11
|
+
Set up and handle Zalo OA webhook server — signature verification, event routing, idempotency, and tunnel for local development.
|
|
12
|
+
|
|
13
|
+
#### Workflow
|
|
14
|
+
|
|
15
|
+
**Step 1 — Register webhook at Zalo Developer Portal**
|
|
16
|
+
Go to [developers.zalo.me](https://developers.zalo.me) → select App → **App Settings → Webhook**. Enter your HTTPS endpoint URL (e.g., `https://your-domain.com/webhook/zalo`). Zalo sends `POST` requests to this URL for every OA event. The URL must be HTTPS — no plain HTTP. For local dev, use ngrok: `ngrok http 3000` and paste the `https://` tunnel URL. Remember to update the URL when the tunnel restarts.
|
|
17
|
+
|
|
18
|
+
**Step 2 — Verify signature on every request (CRITICAL)**
|
|
19
|
+
Every request from Zalo includes `X-ZEvent-Signature` header — HMAC-SHA256 of the raw request body, signed with your **OA Secret Key** (not the App Secret — different keys). Verify before processing. Use `crypto.timingSafeEqual` to prevent timing attacks. Reject with 403 if invalid.
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import crypto from 'crypto'
|
|
23
|
+
|
|
24
|
+
function verifyWebhookSignature(
|
|
25
|
+
body: string,
|
|
26
|
+
signature: string,
|
|
27
|
+
oaSecretKey: string
|
|
28
|
+
): boolean {
|
|
29
|
+
const computed = crypto
|
|
30
|
+
.createHmac('sha256', oaSecretKey)
|
|
31
|
+
.update(body)
|
|
32
|
+
.digest('hex')
|
|
33
|
+
return crypto.timingSafeEqual(
|
|
34
|
+
Buffer.from(computed, 'hex'),
|
|
35
|
+
Buffer.from(signature, 'hex')
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
NEVER skip verification — even in development. NEVER use `===` string compare (timing leak).
|
|
41
|
+
|
|
42
|
+
**Step 3 — Respond within 5 seconds**
|
|
43
|
+
Zalo expects `200 OK` within 5 seconds or it marks the delivery failed and retries up to 3 times. Acknowledge immediately, then process asynchronously:
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
// Return 200 first, then process
|
|
47
|
+
return c.json({ received: true }) // respond immediately
|
|
48
|
+
await queue.push(event) // async processing
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**Step 4 — Implement idempotency**
|
|
52
|
+
Retries cause duplicate events. Use `msg_id` (present on message events) to deduplicate. Check before processing, mark as processed after:
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
const processedIds = new Set<string>() // or Redis for production
|
|
56
|
+
|
|
57
|
+
async function idempotentHandle(event: ZaloEvent): Promise<void> {
|
|
58
|
+
const id = event.message?.msg_id ?? `${event.event_name}:${event.timestamp}`
|
|
59
|
+
if (processedIds.has(id)) return
|
|
60
|
+
processedIds.add(id)
|
|
61
|
+
await routeEvent(event)
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Step 5 — Route events by event_name**
|
|
66
|
+
|
|
67
|
+
| event_name | Trigger | Key payload fields |
|
|
68
|
+
|---|---|---|
|
|
69
|
+
| `user_send_text` | User sends text | `sender.id`, `message.text`, `message.msg_id` |
|
|
70
|
+
| `user_send_image` | User sends image | `sender.id`, `message.attachments[].payload.url` |
|
|
71
|
+
| `user_send_file` | User sends file | `sender.id`, `message.attachments[]` |
|
|
72
|
+
| `user_send_sticker` | User sends sticker | `sender.id`, `message.attachments[]` |
|
|
73
|
+
| `user_send_location` | User sends location | `sender.id`, `message.attachments[].payload.coordinates` |
|
|
74
|
+
| `follow` | User follows OA | `follower.id` |
|
|
75
|
+
| `unfollow` | User unfollows OA | `follower.id` |
|
|
76
|
+
| `user_click_button` | User clicks button | `sender.id`, `message.text` (button payload) |
|
|
77
|
+
| `oa_send_text` | OA message delivered | — |
|
|
78
|
+
|
|
79
|
+
Note: naming is inconsistent — messages use `user_send_*` prefix, follow/unfollow do not.
|
|
80
|
+
|
|
81
|
+
#### Server Implementations
|
|
82
|
+
|
|
83
|
+
**Hono (recommended — edge-ready)**
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
import { Hono } from 'hono'
|
|
87
|
+
import { serve } from '@hono/node-server'
|
|
88
|
+
import crypto from 'crypto'
|
|
89
|
+
|
|
90
|
+
const OA_SECRET_KEY = process.env.ZALO_OA_SECRET_KEY!
|
|
91
|
+
const app = new Hono()
|
|
92
|
+
|
|
93
|
+
app.post('/webhook/zalo', async (c) => {
|
|
94
|
+
const signature = c.req.header('X-ZEvent-Signature') ?? ''
|
|
95
|
+
const body = await c.req.text() // raw body — MUST use text(), not json()
|
|
96
|
+
|
|
97
|
+
if (!verifyWebhookSignature(body, signature, OA_SECRET_KEY)) {
|
|
98
|
+
return c.json({ error: 'Invalid signature' }, 403)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const event: ZaloEvent = JSON.parse(body)
|
|
102
|
+
c.executionCtx?.waitUntil(idempotentHandle(event)) // non-blocking
|
|
103
|
+
return c.json({ received: true })
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
async function routeEvent(event: ZaloEvent): Promise<void> {
|
|
107
|
+
switch (event.event_name) {
|
|
108
|
+
case 'user_send_text': return handleTextMessage(event)
|
|
109
|
+
case 'user_send_image': return handleImageMessage(event)
|
|
110
|
+
case 'user_send_file': return handleFileMessage(event)
|
|
111
|
+
case 'user_send_location': return handleLocation(event)
|
|
112
|
+
case 'follow': return handleFollow(event)
|
|
113
|
+
case 'unfollow': return handleUnfollow(event)
|
|
114
|
+
case 'user_click_button': return handleButtonClick(event)
|
|
115
|
+
default: console.warn('Unhandled Zalo event:', event.event_name)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
serve({ fetch: app.fetch, port: 3000 })
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
**Express**
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
import express from 'express'
|
|
126
|
+
|
|
127
|
+
const app = express()
|
|
128
|
+
|
|
129
|
+
// MUST use raw body parser — not express.json() — to preserve signature input
|
|
130
|
+
app.post('/webhook/zalo', express.raw({ type: 'application/json' }), async (req, res) => {
|
|
131
|
+
const signature = req.headers['x-zevent-signature'] as string ?? ''
|
|
132
|
+
const body = req.body.toString()
|
|
133
|
+
|
|
134
|
+
if (!verifyWebhookSignature(body, signature, OA_SECRET_KEY)) {
|
|
135
|
+
return res.status(403).json({ error: 'Invalid signature' })
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const event: ZaloEvent = JSON.parse(body)
|
|
139
|
+
res.json({ received: true }) // respond first
|
|
140
|
+
idempotentHandle(event).catch(console.error) // then process
|
|
141
|
+
})
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
**Fastify**
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
import Fastify from 'fastify'
|
|
148
|
+
|
|
149
|
+
const fastify = Fastify()
|
|
150
|
+
|
|
151
|
+
fastify.addContentTypeParser('application/json', { parseAs: 'string' }, (req, body, done) => {
|
|
152
|
+
done(null, body) // keep raw string for signature verification
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
fastify.post('/webhook/zalo', async (request, reply) => {
|
|
156
|
+
const signature = request.headers['x-zevent-signature'] as string ?? ''
|
|
157
|
+
const body = request.body as string
|
|
158
|
+
|
|
159
|
+
if (!verifyWebhookSignature(body, signature, OA_SECRET_KEY)) {
|
|
160
|
+
return reply.status(403).send({ error: 'Invalid signature' })
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const event: ZaloEvent = JSON.parse(body)
|
|
164
|
+
reply.send({ received: true })
|
|
165
|
+
idempotentHandle(event).catch(console.error)
|
|
166
|
+
})
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
#### Local Development Tunnel
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
# ngrok (most common)
|
|
173
|
+
ngrok http 3000
|
|
174
|
+
# → copy https://xxxx.ngrok.io → paste to Zalo Developer Portal
|
|
175
|
+
|
|
176
|
+
# cloudflared (free, no account needed for temp tunnels)
|
|
177
|
+
cloudflare tunnel --url http://localhost:3000
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Update webhook URL in Zalo portal every time the tunnel restarts. Use a stable subdomain (`ngrok http --subdomain=myapp 3000`) with a paid ngrok account to avoid this.
|
|
181
|
+
|
|
182
|
+
#### Sharp Edges
|
|
183
|
+
|
|
184
|
+
- **5-second timeout**: If your handler takes longer, Zalo marks it failed and retries. Always return 200 immediately, process async.
|
|
185
|
+
- **Wrong secret key**: Signature uses **OA Secret Key** from OA Management → Settings, NOT the App Secret Key from Developer Portal. Different keys, same name confusion.
|
|
186
|
+
- **Raw body required**: Parse body as raw string before verification. Using `express.json()` or Hono's `.json()` before verification will break the HMAC because the body gets re-serialized.
|
|
187
|
+
- **Inconsistent event naming**: `user_send_text` but just `follow` — not `user_follow`. Handle both patterns in your router.
|
|
188
|
+
- **HTTPS required**: Zalo rejects plain HTTP webhook URLs. ngrok/cloudflared tunnels provide HTTPS automatically.
|
|
189
|
+
- **msg_id deduplication is mandatory in production**: Zalo retries on non-200 (up to 3x), and network issues can cause duplicate deliveries. A Redis-backed `SETNX msg_id EX 86400` is the production-safe pattern.
|