@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,193 +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
|
-
```
|
|
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
|
+
```
|
|
@@ -1,97 +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
|
-
```
|
|
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
|
+
```
|