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.

Files changed (149) hide show
  1. package/README.md +201 -0
  2. package/install.js +1424 -0
  3. package/package.json +35 -0
  4. package/src/hooks/context-staleness.sh +46 -0
  5. package/src/hooks/dirty-tree-guard.sh +33 -0
  6. package/src/hooks/engine-protection.sh +43 -0
  7. package/src/hooks/git-safety.sh +47 -0
  8. package/src/hooks/pre-compact-backup.sh +177 -0
  9. package/src/hooks/session-log-reminder.sh +64 -0
  10. package/src/skills/THIRD-PARTY-LICENSES.md +53 -0
  11. package/src/skills/agent-code-reviewer/SKILL.md +41 -0
  12. package/src/skills/agent-content-researcher/SKILL.md +59 -0
  13. package/src/skills/agent-research-analyst/SKILL.md +53 -0
  14. package/src/skills/agent-security-auditor/SKILL.md +42 -0
  15. package/src/skills/agent-strategy-analyst/SKILL.md +54 -0
  16. package/src/skills/defuddle/SKILL.md +41 -0
  17. package/src/skills/find-bugs/SKILL.md +75 -0
  18. package/src/skills/find-skills/SKILL.md +133 -0
  19. package/src/skills/frontend-design/LICENSE.txt +177 -0
  20. package/src/skills/frontend-design/SKILL.md +42 -0
  21. package/src/skills/human-writer/SKILL.md +185 -0
  22. package/src/skills/json-canvas/SKILL.md +656 -0
  23. package/src/skills/marketingskills/.claude-plugin/marketplace.json +45 -0
  24. package/src/skills/marketingskills/README.md +204 -0
  25. package/src/skills/marketingskills/skills/ab-test-setup/SKILL.md +508 -0
  26. package/src/skills/marketingskills/skills/analytics-tracking/SKILL.md +539 -0
  27. package/src/skills/marketingskills/skills/competitor-alternatives/SKILL.md +750 -0
  28. package/src/skills/marketingskills/skills/copy-editing/SKILL.md +439 -0
  29. package/src/skills/marketingskills/skills/copywriting/SKILL.md +455 -0
  30. package/src/skills/marketingskills/skills/email-sequence/SKILL.md +925 -0
  31. package/src/skills/marketingskills/skills/form-cro/SKILL.md +425 -0
  32. package/src/skills/marketingskills/skills/free-tool-strategy/SKILL.md +576 -0
  33. package/src/skills/marketingskills/skills/launch-strategy/SKILL.md +344 -0
  34. package/src/skills/marketingskills/skills/marketing-ideas/SKILL.md +565 -0
  35. package/src/skills/marketingskills/skills/marketing-psychology/SKILL.md +451 -0
  36. package/src/skills/marketingskills/skills/onboarding-cro/SKILL.md +433 -0
  37. package/src/skills/marketingskills/skills/page-cro/SKILL.md +334 -0
  38. package/src/skills/marketingskills/skills/paid-ads/SKILL.md +551 -0
  39. package/src/skills/marketingskills/skills/paywall-upgrade-cro/SKILL.md +570 -0
  40. package/src/skills/marketingskills/skills/popup-cro/SKILL.md +449 -0
  41. package/src/skills/marketingskills/skills/pricing-strategy/SKILL.md +710 -0
  42. package/src/skills/marketingskills/skills/programmatic-seo/SKILL.md +626 -0
  43. package/src/skills/marketingskills/skills/referral-program/SKILL.md +602 -0
  44. package/src/skills/marketingskills/skills/schema-markup/SKILL.md +596 -0
  45. package/src/skills/marketingskills/skills/seo-audit/SKILL.md +384 -0
  46. package/src/skills/marketingskills/skills/signup-flow-cro/SKILL.md +355 -0
  47. package/src/skills/marketingskills/skills/social-content/SKILL.md +807 -0
  48. package/src/skills/obsidian-bases/SKILL.md +651 -0
  49. package/src/skills/obsidian-cli/SKILL.md +103 -0
  50. package/src/skills/obsidian-markdown/SKILL.md +620 -0
  51. package/src/skills/react-best-practices/SKILL.md +136 -0
  52. package/src/skills/refactoring/SKILL.md +119 -0
  53. package/src/skills/seo-content-writer/SKILL.md +661 -0
  54. package/src/skills/seo-content-writer/references/content-structure-templates.md +875 -0
  55. package/src/skills/seo-content-writer/references/title-formulas.md +339 -0
  56. package/src/skills/skill-creator/LICENSE.txt +202 -0
  57. package/src/skills/skill-creator/SKILL.md +357 -0
  58. package/src/skills/skill-creator/references/output-patterns.md +82 -0
  59. package/src/skills/skill-creator/references/workflows.md +28 -0
  60. package/src/skills/skill-creator/scripts/init_skill.py +303 -0
  61. package/src/skills/skill-creator/scripts/package_skill.py +110 -0
  62. package/src/skills/skill-creator/scripts/quick_validate.py +103 -0
  63. package/src/skills/systematic-debugging/CREATION-LOG.md +119 -0
  64. package/src/skills/systematic-debugging/SKILL.md +296 -0
  65. package/src/skills/systematic-debugging/condition-based-waiting-example.ts +158 -0
  66. package/src/skills/systematic-debugging/condition-based-waiting.md +115 -0
  67. package/src/skills/systematic-debugging/defense-in-depth.md +122 -0
  68. package/src/skills/systematic-debugging/find-polluter.sh +63 -0
  69. package/src/skills/systematic-debugging/root-cause-tracing.md +169 -0
  70. package/src/skills/systematic-debugging/test-academic.md +14 -0
  71. package/src/skills/systematic-debugging/test-pressure-1.md +58 -0
  72. package/src/skills/systematic-debugging/test-pressure-2.md +68 -0
  73. package/src/skills/systematic-debugging/test-pressure-3.md +69 -0
  74. package/src/skills/ui-ux-pro-max/SKILL.md +386 -0
  75. package/src/skills/webhook-handler-patterns/SKILL.md +81 -0
  76. package/src/skills/webhook-handler-patterns/references/error-handling.md +299 -0
  77. package/src/skills/webhook-handler-patterns/references/frameworks/express.md +295 -0
  78. package/src/skills/webhook-handler-patterns/references/frameworks/fastapi.md +415 -0
  79. package/src/skills/webhook-handler-patterns/references/frameworks/nextjs.md +331 -0
  80. package/src/skills/webhook-handler-patterns/references/handler-sequence.md +51 -0
  81. package/src/skills/webhook-handler-patterns/references/idempotency.md +227 -0
  82. package/src/skills/webhook-handler-patterns/references/retry-logic.md +261 -0
  83. package/src/structure/00_Inbox/.gitkeep +0 -0
  84. package/src/structure/00_Inbox/Quick_Capture.md +5 -0
  85. package/src/structure/01_Projects/.gitkeep +0 -0
  86. package/src/structure/01_Projects/_Template Project/plan.md +55 -0
  87. package/src/structure/01_Projects/_Template Project/project_brief.md +45 -0
  88. package/src/structure/01_Projects/_Template Project/project_state.md +19 -0
  89. package/src/structure/02_Areas/Engine/Active_Context.md +126 -0
  90. package/src/structure/02_Areas/Engine/Auto_Learnings.md +36 -0
  91. package/src/structure/02_Areas/Engine/Dailies/.gitkeep +0 -0
  92. package/src/structure/02_Areas/Engine/Daily_Template.md +130 -0
  93. package/src/structure/02_Areas/Engine/Goals.md +65 -0
  94. package/src/structure/02_Areas/Engine/Identity_Prime_template.md +86 -0
  95. package/src/structure/02_Areas/Engine/Metrics_Log.md +45 -0
  96. package/src/structure/02_Areas/Engine/Monthly Reviews/.gitkeep +0 -0
  97. package/src/structure/02_Areas/Engine/Mover_Dossier.md +120 -0
  98. package/src/structure/02_Areas/Engine/Quarterly Reviews/.gitkeep +0 -0
  99. package/src/structure/02_Areas/Engine/Someday_Maybe.md +51 -0
  100. package/src/structure/02_Areas/Engine/Strategy_template.md +65 -0
  101. package/src/structure/02_Areas/Engine/Weekly Reviews/.gitkeep +0 -0
  102. package/src/structure/03_Library/Cheatsheets/.gitkeep +0 -0
  103. package/src/structure/03_Library/Entities/Decisions/_TEMPLATE.md +38 -0
  104. package/src/structure/03_Library/Entities/Entities MOC.md +49 -0
  105. package/src/structure/03_Library/Entities/Organizations/_TEMPLATE.md +28 -0
  106. package/src/structure/03_Library/Entities/People/_TEMPLATE.md +31 -0
  107. package/src/structure/03_Library/Entities/Places/_TEMPLATE.md +26 -0
  108. package/src/structure/03_Library/Inputs/.gitkeep +0 -0
  109. package/src/structure/03_Library/Inputs/Archive/.gitkeep +0 -0
  110. package/src/structure/03_Library/Inputs/Articles/.gitkeep +0 -0
  111. package/src/structure/03_Library/Inputs/Articles/_TEMPLATE.md +29 -0
  112. package/src/structure/03_Library/Inputs/Books/.gitkeep +0 -0
  113. package/src/structure/03_Library/Inputs/Books/_TEMPLATE.md +41 -0
  114. package/src/structure/03_Library/Inputs/Inputs MOC.md +40 -0
  115. package/src/structure/03_Library/Inputs/Videos/.gitkeep +0 -0
  116. package/src/structure/03_Library/Inputs/Videos/_TEMPLATE.md +29 -0
  117. package/src/structure/03_Library/MOCs/.gitkeep +0 -0
  118. package/src/structure/03_Library/Misc/.gitkeep +0 -0
  119. package/src/structure/03_Library/Principles/.gitkeep +0 -0
  120. package/src/structure/03_Library/Principles/Naval_Leverage.md +65 -0
  121. package/src/structure/03_Library/SOPs/Mover_OS_Architecture.md +74 -0
  122. package/src/structure/03_Library/SOPs/Tech_Doctrine.md +123 -0
  123. package/src/structure/03_Library/SOPs/Tech_Stack.md +55 -0
  124. package/src/structure/03_Library/SOPs/Zone_Operating.md +58 -0
  125. package/src/structure/04_Archives/.gitkeep +0 -0
  126. package/src/system/AI_INSTALL_MANIFEST.md +219 -0
  127. package/src/system/Mover_Global_Rules.md +646 -0
  128. package/src/system/V4_CONTEXT.md +223 -0
  129. package/src/workflows/analyse-day.md +376 -0
  130. package/src/workflows/context.md +24 -0
  131. package/src/workflows/debrief.md +199 -0
  132. package/src/workflows/debug-resistance.md +181 -0
  133. package/src/workflows/harvest.md +240 -0
  134. package/src/workflows/history.md +247 -0
  135. package/src/workflows/ignite.md +696 -0
  136. package/src/workflows/init-plan.md +16 -0
  137. package/src/workflows/log.md +314 -0
  138. package/src/workflows/morning.md +209 -0
  139. package/src/workflows/overview.md +203 -0
  140. package/src/workflows/pivot-strategy.md +218 -0
  141. package/src/workflows/plan-tomorrow.md +286 -0
  142. package/src/workflows/primer.md +209 -0
  143. package/src/workflows/project-notes.md +17 -0
  144. package/src/workflows/reboot.md +201 -0
  145. package/src/workflows/refactor-plan.md +135 -0
  146. package/src/workflows/review-week.md +537 -0
  147. package/src/workflows/setup.md +387 -0
  148. package/src/workflows/update.md +411 -0
  149. 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
+ ```