@tyroneross/build-loop 0.30.3

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 (389) hide show
  1. package/.agents/plugins/marketplace.json +10 -0
  2. package/.claude-plugin/marketplace.json +38 -0
  3. package/.claude-plugin/plugin.json +30 -0
  4. package/.codex-plugin/plugin.json +41 -0
  5. package/.cursor/rules/build-loop-surface.mdc +22 -0
  6. package/AGENTS.md +498 -0
  7. package/LICENSE +202 -0
  8. package/README.md +403 -0
  9. package/agents/alignment-checker.md +124 -0
  10. package/agents/api-assessor.md +136 -0
  11. package/agents/architecture-scout.md +185 -0
  12. package/agents/assessment-orchestrator.md +163 -0
  13. package/agents/build-orchestrator.md +198 -0
  14. package/agents/database-assessor.md +133 -0
  15. package/agents/design-contract-specialist.md +239 -0
  16. package/agents/fact-checker.md +69 -0
  17. package/agents/fix-critique.md +161 -0
  18. package/agents/frontend-assessor.md +136 -0
  19. package/agents/implementer.md +185 -0
  20. package/agents/independent-auditor.md +143 -0
  21. package/agents/mock-scanner.md +78 -0
  22. package/agents/optimize-runner.md +112 -0
  23. package/agents/overfitting-reviewer.md +96 -0
  24. package/agents/performance-assessor.md +159 -0
  25. package/agents/plan-critic.md +85 -0
  26. package/agents/promotion-reviewer.md +121 -0
  27. package/agents/recurring-pattern-detector.md +248 -0
  28. package/agents/retrospective-synthesizer.md +109 -0
  29. package/agents/root-cause-investigator.md +285 -0
  30. package/agents/scope-auditor.md +121 -0
  31. package/agents/security-reviewer.md +151 -0
  32. package/agents/self-improvement-architect.md +125 -0
  33. package/agents/synthesis-critic.md +99 -0
  34. package/agents/transcript-pattern-miner.md +165 -0
  35. package/agents/ui-validator.md +193 -0
  36. package/codex-skills/build-loop/SKILL.md +20 -0
  37. package/codex-skills/debug-loop/SKILL.md +18 -0
  38. package/codex-skills/handoff/SKILL.md +22 -0
  39. package/codex-skills/knowledge/SKILL.md +21 -0
  40. package/codex-skills/optimize/SKILL.md +18 -0
  41. package/codex-skills/research/SKILL.md +18 -0
  42. package/commands/agent-rally-point.md +301 -0
  43. package/commands/assess.md +41 -0
  44. package/commands/debug.md +20 -0
  45. package/commands/debugger-detail.md +34 -0
  46. package/commands/debugger-scan.md +15 -0
  47. package/commands/debugger-status.md +20 -0
  48. package/commands/debugger.md +32 -0
  49. package/commands/handoff.md +113 -0
  50. package/commands/knowledge-review.md +16 -0
  51. package/commands/optimize.md +31 -0
  52. package/commands/plan-verify.md +39 -0
  53. package/commands/promote-experiment.md +129 -0
  54. package/commands/research.md +22 -0
  55. package/commands/run.md +12 -0
  56. package/commands/self-review.md +51 -0
  57. package/commands/start-prd.md +57 -0
  58. package/commands/test.md +34 -0
  59. package/dist/src/assessment-orchestrator.d.ts +78 -0
  60. package/dist/src/assessment-orchestrator.d.ts.map +1 -0
  61. package/dist/src/assessment-orchestrator.js +304 -0
  62. package/dist/src/assessment-orchestrator.js.map +1 -0
  63. package/dist/src/audit-miner.d.ts +21 -0
  64. package/dist/src/audit-miner.d.ts.map +1 -0
  65. package/dist/src/audit-miner.js +421 -0
  66. package/dist/src/audit-miner.js.map +1 -0
  67. package/dist/src/batch-operations.d.ts +30 -0
  68. package/dist/src/batch-operations.d.ts.map +1 -0
  69. package/dist/src/batch-operations.js +339 -0
  70. package/dist/src/batch-operations.js.map +1 -0
  71. package/dist/src/config.d.ts +83 -0
  72. package/dist/src/config.d.ts.map +1 -0
  73. package/dist/src/config.js +176 -0
  74. package/dist/src/config.js.map +1 -0
  75. package/dist/src/context-engine.d.ts +45 -0
  76. package/dist/src/context-engine.d.ts.map +1 -0
  77. package/dist/src/context-engine.js +135 -0
  78. package/dist/src/context-engine.js.map +1 -0
  79. package/dist/src/debug-wrapper.d.ts +58 -0
  80. package/dist/src/debug-wrapper.d.ts.map +1 -0
  81. package/dist/src/debug-wrapper.js +285 -0
  82. package/dist/src/debug-wrapper.js.map +1 -0
  83. package/dist/src/index.d.ts +31 -0
  84. package/dist/src/index.d.ts.map +1 -0
  85. package/dist/src/index.js +149 -0
  86. package/dist/src/index.js.map +1 -0
  87. package/dist/src/interactive-verifier.d.ts +26 -0
  88. package/dist/src/interactive-verifier.d.ts.map +1 -0
  89. package/dist/src/interactive-verifier.js +435 -0
  90. package/dist/src/interactive-verifier.js.map +1 -0
  91. package/dist/src/lessons.d.ts +59 -0
  92. package/dist/src/lessons.d.ts.map +1 -0
  93. package/dist/src/lessons.js +172 -0
  94. package/dist/src/lessons.js.map +1 -0
  95. package/dist/src/log-reader.d.ts +39 -0
  96. package/dist/src/log-reader.d.ts.map +1 -0
  97. package/dist/src/log-reader.js +291 -0
  98. package/dist/src/log-reader.js.map +1 -0
  99. package/dist/src/logger.d.ts +59 -0
  100. package/dist/src/logger.d.ts.map +1 -0
  101. package/dist/src/logger.js +328 -0
  102. package/dist/src/logger.js.map +1 -0
  103. package/dist/src/parallel-retrieval.d.ts +52 -0
  104. package/dist/src/parallel-retrieval.d.ts.map +1 -0
  105. package/dist/src/parallel-retrieval.js +309 -0
  106. package/dist/src/parallel-retrieval.js.map +1 -0
  107. package/dist/src/pattern-extractor.d.ts +30 -0
  108. package/dist/src/pattern-extractor.d.ts.map +1 -0
  109. package/dist/src/pattern-extractor.js +386 -0
  110. package/dist/src/pattern-extractor.js.map +1 -0
  111. package/dist/src/result-aggregator.d.ts +44 -0
  112. package/dist/src/result-aggregator.d.ts.map +1 -0
  113. package/dist/src/result-aggregator.js +315 -0
  114. package/dist/src/result-aggregator.js.map +1 -0
  115. package/dist/src/retrieval.d.ts +139 -0
  116. package/dist/src/retrieval.d.ts.map +1 -0
  117. package/dist/src/retrieval.js +724 -0
  118. package/dist/src/retrieval.js.map +1 -0
  119. package/dist/src/setup/auto-setup.d.ts +15 -0
  120. package/dist/src/setup/auto-setup.d.ts.map +1 -0
  121. package/dist/src/setup/auto-setup.js +157 -0
  122. package/dist/src/setup/auto-setup.js.map +1 -0
  123. package/dist/src/setup/configure-hooks.d.ts +8 -0
  124. package/dist/src/setup/configure-hooks.d.ts.map +1 -0
  125. package/dist/src/setup/configure-hooks.js +16 -0
  126. package/dist/src/setup/configure-hooks.js.map +1 -0
  127. package/dist/src/setup/create-slash-commands.d.ts +8 -0
  128. package/dist/src/setup/create-slash-commands.d.ts.map +1 -0
  129. package/dist/src/setup/create-slash-commands.js +101 -0
  130. package/dist/src/setup/create-slash-commands.js.map +1 -0
  131. package/dist/src/setup/index.d.ts +8 -0
  132. package/dist/src/setup/index.d.ts.map +1 -0
  133. package/dist/src/setup/index.js +17 -0
  134. package/dist/src/setup/index.js.map +1 -0
  135. package/dist/src/setup/inject-claude-md.d.ts +10 -0
  136. package/dist/src/setup/inject-claude-md.d.ts.map +1 -0
  137. package/dist/src/setup/inject-claude-md.js +150 -0
  138. package/dist/src/setup/inject-claude-md.js.map +1 -0
  139. package/dist/src/setup/uninstall.d.ts +22 -0
  140. package/dist/src/setup/uninstall.d.ts.map +1 -0
  141. package/dist/src/setup/uninstall.js +235 -0
  142. package/dist/src/setup/uninstall.js.map +1 -0
  143. package/dist/src/storage.d.ts +192 -0
  144. package/dist/src/storage.d.ts.map +1 -0
  145. package/dist/src/storage.js +1037 -0
  146. package/dist/src/storage.js.map +1 -0
  147. package/dist/src/string-similarity.d.ts +8 -0
  148. package/dist/src/string-similarity.d.ts.map +1 -0
  149. package/dist/src/string-similarity.js +57 -0
  150. package/dist/src/string-similarity.js.map +1 -0
  151. package/dist/src/traces/adapters/browser.d.ts +69 -0
  152. package/dist/src/traces/adapters/browser.d.ts.map +1 -0
  153. package/dist/src/traces/adapters/browser.js +280 -0
  154. package/dist/src/traces/adapters/browser.js.map +1 -0
  155. package/dist/src/traces/adapters/index.d.ts +23 -0
  156. package/dist/src/traces/adapters/index.d.ts.map +1 -0
  157. package/dist/src/traces/adapters/index.js +67 -0
  158. package/dist/src/traces/adapters/index.js.map +1 -0
  159. package/dist/src/traces/adapters/langchain.d.ts +42 -0
  160. package/dist/src/traces/adapters/langchain.d.ts.map +1 -0
  161. package/dist/src/traces/adapters/langchain.js +151 -0
  162. package/dist/src/traces/adapters/langchain.js.map +1 -0
  163. package/dist/src/traces/adapters/opentelemetry.d.ts +46 -0
  164. package/dist/src/traces/adapters/opentelemetry.d.ts.map +1 -0
  165. package/dist/src/traces/adapters/opentelemetry.js +164 -0
  166. package/dist/src/traces/adapters/opentelemetry.js.map +1 -0
  167. package/dist/src/traces/adapters/sentry.d.ts +58 -0
  168. package/dist/src/traces/adapters/sentry.d.ts.map +1 -0
  169. package/dist/src/traces/adapters/sentry.js +212 -0
  170. package/dist/src/traces/adapters/sentry.js.map +1 -0
  171. package/dist/src/traces/index.d.ts +10 -0
  172. package/dist/src/traces/index.d.ts.map +1 -0
  173. package/dist/src/traces/index.js +45 -0
  174. package/dist/src/traces/index.js.map +1 -0
  175. package/dist/src/traces/storage.d.ts +78 -0
  176. package/dist/src/traces/storage.d.ts.map +1 -0
  177. package/dist/src/traces/storage.js +399 -0
  178. package/dist/src/traces/storage.js.map +1 -0
  179. package/dist/src/traces/summarizer.d.ts +83 -0
  180. package/dist/src/traces/summarizer.d.ts.map +1 -0
  181. package/dist/src/traces/summarizer.js +247 -0
  182. package/dist/src/traces/summarizer.js.map +1 -0
  183. package/dist/src/traces/types.d.ts +365 -0
  184. package/dist/src/traces/types.d.ts.map +1 -0
  185. package/dist/src/traces/types.js +14 -0
  186. package/dist/src/traces/types.js.map +1 -0
  187. package/dist/src/types.d.ts +495 -0
  188. package/dist/src/types.d.ts.map +1 -0
  189. package/dist/src/types.js +27 -0
  190. package/dist/src/types.js.map +1 -0
  191. package/docs/agent-surface-policy.md +45 -0
  192. package/docs/memory-setup.md +226 -0
  193. package/hooks/_arch_scan_bg.py +88 -0
  194. package/hooks/_plugin_drift_check_bg.py +85 -0
  195. package/hooks/_session_start_lib.sh +42 -0
  196. package/hooks/git/pre-push +141 -0
  197. package/hooks/hooks.json +126 -0
  198. package/hooks/post-commit +30 -0
  199. package/hooks/pre-commit +29 -0
  200. package/hooks/pre-edit-apppulse.sh +6 -0
  201. package/hooks/pre-edit-architecture.sh +106 -0
  202. package/hooks/pre-edit-rally-point.sh +92 -0
  203. package/hooks/session-start-apppulse.sh +6 -0
  204. package/hooks/session-start-architecture.sh +10 -0
  205. package/hooks/session-start-git-hooks.sh +12 -0
  206. package/hooks/session-start-plugin-drift.sh +27 -0
  207. package/hooks/session-start-rally-point.sh +44 -0
  208. package/hooks/session-start-retrieval.sh +24 -0
  209. package/hooks/session-start-worktree-gc.sh +105 -0
  210. package/hooks/test_apppulse_hooks.sh +6 -0
  211. package/hooks/test_rally_point_hooks.sh +388 -0
  212. package/hooks/test_sec008_hooks.sh +66 -0
  213. package/package.json +111 -0
  214. package/scripts/README.md +301 -0
  215. package/scripts/_db_url.py +61 -0
  216. package/scripts/_paths.py +445 -0
  217. package/scripts/blm.py +162 -0
  218. package/scripts/blm_api.py +263 -0
  219. package/scripts/check_cache_sync.py +490 -0
  220. package/scripts/embed_backend.py +475 -0
  221. package/scripts/install_memory.py +556 -0
  222. package/scripts/lessons_index/__init__.py +77 -0
  223. package/scripts/lessons_index/__main__.py +135 -0
  224. package/scripts/lessons_index/ingest.py +288 -0
  225. package/scripts/lessons_index/query.py +285 -0
  226. package/scripts/lessons_index/schema.py +114 -0
  227. package/scripts/memory_context/__init__.py +610 -0
  228. package/scripts/memory_graph/__init__.py +393 -0
  229. package/scripts/project_resolver.py +172 -0
  230. package/scripts/prune_codex_plugin_cache.py +23 -0
  231. package/scripts/prune_plugin_cache.py +359 -0
  232. package/scripts/semantic_index/__init__.py +482 -0
  233. package/scripts/semantic_index/_bench_hybrid.py +175 -0
  234. package/scripts/semantic_index/backfill.py +263 -0
  235. package/scripts/semantic_index/hybrid.py +174 -0
  236. package/scripts/semantic_index/test_backfill.py +292 -0
  237. package/scripts/semantic_index/test_hybrid.py +541 -0
  238. package/scripts/sync_navgator_lessons.py +666 -0
  239. package/scripts/sync_plugin_cache.py +388 -0
  240. package/skills/agent-rally-point/SKILL.md +165 -0
  241. package/skills/agent-rally-watcher/SKILL.md +83 -0
  242. package/skills/api-registry-bridge/SKILL.md +59 -0
  243. package/skills/architecture/dead/SKILL.md +56 -0
  244. package/skills/architecture/impact/SKILL.md +88 -0
  245. package/skills/architecture/review/SKILL.md +227 -0
  246. package/skills/architecture/rules/SKILL.md +72 -0
  247. package/skills/architecture/scan/SKILL.md +79 -0
  248. package/skills/architecture/trace/SKILL.md +65 -0
  249. package/skills/attribution-standard/SKILL.md +122 -0
  250. package/skills/authentication/SKILL.md +81 -0
  251. package/skills/authentication/references/better-auth-magic-link.md +135 -0
  252. package/skills/authentication/references/better-auth-setup.md +224 -0
  253. package/skills/authentication/references/google-calendar-sync.md +278 -0
  254. package/skills/authentication/references/google-cloud-console.md +159 -0
  255. package/skills/authentication/references/google-geocoding-directions.md +126 -0
  256. package/skills/authentication/references/google-lessons-example-web-app.md +177 -0
  257. package/skills/authentication/references/google-maps.md +162 -0
  258. package/skills/authentication/references/google-oauth-setup.md +343 -0
  259. package/skills/authentication/references/google-places.md +173 -0
  260. package/skills/authentication/references/lessons-example-web-app-better-auth.md +126 -0
  261. package/skills/authentication/references/resend-email.md +227 -0
  262. package/skills/authentication/references/resend-otp-magic-link.md +153 -0
  263. package/skills/authentication/references/supabase-auth.md +172 -0
  264. package/skills/auto-decision-capture/SKILL.md +419 -0
  265. package/skills/build-loop/SKILL.md +516 -0
  266. package/skills/build-loop/detect-plugins.mjs +82 -0
  267. package/skills/build-loop/eval-guide.md +65 -0
  268. package/skills/build-loop/fallbacks.md +549 -0
  269. package/skills/build-loop/phases/fact-check.md +42 -0
  270. package/skills/build-loop/phases/ui-validation.md +267 -0
  271. package/skills/build-loop/references/apple-native-planning.md +439 -0
  272. package/skills/build-loop/references/capability-routing.md +181 -0
  273. package/skills/build-loop/references/codex-subagents.md +98 -0
  274. package/skills/build-loop/references/coordination.md +161 -0
  275. package/skills/build-loop/references/correction-aware-capture.md +177 -0
  276. package/skills/build-loop/references/independent-auditor.md +72 -0
  277. package/skills/build-loop/references/intent-capability-pack.md +170 -0
  278. package/skills/build-loop/references/intent-exploration-prompts.md +96 -0
  279. package/skills/build-loop/references/leadership.md +72 -0
  280. package/skills/build-loop/references/memory.md +284 -0
  281. package/skills/build-loop/references/modular-systems-pack.md +96 -0
  282. package/skills/build-loop/references/output-style.md +129 -0
  283. package/skills/build-loop/references/pay-it-forward-arch.md +98 -0
  284. package/skills/build-loop/references/phase-1-assess.md +218 -0
  285. package/skills/build-loop/references/phase-2-plan.md +77 -0
  286. package/skills/build-loop/references/phase-3-execute.md +47 -0
  287. package/skills/build-loop/references/phase-4-review.md +310 -0
  288. package/skills/build-loop/references/phase-5-iterate.md +71 -0
  289. package/skills/build-loop/references/phase-6-learn.md +56 -0
  290. package/skills/build-loop/references/recent-design-structures.md +274 -0
  291. package/skills/build-loop/references/refactor-history/ASSESSMENT.md +85 -0
  292. package/skills/build-loop/references/refactor-history/STANDALONE_TEST_RUN.md +149 -0
  293. package/skills/build-loop/references/refactor-history/scenarios/01-simple-bugfix.md +32 -0
  294. package/skills/build-loop/references/refactor-history/scenarios/02-ui-build-with-iteration.md +48 -0
  295. package/skills/build-loop/references/refactor-history/scenarios/03-multi-failure-escalation.md +60 -0
  296. package/skills/build-loop/references/refactor-history/scenarios/04-ui-build-ibr-absent.md +51 -0
  297. package/skills/build-loop/references/refactor-history/scenarios/05-refactor-navgator-absent.md +71 -0
  298. package/skills/build-loop/references/refactor-history/scenarios/06-recurring-bug-debugger-absent.md +52 -0
  299. package/skills/build-loop/references/refactor-history/trace-comparison.md +202 -0
  300. package/skills/build-loop/references/self-recursive-dev.md +77 -0
  301. package/skills/build-loop/references/self-review.md +232 -0
  302. package/skills/build-loop/references/ui-io-contract.md +116 -0
  303. package/skills/build-loop/scanners/audit-design-rules.mjs +476 -0
  304. package/skills/build-loop/scanners/require-visual-evidence.mjs +239 -0
  305. package/skills/build-loop/templates/backlog-item.md +35 -0
  306. package/skills/build-loop/templates/codex-worker-prompt.md +89 -0
  307. package/skills/build-loop/templates/ui-subagent-prompt.md +179 -0
  308. package/skills/build-loop/templates/ux-fix-plan.md +40 -0
  309. package/skills/building-with-deepagents/SKILL.md +195 -0
  310. package/skills/building-with-deepagents/references/anti-patterns.md +141 -0
  311. package/skills/building-with-deepagents/references/api-quick-reference.md +184 -0
  312. package/skills/capabilities/SKILL.md +91 -0
  313. package/skills/debug-loop/SKILL.md +278 -0
  314. package/skills/debug-loop/references/convergence-rules.md +145 -0
  315. package/skills/debug-loop/references/swiftui-macos-clunky-ui-debugger.html +823 -0
  316. package/skills/debugging/assess/SKILL.md +111 -0
  317. package/skills/debugging/debug-loop/SKILL.md +231 -0
  318. package/skills/debugging/memory/SKILL.md +147 -0
  319. package/skills/debugging/store/SKILL.md +153 -0
  320. package/skills/debugging-memory/SKILL.md +440 -0
  321. package/skills/debugging-memory/examples/incident-example.json +67 -0
  322. package/skills/debugging-memory/references/incident-documentation.md +280 -0
  323. package/skills/debugging-memory/references/ios-notification-alarm-playbook.md +300 -0
  324. package/skills/debugging-memory/references/pattern-extraction.md +163 -0
  325. package/skills/debugging-memory/references/subagent-integration.md +312 -0
  326. package/skills/defenseclaw-bridge/SKILL.md +156 -0
  327. package/skills/defenseclaw-bridge/references/dc-config-mapping.md +162 -0
  328. package/skills/defenseclaw-bridge/references/output-format.md +108 -0
  329. package/skills/handoff/SKILL.md +94 -0
  330. package/skills/ibr-bridge/SKILL.md +68 -0
  331. package/skills/knowledge/SKILL.md +127 -0
  332. package/skills/knowledge/references/recall-integration.md +83 -0
  333. package/skills/knowledge/templates/madr-minimal.md +56 -0
  334. package/skills/knowledge-review/SKILL.md +112 -0
  335. package/skills/logging-tracer/SKILL.md +202 -0
  336. package/skills/logging-tracer/references/log-analysis.md +128 -0
  337. package/skills/logging-tracer/references/stack-templates.md +274 -0
  338. package/skills/logging-tracer-bridge/SKILL.md +70 -0
  339. package/skills/mcp-builder/SKILL.md +278 -0
  340. package/skills/mcp-builder/references/mcp-security.md +106 -0
  341. package/skills/model-tiering/SKILL.md +216 -0
  342. package/skills/native-ax-driver/SKILL.md +220 -0
  343. package/skills/native-ax-driver/scripts/layout_fill.py +222 -0
  344. package/skills/native-ax-driver/scripts/native_driver.py +495 -0
  345. package/skills/native-ax-driver/swift/bl-ax-driver/Package.swift +20 -0
  346. package/skills/native-ax-driver/swift/bl-ax-driver/Sources/main.swift +542 -0
  347. package/skills/optimize/SKILL.md +202 -0
  348. package/skills/optimize/profiles.md +61 -0
  349. package/skills/plan-verify/SKILL.md +115 -0
  350. package/skills/plan-verify/test-fixtures/example-app-v20-findings.json +27 -0
  351. package/skills/plan-verify/test-fixtures/example-app-v20.md +42 -0
  352. package/skills/plan-verify/test-fixtures/example-app-v22-findings.json +15 -0
  353. package/skills/plan-verify/test-fixtures/example-app-v22.md +1608 -0
  354. package/skills/plan-verify/test-fixtures/unrelated-good-plan-findings.json +15 -0
  355. package/skills/plan-verify/test-fixtures/unrelated-good-plan.md +55 -0
  356. package/skills/plugin-builder/SKILL.md +329 -0
  357. package/skills/plugin-builder/references/authoritative-sources.md +104 -0
  358. package/skills/plugin-builder/references/build-loop-phase-guidance.md +94 -0
  359. package/skills/plugin-builder/references/components-guide.md +221 -0
  360. package/skills/plugin-builder/references/distribution.md +317 -0
  361. package/skills/plugin-builder/references/dual-host-claude-codex.md +220 -0
  362. package/skills/plugin-builder/references/hooks-reference.md +210 -0
  363. package/skills/plugin-builder/references/manifest-schema.md +111 -0
  364. package/skills/plugin-builder/references/plugin-hygiene-lessons.md +307 -0
  365. package/skills/plugin-tests/SKILL.md +94 -0
  366. package/skills/prd-bridge/SKILL.md +135 -0
  367. package/skills/research/SKILL.md +80 -0
  368. package/skills/security-methodology/SKILL.md +105 -0
  369. package/skills/security-methodology/references/cross-source-matrix.md +77 -0
  370. package/skills/security-methodology/references/mitre-atlas-starter.md +83 -0
  371. package/skills/security-methodology/references/nist-600-1-mapping.md +119 -0
  372. package/skills/security-methodology/references/owasp-agentic-top-10.md +157 -0
  373. package/skills/security-methodology/references/owasp-llm-top-10.md +144 -0
  374. package/skills/security-methodology/references/owasp-web-top-10.md +65 -0
  375. package/skills/self-improve/SKILL.md +298 -0
  376. package/skills/spec-writing/SKILL.md +548 -0
  377. package/skills/spec-writing/scripts/check_checklist.py +478 -0
  378. package/skills/sync-skills/SKILL.md +132 -0
  379. package/skills/telemetry/SKILL.md +66 -0
  380. package/skills/ui-design/SKILL.md +75 -0
  381. package/skills/ui-design/references/design-patterns-multi.md +165 -0
  382. package/skills/ui-design/references/design-preferences-from-owned-apps.md +150 -0
  383. package/skills/ui-design/references/evidence-capture-policy.md +55 -0
  384. package/skills/ui-design/references/ui-guidance-sources.md +330 -0
  385. package/skills/ui-design/references/universal-design-principles.alt.md +182 -0
  386. package/skills/ui-design/references/universal-design-principles.md +419 -0
  387. package/templates/memory/MEMORY.md.template +55 -0
  388. package/templates/memory/constitution.md.template +64 -0
  389. package/templates/memory/manifest.json +47 -0
