@rune-kit/rune 2.2.0 → 2.2.1

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 (144) hide show
  1. package/README.md +40 -34
  2. package/compiler/__tests__/pack-split.test.js +145 -0
  3. package/compiler/bin/rune.js +0 -5
  4. package/compiler/doctor.js +42 -0
  5. package/compiler/emitter.js +27 -4
  6. package/compiler/parser.js +41 -3
  7. package/compiler/transformer.js +10 -6
  8. package/compiler/transforms/compliance.js +40 -0
  9. package/extensions/ai-ml/PACK.md +38 -474
  10. package/extensions/ai-ml/skills/ai-agents.md +172 -0
  11. package/extensions/ai-ml/skills/code-sandbox.md +187 -0
  12. package/extensions/ai-ml/skills/deep-research.md +146 -0
  13. package/extensions/ai-ml/skills/embedding-search.md +66 -0
  14. package/extensions/ai-ml/skills/fine-tuning-guide.md +74 -0
  15. package/extensions/ai-ml/skills/llm-architect.md +125 -0
  16. package/extensions/ai-ml/skills/llm-integration.md +64 -0
  17. package/extensions/ai-ml/skills/prompt-patterns.md +72 -0
  18. package/extensions/ai-ml/skills/rag-patterns.md +66 -0
  19. package/extensions/ai-ml/skills/web-extraction.md +114 -0
  20. package/extensions/analytics/PACK.md +19 -484
  21. package/extensions/analytics/skills/ab-testing.md +72 -0
  22. package/extensions/analytics/skills/dashboard-patterns.md +83 -0
  23. package/extensions/analytics/skills/data-validation.md +68 -0
  24. package/extensions/analytics/skills/funnel-analysis.md +81 -0
  25. package/extensions/analytics/skills/sql-patterns.md +57 -0
  26. package/extensions/analytics/skills/statistical-analysis.md +79 -0
  27. package/extensions/analytics/skills/tracking-setup.md +71 -0
  28. package/extensions/backend/PACK.md +44 -618
  29. package/extensions/backend/skills/api-patterns.md +84 -0
  30. package/extensions/backend/skills/async-pipeline.md +193 -0
  31. package/extensions/backend/skills/auth-patterns.md +97 -0
  32. package/extensions/backend/skills/background-jobs.md +133 -0
  33. package/extensions/backend/skills/caching-patterns.md +108 -0
  34. package/extensions/backend/skills/cli-generation.md +133 -0
  35. package/extensions/backend/skills/database-patterns.md +87 -0
  36. package/extensions/backend/skills/middleware-patterns.md +104 -0
  37. package/extensions/chrome-ext/PACK.md +19 -921
  38. package/extensions/chrome-ext/skills/cws-preflight.md +143 -0
  39. package/extensions/chrome-ext/skills/cws-publish.md +104 -0
  40. package/extensions/chrome-ext/skills/ext-ai-integration.md +251 -0
  41. package/extensions/chrome-ext/skills/ext-messaging.md +139 -0
  42. package/extensions/chrome-ext/skills/ext-storage.md +133 -0
  43. package/extensions/chrome-ext/skills/mv3-scaffold.md +164 -0
  44. package/extensions/content/PACK.md +43 -335
  45. package/extensions/content/skills/blog-patterns.md +88 -0
  46. package/extensions/content/skills/cms-integration.md +131 -0
  47. package/extensions/content/skills/content-scoring.md +107 -0
  48. package/extensions/content/skills/i18n.md +83 -0
  49. package/extensions/content/skills/mdx-authoring.md +137 -0
  50. package/extensions/content/skills/reference.md +1014 -0
  51. package/extensions/content/skills/seo-patterns.md +67 -0
  52. package/extensions/content/skills/video-repurpose.md +153 -0
  53. package/extensions/devops/PACK.md +38 -457
  54. package/extensions/devops/skills/chaos-testing.md +67 -0
  55. package/extensions/devops/skills/ci-cd.md +75 -0
  56. package/extensions/devops/skills/docker.md +58 -0
  57. package/extensions/devops/skills/edge-serverless.md +163 -0
  58. package/extensions/devops/skills/infra-as-code.md +158 -0
  59. package/extensions/devops/skills/kubernetes.md +110 -0
  60. package/extensions/devops/skills/monitoring.md +57 -0
  61. package/extensions/devops/skills/server-setup.md +64 -0
  62. package/extensions/devops/skills/ssl-domain.md +42 -0
  63. package/extensions/ecommerce/PACK.md +62 -226
  64. package/extensions/ecommerce/skills/cart-system.md +79 -0
  65. package/extensions/ecommerce/skills/inventory-mgmt.md +102 -0
  66. package/extensions/ecommerce/skills/order-management.md +126 -0
  67. package/extensions/ecommerce/skills/payment-integration.md +472 -0
  68. package/extensions/ecommerce/skills/shopify-dev.md +69 -0
  69. package/extensions/ecommerce/skills/subscription-billing.md +93 -0
  70. package/extensions/ecommerce/skills/tax-compliance.md +117 -0
  71. package/extensions/gamedev/PACK.md +66 -317
  72. package/extensions/gamedev/skills/asset-pipeline.md +74 -0
  73. package/extensions/gamedev/skills/audio-system.md +129 -0
  74. package/extensions/gamedev/skills/camera-system.md +87 -0
  75. package/extensions/gamedev/skills/ecs.md +98 -0
  76. package/extensions/gamedev/skills/game-loops.md +72 -0
  77. package/extensions/gamedev/skills/input-system.md +199 -0
  78. package/extensions/gamedev/skills/multiplayer.md +180 -0
  79. package/extensions/gamedev/skills/particles.md +105 -0
  80. package/extensions/gamedev/skills/physics-engine.md +89 -0
  81. package/extensions/gamedev/skills/scene-management.md +146 -0
  82. package/extensions/gamedev/skills/threejs-patterns.md +90 -0
  83. package/extensions/gamedev/skills/webgl.md +71 -0
  84. package/extensions/mobile/PACK.md +56 -223
  85. package/extensions/mobile/skills/app-store-connect.md +152 -0
  86. package/extensions/mobile/skills/app-store-prep.md +66 -0
  87. package/extensions/mobile/skills/deep-linking.md +109 -0
  88. package/extensions/mobile/skills/flutter.md +60 -0
  89. package/extensions/mobile/skills/ios-build-pipeline.md +142 -0
  90. package/extensions/mobile/skills/native-bridge.md +66 -0
  91. package/extensions/mobile/skills/ota-updates.md +97 -0
  92. package/extensions/mobile/skills/push-notifications.md +111 -0
  93. package/extensions/mobile/skills/react-native.md +82 -0
  94. package/extensions/saas/PACK.md +26 -720
  95. package/extensions/saas/skills/billing-integration.md +121 -0
  96. package/extensions/saas/skills/feature-flags.md +130 -0
  97. package/extensions/saas/skills/multi-tenant.md +103 -0
  98. package/extensions/saas/skills/onboarding-flow.md +139 -0
  99. package/extensions/saas/skills/subscription-flow.md +95 -0
  100. package/extensions/saas/skills/team-management.md +144 -0
  101. package/extensions/security/PACK.md +10 -448
  102. package/extensions/security/skills/api-security.md +140 -0
  103. package/extensions/security/skills/compliance.md +68 -0
  104. package/extensions/security/skills/owasp-audit.md +64 -0
  105. package/extensions/security/skills/pentest-patterns.md +77 -0
  106. package/extensions/security/skills/secret-mgmt.md +65 -0
  107. package/extensions/security/skills/supply-chain.md +65 -0
  108. package/extensions/trading/PACK.md +18 -535
  109. package/extensions/trading/skills/chart-components.md +55 -0
  110. package/extensions/trading/skills/experiment-loop.md +125 -0
  111. package/extensions/trading/skills/fintech-patterns.md +47 -0
  112. package/extensions/trading/skills/indicator-library.md +58 -0
  113. package/extensions/trading/skills/quant-analysis.md +111 -0
  114. package/extensions/trading/skills/realtime-data.md +58 -0
  115. package/extensions/trading/skills/trade-logic.md +104 -0
  116. package/extensions/ui/PACK.md +34 -853
  117. package/extensions/ui/skills/a11y-audit.md +91 -0
  118. package/extensions/ui/skills/animation-patterns.md +106 -0
  119. package/extensions/ui/skills/component-patterns.md +75 -0
  120. package/extensions/ui/skills/design-decision.md +98 -0
  121. package/extensions/ui/skills/design-system.md +68 -0
  122. package/extensions/ui/skills/landing-patterns.md +155 -0
  123. package/extensions/ui/skills/palette-picker.md +162 -0
  124. package/extensions/ui/skills/react-health.md +90 -0
  125. package/extensions/ui/skills/type-system.md +125 -0
  126. package/extensions/ui/skills/web-vitals.md +153 -0
  127. package/extensions/zalo/PACK.md +117 -0
  128. package/extensions/zalo/skills/zalo-oa-mcp.md +317 -0
  129. package/extensions/zalo/skills/zalo-oa-messaging.md +429 -0
  130. package/extensions/zalo/skills/zalo-oa-setup.md +236 -0
  131. package/extensions/zalo/skills/zalo-oa-webhook.md +189 -0
  132. package/extensions/zalo/skills/zalo-personal-messaging.md +194 -0
  133. package/extensions/zalo/skills/zalo-personal-setup.md +153 -0
  134. package/extensions/zalo/skills/zalo-rate-guard.md +219 -0
  135. package/package.json +1 -1
  136. package/skills/brainstorm/SKILL.md +63 -1
  137. package/skills/cook/SKILL.md +84 -5
  138. package/skills/mcp-builder/SKILL.md +48 -1
  139. package/skills/review/SKILL.md +42 -5
  140. package/skills/review-intake/SKILL.md +17 -1
  141. package/skills/skill-router/SKILL.md +89 -7
  142. package/skills/team/SKILL.md +24 -1
  143. package/skills/test/SKILL.md +18 -0
  144. package/skills/verification/SKILL.md +40 -1
