@rune-kit/rune 2.10.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 +8 -6
- package/commands/rune.md +168 -168
- 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 -127
- package/extensions/ui/skills/component-patterns.md +100 -100
- 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/hooks.json +111 -111
- package/hooks/post-session-reflect/index.cjs +189 -189
- 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 -71
- 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 +147 -2
- package/skills/autopsy/SKILL.md +335 -335
- package/skills/brainstorm/SKILL.md +342 -342
- 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 -863
- package/skills/db/SKILL.md +273 -273
- package/skills/debug/SKILL.md +465 -465
- 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 -589
- package/skills/doc-processor/SKILL.md +254 -254
- package/skills/docs/SKILL.md +374 -374
- package/skills/docs-seeker/SKILL.md +177 -177
- package/skills/fix/SKILL.md +330 -330
- package/skills/git/SKILL.md +339 -339
- package/skills/hallucination-guard/SKILL.md +219 -219
- package/skills/incident/SKILL.md +254 -253
- package/skills/integrity-check/SKILL.md +169 -169
- package/skills/journal/SKILL.md +240 -240
- package/skills/launch/SKILL.md +344 -344
- package/skills/logic-guardian/SKILL.md +251 -251
- package/skills/marketing/SKILL.md +290 -289
- package/skills/mcp-builder/SKILL.md +425 -425
- 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 -428
- package/skills/preflight/SKILL.md +415 -415
- package/skills/problem-solver/SKILL.md +380 -284
- package/skills/rescue/SKILL.md +474 -474
- package/skills/retro/SKILL.md +3 -1
- package/skills/review/SKILL.md +612 -588
- 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 -287
- package/skills/scope-guard/SKILL.md +180 -180
- package/skills/scout/SKILL.md +263 -263
- package/skills/sentinel/SKILL.md +382 -381
- package/skills/sentinel-env/SKILL.md +254 -254
- package/skills/sequential-thinking/SKILL.md +234 -234
- package/skills/session-bridge/SKILL.md +543 -543
- package/skills/skill-forge/SKILL.md +581 -581
- package/skills/skill-router/SKILL.md +3 -0
- package/skills/surgeon/SKILL.md +215 -215
- package/skills/team/SKILL.md +556 -537
- package/skills/test/SKILL.md +614 -614
- package/skills/trend-scout/SKILL.md +145 -145
- package/skills/verification/SKILL.md +326 -326
- package/skills/video-creator/SKILL.md +201 -201
- package/skills/watchdog/SKILL.md +168 -168
- package/skills/worktree/SKILL.md +140 -140
|
@@ -1,219 +1,219 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: zalo-rate-guard
|
|
3
|
-
pack: "@rune/zalo"
|
|
4
|
-
description: Rate limiting patterns for Zalo OA and personal APIs — token bucket per endpoint, exponential backoff, queue management, quota monitoring, anti-ban strategies.
|
|
5
|
-
model: sonnet
|
|
6
|
-
tools: "Read, Glob, Grep, Bash, Write, Edit"
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# zalo-rate-guard
|
|
10
|
-
|
|
11
|
-
Shared rate limiting layer for both Track A (OA API) and Track B (Personal via zca-js).
|
|
12
|
-
Zalo has **undocumented rate limits** — no official RPM/QPM numbers published.
|
|
13
|
-
Exceeding limits: throttled (429) → warned → OA suspended / account banned.
|
|
14
|
-
Neither `zalo-php-sdk`, `zalo-java-sdk`, nor `zca-js` implement any rate limiting.
|
|
15
|
-
This skill fills that gap.
|
|
16
|
-
|
|
17
|
-
---
|
|
18
|
-
|
|
19
|
-
## Estimated Safe Limits
|
|
20
|
-
|
|
21
|
-
**Track A — OA API:**
|
|
22
|
-
|
|
23
|
-
| Endpoint | Safe RPM | Burst | Notes |
|
|
24
|
-
|----------|----------|-------|-------|
|
|
25
|
-
| Send CS message | 200 | 10 | Per OA, includes all message types |
|
|
26
|
-
| Send broadcast | 50 | 5 | Monthly quota based on follower count |
|
|
27
|
-
| Get user profile | 300 | 20 | Cacheable — use name cache |
|
|
28
|
-
| Get follower list | 100 | 10 | Paginated, cache results |
|
|
29
|
-
| Upload media | 60 | 5 | Large payloads, slower |
|
|
30
|
-
| Global | 500 | 30 | Total across all endpoints |
|
|
31
|
-
|
|
32
|
-
**Track B — Personal (zca-js):**
|
|
33
|
-
|
|
34
|
-
| Action | Safe RPM | Burst | Notes |
|
|
35
|
-
|--------|----------|-------|-------|
|
|
36
|
-
| Send message (DM) | 30 | 5 | Much lower than OA — personal account |
|
|
37
|
-
| Send message (group) | 20 | 3 | Groups are more scrutinized |
|
|
38
|
-
| Friend operations | 10 | 2 | Add/remove friend is very sensitive |
|
|
39
|
-
| Profile lookups | 60 | 10 | Less sensitive, still cache |
|
|
40
|
-
| Global | 100 | 15 | Err on side of caution |
|
|
41
|
-
|
|
42
|
-
---
|
|
43
|
-
|
|
44
|
-
## Token Bucket Implementation
|
|
45
|
-
|
|
46
|
-
```typescript
|
|
47
|
-
import PQueue from 'p-queue'
|
|
48
|
-
|
|
49
|
-
interface RateLimitConfig {
|
|
50
|
-
rpm: number // requests per minute
|
|
51
|
-
burst: number // max concurrent
|
|
52
|
-
retryAfter: number // ms to wait on 429
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const LIMITS: Record<string, RateLimitConfig> = {
|
|
56
|
-
'oa:send_message': { rpm: 200, burst: 10, retryAfter: 5000 },
|
|
57
|
-
'oa:broadcast': { rpm: 50, burst: 5, retryAfter: 10000 },
|
|
58
|
-
'oa:get_profile': { rpm: 300, burst: 20, retryAfter: 3000 },
|
|
59
|
-
'oa:upload': { rpm: 60, burst: 5, retryAfter: 5000 },
|
|
60
|
-
'personal:send_dm': { rpm: 30, burst: 5, retryAfter: 10000 },
|
|
61
|
-
'personal:send_grp': { rpm: 20, burst: 3, retryAfter: 15000 },
|
|
62
|
-
'personal:friend': { rpm: 10, burst: 2, retryAfter: 30000 },
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export class ZaloRateLimiter {
|
|
66
|
-
private queues = new Map<string, PQueue>()
|
|
67
|
-
|
|
68
|
-
constructor() {
|
|
69
|
-
for (const [key, config] of Object.entries(LIMITS)) {
|
|
70
|
-
this.queues.set(key, new PQueue({
|
|
71
|
-
concurrency: config.burst,
|
|
72
|
-
intervalCap: config.rpm,
|
|
73
|
-
interval: 60_000, // per minute window
|
|
74
|
-
}))
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
async execute<T>(endpoint: string, fn: () => Promise<T>): Promise<T> {
|
|
79
|
-
const queue = this.queues.get(endpoint)
|
|
80
|
-
if (!queue) throw new Error(`Unknown endpoint: ${endpoint}`)
|
|
81
|
-
return queue.add(fn) as Promise<T>
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
queueSize(endpoint: string): number {
|
|
85
|
-
return this.queues.get(endpoint)?.size ?? 0
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
pending(endpoint: string): number {
|
|
89
|
-
return this.queues.get(endpoint)?.pending ?? 0
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
---
|
|
95
|
-
|
|
96
|
-
## Exponential Backoff on 429
|
|
97
|
-
|
|
98
|
-
```typescript
|
|
99
|
-
export async function withBackoff<T>(
|
|
100
|
-
fn: () => Promise<T>,
|
|
101
|
-
maxRetries = 3,
|
|
102
|
-
baseDelay = 1000
|
|
103
|
-
): Promise<T> {
|
|
104
|
-
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
105
|
-
try {
|
|
106
|
-
return await fn()
|
|
107
|
-
} catch (error: any) {
|
|
108
|
-
const isRateLimit = error?.status === 429 || error?.error_code === 429
|
|
109
|
-
if (!isRateLimit || attempt === maxRetries) throw error
|
|
110
|
-
const delay = baseDelay * Math.pow(2, attempt) + Math.random() * 1000
|
|
111
|
-
console.warn(`[zalo-rate-guard] Rate limited. Retry ${attempt + 1}/${maxRetries} in ${Math.round(delay)}ms`)
|
|
112
|
-
await new Promise(r => setTimeout(r, delay))
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
throw new Error('Unreachable')
|
|
116
|
-
}
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
---
|
|
120
|
-
|
|
121
|
-
## Quota Monitoring (OA Broadcast)
|
|
122
|
-
|
|
123
|
-
Broadcast quota is a **hard monthly limit** — exceeding it silently drops messages, no error returned.
|
|
124
|
-
|
|
125
|
-
```typescript
|
|
126
|
-
interface QuotaTracker {
|
|
127
|
-
monthly_limit: number // based on follower count + OA level
|
|
128
|
-
used: number
|
|
129
|
-
resets_at: Date // 1st of each month
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
export function canBroadcast(tracker: QuotaTracker, recipientCount: number): boolean {
|
|
133
|
-
const remaining = tracker.monthly_limit - tracker.used
|
|
134
|
-
if (recipientCount > remaining) {
|
|
135
|
-
console.error(
|
|
136
|
-
`[zalo-rate-guard] Broadcast quota insufficient: need ${recipientCount}, have ${remaining}/${tracker.monthly_limit}`
|
|
137
|
-
)
|
|
138
|
-
return false
|
|
139
|
-
}
|
|
140
|
-
return true
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
export function trackBroadcastUsed(tracker: QuotaTracker, sent: number): QuotaTracker {
|
|
144
|
-
return { ...tracker, used: tracker.used + sent }
|
|
145
|
-
}
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
---
|
|
149
|
-
|
|
150
|
-
## Integration Pattern
|
|
151
|
-
|
|
152
|
-
```typescript
|
|
153
|
-
// Singleton — shared across the app
|
|
154
|
-
export const limiter = new ZaloRateLimiter()
|
|
155
|
-
|
|
156
|
-
// Track A: OA message send with rate limiting
|
|
157
|
-
export async function sendOaMessage(userId: string, text: string) {
|
|
158
|
-
return limiter.execute('oa:send_message', () =>
|
|
159
|
-
withBackoff(() =>
|
|
160
|
-
oaApiCall('/message/cs', {
|
|
161
|
-
recipient: { user_id: userId },
|
|
162
|
-
message: { text },
|
|
163
|
-
})
|
|
164
|
-
)
|
|
165
|
-
)
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Track B: Personal DM with rate limiting + human jitter
|
|
169
|
-
export async function sendPersonalMessage(threadId: string, text: string) {
|
|
170
|
-
const jitter = 500 + Math.random() * 1500 // 500–2000ms
|
|
171
|
-
await new Promise(r => setTimeout(r, jitter))
|
|
172
|
-
return limiter.execute('personal:send_dm', () =>
|
|
173
|
-
withBackoff(() => api.sendMessage(text, threadId, 'User'))
|
|
174
|
-
)
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// Track B: Friend operation — highest-risk, extra jitter
|
|
178
|
-
export async function addFriend(userId: string) {
|
|
179
|
-
const jitter = 2000 + Math.random() * 3000 // 2–5s
|
|
180
|
-
await new Promise(r => setTimeout(r, jitter))
|
|
181
|
-
return limiter.execute('personal:friend', () =>
|
|
182
|
-
withBackoff(() => api.sendFriendRequest(userId), 2, 5000)
|
|
183
|
-
)
|
|
184
|
-
}
|
|
185
|
-
```
|
|
186
|
-
|
|
187
|
-
---
|
|
188
|
-
|
|
189
|
-
## Anti-Ban Strategies
|
|
190
|
-
|
|
191
|
-
**Track A (OA):**
|
|
192
|
-
1. Stay under safe RPM limits (table above)
|
|
193
|
-
2. Exponential backoff on ALL 429 responses — never retry immediately
|
|
194
|
-
3. Cache user profiles — avoid repeated lookups for the same user
|
|
195
|
-
4. Spread broadcasts over time — don't burst the entire follower list at once
|
|
196
|
-
5. Monitor quota before each broadcast batch — stop before hitting monthly limit
|
|
197
|
-
6. Use `appsecret_proof` on all requests — proves you're the legitimate app owner
|
|
198
|
-
|
|
199
|
-
**Track B (Personal):**
|
|
200
|
-
1. Much lower limits than OA — personal accounts are watched more closely
|
|
201
|
-
2. Add human-like jitter: 500–2000ms random delay between messages (not optional)
|
|
202
|
-
3. Avoid 3–6 AM (VN timezone) — traffic at those hours flags automated activity
|
|
203
|
-
4. Never change profile info programmatically — triggers manual review
|
|
204
|
-
5. Friend operations are highest-risk — max 10 RPM, prefer lower in practice
|
|
205
|
-
6. Keep sessions long-lived — repeated login/logout is a strong ban signal
|
|
206
|
-
7. Use a consistent device fingerprint (`userAgent` + `IMEI`) per account
|
|
207
|
-
8. On `DuplicateConnection` (error 3000): wait 30s before reconnecting, never spam reconnects
|
|
208
|
-
|
|
209
|
-
---
|
|
210
|
-
|
|
211
|
-
## Sharp Edges
|
|
212
|
-
|
|
213
|
-
- Rate limits are **estimated** — Zalo does not publish official numbers; treat all figures as conservative targets
|
|
214
|
-
- `p-queue` `intervalCap` applies per window, not per request — test behavior under burst
|
|
215
|
-
- 429 without backoff = accelerating toward ban, not slowing down
|
|
216
|
-
- Broadcast quota overflow **silently drops messages** — no 429, no error, just lost sends
|
|
217
|
-
- Stale cached profile data is acceptable; hitting rate limits for fresh data is not
|
|
218
|
-
- Personal account friend operations are the single highest-risk action — handle with care
|
|
219
|
-
- Human jitter for personal track is a survival strategy, not a nice-to-have
|
|
1
|
+
---
|
|
2
|
+
name: zalo-rate-guard
|
|
3
|
+
pack: "@rune/zalo"
|
|
4
|
+
description: Rate limiting patterns for Zalo OA and personal APIs — token bucket per endpoint, exponential backoff, queue management, quota monitoring, anti-ban strategies.
|
|
5
|
+
model: sonnet
|
|
6
|
+
tools: "Read, Glob, Grep, Bash, Write, Edit"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# zalo-rate-guard
|
|
10
|
+
|
|
11
|
+
Shared rate limiting layer for both Track A (OA API) and Track B (Personal via zca-js).
|
|
12
|
+
Zalo has **undocumented rate limits** — no official RPM/QPM numbers published.
|
|
13
|
+
Exceeding limits: throttled (429) → warned → OA suspended / account banned.
|
|
14
|
+
Neither `zalo-php-sdk`, `zalo-java-sdk`, nor `zca-js` implement any rate limiting.
|
|
15
|
+
This skill fills that gap.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Estimated Safe Limits
|
|
20
|
+
|
|
21
|
+
**Track A — OA API:**
|
|
22
|
+
|
|
23
|
+
| Endpoint | Safe RPM | Burst | Notes |
|
|
24
|
+
|----------|----------|-------|-------|
|
|
25
|
+
| Send CS message | 200 | 10 | Per OA, includes all message types |
|
|
26
|
+
| Send broadcast | 50 | 5 | Monthly quota based on follower count |
|
|
27
|
+
| Get user profile | 300 | 20 | Cacheable — use name cache |
|
|
28
|
+
| Get follower list | 100 | 10 | Paginated, cache results |
|
|
29
|
+
| Upload media | 60 | 5 | Large payloads, slower |
|
|
30
|
+
| Global | 500 | 30 | Total across all endpoints |
|
|
31
|
+
|
|
32
|
+
**Track B — Personal (zca-js):**
|
|
33
|
+
|
|
34
|
+
| Action | Safe RPM | Burst | Notes |
|
|
35
|
+
|--------|----------|-------|-------|
|
|
36
|
+
| Send message (DM) | 30 | 5 | Much lower than OA — personal account |
|
|
37
|
+
| Send message (group) | 20 | 3 | Groups are more scrutinized |
|
|
38
|
+
| Friend operations | 10 | 2 | Add/remove friend is very sensitive |
|
|
39
|
+
| Profile lookups | 60 | 10 | Less sensitive, still cache |
|
|
40
|
+
| Global | 100 | 15 | Err on side of caution |
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Token Bucket Implementation
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
import PQueue from 'p-queue'
|
|
48
|
+
|
|
49
|
+
interface RateLimitConfig {
|
|
50
|
+
rpm: number // requests per minute
|
|
51
|
+
burst: number // max concurrent
|
|
52
|
+
retryAfter: number // ms to wait on 429
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const LIMITS: Record<string, RateLimitConfig> = {
|
|
56
|
+
'oa:send_message': { rpm: 200, burst: 10, retryAfter: 5000 },
|
|
57
|
+
'oa:broadcast': { rpm: 50, burst: 5, retryAfter: 10000 },
|
|
58
|
+
'oa:get_profile': { rpm: 300, burst: 20, retryAfter: 3000 },
|
|
59
|
+
'oa:upload': { rpm: 60, burst: 5, retryAfter: 5000 },
|
|
60
|
+
'personal:send_dm': { rpm: 30, burst: 5, retryAfter: 10000 },
|
|
61
|
+
'personal:send_grp': { rpm: 20, burst: 3, retryAfter: 15000 },
|
|
62
|
+
'personal:friend': { rpm: 10, burst: 2, retryAfter: 30000 },
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export class ZaloRateLimiter {
|
|
66
|
+
private queues = new Map<string, PQueue>()
|
|
67
|
+
|
|
68
|
+
constructor() {
|
|
69
|
+
for (const [key, config] of Object.entries(LIMITS)) {
|
|
70
|
+
this.queues.set(key, new PQueue({
|
|
71
|
+
concurrency: config.burst,
|
|
72
|
+
intervalCap: config.rpm,
|
|
73
|
+
interval: 60_000, // per minute window
|
|
74
|
+
}))
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async execute<T>(endpoint: string, fn: () => Promise<T>): Promise<T> {
|
|
79
|
+
const queue = this.queues.get(endpoint)
|
|
80
|
+
if (!queue) throw new Error(`Unknown endpoint: ${endpoint}`)
|
|
81
|
+
return queue.add(fn) as Promise<T>
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
queueSize(endpoint: string): number {
|
|
85
|
+
return this.queues.get(endpoint)?.size ?? 0
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
pending(endpoint: string): number {
|
|
89
|
+
return this.queues.get(endpoint)?.pending ?? 0
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Exponential Backoff on 429
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
export async function withBackoff<T>(
|
|
100
|
+
fn: () => Promise<T>,
|
|
101
|
+
maxRetries = 3,
|
|
102
|
+
baseDelay = 1000
|
|
103
|
+
): Promise<T> {
|
|
104
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
105
|
+
try {
|
|
106
|
+
return await fn()
|
|
107
|
+
} catch (error: any) {
|
|
108
|
+
const isRateLimit = error?.status === 429 || error?.error_code === 429
|
|
109
|
+
if (!isRateLimit || attempt === maxRetries) throw error
|
|
110
|
+
const delay = baseDelay * Math.pow(2, attempt) + Math.random() * 1000
|
|
111
|
+
console.warn(`[zalo-rate-guard] Rate limited. Retry ${attempt + 1}/${maxRetries} in ${Math.round(delay)}ms`)
|
|
112
|
+
await new Promise(r => setTimeout(r, delay))
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
throw new Error('Unreachable')
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Quota Monitoring (OA Broadcast)
|
|
122
|
+
|
|
123
|
+
Broadcast quota is a **hard monthly limit** — exceeding it silently drops messages, no error returned.
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
interface QuotaTracker {
|
|
127
|
+
monthly_limit: number // based on follower count + OA level
|
|
128
|
+
used: number
|
|
129
|
+
resets_at: Date // 1st of each month
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function canBroadcast(tracker: QuotaTracker, recipientCount: number): boolean {
|
|
133
|
+
const remaining = tracker.monthly_limit - tracker.used
|
|
134
|
+
if (recipientCount > remaining) {
|
|
135
|
+
console.error(
|
|
136
|
+
`[zalo-rate-guard] Broadcast quota insufficient: need ${recipientCount}, have ${remaining}/${tracker.monthly_limit}`
|
|
137
|
+
)
|
|
138
|
+
return false
|
|
139
|
+
}
|
|
140
|
+
return true
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function trackBroadcastUsed(tracker: QuotaTracker, sent: number): QuotaTracker {
|
|
144
|
+
return { ...tracker, used: tracker.used + sent }
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Integration Pattern
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
// Singleton — shared across the app
|
|
154
|
+
export const limiter = new ZaloRateLimiter()
|
|
155
|
+
|
|
156
|
+
// Track A: OA message send with rate limiting
|
|
157
|
+
export async function sendOaMessage(userId: string, text: string) {
|
|
158
|
+
return limiter.execute('oa:send_message', () =>
|
|
159
|
+
withBackoff(() =>
|
|
160
|
+
oaApiCall('/message/cs', {
|
|
161
|
+
recipient: { user_id: userId },
|
|
162
|
+
message: { text },
|
|
163
|
+
})
|
|
164
|
+
)
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Track B: Personal DM with rate limiting + human jitter
|
|
169
|
+
export async function sendPersonalMessage(threadId: string, text: string) {
|
|
170
|
+
const jitter = 500 + Math.random() * 1500 // 500–2000ms
|
|
171
|
+
await new Promise(r => setTimeout(r, jitter))
|
|
172
|
+
return limiter.execute('personal:send_dm', () =>
|
|
173
|
+
withBackoff(() => api.sendMessage(text, threadId, 'User'))
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Track B: Friend operation — highest-risk, extra jitter
|
|
178
|
+
export async function addFriend(userId: string) {
|
|
179
|
+
const jitter = 2000 + Math.random() * 3000 // 2–5s
|
|
180
|
+
await new Promise(r => setTimeout(r, jitter))
|
|
181
|
+
return limiter.execute('personal:friend', () =>
|
|
182
|
+
withBackoff(() => api.sendFriendRequest(userId), 2, 5000)
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## Anti-Ban Strategies
|
|
190
|
+
|
|
191
|
+
**Track A (OA):**
|
|
192
|
+
1. Stay under safe RPM limits (table above)
|
|
193
|
+
2. Exponential backoff on ALL 429 responses — never retry immediately
|
|
194
|
+
3. Cache user profiles — avoid repeated lookups for the same user
|
|
195
|
+
4. Spread broadcasts over time — don't burst the entire follower list at once
|
|
196
|
+
5. Monitor quota before each broadcast batch — stop before hitting monthly limit
|
|
197
|
+
6. Use `appsecret_proof` on all requests — proves you're the legitimate app owner
|
|
198
|
+
|
|
199
|
+
**Track B (Personal):**
|
|
200
|
+
1. Much lower limits than OA — personal accounts are watched more closely
|
|
201
|
+
2. Add human-like jitter: 500–2000ms random delay between messages (not optional)
|
|
202
|
+
3. Avoid 3–6 AM (VN timezone) — traffic at those hours flags automated activity
|
|
203
|
+
4. Never change profile info programmatically — triggers manual review
|
|
204
|
+
5. Friend operations are highest-risk — max 10 RPM, prefer lower in practice
|
|
205
|
+
6. Keep sessions long-lived — repeated login/logout is a strong ban signal
|
|
206
|
+
7. Use a consistent device fingerprint (`userAgent` + `IMEI`) per account
|
|
207
|
+
8. On `DuplicateConnection` (error 3000): wait 30s before reconnecting, never spam reconnects
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## Sharp Edges
|
|
212
|
+
|
|
213
|
+
- Rate limits are **estimated** — Zalo does not publish official numbers; treat all figures as conservative targets
|
|
214
|
+
- `p-queue` `intervalCap` applies per window, not per request — test behavior under burst
|
|
215
|
+
- 429 without backoff = accelerating toward ban, not slowing down
|
|
216
|
+
- Broadcast quota overflow **silently drops messages** — no 429, no error, just lost sends
|
|
217
|
+
- Stale cached profile data is acceptable; hitting rate limits for fresh data is not
|
|
218
|
+
- Personal account friend operations are the single highest-risk action — handle with care
|
|
219
|
+
- Human jitter for personal track is a survival strategy, not a nice-to-have
|
|
@@ -1,48 +1,48 @@
|
|
|
1
|
-
// Rune Auto-Format Hook
|
|
2
|
-
// PostToolUse hook on Edit/Write — runs Prettier on JS/TS files after modification
|
|
3
|
-
//
|
|
4
|
-
// Only runs if Prettier is available in the project.
|
|
5
|
-
// Silent pass-through if not applicable.
|
|
6
|
-
|
|
7
|
-
const { execSync } = require('child_process');
|
|
8
|
-
const path = require('path');
|
|
9
|
-
|
|
10
|
-
const input = JSON.parse(process.env.CLAUDE_TOOL_INPUT || '{}');
|
|
11
|
-
const filePath = input.file_path || input.filePath || '';
|
|
12
|
-
|
|
13
|
-
// Only format JS/TS/JSON/CSS files
|
|
14
|
-
if (!/\.(js|jsx|ts|tsx|json|css|scss|md|html|yaml|yml)$/i.test(filePath)) {
|
|
15
|
-
process.exit(0);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// Check if file exists and is within a project with Prettier
|
|
19
|
-
const dir = path.dirname(filePath);
|
|
20
|
-
|
|
21
|
-
// Try to find prettier in the project
|
|
22
|
-
let hasPrettier = false;
|
|
23
|
-
try {
|
|
24
|
-
execSync('npx prettier --version', { cwd: dir, encoding: 'utf-8', stdio: 'pipe', timeout: 5000 });
|
|
25
|
-
hasPrettier = true;
|
|
26
|
-
} catch {
|
|
27
|
-
// No Prettier available — skip silently
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
if (!hasPrettier) {
|
|
31
|
-
process.exit(0);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// Run Prettier on the file
|
|
35
|
-
try {
|
|
36
|
-
execSync(`npx prettier --write "${filePath}"`, {
|
|
37
|
-
cwd: dir,
|
|
38
|
-
encoding: 'utf-8',
|
|
39
|
-
stdio: 'pipe',
|
|
40
|
-
timeout: 10000,
|
|
41
|
-
});
|
|
42
|
-
// Silent success — formatted files are seamless
|
|
43
|
-
} catch (e) {
|
|
44
|
-
// Prettier failed — non-critical, just warn
|
|
45
|
-
console.log(`[Rune auto-format] Prettier failed on ${path.basename(filePath)}: ${e.message.split('\n')[0]}`);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
process.exit(0);
|
|
1
|
+
// Rune Auto-Format Hook
|
|
2
|
+
// PostToolUse hook on Edit/Write — runs Prettier on JS/TS files after modification
|
|
3
|
+
//
|
|
4
|
+
// Only runs if Prettier is available in the project.
|
|
5
|
+
// Silent pass-through if not applicable.
|
|
6
|
+
|
|
7
|
+
const { execSync } = require('child_process');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
const input = JSON.parse(process.env.CLAUDE_TOOL_INPUT || '{}');
|
|
11
|
+
const filePath = input.file_path || input.filePath || '';
|
|
12
|
+
|
|
13
|
+
// Only format JS/TS/JSON/CSS files
|
|
14
|
+
if (!/\.(js|jsx|ts|tsx|json|css|scss|md|html|yaml|yml)$/i.test(filePath)) {
|
|
15
|
+
process.exit(0);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Check if file exists and is within a project with Prettier
|
|
19
|
+
const dir = path.dirname(filePath);
|
|
20
|
+
|
|
21
|
+
// Try to find prettier in the project
|
|
22
|
+
let hasPrettier = false;
|
|
23
|
+
try {
|
|
24
|
+
execSync('npx prettier --version', { cwd: dir, encoding: 'utf-8', stdio: 'pipe', timeout: 5000 });
|
|
25
|
+
hasPrettier = true;
|
|
26
|
+
} catch {
|
|
27
|
+
// No Prettier available — skip silently
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (!hasPrettier) {
|
|
31
|
+
process.exit(0);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Run Prettier on the file
|
|
35
|
+
try {
|
|
36
|
+
execSync(`npx prettier --write "${filePath}"`, {
|
|
37
|
+
cwd: dir,
|
|
38
|
+
encoding: 'utf-8',
|
|
39
|
+
stdio: 'pipe',
|
|
40
|
+
timeout: 10000,
|
|
41
|
+
});
|
|
42
|
+
// Silent success — formatted files are seamless
|
|
43
|
+
} catch (e) {
|
|
44
|
+
// Prettier failed — non-critical, just warn
|
|
45
|
+
console.log(`[Rune auto-format] Prettier failed on ${path.basename(filePath)}: ${e.message.split('\n')[0]}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
process.exit(0);
|