@@ -0,0 +1,173 @@
1
+ <!-- SPDX-FileCopyrightText: 2025-2026 Tyrone Ross, Jr <46267523+tyroneross@users.noreply.github.com> | SPDX-License-Identifier: Apache-2.0 -->
2
+
3
+ # Places API (New) v1 — 2026
4
+
5
+ The legacy Places API can no longer be enabled on new projects. Use **Places API (New)** with the v1 REST endpoints and **field masks** on every call.
6
+
7
+ ## Core Endpoints
8
+
9
+ | Endpoint | Method | Use |
10
+ |----------|--------|-----|
11
+ | `POST https://places.googleapis.com/v1/places:searchText` | POST | Text Search — "summer camps near me" |
12
+ | `POST https://places.googleapis.com/v1/places:searchNearby` | POST | Nearby Search — points within radius |
13
+ | `POST https://places.googleapis.com/v1/places:autocomplete` | POST | Autocomplete — as user types |
14
+ | `GET https://places.googleapis.com/v1/places/{PLACE_ID}` | GET | Place Details — after picking a result |
15
+
16
+ ## Field Masks (MANDATORY — affects pricing)
17
+
18
+ Every Places API (New) request requires an `X-Goog-FieldMask` header. Google **charges per field category requested**. Don't request what you won't use.
19
+
20
+ ```
21
+ X-Goog-FieldMask: places.id,places.displayName,places.formattedAddress,places.location
22
+ ```
23
+
24
+ Categories (as of April 2026):
25
+
26
+ - **IDs Only** (cheapest): `places.id`, `places.name`
27
+ - **Essentials**: `places.displayName`, `places.formattedAddress`, `places.location`, `places.shortFormattedAddress`
28
+ - **Pro**: adds `photos`, `priceLevel`, `rating`, `userRatingCount`, `businessStatus`, `types`
29
+ - **Enterprise**: adds `reviews`, `editorialSummary`, `openingHours`, `websiteUri`, etc.
30
+
31
+ The finer the mask, the cheaper the call. Start with Essentials and add fields only when the UI needs them.
32
+
33
+ ## Text Search Example
34
+
35
+ ```ts
36
+ // Server-side only — use the API key that's restricted to your server's IP,
37
+ // NOT the browser-exposed Maps JS key.
38
+ async function searchPlaces(query: string, bias?: { lat: number; lng: number; radius: number }) {
39
+ const res = await fetch('https://places.googleapis.com/v1/places:searchText', {
40
+ method: 'POST',
41
+ headers: {
42
+ 'Content-Type': 'application/json',
43
+ 'X-Goog-Api-Key': process.env.GOOGLE_PLACES_API_KEY!,
44
+ 'X-Goog-FieldMask': [
45
+ 'places.id',
46
+ 'places.displayName',
47
+ 'places.formattedAddress',
48
+ 'places.location',
49
+ 'places.types',
50
+ 'places.primaryType',
51
+ 'places.primaryTypeDisplayName',
52
+ ].join(','),
53
+ },
54
+ body: JSON.stringify({
55
+ textQuery: query,
56
+ ...(bias && {
57
+ locationBias: {
58
+ circle: {
59
+ center: { latitude: bias.lat, longitude: bias.lng },
60
+ radius: bias.radius,
61
+ },
62
+ },
63
+ }),
64
+ maxResultCount: 10,
65
+ }),
66
+ })
67
+
68
+ if (!res.ok) throw new Error(`Places API ${res.status}: ${await res.text()}`)
69
+ return res.json() as Promise<{
70
+ places: Array<{
71
+ id: string
72
+ displayName: { text: string; languageCode: string }
73
+ formattedAddress: string
74
+ location: { latitude: number; longitude: number }
75
+ types?: string[]
76
+ primaryType?: string
77
+ primaryTypeDisplayName?: { text: string }
78
+ }>
79
+ }>
80
+ }
81
+ ```
82
+
83
+ **Key Places API (New) note**: Use `X-Goog-Api-Key` header, NOT `?key=` query param. The query-param style is legacy.
84
+
85
+ ## Autocomplete + Session Tokens
86
+
87
+ Session tokens bundle a series of Autocomplete requests with a single final Place Details request into one session → Google bills it as one request instead of N+1. Use a fresh UUID per autocomplete "session" (one user typing → one selection).
88
+
89
+ ```ts
90
+ import { randomUUID } from 'crypto'
91
+
92
+ // Start a session when the user focuses the input
93
+ const sessionToken = randomUUID()
94
+
95
+ // Each keystroke
96
+ async function autocomplete(input: string) {
97
+ return fetch('https://places.googleapis.com/v1/places:autocomplete', {
98
+ method: 'POST',
99
+ headers: {
100
+ 'Content-Type': 'application/json',
101
+ 'X-Goog-Api-Key': process.env.GOOGLE_PLACES_API_KEY!,
102
+ },
103
+ body: JSON.stringify({
104
+ input,
105
+ sessionToken, // same for every keystroke in this session
106
+ locationBias: { /* optional */ },
107
+ }),
108
+ }).then(r => r.json())
109
+ }
110
+
111
+ // User picks a result → fetch details with the same token
112
+ async function getDetails(placeId: string) {
113
+ return fetch(`https://places.googleapis.com/v1/places/${placeId}?sessionToken=${sessionToken}`, {
114
+ headers: {
115
+ 'X-Goog-Api-Key': process.env.GOOGLE_PLACES_API_KEY!,
116
+ 'X-Goog-FieldMask': 'id,displayName,formattedAddress,location,websiteUri,internationalPhoneNumber',
117
+ },
118
+ }).then(r => r.json())
119
+ }
120
+ ```
121
+
122
+ After the Place Details call, the session is consumed. Start a fresh token for the next query.
123
+
124
+ ## The "Camp at a Parent Location" Problem
125
+
126
+ The Trip Planner / Summer Camps use case has a wrinkle: a camp is an _activity_ that runs at a _venue_ that has its own Google Place. E.g. "Kids Coding Camp" at "YMCA Brooklyn". You want to store both.
127
+
128
+ **Data model approach:**
129
+
130
+ ```
131
+ camps
132
+ id
133
+ name -- "Kids Coding Camp"
134
+ provider -- "Code Ninjas" (who runs the camp)
135
+ provider_place_id -- Google Place ID for Code Ninjas HQ (optional)
136
+ venue_place_id -- Google Place ID for YMCA Brooklyn (where it actually meets)
137
+ venue_name -- cached displayName
138
+ venue_address -- cached formattedAddress
139
+ venue_lat, venue_lng
140
+ start_date, end_date
141
+ daily_start, daily_end -- e.g. 09:00 - 15:00
142
+ ages_min, ages_max
143
+ cost
144
+ url
145
+ notes
146
+ ```
147
+
148
+ The key insight: **`venue_place_id` and `provider_place_id` are different Google Places**, and you should store and display both. Users care about where to drop off (venue) AND who to contact (provider).
149
+
150
+ When searching Places for a camp venue, query for the venue name directly — don't try to match the camp name to a Google Place, it won't exist as one.
151
+
152
+ ## Caching + Terms of Service
153
+
154
+ Google's Places API ToS allow you to cache Place IDs **indefinitely** (they're considered stable identifiers), but most other Place data is capped at **30 days** before you must refetch. In practice:
155
+
156
+ - Store `place_id`, `displayName`, `formattedAddress`, `location` in your DB
157
+ - Refresh from Google if the cached row is > 30 days old
158
+ - Never display cached data older than 30 days without refetching
159
+
160
+ ## Common Failures
161
+
162
+ - **HTTP 400 "Field mask is required"** — add `X-Goog-FieldMask` header
163
+ - **HTTP 400 "Invalid field mask"** — you can't request `places.name` alongside other `places.*` fields without the correct prefix. Check the [field-mask docs](https://developers.google.com/maps/documentation/places/web-service/text-search#fieldmask)
164
+ - **HTTP 403 "API has not been used"** — enable Places API (New), not legacy
165
+ - **HTTP 429** — rate limited. Default quota is generous but enforce client-side debounce on autocomplete (200-400ms)
166
+ - **Empty results but no error** — check `languageCode` and `regionCode` in the request body
167
+
168
+ ## Sources
169
+
170
+ - [Places API New overview](https://developers.google.com/maps/documentation/places/web-service/overview)
171
+ - [Text Search (New)](https://developers.google.com/maps/documentation/places/web-service/text-search)
172
+ - [Place Details (New)](https://developers.google.com/maps/documentation/places/web-service/place-details)
173
+ - [Migrate to New from Legacy](https://developers.google.com/maps/documentation/places/web-service/legacy/migrate-text)
@@ -0,0 +1,126 @@
1
+ <!-- SPDX-FileCopyrightText: 2025-2026 Tyrone Ross, Jr <46267523+tyroneross@users.noreply.github.com> | SPDX-License-Identifier: Apache-2.0 -->
2
+
3
+ # Example Web App — Better Auth lessons
4
+
5
+ ## When to use
6
+
7
+ Read this when you're hitting a class of problem the Example Web App build hit during the migration from `@supabase/ssr` to Better Auth + Drizzle + Neon. Each section is a real incident or near-miss documented at the time. Diagnostic-style — symptom first, cause, what to look for, how it was fixed.
8
+
9
+ For runnable Better Auth config, see `better-auth-setup.md`.
10
+ For the Google-API-side lessons (Calendar, Places, Maps, Cloud Console), see the older `google-lessons-example-web-app.md`.
11
+
12
+ ## Incident: Production sign-in works, then the next request 401s
13
+
14
+ **Symptom.** Sign-in completes successfully, browser redirects to the app, the dashboard renders briefly, then every subsequent API call returns 401. Hard refresh shows logged-out state.
15
+
16
+ **Cause.** `BETTER_AUTH_URL` was set to `https://my-app.vercel.app` but the actual production deployment was reachable at a custom CNAME `https://app.example.com`. Better Auth's cookie was scoped to `my-app.vercel.app`; the browser was on `app.example.com`; cookies didn't apply.
17
+
18
+ **What to look for.**
19
+ - `set-cookie` response header on `/api/auth/callback/google` lists a `Domain=` that doesn't match the URL bar
20
+ - Sign-in works on the `*.vercel.app` URL but not on the custom domain
21
+ - `Network` tab shows session reads returning empty before any API call
22
+
23
+ **Fix.** Boot-time validation that asserts `process.env.BETTER_AUTH_URL` matches `process.env.VERCEL_URL` (with `https://` prefix) OR a known custom domain. Crash the boot if it doesn't — silent mismatch was the killer here.
24
+
25
+ Cross-reference: Universal footgun #7 (`NEXT_PUBLIC_APP_URL` / `BETTER_AUTH_URL` mismatch on Vercel CNAMEs) in `../SKILL.md`.
26
+
27
+ ## Incident: Calendar API call fails with `invalid_grant` after 7 days
28
+
29
+ **Symptom.** Family Calendar feature works for a week after the user signs in. Then every Google Calendar API call returns `invalid_grant`. Re-signing in fixes it for another week.
30
+
31
+ **Cause.** Google access tokens expire in ~60 minutes. Without a `refresh_token`, the access token can't be silently renewed. The `refresh_token` is only granted on first consent — second sign-in (without `prompt: 'consent'`) returns no `refresh_token`, and the row in `account.refresh_token` is null.
32
+
33
+ **What to look for.**
34
+ - `account.refresh_token` is null in your DB for users who can't make Google API calls
35
+ - `account.expires_at` is in the past
36
+ - Google Cloud Console "Credentials" page shows OAuth consent has been granted, but the app never asked for offline access
37
+
38
+ **Fix.** Better Auth Google provider config:
39
+
40
+ ```ts
41
+ google: {
42
+ clientId: process.env.GOOGLE_CLIENT_ID!,
43
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
44
+ accessType: 'offline', // requests refresh_token
45
+ prompt: 'select_account consent', // forces consent screen every sign-in
46
+ scope: ['openid', 'email', 'profile', 'https://www.googleapis.com/auth/calendar.app.created'],
47
+ }
48
+ ```
49
+
50
+ Both `accessType` AND `prompt` are required. `accessType: 'offline'` alone won't issue a `refresh_token` if Google thinks the user has already consented to offline access in a previous session.
51
+
52
+ Verify before shipping by signing in twice (sign out between) and confirming `account.refresh_token` is non-null both times.
53
+
54
+ Cross-reference: Universal footgun #8 in `../SKILL.md`.
55
+
56
+ ## Incident: Family Calendar creation triggers a second consent prompt
57
+
58
+ **Symptom.** User signs in to Example Web App (Google OAuth, basic scopes). Clicks "Create Family Calendar." Browser redirects back to Google for ANOTHER consent screen requesting Calendar scope. Half of users abandon at this second prompt.
59
+
60
+ **Cause.** The initial sign-in only requested `openid email profile`. Calendar scope (`https://www.googleapis.com/auth/calendar.app.created`) was added in a later `auth.api.linkOAuth` call.
61
+
62
+ **Fix.** Pre-approve every scope you'll EVER need at sign-in time:
63
+
64
+ ```ts
65
+ scope: [
66
+ 'openid',
67
+ 'email',
68
+ 'profile',
69
+ 'https://www.googleapis.com/auth/calendar.app.created',
70
+ ]
71
+ ```
72
+
73
+ Trade-off: more scopes = scarier consent screen at first sign-in. But far better than losing 50% of users at a second prompt mid-flow.
74
+
75
+ If a scope is genuinely optional or rare, defer it — but acknowledge you'll lose some users at the second prompt. Document the choice.
76
+
77
+ ## Incident: User A could query User B's data
78
+
79
+ **Symptom.** Found during a code review pre-launch. The `getCamps()` route handler did `db.select().from(camps).where(eq(camps.id, request.body.campId))` — no user check.
80
+
81
+ **Cause.** Better Auth gives you a `session.user.id`, but it does NOT enforce per-row authorization. Every query has to manually scope to the session's user id. With dozens of route handlers, "manually" inevitably misses spots — exactly what happened here.
82
+
83
+ **Fix.** A single `dbForUser(userId)` factory in `lib/db/index.ts` that returns CRUD methods pre-bound to that user. Route handlers MUST go through it. Code review can grep for any `from(schema.X)` outside `dbForUser` to find violations.
84
+
85
+ ```ts
86
+ const userDb = dbForUser(session.user.id)
87
+ const camp = await userDb.camps.get(campId) // returns [] if user doesn't own it
88
+ ```
89
+
90
+ The grep audit caught 7 violations that had passed PR review.
91
+
92
+ Cross-reference: Universal footgun #9 in `../SKILL.md`. Implementation pattern in `better-auth-setup.md`.
93
+
94
+ ## Incident: `sameSite: 'strict'` broke Google sign-in
95
+
96
+ **Symptom.** Tried to harden cookies by setting `sameSite: 'strict'`. Google sign-in stopped working — the OAuth callback could not see the auth cookie.
97
+
98
+ **Cause.** `sameSite: 'strict'` blocks cookies on cross-site redirect responses. The Google OAuth callback IS a cross-site redirect. Cookie isn't sent → Better Auth treats the callback as a fresh session → no link between callback and the sign-in attempt.
99
+
100
+ **Fix.** `sameSite: 'lax'` is the correct value for cookies that need to survive OAuth redirects. Better Auth's default is correct; the override was the bug.
101
+
102
+ ```ts
103
+ // betterAuth({ ... advancedCookies: { sameSite: 'lax' } }) // default — leave it
104
+ ```
105
+
106
+ Cross-reference: Universal footgun #11 in `../SKILL.md`.
107
+
108
+ ## Migration narrative: `@supabase/ssr` → Better Auth
109
+
110
+ The Example Web App migration took ~2 weeks part-time. Order of operations:
111
+
112
+ 1. **Stand up Better Auth in parallel**, on `/api/auth/[...all]`. Supabase auth still active on the old `/auth/callback`. App routes fall back to either depending on a feature flag.
113
+ 2. **Migrate user records** with a one-time script. Map Supabase `auth.users` → Better Auth `user`, add corresponding `account` rows for OAuth provider linkage. Test on a copy of prod first.
114
+ 3. **Switch the route handler** from Supabase callbacks to `app/api/auth/[...all]/route.ts`. Update sign-in / sign-out buttons to call `authClient.signIn` / `signOut`.
115
+ 4. **Delete Supabase-specific cookie code.** This was the biggest source of bugs — orphaned `getAll`/`setAll` adapters, leftover `createServerClient` imports.
116
+ 5. **Replace RLS-based auth with `dbForUser(userId)`.** Audit every route handler. The migration triggered the IDOR-pattern adoption.
117
+
118
+ The biggest risks were in step 2 (schema mismatch) and step 4 (latent dead code). Spent the most time there.
119
+
120
+ ## What we'd do differently next time
121
+
122
+ - **Boot-time URL validation from day one.** The Vercel CNAME mismatch ate a deploy.
123
+ - **Pre-approve scopes at sign-in.** The mid-flow consent prompt cost users.
124
+ - **Adopt `dbForUser` from the first route handler.** Retrofitting it is fine, but you'll find IDORs you didn't expect.
125
+ - **Don't override Better Auth cookie defaults.** They're correct; "hardening" introduced the `sameSite: 'strict'` regression.
126
+ - **Test sign-in twice (sign out between)** before declaring the OAuth flow shipped — that's the only way to catch missing `refresh_token` on second consent.
@@ -0,0 +1,227 @@
1
+ <!-- SPDX-FileCopyrightText: 2025-2026 Tyrone Ross, Jr <46267523+tyroneross@users.noreply.github.com> | SPDX-License-Identifier: Apache-2.0 -->
2
+
3
+ # Resend — Transactional Email + Webhooks
4
+
5
+ ## When to use
6
+
7
+ - Sending transactional email from a Better Auth or custom backend
8
+ - Wiring `Better Auth`'s `sendVerificationEmail` or `magicLink.sendMagicLink` to Resend
9
+ - Verifying Resend webhook signatures (so bounces/complaints update your DB safely)
10
+ - Tracking delivery status (sent → delivered → bounced) per message
11
+
12
+ For magic-link / OTP delivery patterns specifically (templates, retry, bounce handling at the user-flow level), see `resend-otp-magic-link.md`.
13
+
14
+ For runtime API doc lookups, use `mcp__plugin_context7_context7__query-docs` with library `/websites/resend` (broader SDK surface) or `/resend/resend-skills` (better tuned for agent-flavored questions).
15
+
16
+ ## Sending — basic transactional
17
+
18
+ ```ts
19
+ // lib/email/resend.ts
20
+ import { Resend } from 'resend'
21
+
22
+ const resend = new Resend(process.env.RESEND_API_KEY!)
23
+
24
+ export async function sendTransactional({
25
+ to,
26
+ subject,
27
+ html,
28
+ tags,
29
+ }: {
30
+ to: string | string[]
31
+ subject: string
32
+ html: string
33
+ tags?: Array<{ name: string; value: string }>
34
+ }) {
35
+ const { data, error } = await resend.emails.send({
36
+ from: process.env.AUTH_EMAIL_FROM || 'YourApp <noreply@example.com>',
37
+ to,
38
+ subject,
39
+ html,
40
+ tags, // showed up in webhook payloads — useful for routing webhooks to the right tracker
41
+ })
42
+ if (error) throw new Error(`Resend: ${error.message}`)
43
+ return data!.id // store this; webhook events reference it
44
+ }
45
+ ```
46
+
47
+ ## Wiring Better Auth's `sendVerificationEmail` (or `sendMagicLink`)
48
+
49
+ Pattern from example-app. The handler is small because Better Auth gives you the URL and email; Resend just delivers:
50
+
51
+ ```ts
52
+ // lib/email/send-magic-link.ts
53
+ import { Resend } from 'resend'
54
+
55
+ export async function sendMagicLinkEmail({ email, url }: { email: string; url: string }) {
56
+ if (!process.env.RESEND_API_KEY) {
57
+ if (process.env.NODE_ENV === 'production') {
58
+ throw new Error('[auth] sendMagicLinkEmail: RESEND_API_KEY missing')
59
+ }
60
+ // Dev: log to console (see better-auth-magic-link.md for the full pattern)
61
+ console.log(`[dev] magic link → ${email}: ${url}`)
62
+ return
63
+ }
64
+
65
+ const resend = new Resend(process.env.RESEND_API_KEY)
66
+ const { error } = await resend.emails.send({
67
+ from: process.env.AUTH_EMAIL_FROM || 'YourApp <noreply@example.com>',
68
+ to: email,
69
+ subject: 'Sign in to YourApp',
70
+ html: `<a href="${url}">Sign in</a>`, // see better-auth-magic-link.md for a styled template
71
+ })
72
+ if (error) throw new Error(`[auth] Resend send failed: ${error.message}`)
73
+ }
74
+ ```
75
+
76
+ Then in `lib/auth.ts`:
77
+
78
+ ```ts
79
+ import { magicLink } from 'better-auth/plugins'
80
+ import { sendMagicLinkEmail } from '@/lib/email/send-magic-link'
81
+
82
+ magicLink({
83
+ expiresIn: 60 * 10,
84
+ sendMagicLink: async ({ email, url }) => sendMagicLinkEmail({ email, url }),
85
+ })
86
+ ```
87
+
88
+ For email-verification on email/password sign-up, swap `magicLink` for `emailVerification` and pass `sendVerificationEmail` instead — same handoff shape.
89
+
90
+ ## Webhook signature verification
91
+
92
+ **Critical:** without signature verification, an attacker can POST fake `email.bounced` events at your webhook to mark legit emails as bounced (potentially blocking real users from receiving mail, depending on your bounce-handling policy).
93
+
94
+ Pattern from `Example Web App/app/api/webhooks/resend/route.ts`:
95
+
96
+ ```ts
97
+ // app/api/webhooks/resend/route.ts
98
+ import { NextRequest, NextResponse } from 'next/server'
99
+ import { headers } from 'next/headers'
100
+ import { createHash, timingSafeEqual } from 'crypto'
101
+
102
+ interface ResendWebhookEvent {
103
+ type: 'email.sent' | 'email.delivered' | 'email.bounced' | 'email.complained' | 'email.delivery_delayed'
104
+ created_at: string
105
+ data: {
106
+ id: string
107
+ to: string[]
108
+ from: string
109
+ subject: string
110
+ created_at: string
111
+ tags?: Array<{ name: string; value: string }>
112
+ }
113
+ }
114
+
115
+ function verifyWebhookSignature(body: string, signature: string, secret: string): boolean {
116
+ try {
117
+ const expected = createHash('sha256').update(body).update(secret).digest('hex')
118
+ const provided = signature.replace('sha256=', '')
119
+ return timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(provided, 'hex'))
120
+ } catch (err) {
121
+ console.error('webhook signature verify failed:', err)
122
+ return false
123
+ }
124
+ }
125
+
126
+ export async function POST(request: NextRequest) {
127
+ const headersList = await headers()
128
+ const signature = headersList.get('resend-signature')
129
+ const webhookSecret = process.env.RESEND_WEBHOOK_SECRET
130
+
131
+ if (!webhookSecret) {
132
+ // Decision call: warn vs hard-fail. Example Web App warns. For a stricter
133
+ // posture, return 401 here when the secret isn't configured in prod.
134
+ console.warn('RESEND_WEBHOOK_SECRET not configured — verification disabled')
135
+ }
136
+
137
+ const body = await request.text() // MUST read raw text BEFORE JSON.parse — signature is over the raw bytes
138
+ if (webhookSecret && signature) {
139
+ if (!verifyWebhookSignature(body, signature, webhookSecret)) {
140
+ return NextResponse.json({ error: 'Invalid signature' }, { status: 401 })
141
+ }
142
+ }
143
+
144
+ let event: ResendWebhookEvent
145
+ try {
146
+ event = JSON.parse(body)
147
+ } catch {
148
+ return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 })
149
+ }
150
+
151
+ // Idempotency: events can deliver more than once. Use event.data.id + event.type
152
+ // as the dedupe key against your delivery-status table.
153
+
154
+ // Map and persist:
155
+ const status = (
156
+ {
157
+ 'email.sent': 'sent',
158
+ 'email.delivered': 'delivered',
159
+ 'email.bounced': 'bounced',
160
+ 'email.complained': 'complained',
161
+ 'email.delivery_delayed': 'sent', // keep as sent, log the delay separately
162
+ } as const
163
+ )[event.type]
164
+
165
+ if (!status) {
166
+ return NextResponse.json({ received: true })
167
+ }
168
+
169
+ await updateEmailDeliveryStatus(event.data.id, status, {
170
+ webhook_type: event.type,
171
+ webhook_received_at: new Date().toISOString(),
172
+ resend_created_at: event.created_at,
173
+ recipients: event.data.to,
174
+ tags: event.data.tags,
175
+ })
176
+
177
+ return NextResponse.json({ received: true, messageId: event.data.id, status })
178
+ }
179
+
180
+ export async function GET() {
181
+ // Health check — useful for verifying the webhook URL is wired before pointing Resend at it
182
+ return NextResponse.json({
183
+ status: 'ok',
184
+ configured: {
185
+ webhook_secret: !!process.env.RESEND_WEBHOOK_SECRET,
186
+ resend_api_key: !!process.env.RESEND_API_KEY,
187
+ },
188
+ })
189
+ }
190
+ ```
191
+
192
+ ### Why the specific crypto choices
193
+
194
+ - **`timingSafeEqual`** (not `===`) — prevents timing-based signature forgery. `===` short-circuits on first mismatched byte and leaks signature length info.
195
+ - **`createHash('sha256').update(body).update(secret)`** — the order matches what Resend signs. If you swap them, signatures won't verify.
196
+ - **Read raw text BEFORE `JSON.parse`** — Next.js will not give you the unparsed body if you call `request.json()` first; the bytes for signature verification have to be the exact wire bytes.
197
+
198
+ ## Idempotency — same event twice
199
+
200
+ Resend webhooks are at-least-once. If your `updateEmailDeliveryStatus` is non-idempotent, a redelivered event flips `bounced` → `sent` → `bounced`. Two patterns:
201
+
202
+ 1. **State-machine guard** — `delivered` is terminal; once a message is `delivered`, ignore subsequent non-`bounced` events. `bounced`/`complained` always wins.
203
+ 2. **Event-id dedupe** — store `(message_id, event_type, received_at)` as a unique tuple; if it's a duplicate, no-op.
204
+
205
+ Pattern 1 is simpler; pattern 2 is correct under arbitrary event ordering. For most apps, pattern 1 is fine.
206
+
207
+ ## Required env vars
208
+
209
+ ```env
210
+ RESEND_API_KEY=re_...
211
+ RESEND_WEBHOOK_SECRET=whsec_... # configure in Resend dashboard, then paste here
212
+ AUTH_EMAIL_FROM="YourApp <noreply@yourdomain.com>" # MUST match a verified domain
213
+ ```
214
+
215
+ ## Verification
216
+
217
+ 1. Send a test email via the SDK. Confirm Resend dashboard shows it.
218
+ 2. Check the webhook health endpoint: `GET /api/webhooks/resend` should return `configured: { webhook_secret: true, resend_api_key: true }`.
219
+ 3. From the Resend dashboard, send a test webhook. Confirm 200 response with valid signature; tamper with the body and confirm 401.
220
+ 4. Send to a bouncing address (`bounce@simulator.amazonses.com` if Resend is on SES) and confirm the `email.bounced` event lands and updates your tracker.
221
+ 5. Confirm the `from` address matches a verified domain — sending from an unverified domain returns a permission error from the SDK, not a webhook event.
222
+
223
+ ## Cross-references
224
+
225
+ - `resend-otp-magic-link.md` — delivery / retry / bounce-handling patterns at the user-flow level
226
+ - `better-auth-magic-link.md` — caller-side handoff pattern
227
+ - For domain verification, DKIM, and SPF setup: query Context7 with `library: "/websites/resend"`, `topic: "domain verification"`
@@ -0,0 +1,153 @@
1
+ <!-- SPDX-FileCopyrightText: 2025-2026 Tyrone Ross, Jr <46267523+tyroneross@users.noreply.github.com> | SPDX-License-Identifier: Apache-2.0 -->
2
+
3
+ # Resend — OTP / Magic-Link Delivery Patterns
4
+
5
+ ## When to use
6
+
7
+ - Designing the user-flow side of magic-link or OTP delivery (templates, retry, bounce handling)
8
+ - Choosing between magic-link and OTP for your app
9
+ - Handling delivery failures gracefully without exposing email-existence to attackers
10
+
11
+ For the SDK + webhook signature verification side, see `resend-email.md`.
12
+ For the Better Auth plugin side (server config, expiry, single-use), see `better-auth-magic-link.md`.
13
+ For runtime API doc lookups, use `mcp__plugin_context7_context7__query-docs` with `library: "/websites/resend"` (broader) or `/resend/resend-skills` (agent patterns).
14
+
15
+ ## Magic-link vs OTP — pick one
16
+
17
+ | Factor | Magic Link | OTP (numeric code) |
18
+ |---|---|---|
19
+ | UX on the device that requested sign-in | One click — best | Type 6 digits |
20
+ | UX on a different device than the request | Awful (have to forward link) | Good (read code on phone, type on laptop) |
21
+ | Mobile app sign-in | OK with deep links, but easy to misconfigure | Better — code transcription works regardless of platform |
22
+ | Phishing surface | Higher — link can be cloaked | Lower — code in body is hard to fake |
23
+ | Server complexity | Same plugin, simpler delivery | Same plugin, slightly different UX |
24
+
25
+ **Default**: magic-link for web-first apps, OTP for mobile-first or multi-device flows. Both can co-exist behind a "Send code" / "Email link instead" toggle.
26
+
27
+ ## Template — magic-link (HTML)
28
+
29
+ Inline styles only — many email clients strip `<style>` blocks. Keep the styled CTA above the fold (most clients only show the first ~600px).
30
+
31
+ ```ts
32
+ const html = `
33
+ <!DOCTYPE html>
34
+ <html>
35
+ <body style="margin: 0; background: #f5f5f7; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;">
36
+ <div style="max-width: 480px; margin: 0 auto; padding: 32px 24px;">
37
+ <h1 style="font-size: 22px; color: #111; margin: 0 0 12px;">Sign in to ${appName}</h1>
38
+ <p style="color: #555; line-height: 1.5; margin: 0 0 24px;">
39
+ Click the button below to sign in. This link expires in 10 minutes and can only be used once.
40
+ </p>
41
+ <a href="${url}" style="display: inline-block; padding: 14px 24px; background: #111; color: #fff; text-decoration: none; border-radius: 8px; font-weight: 500;">
42
+ Sign in
43
+ </a>
44
+ <p style="color: #888; font-size: 12px; margin: 32px 0 0; line-height: 1.5;">
45
+ If the button doesn't work, paste this into your browser:<br>
46
+ <span style="color: #555; word-break: break-all;">${url}</span>
47
+ </p>
48
+ <p style="color: #aaa; font-size: 12px; margin: 16px 0 0;">
49
+ If you didn't request this, you can ignore this email.
50
+ </p>
51
+ </div>
52
+ </body>
53
+ </html>
54
+ `
55
+ ```
56
+
57
+ ## Template — OTP (HTML)
58
+
59
+ ```ts
60
+ const html = `
61
+ <!DOCTYPE html>
62
+ <html>
63
+ <body style="margin: 0; background: #f5f5f7; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;">
64
+ <div style="max-width: 480px; margin: 0 auto; padding: 32px 24px;">
65
+ <h1 style="font-size: 22px; color: #111; margin: 0 0 12px;">Your sign-in code</h1>
66
+ <p style="color: #555; line-height: 1.5; margin: 0 0 24px;">
67
+ Enter this code in ${appName} to sign in. It expires in 10 minutes.
68
+ </p>
69
+ <div style="font-size: 32px; letter-spacing: 8px; font-weight: 700; text-align: center; padding: 20px; background: #fff; border: 1px solid #e5e5e7; border-radius: 8px; color: #111; font-family: 'SF Mono', Monaco, Consolas, monospace;">
70
+ ${code}
71
+ </div>
72
+ <p style="color: #aaa; font-size: 12px; margin: 24px 0 0;">
73
+ If you didn't request this, you can ignore this email.
74
+ </p>
75
+ </div>
76
+ </body>
77
+ </html>
78
+ `
79
+ ```
80
+
81
+ OTP-specific UX rules:
82
+ - **Don't include the OTP in the email subject line** — visible in lock-screen previews. Use a generic subject ("Your sign-in code").
83
+ - **Plain-text fallback** — many auto-readers (smart watches, accessibility tools) parse plain text only.
84
+ - **No clickable URL alongside the code** — defeats the phishing-resistance benefit.
85
+
86
+ ## Retry / backoff on 429
87
+
88
+ Resend returns 429 if you exceed your rate plan. Pattern:
89
+
90
+ ```ts
91
+ async function sendWithRetry({ to, subject, html, attempt = 0 }: {
92
+ to: string; subject: string; html: string; attempt?: number
93
+ }): Promise<{ id: string }> {
94
+ const resend = new Resend(process.env.RESEND_API_KEY!)
95
+ const { data, error } = await resend.emails.send({
96
+ from: process.env.AUTH_EMAIL_FROM!, to, subject, html,
97
+ })
98
+
99
+ if (!error) return { id: data!.id }
100
+
101
+ // Retry on 429 with jittered exponential backoff, max 3 attempts
102
+ if (error.name === 'rate_limit_exceeded' && attempt < 2) {
103
+ const delay = (2 ** attempt) * 500 + Math.random() * 250
104
+ await new Promise((r) => setTimeout(r, delay))
105
+ return sendWithRetry({ to, subject, html, attempt: attempt + 1 })
106
+ }
107
+
108
+ // 4xx other than 429 are config errors — don't retry, surface up
109
+ // 5xx are Resend-side; retry once then surface
110
+ if (error.name === 'application_error' && attempt < 1) {
111
+ await new Promise((r) => setTimeout(r, 1000))
112
+ return sendWithRetry({ to, subject, html, attempt: attempt + 1 })
113
+ }
114
+
115
+ throw new Error(`Resend send failed: ${error.message}`)
116
+ }
117
+ ```
118
+
119
+ Don't queue magic-link / OTP sends through a worker — they're sub-second user-blocking actions. If retries fail, surface "Couldn't send code, try again" rather than silently failing.
120
+
121
+ ## Bounce handling — what to show the user
122
+
123
+ When Resend webhooks report `email.bounced` for a magic-link / OTP send, the user is sitting at the "check your email" screen. Options:
124
+
125
+ 1. **Generic message** ("If your email is registered, you'll receive a code in a minute") — does NOT distinguish bounce from success. Best for security; protects against email-existence enumeration.
126
+ 2. **Specific message after timeout** — after, say, 45s without delivery confirmation, show "We couldn't reach that email — double-check the address." Slightly leaks existence info but better UX.
127
+
128
+ **Default to (1) unless your threat model says otherwise.** Email-existence enumeration is a real attack class (drives credential-stuffing target lists).
129
+
130
+ For ops: log every `bounced` and `complained` event with context. A spike usually means either a misconfigured domain (DKIM/SPF) or an attacker enumerating addresses.
131
+
132
+ ## Idempotent re-send
133
+
134
+ Users will tap "Resend code" / "Send link again" repeatedly. Behavior:
135
+
136
+ - **Within `expiresIn` window**: invalidate the previous token, send a new one. Don't accumulate active tokens.
137
+ - **Rate-limit per email**: max 3 sends per 10 minutes. After that, return generic success without sending — don't reveal the rate-limit to a flood attacker.
138
+ - **Track resend count in your DB** so abuse stands out.
139
+
140
+ ## Verification
141
+
142
+ 1. Send a magic-link, click it, confirm session.
143
+ 2. Send a magic-link, wait 11 min, click it — confirm "expired" state.
144
+ 3. Send a magic-link, then immediately request another — confirm the first is invalidated (or both work, depending on your design; pick one and document).
145
+ 4. Send to a bouncing address — confirm the user sees the generic "check your email" UI, not an error, and the bounce is logged server-side.
146
+ 5. Resend 4 times in a minute — confirm rate-limit kicks in, user sees identical UI.
147
+ 6. Open the email on a phone — verify CTA button is finger-sized (≥44pt) and OTP code is selectable as a single block.
148
+
149
+ ## Cross-references
150
+
151
+ - `resend-email.md` — SDK + webhook signature verification
152
+ - `better-auth-magic-link.md` — server-side plugin config (`expiresIn`, single-use)
153
+ - Universal footgun #10 (magic-link expiry & idempotency) in `../SKILL.md`