@@ -0,0 +1,84 @@
1
+ ---
2
+ name: "api-patterns"
3
+ pack: "@rune/backend"
4
+ description: "RESTful and GraphQL API design patterns — resource naming, pagination, filtering, error responses, versioning, rate limiting, OpenAPI generation."
5
+ model: sonnet
6
+ tools: [Read, Edit, Write, Grep, Glob, Bash]
7
+ ---
8
+
9
+ # api-patterns
10
+
11
+ RESTful and GraphQL API design patterns — resource naming, pagination, filtering, error responses, versioning, rate limiting, OpenAPI generation.
12
+
13
+ #### Workflow
14
+
15
+ **Step 1 — Detect API surface**
16
+ Use Grep to find route definitions (`app.get`, `app.post`, `router.`, `@Get()`, `@Post()`, `@Query`, `@Mutation`). Read each route file to inventory: endpoint paths, HTTP methods, response shapes, error handling approach.
17
+
18
+ **Step 2 — Audit naming and structure**
19
+ Check each endpoint against REST conventions: plural nouns for collections (`/users` not `/getUsers`), nested resources for relationships (`/users/:id/posts`), query params for filtering (`?status=active`), consistent error envelope. Flag violations with specific fix for each.
20
+
21
+ **Step 3 — Add missing pagination and filtering**
22
+ For list endpoints returning unbounded arrays, emit cursor-based or offset pagination. For endpoints with no filtering, add query param parsing with Zod/Joi validation. Emit the middleware or decorator that enforces the pattern.
23
+
24
+ **Step 4 — API versioning strategy**
25
+ Choose versioning approach based on project context: URL path (`/v2/users`) for public APIs with long deprecation windows; `Accept-Version: 2` header for internal APIs needing cleaner URLs; query param (`?version=2`) for simple cases. Emit version routing middleware and a deprecation warning header (`Deprecation: true, Sunset: <date>`) on v1 routes. Document migration path in the route file as a comment.
26
+
27
+ **Step 5 — OpenAPI/Swagger and GraphQL patterns**
28
+ For REST: emit OpenAPI 3.1 schema from route definitions using tsoa decorators (TypeScript), Fastify's built-in JSON Schema (`schema: { body, querystring, response }`), or NestJS `@ApiProperty`. For GraphQL: if schema-first, validate resolvers match schema types; if code-first (NestJS), check `@ObjectType` / `@Field` decorators. Add DataLoader to any resolver with a per-request DB call to prevent N+1 at the GraphQL layer. Emit subscription pattern (WebSocket transport) for real-time fields.
29
+
30
+ #### Example
31
+
32
+ ```typescript
33
+ // BEFORE: inconsistent naming, no pagination, bare error
34
+ app.get('/getUsers', async (req, res) => {
35
+ const users = await db.query('SELECT * FROM users');
36
+ res.json(users);
37
+ });
38
+
39
+ // AFTER: REST naming, cursor pagination, error envelope, Zod validation
40
+ const paginationSchema = z.object({
41
+ query: z.object({
42
+ cursor: z.string().optional(),
43
+ limit: z.coerce.number().int().min(1).max(100).default(20),
44
+ status: z.enum(['active', 'inactive']).optional(),
45
+ }),
46
+ });
47
+
48
+ app.get('/users', validate(paginationSchema), async (req, res) => {
49
+ const { cursor, limit, status } = req.query;
50
+ const users = await userRepo.findMany({ cursor, limit: limit + 1, status });
51
+ const hasNext = users.length > limit;
52
+ res.json({
53
+ data: users.slice(0, limit),
54
+ pagination: { next_cursor: hasNext ? users[limit - 1].id : null, has_more: hasNext },
55
+ });
56
+ });
57
+
58
+ // Rate limiting: sliding window with Redis (atomic, no race condition)
59
+ const rateLimitMiddleware = async (req, res, next) => {
60
+ const key = `rl:${req.ip}:${Math.floor(Date.now() / 60_000)}`; // 1-minute window
61
+ const multi = redis.multi();
62
+ multi.incr(key);
63
+ multi.expire(key, 60);
64
+ const [count] = await multi.exec();
65
+ if (count > 100) return res.status(429).json({ error: { code: 'RATE_LIMITED', message: 'Too many requests' } });
66
+ res.setHeader('X-RateLimit-Remaining', 100 - count);
67
+ next();
68
+ };
69
+
70
+ // Fastify: built-in schema validation + OpenAPI generation
71
+ fastify.get('/users/:id', {
72
+ schema: {
73
+ params: { type: 'object', properties: { id: { type: 'string', format: 'uuid' } }, required: ['id'] },
74
+ response: { 200: UserSchema, 404: ErrorSchema },
75
+ },
76
+ }, async (req, reply) => { /* handler */ });
77
+
78
+ // GraphQL: DataLoader prevents N+1 in resolvers
79
+ const userLoader = new DataLoader(async (userIds: string[]) => {
80
+ const users = await prisma.user.findMany({ where: { id: { in: userIds } } });
81
+ return userIds.map(id => users.find(u => u.id === id) ?? new Error(`User ${id} not found`));
82
+ });
83
+ // In resolver: return userLoader.load(post.authorId) — batches all loads per request
84
+ ```
@@ -0,0 +1,193 @@
1
+ ---
2
+ name: "async-pipeline"
3
+ pack: "@rune/backend"
4
+ description: "Multi-stage async processing pipelines with waterfall engine selection, progress streaming, and credit-based billing. Patterns for building services that process data through multiple fallback strategies with real-time status updates."
5
+ model: sonnet
6
+ tools: [Read, Edit, Write, Grep, Glob, Bash]
7
+ ---
8
+
9
+ # async-pipeline
10
+
11
+ Multi-stage async processing pipelines with waterfall engine selection, progress streaming, and credit-based billing. Patterns for building services that process data through multiple fallback strategies with real-time status updates.
12
+
13
+ #### Workflow
14
+
15
+ **Step 1 — Design engine waterfall**
16
+ Multiple processing engines ranked by quality and cost:
17
+ ```typescript
18
+ interface ProcessingEngine {
19
+ name: string;
20
+ quality: number; // higher = preferred
21
+ costMultiplier: number; // credit cost factor
22
+ execute: (input: Input) => Promise<Result>;
23
+ canHandle: (input: Input) => boolean;
24
+ }
25
+
26
+ // Engines race with staggered delays — first valid result wins
27
+ async function waterfallExecute(
28
+ engines: ProcessingEngine[],
29
+ input: Input,
30
+ staggerDelayMs: number = 500
31
+ ): Promise<{ result: Result; engine: string }> {
32
+ const sorted = engines
33
+ .filter(e => e.canHandle(input))
34
+ .sort((a, b) => b.quality - a.quality);
35
+
36
+ const controller = new AbortController();
37
+
38
+ const races = sorted.map((engine, i) =>
39
+ new Promise<{ result: Result; engine: string }>(async (resolve, reject) => {
40
+ // Stagger start: engine 0 starts immediately, engine 1 after 500ms, etc.
41
+ if (i > 0) await delay(i * staggerDelayMs);
42
+ if (controller.signal.aborted) return reject(new Error('aborted'));
43
+
44
+ try {
45
+ const result = await engine.execute(input);
46
+ if (isValid(result)) {
47
+ controller.abort(); // cancel slower engines
48
+ resolve({ result, engine: engine.name });
49
+ } else {
50
+ reject(new Error(`${engine.name}: invalid result`));
51
+ }
52
+ } catch (err) {
53
+ reject(err);
54
+ }
55
+ })
56
+ );
57
+
58
+ return Promise.any(races);
59
+ }
60
+ ```
61
+
62
+ **Step 2 — Implement transform pipeline**
63
+ Chain transforms that process data sequentially:
64
+ ```typescript
65
+ type Transformer<T> = (data: T, context: PipelineContext) => Promise<T>;
66
+
67
+ async function runPipeline<T>(
68
+ data: T,
69
+ transformers: Transformer<T>[],
70
+ onProgress: (stage: string, pct: number) => void
71
+ ): Promise<T> {
72
+ let current = data;
73
+ for (let i = 0; i < transformers.length; i++) {
74
+ onProgress(transformers[i].name, (i / transformers.length) * 100);
75
+ current = await transformers[i](current, context);
76
+ }
77
+ onProgress('complete', 100);
78
+ return current;
79
+ }
80
+ ```
81
+
82
+ **Step 3 — Stream progress via SSE**
83
+ Real-time progress from worker to client:
84
+ ```typescript
85
+ // Worker side: publish progress to Redis pub/sub
86
+ async function publishProgress(jobId: string, stage: string, pct: number) {
87
+ await redis.publish(`job:${jobId}:progress`, JSON.stringify({ stage, pct, ts: Date.now() }));
88
+ }
89
+
90
+ // API side: SSE endpoint
91
+ app.get('/jobs/:id/progress', async (req, res) => {
92
+ res.setHeader('Content-Type', 'text/event-stream');
93
+ res.setHeader('Cache-Control', 'no-cache');
94
+ res.setHeader('Connection', 'keep-alive');
95
+
96
+ const subscriber = redis.duplicate();
97
+ await subscriber.subscribe(`job:${req.params.id}:progress`);
98
+
99
+ subscriber.on('message', (_channel, message) => {
100
+ res.write(`data: ${message}\n\n`);
101
+ });
102
+
103
+ req.on('close', () => subscriber.unsubscribe());
104
+ });
105
+ ```
106
+
107
+ **Step 4 — Two-tier concurrency control**
108
+ ```typescript
109
+ // Team-level: limit concurrent jobs per team
110
+ async function canEnqueue(teamId: string): Promise<boolean> {
111
+ const active = await redis.zcard(`team:${teamId}:active`);
112
+ const limit = await getTeamConcurrencyLimit(teamId);
113
+ return active < limit;
114
+ }
115
+
116
+ // Job-level: track active jobs with TTL (auto-cleanup on crash)
117
+ async function markActive(teamId: string, jobId: string) {
118
+ await redis.zadd(`team:${teamId}:active`, Date.now(), jobId);
119
+ await redis.expire(`team:${teamId}:active`, 3600); // 1h TTL safety net
120
+ }
121
+
122
+ async function markComplete(teamId: string, jobId: string) {
123
+ await redis.zrem(`team:${teamId}:active`, jobId);
124
+ }
125
+ ```
126
+
127
+ **Step 5 — Dynamic credit billing**
128
+ ```typescript
129
+ interface CreditCost {
130
+ base: number;
131
+ engineMultiplier: number; // stealth proxy = 4x
132
+ formatMultiplier: number; // JSON extraction = 5x
133
+ extras: number; // per-page for PDFs, per-territory for pricing
134
+ }
135
+
136
+ function calculateCredits(job: CompletedJob): number {
137
+ let cost = job.cost.base;
138
+ cost *= job.cost.engineMultiplier;
139
+ cost *= job.cost.formatMultiplier;
140
+ cost += job.cost.extras;
141
+ return Math.ceil(cost);
142
+ }
143
+ ```
144
+
145
+ **Step 6 — Dead letter queue with retry classification**
146
+ ```typescript
147
+ interface FailedJob {
148
+ id: string;
149
+ error: string;
150
+ errorCode: 'TRANSIENT' | 'PERMANENT' | 'TIMEOUT' | 'RATE_LIMITED';
151
+ attempts: number;
152
+ stageTiming: Record<string, number>; // per-stage perf data
153
+ }
154
+
155
+ // Retry only transient failures; permanent goes to dead letter
156
+ function shouldRetry(job: FailedJob): boolean {
157
+ if (job.errorCode === 'PERMANENT') return false;
158
+ if (job.attempts >= 3) return false;
159
+ return true;
160
+ }
161
+ ```
162
+
163
+ #### Example
164
+
165
+ ```typescript
166
+ // Complete async pipeline for document processing
167
+ const docPipeline = createPipeline({
168
+ engines: [
169
+ { name: 'native-parser', quality: 100, costMultiplier: 1, execute: nativeParse },
170
+ { name: 'llm-extraction', quality: 80, costMultiplier: 5, execute: llmExtract },
171
+ { name: 'ocr-fallback', quality: 50, costMultiplier: 3, execute: ocrExtract },
172
+ ],
173
+ transforms: [
174
+ cleanHTML,
175
+ extractMetadata,
176
+ convertToMarkdown,
177
+ generateSummary,
178
+ indexForSearch,
179
+ ],
180
+ concurrency: { perTeam: 10, perJob: 3 },
181
+ billing: { base: 1, jsonFormat: 5 },
182
+ deadLetter: { maxRetries: 3, alertThreshold: 10 },
183
+ });
184
+
185
+ // Enqueue
186
+ const jobId = await docPipeline.enqueue(teamId, { url, format: 'json' });
187
+
188
+ // Stream progress
189
+ const progress = docPipeline.streamProgress(jobId);
190
+ for await (const update of progress) {
191
+ console.log(`${update.stage}: ${update.pct}%`);
192
+ }
193
+ ```
@@ -0,0 +1,97 @@
1
+ ---
2
+ name: "auth-patterns"
3
+ pack: "@rune/backend"
4
+ description: "Authentication and authorization patterns — JWT, OAuth 2.0 / OIDC, passkeys/WebAuthn, session management, RBAC, API key management, MFA flows."
5
+ model: sonnet
6
+ tools: [Read, Edit, Write, Grep, Glob, Bash]
7
+ ---
8
+
9
+ # auth-patterns
10
+
11
+ Authentication and authorization patterns — JWT, OAuth 2.0 / OIDC, passkeys/WebAuthn, session management, RBAC, API key management, MFA flows.
12
+
13
+ #### Workflow
14
+
15
+ **Step 1 — Detect auth implementation**
16
+ Use Grep to find auth-related code: `jwt.sign`, `jwt.verify`, `bcrypt`, `passport`, `next-auth`, `lucia`, `cookie`, `session`, `Bearer`, `x-api-key`, `WebAuthn`, `passkey`. Read auth middleware and login/register handlers to understand the current approach.
17
+
18
+ **Step 2 — Audit security posture**
19
+ Check for: tokens stored in localStorage (XSS risk → use httpOnly cookies), missing refresh token rotation, JWT without expiry, password hashing without salt rounds check, missing CSRF protection on cookie-based auth, hardcoded secrets. Flag each with severity and specific fix.
20
+
21
+ **Step 3 — Emit secure auth flow**
22
+ Based on detected framework (Express, Fastify, Next.js, etc.), emit the corrected auth flow: access token (short-lived, 15min) + refresh token (httpOnly cookie, 7d, rotation on use), proper password hashing (bcrypt rounds ≥ 12), RBAC middleware with role hierarchy.
23
+
24
+ **Step 4 — OAuth 2.0 / OIDC integration**
25
+ Emit OAuth 2.0 authorization code flow with PKCE (required for public clients). Support Google, GitHub, or custom OIDC provider. Key points: validate `state` parameter to prevent CSRF, validate `id_token` signature and `aud`/`iss` claims, exchange code server-side (never client-side), store provider `sub` as stable user identifier. Use `openid-client` (Node.js) or `authlib` (Python) — never hand-roll token exchange.
26
+
27
+ **Step 5 — API key management and passkeys**
28
+ For API keys: generate with `crypto.randomBytes(32).toString('base64url')`, store hashed (`sha256` is sufficient — no need for bcrypt, keys are long), never store plaintext after initial display. Add scopes (read-only vs read-write), per-key rate limits, and rotation endpoint. For passkeys/WebAuthn: emit registration and authentication ceremonies using `@simplewebauthn/server`. WebAuthn is the correct long-term replacement for passwords — emit as opt-in upgrade path. Stateless vs stateful tradeoff: JWT = stateless, easy to scale horizontally, hard to revoke; sessions = stateful, easy to revoke, requires sticky sessions or shared store (Redis). Recommend JWT + token blacklist on logout for most cases; sessions for admin panels where immediate revocation matters.
29
+
30
+ #### Example
31
+
32
+ ```typescript
33
+ // BEFORE: JWT in localStorage, no refresh, no expiry
34
+ const token = jwt.sign({ userId: user.id }, SECRET);
35
+ res.json({ token });
36
+
37
+ // AFTER: short-lived access + httpOnly refresh cookie with rotation
38
+ const accessToken = jwt.sign(
39
+ { sub: user.id, role: user.role },
40
+ ACCESS_SECRET,
41
+ { expiresIn: '15m' }
42
+ );
43
+ const refreshToken = jwt.sign(
44
+ { sub: user.id, jti: crypto.randomUUID() },
45
+ REFRESH_SECRET,
46
+ { expiresIn: '7d' }
47
+ );
48
+ await tokenStore.save(refreshToken, user.id); // rotation tracking — invalidate old on reuse
49
+
50
+ res.cookie('refresh_token', refreshToken, {
51
+ httpOnly: true, secure: true, sameSite: 'strict',
52
+ maxAge: 7 * 24 * 60 * 60 * 1000,
53
+ });
54
+ res.json({ access_token: accessToken, expires_in: 900 });
55
+
56
+ // API key management
57
+ const generateApiKey = async (userId: string, scopes: string[]): Promise<{ key: string; keyId: string }> => {
58
+ const rawKey = `rk_${crypto.randomBytes(32).toString('base64url')}`;
59
+ const keyHash = crypto.createHash('sha256').update(rawKey).digest('hex');
60
+ const keyId = crypto.randomUUID();
61
+ await db.apiKey.create({ data: { id: keyId, userId, keyHash, scopes, createdAt: new Date() } });
62
+ return { key: rawKey, keyId }; // rawKey shown ONCE — never stored plaintext
63
+ };
64
+
65
+ const authenticateApiKey = async (req, res, next) => {
66
+ const raw = req.headers['x-api-key'];
67
+ if (!raw) return next(); // fallback to JWT auth
68
+ const hash = crypto.createHash('sha256').update(raw).digest('hex');
69
+ const apiKey = await db.apiKey.findUnique({ where: { keyHash: hash } });
70
+ if (!apiKey || apiKey.revokedAt) return res.status(401).json({ error: { code: 'INVALID_API_KEY' } });
71
+ req.user = { id: apiKey.userId, scopes: apiKey.scopes };
72
+ next();
73
+ };
74
+
75
+ // OAuth 2.0 with PKCE (using openid-client)
76
+ import { generators, Issuer } from 'openid-client';
77
+
78
+ const googleIssuer = await Issuer.discover('https://accounts.google.com');
79
+ const client = new googleIssuer.Client({ client_id: GOOGLE_CLIENT_ID, redirect_uris: [CALLBACK_URL], response_types: ['code'] });
80
+
81
+ app.get('/auth/google', (req, res) => {
82
+ const codeVerifier = generators.codeVerifier();
83
+ const codeChallenge = generators.codeChallenge(codeVerifier);
84
+ const state = generators.state();
85
+ req.session.codeVerifier = codeVerifier;
86
+ req.session.state = state;
87
+ res.redirect(client.authorizationUrl({ scope: 'openid email profile', code_challenge: codeChallenge, code_challenge_method: 'S256', state }));
88
+ });
89
+
90
+ app.get('/auth/google/callback', async (req, res) => {
91
+ const params = client.callbackParams(req);
92
+ const tokens = await client.callback(CALLBACK_URL, params, { code_verifier: req.session.codeVerifier, state: req.session.state });
93
+ const claims = tokens.claims(); // validated: iss, aud, exp
94
+ const user = await userRepo.upsertByProvider('google', claims.sub, claims.email);
95
+ // issue internal JWT...
96
+ });
97
+ ```
@@ -0,0 +1,133 @@
1
+ ---
2
+ name: "background-jobs"
3
+ pack: "@rune/backend"
4
+ description: "Queue-based async processing — BullMQ (Node.js), job patterns, retry strategies, idempotency, dead letter queues, monitoring."
5
+ model: sonnet
6
+ tools: [Read, Edit, Write, Grep, Glob, Bash]
7
+ ---
8
+
9
+ # background-jobs
10
+
11
+ Queue-based async processing — BullMQ (Node.js), job patterns, retry strategies, idempotency, dead letter queues, monitoring.
12
+
13
+ #### Workflow
14
+
15
+ **Step 1 — Identify async operations**
16
+ Scan route handlers and service functions for operations that: (a) take > 200ms (PDF generation, image resizing, report aggregation), (b) are non-user-facing (email sending, webhook delivery, analytics events), (c) can tolerate eventual consistency (data sync, cache warming, notification dispatch). Flag these as candidates for background jobs. Output a classification: fire-and-forget vs delayed vs scheduled (cron) vs fan-out.
17
+
18
+ **Step 2 — Choose queue system**
19
+ Node.js: BullMQ (Redis-backed, TypeScript-native, built-in retry/delay/priority/rate-limiting — recommended). Python: Celery + Redis/RabbitMQ broker (mature, distributed workers, beat scheduler for cron). For very simple use cases (single server, low volume): `node-cron` + in-process worker. Avoid in-process queues in production — they die with the process and lose jobs.
20
+
21
+ **Step 3 — Implement job with retry strategy**
22
+ Emit job producer (enqueue) and worker (processor) as separate files. Retry strategy: exponential backoff with jitter (`attempts: 5, backoff: { type: 'exponential', delay: 1000 }`). Idempotency: every job MUST have an idempotency key — use a deterministic ID from the operation (e.g., `email:welcome:${userId}` not a random UUID). This ensures duplicate enqueues (from retries, double-clicks) process exactly once. Dead letter queue: after max retries, move job to a `{queue-name}:failed` queue for inspection and manual replay — never silently drop.
23
+
24
+ **Step 4 — Add monitoring and alerting**
25
+ BullMQ Board or Bull Dashboard for visual queue monitoring. Emit metrics: queue depth (jobs waiting), processing rate (jobs/sec), failure rate (failed/total). Alert when: queue depth > threshold (workers not keeping up), failure rate > 5% (systematic error in processor), job age > expected TTL (stuck job). Use BullMQ events (`queue.on('failed', ...)`) to push metrics to Prometheus or Datadog.
26
+
27
+ **Step 5 — Handle dead letters**
28
+ Emit dead letter inspection endpoint: list failed jobs with error reason, retry count, and last error. Emit replay endpoint: re-enqueue a specific failed job with a fresh retry budget. Purge endpoint: clear dead letter queue after investigation. Add alerting on dead letter queue depth > 0 for critical job types (payment processing, compliance logging).
29
+
30
+ #### Example
31
+
32
+ ```typescript
33
+ // BullMQ setup with TypeScript — producer + worker
34
+ import { Queue, Worker, Job } from 'bullmq';
35
+
36
+ const connection = { host: REDIS_HOST, port: 6379 };
37
+
38
+ // Job type definitions
39
+ interface EmailJob { to: string; template: string; data: Record<string, unknown> }
40
+ interface PdfJob { reportId: string; userId: string; format: 'pdf' | 'xlsx' }
41
+
42
+ // Producers
43
+ export const emailQueue = new Queue<EmailJob>('email', { connection });
44
+ export const pdfQueue = new Queue<PdfJob>('pdf', { connection });
45
+
46
+ // Enqueue with idempotency key (jobId = idempotent identifier)
47
+ export const sendWelcomeEmail = (userId: string, email: string) =>
48
+ emailQueue.add('welcome', { to: email, template: 'welcome', data: { userId } }, {
49
+ jobId: `email:welcome:${userId}`, // prevents duplicate welcome emails
50
+ attempts: 3,
51
+ backoff: { type: 'exponential', delay: 2_000 },
52
+ removeOnComplete: { count: 1000 }, // keep last 1000 completed for audit
53
+ removeOnFail: false, // keep all failed for dead letter review
54
+ });
55
+
56
+ // Scheduled/delayed job
57
+ export const sendReminderEmail = (userId: string, delayMs: number) =>
58
+ emailQueue.add('reminder', { to: userId, template: 'reminder', data: {} }, {
59
+ delay: delayMs,
60
+ attempts: 5,
61
+ backoff: { type: 'exponential', delay: 5_000 },
62
+ });
63
+
64
+ // Worker processor with error handling
65
+ const emailWorker = new Worker<EmailJob>('email', async (job: Job<EmailJob>) => {
66
+ const { to, template, data } = job.data;
67
+ // Validate job data — serialized payload may be stale
68
+ if (!to || !template) throw new Error(`Invalid job payload: ${JSON.stringify(job.data)}`);
69
+ await emailService.send({ to, template, data });
70
+ // Return value is stored in job.returnvalue for audit
71
+ return { sentAt: new Date().toISOString() };
72
+ }, {
73
+ connection,
74
+ concurrency: 10, // process up to 10 emails in parallel
75
+ limiter: { max: 100, duration: 60_000 }, // rate limit: 100/min
76
+ });
77
+
78
+ emailWorker.on('failed', async (job, err) => {
79
+ logger.error({ jobId: job?.id, queue: 'email', error: err.message, attempts: job?.attemptsMade });
80
+ if (job?.attemptsMade >= job?.opts.attempts!) {
81
+ // max retries exhausted → alert
82
+ await alerting.notify(`Dead letter: email job ${job.id} failed after ${job.attemptsMade} attempts`);
83
+ }
84
+ });
85
+
86
+ // Fan-out pattern: one job enqueues many children
87
+ const fanOutNotification = async (eventId: string, userIds: string[]) => {
88
+ const jobs = userIds.map(userId => ({
89
+ name: 'notify',
90
+ data: { userId, eventId },
91
+ opts: {
92
+ jobId: `notify:${eventId}:${userId}`,
93
+ attempts: 3,
94
+ backoff: { type: 'exponential', delay: 1_000 },
95
+ },
96
+ }));
97
+ await notificationQueue.addBulk(jobs);
98
+ };
99
+
100
+ // Dead letter inspection API
101
+ app.get('/admin/jobs/failed', authenticate, authorize('admin'), async (req, res) => {
102
+ const failed = await emailQueue.getFailed(0, 50);
103
+ res.json({ count: failed.length, jobs: failed.map(j => ({ id: j.id, data: j.data, reason: j.failedReason, attempts: j.attemptsMade })) });
104
+ });
105
+
106
+ app.post('/admin/jobs/:id/retry', authenticate, authorize('admin'), async (req, res) => {
107
+ const job = await emailQueue.getJob(req.params.id);
108
+ if (!job) return res.status(404).json({ error: { code: 'NOT_FOUND' } });
109
+ await job.retry();
110
+ res.json({ status: 'retried' });
111
+ });
112
+
113
+ // Celery equivalent (Python) — minimal pattern
114
+ # tasks.py
115
+ from celery import Celery
116
+ from celery.utils.log import get_task_logger
117
+
118
+ app = Celery('tasks', broker=REDIS_URL, backend=REDIS_URL)
119
+ app.conf.task_acks_late = True # at-least-once delivery
120
+ app.conf.task_reject_on_worker_lost = True # requeue on worker crash
121
+ logger = get_task_logger(__name__)
122
+
123
+ @app.task(bind=True, max_retries=5, default_retry_delay=60)
124
+ def send_email(self, to: str, template: str, data: dict) -> dict:
125
+ try:
126
+ result = email_service.send(to=to, template=template, data=data)
127
+ return {'sent_at': result.timestamp.isoformat()}
128
+ except TransientError as exc:
129
+ raise self.retry(exc=exc, countdown=2 ** self.request.retries * 60)
130
+ except PermanentError as exc:
131
+ logger.error(f"Permanent failure for {to}: {exc}")
132
+ raise # no retry — goes to dead letter
133
+ ```
@@ -0,0 +1,108 @@
1
+ ---
2
+ name: "caching-patterns"
3
+ pack: "@rune/backend"
4
+ description: "Caching strategies for backend applications — in-memory LRU, Redis distributed cache, CDN/edge cache, browser cache headers, invalidation, and stampede prevention."
5
+ model: sonnet
6
+ tools: [Read, Edit, Write, Grep, Glob, Bash]
7
+ ---
8
+
9
+ # caching-patterns
10
+
11
+ Caching strategies for backend applications — in-memory LRU, Redis distributed cache, CDN/edge cache, browser cache headers, invalidation, and stampede prevention.
12
+
13
+ #### Workflow
14
+
15
+ **Step 1 — Identify cacheable endpoints**
16
+ Scan routes for: (a) read-heavy endpoints called frequently with the same inputs (user profile, product catalog, config lookups), (b) expensive computations (aggregations, report generation), (c) external API calls that are rate-limited or slow. Flag endpoints that mutate state as NOT cacheable at the response level (cache the data layer instead). Output a cacheable/non-cacheable classification per endpoint.
17
+
18
+ **Step 2 — Select cache layer**
19
+ Choose layer based on access pattern: in-memory (node-cache, LRU-cache) for single-process data with sub-millisecond access and low cardinality; Redis for distributed cache shared across multiple server instances or processes; CDN (Cloudflare, Fastly) for public, user-agnostic responses (marketing pages, public API responses); browser cache (`Cache-Control` headers) for static assets and safe GET responses. Hybrid: in-memory L1 + Redis L2 for hot-path data that justifies two-layer lookup.
20
+
21
+ **Step 3 — Implement cache pattern**
22
+ Cache-aside (most common): application checks cache first, on miss fetches from DB, writes to cache. Write-through: write to cache and DB together on every write (cache always warm, higher write latency). Write-behind (write-back): write to cache immediately, flush to DB asynchronously (lowest write latency, risk of data loss on crash). Read-through: cache sits in front of DB, handles miss transparently (simpler app code, less control). For most web APIs: cache-aside for reads + TTL-based expiry is the correct default.
23
+
24
+ **Step 4 — Add invalidation strategy**
25
+ TTL-based: set appropriate TTL per data type (user session: match auth token TTL; product catalog: 5–15min; config: 1hr). Event-driven: on mutation, publish event to Redis pub/sub, cache subscribers delete affected keys. Versioned keys: `cache:user:v3:{id}` — bump version in config to invalidate all users atomically. Tag-based: associate keys with tags (`tag:user:123`), delete all keys for a tag on mutation. Stale-while-revalidate: serve stale data immediately, refresh in background — valid for data where slight staleness is acceptable (leaderboards, stats). Emit invalidation hook alongside every write operation.
26
+
27
+ **Step 5 — Monitor hit/miss ratio**
28
+ Instrument cache calls to emit metrics: hit count, miss count, eviction count, cache size. Redis provides `INFO stats` — parse `keyspace_hits` and `keyspace_misses`. Target hit ratio > 80% for hot-path caches; < 50% indicates wrong key granularity or TTL too short. Alert on sudden hit ratio drop (invalidation bug) or memory > 80% of `maxmemory` (eviction risk).
29
+
30
+ #### Example
31
+
32
+ ```typescript
33
+ // Redis cache-aside middleware for Express/Fastify
34
+ import { Redis } from 'ioredis';
35
+ const redis = new Redis(REDIS_URL);
36
+
37
+ const cacheMiddleware = (ttlSeconds: number, keyFn?: (req) => string) =>
38
+ async (req, res, next) => {
39
+ const key = keyFn ? keyFn(req) : `cache:${req.method}:${req.originalUrl}`;
40
+ const cached = await redis.get(key);
41
+ if (cached) {
42
+ res.setHeader('X-Cache', 'HIT');
43
+ return res.json(JSON.parse(cached));
44
+ }
45
+ const originalJson = res.json.bind(res);
46
+ res.json = (data) => {
47
+ // Only cache successful responses
48
+ if (res.statusCode < 400) redis.setex(key, ttlSeconds, JSON.stringify(data));
49
+ res.setHeader('X-Cache', 'MISS');
50
+ return originalJson(data);
51
+ };
52
+ next();
53
+ };
54
+
55
+ // Usage: cache product list for 5 minutes
56
+ app.get('/products', cacheMiddleware(300), async (req, res) => { /* handler */ });
57
+
58
+ // Cache stampede prevention: mutex lock on cache miss
59
+ const getWithLock = async <T>(key: string, fetchFn: () => Promise<T>, ttl: number): Promise<T> => {
60
+ const cached = await redis.get(key);
61
+ if (cached) return JSON.parse(cached);
62
+
63
+ const lockKey = `lock:${key}`;
64
+ const lock = await redis.set(lockKey, '1', 'EX', 10, 'NX'); // 10s lock
65
+ if (!lock) {
66
+ // Another process is fetching — wait briefly and retry
67
+ await new Promise(r => setTimeout(r, 100));
68
+ return getWithLock(key, fetchFn, ttl); // retry (max ~10 cycles within 10s lock)
69
+ }
70
+
71
+ try {
72
+ const data = await fetchFn();
73
+ await redis.setex(key, ttl, JSON.stringify(data));
74
+ return data;
75
+ } finally {
76
+ await redis.del(lockKey);
77
+ }
78
+ };
79
+
80
+ // Event-driven invalidation with Redis pub/sub
81
+ const invalidateOnMutation = async (userId: string) => {
82
+ await redis.del(`cache:user:${userId}`);
83
+ await redis.publish('cache:invalidate', JSON.stringify({ type: 'user', id: userId }));
84
+ };
85
+
86
+ // Cache-Control headers for browser/CDN caching
87
+ app.get('/products', (req, res) => {
88
+ res.setHeader('Cache-Control', 'public, max-age=300, stale-while-revalidate=60');
89
+ // ^ CDN caches 5min, serves stale for extra 60s while revalidating in background
90
+ res.json(products);
91
+ });
92
+
93
+ app.get('/user/profile', authenticate, (req, res) => {
94
+ res.setHeader('Cache-Control', 'private, max-age=60'); // user-specific, browser only
95
+ res.json(profile);
96
+ });
97
+
98
+ // In-memory LRU cache for single-process hot data
99
+ import LRU from 'lru-cache';
100
+ const configCache = new LRU<string, unknown>({ max: 500, ttl: 60_000 }); // 500 entries, 1min TTL
101
+
102
+ const getConfig = async (key: string) => {
103
+ if (configCache.has(key)) return configCache.get(key);
104
+ const value = await db.config.findUnique({ where: { key } });
105
+ configCache.set(key, value);
106
+ return value;
107
+ };
108
+ ```