mover-os 4.0.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.
Potentially problematic release.
This version of mover-os might be problematic. Click here for more details.
- package/README.md +201 -0
- package/install.js +1424 -0
- package/package.json +35 -0
- package/src/hooks/context-staleness.sh +46 -0
- package/src/hooks/dirty-tree-guard.sh +33 -0
- package/src/hooks/engine-protection.sh +43 -0
- package/src/hooks/git-safety.sh +47 -0
- package/src/hooks/pre-compact-backup.sh +177 -0
- package/src/hooks/session-log-reminder.sh +64 -0
- package/src/skills/THIRD-PARTY-LICENSES.md +53 -0
- package/src/skills/agent-code-reviewer/SKILL.md +41 -0
- package/src/skills/agent-content-researcher/SKILL.md +59 -0
- package/src/skills/agent-research-analyst/SKILL.md +53 -0
- package/src/skills/agent-security-auditor/SKILL.md +42 -0
- package/src/skills/agent-strategy-analyst/SKILL.md +54 -0
- package/src/skills/defuddle/SKILL.md +41 -0
- package/src/skills/find-bugs/SKILL.md +75 -0
- package/src/skills/find-skills/SKILL.md +133 -0
- package/src/skills/frontend-design/LICENSE.txt +177 -0
- package/src/skills/frontend-design/SKILL.md +42 -0
- package/src/skills/human-writer/SKILL.md +185 -0
- package/src/skills/json-canvas/SKILL.md +656 -0
- package/src/skills/marketingskills/.claude-plugin/marketplace.json +45 -0
- package/src/skills/marketingskills/README.md +204 -0
- package/src/skills/marketingskills/skills/ab-test-setup/SKILL.md +508 -0
- package/src/skills/marketingskills/skills/analytics-tracking/SKILL.md +539 -0
- package/src/skills/marketingskills/skills/competitor-alternatives/SKILL.md +750 -0
- package/src/skills/marketingskills/skills/copy-editing/SKILL.md +439 -0
- package/src/skills/marketingskills/skills/copywriting/SKILL.md +455 -0
- package/src/skills/marketingskills/skills/email-sequence/SKILL.md +925 -0
- package/src/skills/marketingskills/skills/form-cro/SKILL.md +425 -0
- package/src/skills/marketingskills/skills/free-tool-strategy/SKILL.md +576 -0
- package/src/skills/marketingskills/skills/launch-strategy/SKILL.md +344 -0
- package/src/skills/marketingskills/skills/marketing-ideas/SKILL.md +565 -0
- package/src/skills/marketingskills/skills/marketing-psychology/SKILL.md +451 -0
- package/src/skills/marketingskills/skills/onboarding-cro/SKILL.md +433 -0
- package/src/skills/marketingskills/skills/page-cro/SKILL.md +334 -0
- package/src/skills/marketingskills/skills/paid-ads/SKILL.md +551 -0
- package/src/skills/marketingskills/skills/paywall-upgrade-cro/SKILL.md +570 -0
- package/src/skills/marketingskills/skills/popup-cro/SKILL.md +449 -0
- package/src/skills/marketingskills/skills/pricing-strategy/SKILL.md +710 -0
- package/src/skills/marketingskills/skills/programmatic-seo/SKILL.md +626 -0
- package/src/skills/marketingskills/skills/referral-program/SKILL.md +602 -0
- package/src/skills/marketingskills/skills/schema-markup/SKILL.md +596 -0
- package/src/skills/marketingskills/skills/seo-audit/SKILL.md +384 -0
- package/src/skills/marketingskills/skills/signup-flow-cro/SKILL.md +355 -0
- package/src/skills/marketingskills/skills/social-content/SKILL.md +807 -0
- package/src/skills/obsidian-bases/SKILL.md +651 -0
- package/src/skills/obsidian-cli/SKILL.md +103 -0
- package/src/skills/obsidian-markdown/SKILL.md +620 -0
- package/src/skills/react-best-practices/SKILL.md +136 -0
- package/src/skills/refactoring/SKILL.md +119 -0
- package/src/skills/seo-content-writer/SKILL.md +661 -0
- package/src/skills/seo-content-writer/references/content-structure-templates.md +875 -0
- package/src/skills/seo-content-writer/references/title-formulas.md +339 -0
- package/src/skills/skill-creator/LICENSE.txt +202 -0
- package/src/skills/skill-creator/SKILL.md +357 -0
- package/src/skills/skill-creator/references/output-patterns.md +82 -0
- package/src/skills/skill-creator/references/workflows.md +28 -0
- package/src/skills/skill-creator/scripts/init_skill.py +303 -0
- package/src/skills/skill-creator/scripts/package_skill.py +110 -0
- package/src/skills/skill-creator/scripts/quick_validate.py +103 -0
- package/src/skills/systematic-debugging/CREATION-LOG.md +119 -0
- package/src/skills/systematic-debugging/SKILL.md +296 -0
- package/src/skills/systematic-debugging/condition-based-waiting-example.ts +158 -0
- package/src/skills/systematic-debugging/condition-based-waiting.md +115 -0
- package/src/skills/systematic-debugging/defense-in-depth.md +122 -0
- package/src/skills/systematic-debugging/find-polluter.sh +63 -0
- package/src/skills/systematic-debugging/root-cause-tracing.md +169 -0
- package/src/skills/systematic-debugging/test-academic.md +14 -0
- package/src/skills/systematic-debugging/test-pressure-1.md +58 -0
- package/src/skills/systematic-debugging/test-pressure-2.md +68 -0
- package/src/skills/systematic-debugging/test-pressure-3.md +69 -0
- package/src/skills/ui-ux-pro-max/SKILL.md +386 -0
- package/src/skills/webhook-handler-patterns/SKILL.md +81 -0
- package/src/skills/webhook-handler-patterns/references/error-handling.md +299 -0
- package/src/skills/webhook-handler-patterns/references/frameworks/express.md +295 -0
- package/src/skills/webhook-handler-patterns/references/frameworks/fastapi.md +415 -0
- package/src/skills/webhook-handler-patterns/references/frameworks/nextjs.md +331 -0
- package/src/skills/webhook-handler-patterns/references/handler-sequence.md +51 -0
- package/src/skills/webhook-handler-patterns/references/idempotency.md +227 -0
- package/src/skills/webhook-handler-patterns/references/retry-logic.md +261 -0
- package/src/structure/00_Inbox/.gitkeep +0 -0
- package/src/structure/00_Inbox/Quick_Capture.md +5 -0
- package/src/structure/01_Projects/.gitkeep +0 -0
- package/src/structure/01_Projects/_Template Project/plan.md +55 -0
- package/src/structure/01_Projects/_Template Project/project_brief.md +45 -0
- package/src/structure/01_Projects/_Template Project/project_state.md +19 -0
- package/src/structure/02_Areas/Engine/Active_Context.md +126 -0
- package/src/structure/02_Areas/Engine/Auto_Learnings.md +36 -0
- package/src/structure/02_Areas/Engine/Dailies/.gitkeep +0 -0
- package/src/structure/02_Areas/Engine/Daily_Template.md +130 -0
- package/src/structure/02_Areas/Engine/Goals.md +65 -0
- package/src/structure/02_Areas/Engine/Identity_Prime_template.md +86 -0
- package/src/structure/02_Areas/Engine/Metrics_Log.md +45 -0
- package/src/structure/02_Areas/Engine/Monthly Reviews/.gitkeep +0 -0
- package/src/structure/02_Areas/Engine/Mover_Dossier.md +120 -0
- package/src/structure/02_Areas/Engine/Quarterly Reviews/.gitkeep +0 -0
- package/src/structure/02_Areas/Engine/Someday_Maybe.md +51 -0
- package/src/structure/02_Areas/Engine/Strategy_template.md +65 -0
- package/src/structure/02_Areas/Engine/Weekly Reviews/.gitkeep +0 -0
- package/src/structure/03_Library/Cheatsheets/.gitkeep +0 -0
- package/src/structure/03_Library/Entities/Decisions/_TEMPLATE.md +38 -0
- package/src/structure/03_Library/Entities/Entities MOC.md +49 -0
- package/src/structure/03_Library/Entities/Organizations/_TEMPLATE.md +28 -0
- package/src/structure/03_Library/Entities/People/_TEMPLATE.md +31 -0
- package/src/structure/03_Library/Entities/Places/_TEMPLATE.md +26 -0
- package/src/structure/03_Library/Inputs/.gitkeep +0 -0
- package/src/structure/03_Library/Inputs/Archive/.gitkeep +0 -0
- package/src/structure/03_Library/Inputs/Articles/.gitkeep +0 -0
- package/src/structure/03_Library/Inputs/Articles/_TEMPLATE.md +29 -0
- package/src/structure/03_Library/Inputs/Books/.gitkeep +0 -0
- package/src/structure/03_Library/Inputs/Books/_TEMPLATE.md +41 -0
- package/src/structure/03_Library/Inputs/Inputs MOC.md +40 -0
- package/src/structure/03_Library/Inputs/Videos/.gitkeep +0 -0
- package/src/structure/03_Library/Inputs/Videos/_TEMPLATE.md +29 -0
- package/src/structure/03_Library/MOCs/.gitkeep +0 -0
- package/src/structure/03_Library/Misc/.gitkeep +0 -0
- package/src/structure/03_Library/Principles/.gitkeep +0 -0
- package/src/structure/03_Library/Principles/Naval_Leverage.md +65 -0
- package/src/structure/03_Library/SOPs/Mover_OS_Architecture.md +74 -0
- package/src/structure/03_Library/SOPs/Tech_Doctrine.md +123 -0
- package/src/structure/03_Library/SOPs/Tech_Stack.md +55 -0
- package/src/structure/03_Library/SOPs/Zone_Operating.md +58 -0
- package/src/structure/04_Archives/.gitkeep +0 -0
- package/src/system/AI_INSTALL_MANIFEST.md +219 -0
- package/src/system/Mover_Global_Rules.md +646 -0
- package/src/system/V4_CONTEXT.md +223 -0
- package/src/workflows/analyse-day.md +376 -0
- package/src/workflows/context.md +24 -0
- package/src/workflows/debrief.md +199 -0
- package/src/workflows/debug-resistance.md +181 -0
- package/src/workflows/harvest.md +240 -0
- package/src/workflows/history.md +247 -0
- package/src/workflows/ignite.md +696 -0
- package/src/workflows/init-plan.md +16 -0
- package/src/workflows/log.md +314 -0
- package/src/workflows/morning.md +209 -0
- package/src/workflows/overview.md +203 -0
- package/src/workflows/pivot-strategy.md +218 -0
- package/src/workflows/plan-tomorrow.md +286 -0
- package/src/workflows/primer.md +209 -0
- package/src/workflows/project-notes.md +17 -0
- package/src/workflows/reboot.md +201 -0
- package/src/workflows/refactor-plan.md +135 -0
- package/src/workflows/review-week.md +537 -0
- package/src/workflows/setup.md +387 -0
- package/src/workflows/update.md +411 -0
- package/src/workflows/walkthrough.md +259 -0
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
# FastAPI Webhook Patterns
|
|
2
|
+
|
|
3
|
+
## Reading the Raw Body
|
|
4
|
+
|
|
5
|
+
FastAPI's `Request` object provides access to the raw request body for signature verification.
|
|
6
|
+
|
|
7
|
+
### Basic Pattern
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
from fastapi import FastAPI, Request, HTTPException
|
|
11
|
+
|
|
12
|
+
app = FastAPI()
|
|
13
|
+
|
|
14
|
+
@app.post("/webhooks/stripe")
|
|
15
|
+
async def stripe_webhook(request: Request):
|
|
16
|
+
# Get raw body for signature verification
|
|
17
|
+
raw_body = await request.body()
|
|
18
|
+
|
|
19
|
+
# Get headers
|
|
20
|
+
signature = request.headers.get("stripe-signature")
|
|
21
|
+
|
|
22
|
+
# Verify and process...
|
|
23
|
+
|
|
24
|
+
return {"received": True}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Important: Read Body Once
|
|
28
|
+
|
|
29
|
+
The request body can only be read once. If you need both raw and parsed body:
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
@app.post("/webhooks/stripe")
|
|
33
|
+
async def stripe_webhook(request: Request):
|
|
34
|
+
# Read raw body first
|
|
35
|
+
raw_body = await request.body()
|
|
36
|
+
|
|
37
|
+
# Parse JSON manually after verification
|
|
38
|
+
import json
|
|
39
|
+
payload = json.loads(raw_body)
|
|
40
|
+
|
|
41
|
+
# DON'T do this - body already consumed
|
|
42
|
+
# body2 = await request.body() # Returns empty bytes!
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Dependency Injection for Verification
|
|
46
|
+
|
|
47
|
+
Use FastAPI's dependency injection for clean verification:
|
|
48
|
+
|
|
49
|
+
### Pattern 1: Verification Dependency
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from fastapi import Depends, Header, HTTPException
|
|
53
|
+
import hmac
|
|
54
|
+
import hashlib
|
|
55
|
+
import os
|
|
56
|
+
|
|
57
|
+
async def verify_stripe_signature(
|
|
58
|
+
request: Request,
|
|
59
|
+
stripe_signature: str = Header(alias="stripe-signature")
|
|
60
|
+
) -> bytes:
|
|
61
|
+
"""Dependency that verifies Stripe signature and returns raw body."""
|
|
62
|
+
raw_body = await request.body()
|
|
63
|
+
|
|
64
|
+
# Parse signature header
|
|
65
|
+
# Format: t=timestamp,v1=signature
|
|
66
|
+
parts = dict(p.split("=") for p in stripe_signature.split(","))
|
|
67
|
+
timestamp = parts.get("t")
|
|
68
|
+
signature = parts.get("v1")
|
|
69
|
+
|
|
70
|
+
if not timestamp or not signature:
|
|
71
|
+
raise HTTPException(status_code=400, detail="Invalid signature format")
|
|
72
|
+
|
|
73
|
+
# Compute expected signature
|
|
74
|
+
secret = os.environ["STRIPE_WEBHOOK_SECRET"]
|
|
75
|
+
signed_payload = f"{timestamp}.{raw_body.decode()}"
|
|
76
|
+
expected = hmac.new(
|
|
77
|
+
secret.encode(),
|
|
78
|
+
signed_payload.encode(),
|
|
79
|
+
hashlib.sha256
|
|
80
|
+
).hexdigest()
|
|
81
|
+
|
|
82
|
+
if not hmac.compare_digest(signature, expected):
|
|
83
|
+
raise HTTPException(status_code=401, detail="Invalid signature")
|
|
84
|
+
|
|
85
|
+
return raw_body
|
|
86
|
+
|
|
87
|
+
@app.post("/webhooks/stripe")
|
|
88
|
+
async def stripe_webhook(raw_body: bytes = Depends(verify_stripe_signature)):
|
|
89
|
+
import json
|
|
90
|
+
event = json.loads(raw_body)
|
|
91
|
+
|
|
92
|
+
# Handle event...
|
|
93
|
+
print(f"Received: {event['type']}")
|
|
94
|
+
|
|
95
|
+
return {"received": True}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Pattern 2: Reusable Verification Class
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from fastapi import Depends, Header, HTTPException, Request
|
|
102
|
+
import hmac
|
|
103
|
+
import hashlib
|
|
104
|
+
import base64
|
|
105
|
+
|
|
106
|
+
class WebhookVerifier:
|
|
107
|
+
def __init__(self, secret_env_var: str, header_name: str, encoding: str = "hex"):
|
|
108
|
+
self.secret_env_var = secret_env_var
|
|
109
|
+
self.header_name = header_name
|
|
110
|
+
self.encoding = encoding
|
|
111
|
+
|
|
112
|
+
async def __call__(
|
|
113
|
+
self,
|
|
114
|
+
request: Request,
|
|
115
|
+
) -> bytes:
|
|
116
|
+
raw_body = await request.body()
|
|
117
|
+
signature = request.headers.get(self.header_name)
|
|
118
|
+
|
|
119
|
+
if not signature:
|
|
120
|
+
raise HTTPException(status_code=400, detail=f"Missing {self.header_name} header")
|
|
121
|
+
|
|
122
|
+
secret = os.environ.get(self.secret_env_var)
|
|
123
|
+
if not secret:
|
|
124
|
+
raise HTTPException(status_code=500, detail="Webhook secret not configured")
|
|
125
|
+
|
|
126
|
+
computed = hmac.new(secret.encode(), raw_body, hashlib.sha256)
|
|
127
|
+
|
|
128
|
+
if self.encoding == "base64":
|
|
129
|
+
expected = base64.b64encode(computed.digest()).decode()
|
|
130
|
+
else:
|
|
131
|
+
expected = computed.hexdigest()
|
|
132
|
+
|
|
133
|
+
# Handle signature format variations
|
|
134
|
+
received = signature.replace("sha256=", "")
|
|
135
|
+
|
|
136
|
+
if not hmac.compare_digest(expected, received):
|
|
137
|
+
raise HTTPException(status_code=401, detail="Invalid signature")
|
|
138
|
+
|
|
139
|
+
return raw_body
|
|
140
|
+
|
|
141
|
+
# Create verifiers for different providers
|
|
142
|
+
verify_github = WebhookVerifier("GITHUB_WEBHOOK_SECRET", "x-hub-signature-256", "hex")
|
|
143
|
+
verify_shopify = WebhookVerifier("SHOPIFY_API_SECRET", "x-shopify-hmac-sha256", "base64")
|
|
144
|
+
|
|
145
|
+
@app.post("/webhooks/github")
|
|
146
|
+
async def github_webhook(raw_body: bytes = Depends(verify_github)):
|
|
147
|
+
event = json.loads(raw_body)
|
|
148
|
+
# Handle event...
|
|
149
|
+
|
|
150
|
+
@app.post("/webhooks/shopify")
|
|
151
|
+
async def shopify_webhook(raw_body: bytes = Depends(verify_shopify)):
|
|
152
|
+
event = json.loads(raw_body)
|
|
153
|
+
# Handle event...
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Background Tasks
|
|
157
|
+
|
|
158
|
+
For long-running processing, use FastAPI's BackgroundTasks:
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
from fastapi import BackgroundTasks
|
|
162
|
+
|
|
163
|
+
def process_event(event: dict):
|
|
164
|
+
"""Process webhook event (runs in background)."""
|
|
165
|
+
event_type = event.get("type")
|
|
166
|
+
|
|
167
|
+
if event_type == "payment_intent.succeeded":
|
|
168
|
+
# Long-running operation
|
|
169
|
+
fulfill_order(event["data"]["object"])
|
|
170
|
+
|
|
171
|
+
# More processing...
|
|
172
|
+
|
|
173
|
+
@app.post("/webhooks/stripe")
|
|
174
|
+
async def stripe_webhook(
|
|
175
|
+
background_tasks: BackgroundTasks,
|
|
176
|
+
raw_body: bytes = Depends(verify_stripe_signature)
|
|
177
|
+
):
|
|
178
|
+
event = json.loads(raw_body)
|
|
179
|
+
|
|
180
|
+
# Add to background tasks
|
|
181
|
+
background_tasks.add_task(process_event, event)
|
|
182
|
+
|
|
183
|
+
# Return immediately
|
|
184
|
+
return {"received": True}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### With Task Queues (Celery)
|
|
188
|
+
|
|
189
|
+
For production, use a proper task queue:
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
from celery import Celery
|
|
193
|
+
|
|
194
|
+
celery_app = Celery("tasks", broker="redis://localhost:6379")
|
|
195
|
+
|
|
196
|
+
@celery_app.task
|
|
197
|
+
def process_webhook_task(event: dict):
|
|
198
|
+
# Process in worker
|
|
199
|
+
pass
|
|
200
|
+
|
|
201
|
+
@app.post("/webhooks/stripe")
|
|
202
|
+
async def stripe_webhook(raw_body: bytes = Depends(verify_stripe_signature)):
|
|
203
|
+
event = json.loads(raw_body)
|
|
204
|
+
|
|
205
|
+
# Queue for async processing
|
|
206
|
+
process_webhook_task.delay(event)
|
|
207
|
+
|
|
208
|
+
return {"received": True}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Middleware Pattern
|
|
212
|
+
|
|
213
|
+
For logging and monitoring across all webhooks:
|
|
214
|
+
|
|
215
|
+
```python
|
|
216
|
+
from fastapi import Request
|
|
217
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
218
|
+
import time
|
|
219
|
+
import logging
|
|
220
|
+
|
|
221
|
+
logger = logging.getLogger(__name__)
|
|
222
|
+
|
|
223
|
+
class WebhookLoggingMiddleware(BaseHTTPMiddleware):
|
|
224
|
+
async def dispatch(self, request: Request, call_next):
|
|
225
|
+
if not request.url.path.startswith("/webhooks/"):
|
|
226
|
+
return await call_next(request)
|
|
227
|
+
|
|
228
|
+
start_time = time.time()
|
|
229
|
+
|
|
230
|
+
# Log incoming webhook
|
|
231
|
+
logger.info(
|
|
232
|
+
"Webhook received",
|
|
233
|
+
extra={
|
|
234
|
+
"path": request.url.path,
|
|
235
|
+
"method": request.method,
|
|
236
|
+
"headers": dict(request.headers),
|
|
237
|
+
}
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
response = await call_next(request)
|
|
241
|
+
|
|
242
|
+
duration = time.time() - start_time
|
|
243
|
+
|
|
244
|
+
# Log result
|
|
245
|
+
logger.info(
|
|
246
|
+
"Webhook processed",
|
|
247
|
+
extra={
|
|
248
|
+
"path": request.url.path,
|
|
249
|
+
"status_code": response.status_code,
|
|
250
|
+
"duration_ms": duration * 1000,
|
|
251
|
+
}
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
return response
|
|
255
|
+
|
|
256
|
+
app.add_middleware(WebhookLoggingMiddleware)
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
## Complete FastAPI Example
|
|
260
|
+
|
|
261
|
+
```python
|
|
262
|
+
import os
|
|
263
|
+
import json
|
|
264
|
+
import hmac
|
|
265
|
+
import hashlib
|
|
266
|
+
from dotenv import load_dotenv
|
|
267
|
+
from fastapi import FastAPI, Request, HTTPException, BackgroundTasks, Depends, Header
|
|
268
|
+
|
|
269
|
+
load_dotenv()
|
|
270
|
+
|
|
271
|
+
app = FastAPI()
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
async def verify_stripe_signature(
|
|
275
|
+
request: Request,
|
|
276
|
+
stripe_signature: str = Header(alias="stripe-signature")
|
|
277
|
+
) -> bytes:
|
|
278
|
+
"""Verify Stripe webhook signature."""
|
|
279
|
+
raw_body = await request.body()
|
|
280
|
+
|
|
281
|
+
# Parse signature header
|
|
282
|
+
parts = {}
|
|
283
|
+
for part in stripe_signature.split(","):
|
|
284
|
+
key, value = part.split("=", 1)
|
|
285
|
+
parts[key] = value
|
|
286
|
+
|
|
287
|
+
timestamp = parts.get("t")
|
|
288
|
+
signature = parts.get("v1")
|
|
289
|
+
|
|
290
|
+
if not timestamp or not signature:
|
|
291
|
+
raise HTTPException(status_code=400, detail="Invalid signature format")
|
|
292
|
+
|
|
293
|
+
# Compute expected signature
|
|
294
|
+
secret = os.environ["STRIPE_WEBHOOK_SECRET"]
|
|
295
|
+
signed_payload = f"{timestamp}.{raw_body.decode()}"
|
|
296
|
+
expected = hmac.new(
|
|
297
|
+
secret.encode(),
|
|
298
|
+
signed_payload.encode(),
|
|
299
|
+
hashlib.sha256
|
|
300
|
+
).hexdigest()
|
|
301
|
+
|
|
302
|
+
if not hmac.compare_digest(signature, expected):
|
|
303
|
+
raise HTTPException(status_code=401, detail="Invalid signature")
|
|
304
|
+
|
|
305
|
+
return raw_body
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def process_payment_succeeded(payment_intent: dict):
|
|
309
|
+
"""Background task to process successful payment."""
|
|
310
|
+
print(f"Processing payment: {payment_intent['id']}")
|
|
311
|
+
# Fulfill order, send email, etc.
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def process_subscription_created(subscription: dict):
|
|
315
|
+
"""Background task to process new subscription."""
|
|
316
|
+
print(f"Processing subscription: {subscription['id']}")
|
|
317
|
+
# Provision access, welcome email, etc.
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
@app.post("/webhooks/stripe")
|
|
321
|
+
async def stripe_webhook(
|
|
322
|
+
background_tasks: BackgroundTasks,
|
|
323
|
+
raw_body: bytes = Depends(verify_stripe_signature)
|
|
324
|
+
):
|
|
325
|
+
event = json.loads(raw_body)
|
|
326
|
+
event_type = event["type"]
|
|
327
|
+
data_object = event["data"]["object"]
|
|
328
|
+
|
|
329
|
+
print(f"Received {event_type} event: {event['id']}")
|
|
330
|
+
|
|
331
|
+
# Route to appropriate handler
|
|
332
|
+
if event_type == "payment_intent.succeeded":
|
|
333
|
+
background_tasks.add_task(process_payment_succeeded, data_object)
|
|
334
|
+
|
|
335
|
+
elif event_type == "customer.subscription.created":
|
|
336
|
+
background_tasks.add_task(process_subscription_created, data_object)
|
|
337
|
+
|
|
338
|
+
else:
|
|
339
|
+
print(f"Unhandled event type: {event_type}")
|
|
340
|
+
|
|
341
|
+
return {"received": True}
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
@app.get("/health")
|
|
345
|
+
async def health():
|
|
346
|
+
return {"status": "ok"}
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
if __name__ == "__main__":
|
|
350
|
+
import uvicorn
|
|
351
|
+
uvicorn.run(app, host="0.0.0.0", port=3000)
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
## Testing FastAPI Webhooks
|
|
355
|
+
|
|
356
|
+
```python
|
|
357
|
+
# test_webhooks.py
|
|
358
|
+
from fastapi.testclient import TestClient
|
|
359
|
+
import hmac
|
|
360
|
+
import hashlib
|
|
361
|
+
import time
|
|
362
|
+
import os
|
|
363
|
+
|
|
364
|
+
from main import app
|
|
365
|
+
|
|
366
|
+
client = TestClient(app)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def generate_stripe_signature(payload: str, secret: str) -> str:
|
|
370
|
+
timestamp = str(int(time.time()))
|
|
371
|
+
signed_payload = f"{timestamp}.{payload}"
|
|
372
|
+
signature = hmac.new(
|
|
373
|
+
secret.encode(),
|
|
374
|
+
signed_payload.encode(),
|
|
375
|
+
hashlib.sha256
|
|
376
|
+
).hexdigest()
|
|
377
|
+
return f"t={timestamp},v1={signature}"
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def test_valid_webhook():
|
|
381
|
+
payload = json.dumps({
|
|
382
|
+
"id": "evt_test",
|
|
383
|
+
"type": "payment_intent.succeeded",
|
|
384
|
+
"data": {"object": {"id": "pi_test"}}
|
|
385
|
+
})
|
|
386
|
+
signature = generate_stripe_signature(
|
|
387
|
+
payload,
|
|
388
|
+
os.environ["STRIPE_WEBHOOK_SECRET"]
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
response = client.post(
|
|
392
|
+
"/webhooks/stripe",
|
|
393
|
+
content=payload,
|
|
394
|
+
headers={
|
|
395
|
+
"Content-Type": "application/json",
|
|
396
|
+
"Stripe-Signature": signature,
|
|
397
|
+
}
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
assert response.status_code == 200
|
|
401
|
+
assert response.json() == {"received": True}
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def test_invalid_signature():
|
|
405
|
+
response = client.post(
|
|
406
|
+
"/webhooks/stripe",
|
|
407
|
+
content="{}",
|
|
408
|
+
headers={
|
|
409
|
+
"Content-Type": "application/json",
|
|
410
|
+
"Stripe-Signature": "invalid",
|
|
411
|
+
}
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
assert response.status_code == 401
|
|
415
|
+
```
|