create-ailk 0.18.0 → 0.19.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. package/LICENSE +6 -5
  2. package/NOTICE +2 -0
  3. package/dist/add-module.js +11 -3
  4. package/dist/category-profile.d.ts +10 -0
  5. package/dist/category-profile.js +21 -0
  6. package/dist/derive-architecture.d.ts +5 -1
  7. package/dist/derive-architecture.js +8 -3
  8. package/dist/index.d.ts +1 -1
  9. package/dist/index.js +1 -1
  10. package/dist/lib/license-key-files.d.ts +6 -3
  11. package/dist/lib/license-key-files.js +8 -2
  12. package/dist/lib/scaffold-render-sweep.d.ts +32 -0
  13. package/dist/lib/scaffold-render-sweep.js +76 -3
  14. package/dist/license-set.d.ts +1 -1
  15. package/dist/license-set.js +13 -8
  16. package/dist/module-architecture.js +128 -3
  17. package/dist/programmatic.js +11 -1
  18. package/dist/site-roster.js +21 -4
  19. package/dist/spoke-checkout.d.ts +11 -3
  20. package/dist/spoke-checkout.js +62 -12
  21. package/dist/starter-grid.d.ts +1 -1
  22. package/dist/surfaces.d.ts +14 -0
  23. package/dist/surfaces.js +170 -1
  24. package/dist/template-manifest.d.ts +4 -3
  25. package/dist/topology.d.ts +7 -2
  26. package/dist/topology.js +88 -3
  27. package/package.json +5 -4
  28. package/templates/.env.example +3 -3
  29. package/templates/.github/workflows/ci.yml +5 -0
  30. package/templates/apps/api/.env.example +17 -1
  31. package/templates/apps/api/CLAUDE.md +31 -7
  32. package/templates/apps/api/LICENSE +6 -5
  33. package/templates/apps/api/NOTICE +2 -0
  34. package/templates/apps/api/jest.config.cjs +4 -29
  35. package/templates/apps/api/package.json +1 -0
  36. package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +19 -0
  37. package/templates/apps/api/src/__tests__/stripe-boot.test.ts +132 -0
  38. package/templates/apps/api/src/lib/__mocks__/prisma.ts +17 -0
  39. package/templates/apps/api/src/lib/__tests__/consent-ledger-double.ts +237 -0
  40. package/templates/apps/api/src/lib/__tests__/consent-store.test.ts +334 -0
  41. package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +38 -0
  42. package/templates/apps/api/src/lib/__tests__/stripe-admission.test.ts +217 -0
  43. package/templates/apps/api/src/lib/__tests__/stripe.test.ts +7 -0
  44. package/templates/apps/api/src/lib/consent-store.ts +335 -0
  45. package/templates/apps/api/src/lib/deliverable-asset-store.ts +5 -4
  46. package/templates/apps/api/src/lib/stripe.ts +176 -3
  47. package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +272 -39
  48. package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +51 -0
  49. package/templates/apps/api/src/middleware/auth.ts +214 -46
  50. package/templates/apps/api/src/middleware/rate-limit.ts +5 -0
  51. package/templates/apps/api/src/routes/checkout/__tests__/live-key.test.ts +167 -0
  52. package/templates/apps/api/src/routes/checkout/session.ts +3 -3
  53. package/templates/apps/api/src/routes/checkout/sessions.ts +6 -4
  54. package/templates/apps/api/src/routes/consent/cookie.ts +90 -0
  55. package/templates/apps/api/src/routes/consent/index.ts +18 -0
  56. package/templates/apps/api/src/routes/consent/unsubscribe.ts +127 -0
  57. package/templates/apps/api/src/routes/leads/index.ts +19 -0
  58. package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +5 -0
  59. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +17 -4
  60. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +2 -0
  61. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-livemode-live.test.ts +159 -0
  62. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-livemode.test.ts +158 -0
  63. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +2 -0
  64. package/templates/apps/api/src/routes/webhooks/stripe.ts +17 -0
  65. package/templates/apps/api/src/server.ts +218 -16
  66. package/templates/apps/api/src/services/__tests__/consent-ledger.test.ts +503 -0
  67. package/templates/apps/api/src/services/__tests__/consent.test.ts +18 -3
  68. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +173 -4
  69. package/templates/apps/api/src/services/__tests__/person.test.ts +392 -0
  70. package/templates/apps/api/src/services/__tests__/request-confirm.test.ts +87 -0
  71. package/templates/apps/api/src/services/__tests__/revalidation.test.ts +57 -17
  72. package/templates/apps/api/src/services/consent.ts +488 -3
  73. package/templates/apps/api/src/services/lead-routing.ts +135 -14
  74. package/templates/apps/api/src/services/person.ts +501 -1
  75. package/templates/apps/api/src/services/project-listings.ts +7 -2
  76. package/templates/apps/api/src/services/request-confirm.ts +52 -0
  77. package/templates/apps/api/src/services/revalidation.ts +11 -1
  78. package/templates/apps/api/vercel.json +2 -2
  79. package/templates/apps/mcp/.env.example +10 -0
  80. package/templates/apps/mcp/CLAUDE.md +9 -4
  81. package/templates/apps/mcp/LICENSE +6 -5
  82. package/templates/apps/mcp/NOTICE +2 -0
  83. package/templates/apps/mcp/__tests__/abuse/guard.test.ts +198 -0
  84. package/templates/apps/mcp/__tests__/audit-single-page.test.ts +15 -3
  85. package/templates/apps/mcp/__tests__/boot.test.ts +78 -1
  86. package/templates/apps/mcp/__tests__/http-transport.test.ts +291 -2
  87. package/templates/apps/mcp/__tests__/request_callback.test.ts +69 -7
  88. package/templates/apps/mcp/__tests__/submit_lead.test.ts +69 -8
  89. package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +69 -7
  90. package/templates/apps/mcp/jest.config.cjs +3 -44
  91. package/templates/apps/mcp/src/abuse/index.ts +117 -21
  92. package/templates/apps/mcp/src/boot.ts +5 -23
  93. package/templates/apps/mcp/src/http-transport.ts +93 -73
  94. package/templates/apps/mcp/src/index.ts +2 -0
  95. package/templates/apps/mcp/src/request-context.ts +52 -0
  96. package/templates/apps/mcp/src/tools/capture_lead.ts +9 -1
  97. package/templates/apps/mcp/src/tools/request_callback.ts +9 -1
  98. package/templates/apps/mcp/src/tools/subscribe_newsletter.ts +9 -1
  99. package/templates/apps/web/LICENSE +6 -5
  100. package/templates/apps/web/NOTICE +2 -0
  101. package/templates/apps/web/app/.well-known/ai-plugin.json/route.ts +3 -3
  102. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/account-frame-sign-out.test.tsx +175 -0
  103. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/billing.test.tsx +302 -0
  104. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/general.test.tsx +724 -0
  105. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/hub-mode.test.tsx +346 -41
  106. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/item.test.tsx +11 -13
  107. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout-sign-out.test.tsx +146 -0
  108. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout.test.tsx +232 -29
  109. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/new-menu.test.tsx +305 -0
  110. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/new-popups.test.tsx +82 -0
  111. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/notifications.test.tsx +140 -0
  112. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +163 -72
  113. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/popups.test.tsx +451 -0
  114. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/security.test.tsx +409 -0
  115. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/settings.test.tsx +65 -380
  116. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/support.test.tsx +16 -11
  117. package/templates/apps/web/app/[locale]/(authed)/account/account-data.ts +1 -1
  118. package/templates/apps/web/app/[locale]/(authed)/account/account-frame.tsx +188 -54
  119. package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +24 -45
  120. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/page.tsx +253 -0
  121. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/review-form.tsx +145 -0
  122. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/versions/[version]/file/route.ts +100 -0
  123. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/file-route.test.ts +179 -0
  124. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/page.test.tsx +514 -0
  125. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/review.test.tsx +288 -0
  126. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/artifact-actions.ts +123 -0
  127. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/artifact-data.ts +82 -0
  128. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/page.tsx +174 -0
  129. package/templates/apps/web/app/[locale]/(authed)/account/billing/page.tsx +172 -0
  130. package/templates/apps/web/app/[locale]/(authed)/account/billing-source.ts +147 -0
  131. package/templates/apps/web/app/[locale]/(authed)/account/components/request-quote.tsx +229 -0
  132. package/templates/apps/web/app/[locale]/(authed)/account/components/schedule-service.tsx +276 -0
  133. package/templates/apps/web/app/[locale]/(authed)/account/components/send-message.tsx +128 -0
  134. package/templates/apps/web/app/[locale]/(authed)/account/general/billing-anchor.tsx +27 -0
  135. package/templates/apps/web/app/[locale]/(authed)/account/general/contact-actions.ts +81 -0
  136. package/templates/apps/web/app/[locale]/(authed)/account/general/deletion-request.tsx +132 -0
  137. package/templates/apps/web/app/[locale]/(authed)/account/general/page.tsx +193 -0
  138. package/templates/apps/web/app/[locale]/(authed)/account/general/profile-form.tsx +104 -0
  139. package/templates/apps/web/app/[locale]/(authed)/account/general/profile-source.ts +73 -0
  140. package/templates/apps/web/app/[locale]/(authed)/account/inbox/message/[id]/file/__tests__/route.test.ts +18 -8
  141. package/templates/apps/web/app/[locale]/(authed)/account/inbox/message/[id]/file/route.ts +4 -6
  142. package/templates/apps/web/app/[locale]/(authed)/account/layout.tsx +30 -4
  143. package/templates/apps/web/app/[locale]/(authed)/account/new-actions.ts +219 -0
  144. package/templates/apps/web/app/[locale]/(authed)/account/new-menu.tsx +340 -0
  145. package/templates/apps/web/app/[locale]/(authed)/account/new-popups.tsx +107 -0
  146. package/templates/apps/web/app/[locale]/(authed)/account/notifications/page.tsx +57 -0
  147. package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +85 -26
  148. package/templates/apps/web/app/[locale]/(authed)/account/profile-actions.ts +116 -0
  149. package/templates/apps/web/app/[locale]/(authed)/account/security/page.tsx +132 -0
  150. package/templates/apps/web/app/[locale]/(authed)/account/security/security-panel.tsx +102 -0
  151. package/templates/apps/web/app/[locale]/(authed)/account/security/security-source.ts +62 -0
  152. package/templates/apps/web/app/[locale]/(authed)/account/settings/page.tsx +13 -354
  153. package/templates/apps/web/app/[locale]/(authed)/account/settings-frame.tsx +116 -0
  154. package/templates/apps/web/app/[locale]/(authed)/account/sign-in-method.ts +37 -0
  155. package/templates/apps/web/app/[locale]/(authed)/account/support-actions.ts +9 -10
  156. package/templates/apps/web/app/[locale]/layout.tsx +23 -4
  157. package/templates/apps/web/app/api/auth/hub/__tests__/continuation.test.ts +143 -0
  158. package/templates/apps/web/app/api/auth/hub/authorize/__tests__/route.test.ts +62 -10
  159. package/templates/apps/web/app/api/auth/hub/authorize/route.ts +22 -7
  160. package/templates/apps/web/app/api/auth/hub/continuation.ts +73 -0
  161. package/templates/apps/web/app/api/auth/hub/continue/[ref]/__tests__/route.test.ts +188 -0
  162. package/templates/apps/web/app/api/auth/hub/continue/[ref]/route.ts +53 -0
  163. package/templates/apps/web/app/robots.txt/route.ts +10 -4
  164. package/templates/apps/web/auth.ts +25 -2
  165. package/templates/apps/web/lib/__tests__/account-config.test.ts +258 -46
  166. package/templates/apps/web/lib/__tests__/auth-email.test.ts +84 -0
  167. package/templates/apps/web/lib/account-config.ts +188 -52
  168. package/templates/apps/web/lib/auth-email.ts +45 -10
  169. package/templates/apps/web/lib/hub-session/__tests__/hub-session.test.ts +143 -0
  170. package/templates/apps/web/lib/hub-session/index.ts +1 -0
  171. package/templates/apps/web/lib/hub-session/reseal.ts +90 -0
  172. package/templates/apps/web/lib/hub-session/seal.ts +13 -9
  173. package/templates/apps/web/lib/topology.generated.ts +33 -0
  174. package/templates/apps/web/messages/en.json +194 -33
  175. package/templates/database/CHANGELOG.md +37 -0
  176. package/templates/database/LICENSE +6 -5
  177. package/templates/database/NOTICE +2 -0
  178. package/templates/database/README.md +1 -1
  179. package/templates/database/__tests__/consent-ledger.db.test.ts +345 -0
  180. package/templates/database/__tests__/fence-fan-out.db.test.ts +6 -2
  181. package/templates/database/__tests__/fixtures/person-contact-backfill.json +182 -0
  182. package/templates/database/__tests__/member-area.db.test.ts +15 -3
  183. package/templates/database/__tests__/member-contract-migration.test.ts +85 -0
  184. package/templates/database/__tests__/member-contract.db.test.ts +413 -0
  185. package/templates/database/__tests__/person-contact.db.test.ts +563 -0
  186. package/templates/database/__tests__/person.db.test.ts +3 -1
  187. package/templates/database/__tests__/seed.test.ts +54 -2
  188. package/templates/database/inbox/schema.prisma +166 -25
  189. package/templates/database/migrations/20261012120000_member_contract/migration.sql +192 -0
  190. package/templates/database/migrations/20261013120000_lead_spoke_checkout/migration.sql +20 -0
  191. package/templates/database/migrations/20261014120000_consent_ledger/migration.sql +122 -0
  192. package/templates/database/migrations/20261014120000_person_contact/migration.sql +231 -0
  193. package/templates/database/package.json +3 -2
  194. package/templates/database/seed.ts +34 -2
  195. package/versions.json +36 -36
  196. package/templates/apps/api/src/__tests__/content-write-production-boot.test.ts +0 -104
  197. package/templates/apps/api/src/__tests__/cors.test.ts +0 -276
  198. package/templates/apps/api/src/__tests__/module-exclusion.test.ts +0 -279
  199. package/templates/apps/api/src/__tests__/openapi-keyless-hub.test.ts +0 -122
  200. package/templates/apps/api/src/__tests__/review-surface-registration.test.ts +0 -167
  201. package/templates/apps/api/src/__tests__/root-modules.test.ts +0 -175
  202. package/templates/apps/api/src/__tests__/schedule-forwarded-rate-limit.test.ts +0 -117
  203. package/templates/apps/api/src/__tests__/server.test.ts +0 -591
  204. package/templates/apps/api/src/__tests__/workspace-module-registration.test.ts +0 -175
  205. package/templates/apps/api/src/openapi/__tests__/surface-drift.test.ts +0 -293
  206. package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +0 -1620
  207. package/templates/apps/api/src/routes/project-listings/__tests__/directory.test.ts +0 -528
  208. package/templates/apps/api/src/routes/workspaces/__tests__/inbox-wire-schemas.test.ts +0 -198
  209. package/templates/apps/api/src/services/__tests__/aeo-score.test.ts +0 -652
  210. package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +0 -408
  211. package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +0 -476
  212. package/templates/apps/mcp/__tests__/audit.test.ts +0 -346
  213. package/templates/apps/mcp/__tests__/parity.test.ts +0 -325
  214. package/templates/apps/mcp/__tests__/workspace-surface.test.ts +0 -364
  215. package/templates/apps/web/app/[locale]/(authed)/account/settings/color-mode.tsx +0 -68
  216. package/templates/apps/web/app/[locale]/(authed)/account/settings/deletion-request.tsx +0 -55
  217. package/templates/apps/web/app/[locale]/(authed)/account/settings/name-form.tsx +0 -118
  218. package/templates/database/__tests__/build.test.ts +0 -131
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 2,
3
- "ignoreCommand": "git diff --quiet HEAD^ HEAD -- apps/api packages database",
4
- "buildCommand": "cd ../.. && pnpm run --filter @ai-launch-kit/backend build:vercel",
3
+ "ignoreCommand": "[ -n \"$VERCEL_GIT_PREVIOUS_SHA\" ] && cd ../.. && git diff --quiet \"$VERCEL_GIT_PREVIOUS_SHA\" HEAD -- apps/api packages database pnpm-lock.yaml || exit 1",
4
+ "buildCommand": "cd ../.. && pnpm run --filter @working-theory/api build:vercel",
5
5
  "installCommand": "cd ../.. && pnpm install --frozen-lockfile",
6
6
  "outputDirectory": "public",
7
7
  "framework": null,
@@ -58,6 +58,16 @@ MCP_ABUSE_CAPTURE_LEAD_DAILY_CAP=50
58
58
  # hosted Checkout Session, so the default is deliberately lower.
59
59
  MCP_ABUSE_START_PURCHASE_DAILY_CAP=20
60
60
 
61
+ # Maximum number of create_booking calls per IP per day (#8071). Its own
62
+ # budget — each call creates a real calendar event and emails the attendee an
63
+ # invite, so the default matches start_purchase's.
64
+ MCP_ABUSE_CREATE_BOOKING_DAILY_CAP=20
65
+
66
+ # Maximum number of submit_flow_step calls per IP per day (#8071). Its own
67
+ # budget, counted per step, not per flow — equal to capture_lead's, because a
68
+ # one-step flow creates a Lead in one call.
69
+ MCP_ABUSE_SUBMIT_FLOW_STEP_DAILY_CAP=50
70
+
61
71
  # Comma-separated list of User-Agent substrings to block (case-insensitive).
62
72
  # Built-in list already blocks curl, wget, python-requests, scrapy, etc.
63
73
  # Add extra patterns here as needed.
@@ -44,7 +44,8 @@ Inherits root `CLAUDE.md`. This file adds MCP-specific rules.
44
44
  that proxy. It is a function, not `trustProxy: 1`, because Fastify 5.12 treats a numeric
45
45
  hop count as trusting nothing. The long-running entry trusts no proxy.
46
46
  - **Per address, per instance.** 60 requests a minute across reads and writes (the write rate
47
- is parsed but not enforced); `capture_lead` 50 a day and `start_purchase` 20 a day.
47
+ is parsed but not enforced); `capture_lead` 50 a day, `start_purchase` 20 a day,
48
+ `create_booking` 20 a day and `submit_flow_step` 50 a day, each on its own budget.
48
49
  `subscribe_newsletter` and `request_callback` have no daily cap. Each warm instance counts
49
50
  on its own and a cold start resets the counts, so a caller reaching N instances gets up to
50
51
  N times each limit.
@@ -70,11 +71,13 @@ Every tool maps 1:1 to an `apps/api` endpoint. When you change a tool, change th
70
71
 
71
72
  ## The free anonymous tools (enabled at launch)
72
73
 
74
+ **The HTTP write branch (6).** `WRITE_TOOLS` in `src/http-transport.ts` — `capture_lead`, `subscribe_newsletter`, `request_callback`, `start_purchase`, `create_booking`, `submit_flow_step` — takes the write-origin allow-list and the AbuseGuard (UA, honeypot, email format, per-IP daily cap); every other tool takes the read branch (UA only). A tool that sends caller-chosen data to the hub belongs in it: `__tests__/write-tool-guard.test.ts` fails when a catalog tool that sends a non-GET request through `@working-theory/api-client` is missing, and makes every new catalog tool pick a branch (#8071).
75
+
73
76
  **Read (9):** `list_pages`, `read_content`, `search_content`, `get_site_info`, `get_business_hours`, `get_locations`, `get_offerings`, `get_pricing`, `get_faq`
74
77
  **Conversion-write (3):** `capture_lead`, `subscribe_newsletter`, `request_callback`
75
78
  **Maintenance (2):** `audit`, `create_project`
76
- **Flow (2):** `get_flow`, `submit_flow_step` — module-gated (`module: "signal-collectors"`, D-48, #5366) and licence-feature-tagged (`feature: "flows-deliverables"`, #7136): identity-blind like every tool in this list, registered only when the signal-collectors module is present, and gated on the deployment's `flows-deliverables` licence feature under `MCP_FEATURE_GATE_MODE`, the same shape Booking below has
77
- **Booking (3):** `list_availability`, `get_event_meta`, `create_booking` — module-gated (`module: "scheduling"`) and licence-feature-tagged (`feature: "scheduling"`, #7136), gated on the deployment's `scheduling` licence feature under `MCP_FEATURE_GATE_MODE`
79
+ **Flow (2):** `get_flow`, `submit_flow_step` — module-gated (`module: "signal-collectors"`, D-48, #5366) and licence-feature-tagged (`feature: "flows-deliverables"`, #7136): identity-blind like every tool in this list, registered only when the signal-collectors module is present, and gated on the deployment's `flows-deliverables` licence feature under `MCP_FEATURE_GATE_MODE`, the same shape Booking below has. `submit_flow_step` takes the HTTP write branch (its own daily cap, `MCP_ABUSE_SUBMIT_FLOW_STEP_DAILY_CAP`, default 50 — #8071)
80
+ **Booking (3):** `list_availability`, `get_event_meta`, `create_booking` — module-gated (`module: "scheduling"`) and licence-feature-tagged (`feature: "scheduling"`, #7136), gated on the deployment's `scheduling` licence feature under `MCP_FEATURE_GATE_MODE`. `create_booking` takes the HTTP write branch (its own daily cap, `MCP_ABUSE_CREATE_BOOKING_DAILY_CAP`, default 20, and an `attendee.email` format check — #8071)
78
81
 
79
82
  ### Why the parity test counts 20 and a keyless server registers 19
80
83
 
@@ -208,7 +211,9 @@ Example (lead routing):
208
211
  - Don't add a tool without the matching `apps/api` endpoint.
209
212
  - Don't put business logic in tool handlers — they're adapters.
210
213
 
211
- Last updated: 2026-10-10 — #8070 (amendment 2): the Vercel entry exports a handler, no `vercel.json`; § Two entrypoints, one boot — the Vercel entry `src/app.ts`, the shared `src/boot.ts`, `MCP_HTTP_STATELESS`, `mcpContentRoot()`, `create_project` withheld on a spoke, and what a spoke's MCP host enforces — verified against `apps/mcp/src/{app,boot,serverless-app,index,http-transport,server}.ts` and `lib/content-adapter.ts`.
214
+ Last updated: 2026-10-10 — #8071: `create_booking` and `submit_flow_step` join the HTTP write branch — § The free anonymous tools names its six members, the Booking and Flow rows record it, and § What a spoke's MCP host on Vercel enforces lists the two new daily caps — verified against `apps/mcp/src/http-transport.ts` and `apps/mcp/src/abuse/index.ts`.
215
+
216
+ Prior: 2026-10-10 — #8070 (amendment 2): the Vercel entry exports a handler, no `vercel.json`; § Two entrypoints, one boot — the Vercel entry `src/app.ts`, the shared `src/boot.ts`, `MCP_HTTP_STATELESS`, `mcpContentRoot()`, `create_project` withheld on a spoke, and what a spoke's MCP host enforces — verified against `apps/mcp/src/{app,boot,serverless-app,index,http-transport,server}.ts` and `lib/content-adapter.ts`.
212
217
 
213
218
  Prior: 2026-10-04 — #3591 PR-E (#7684): the identity-blind rule is scoped to the anonymous `/mcp`, and the new § The signed-in workspace surface records `/mcp/workspace` — POST-only and stateless, an agent key per request forwarded to each tool's apps/api twin, eleven `surface: "workspace"` catalog rows that are parity `twin`s and never in the discovery manifest — verified against `apps/mcp/src/http-transport.ts`, `apps/mcp/src/workspace-server.ts` and `packages/metadata/src/mcp-tool-catalog.ts`.
214
219
 
@@ -1,3 +1,4 @@
1
+
1
2
  Apache License
2
3
  Version 2.0, January 2004
3
4
  http://www.apache.org/licenses/
@@ -137,8 +138,8 @@
137
138
 
138
139
  6. Trademarks. This License does not grant permission to use the trade
139
140
  names, trademarks, service marks, or product names of the Licensor,
140
- except as required for describing the origin of the Work and
141
- reproducing the content of the NOTICE file.
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
142
143
 
143
144
  7. Disclaimer of Warranty. Unless required by applicable law or
144
145
  agreed to in writing, Licensor provides the Work (and each
@@ -162,7 +163,7 @@
162
163
  other commercial damages or losses), even if such Contributor
163
164
  has been advised of the possibility of such damages.
164
165
 
165
- 9. Accepting Warranty or Support. While redistributing
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
167
  the Work or Derivative Works thereof, You may choose to offer,
167
168
  and charge a fee for, acceptance of support, warranty, indemnity,
168
169
  or other liability obligations and/or rights consistent with this
@@ -171,7 +172,7 @@
171
172
  of any other Contributor, and only if You agree to indemnify,
172
173
  defend, and hold each Contributor harmless for any liability
173
174
  incurred by, or claims asserted against, such Contributor by reason
174
- of your accepting any such warranty or support.
175
+ of your accepting any such warranty or additional liability.
175
176
 
176
177
  END OF TERMS AND CONDITIONS
177
178
 
@@ -186,7 +187,7 @@
186
187
  same "printed page" as the copyright notice for easier
187
188
  identification within third-party archives.
188
189
 
189
- Copyright 2026 Tailwind AI
190
+ Copyright [yyyy] [name of copyright owner]
190
191
 
191
192
  Licensed under the Apache License, Version 2.0 (the "License");
192
193
  you may not use this file except in compliance with the License.
@@ -0,0 +1,2 @@
1
+ AI Launch Kit
2
+ Copyright 2026 SalesSmyth LLC
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Guard-level tests for abuse/index.ts — the AbuseGuard as a whole, where
3
+ * daily-cap.test.ts and its siblings test one rule each (#8071).
4
+ *
5
+ * Pins:
6
+ * - AC-4: the four daily-capped tools (capture_lead, start_purchase,
7
+ * create_booking, submit_flow_step) each spend their OWN tracker — spending
8
+ * one tool's budget from an IP leaves the other three callable from it.
9
+ * - AC-5: a blank or non-numeric MCP_ABUSE_CREATE_BOOKING_DAILY_CAP /
10
+ * MCP_ABUSE_SUBMIT_FLOW_STEP_DAILY_CAP yields the default cap (20 / 50),
11
+ * never an unlimited one: the call at default + 1 is refused.
12
+ * - D3: create_booking's attendee.email is format-checked, and a non-object
13
+ * attendee is treated as a missing email rather than throwing.
14
+ */
15
+
16
+ import type { FastifyRequest } from "fastify";
17
+
18
+ import {
19
+ AbuseGuard,
20
+ AbuseReasonCode,
21
+ createAbuseGuardFromEnv,
22
+ DEFAULT_CREATE_BOOKING_DAILY_CAP,
23
+ DEFAULT_SUBMIT_FLOW_STEP_DAILY_CAP,
24
+ } from "../../src/abuse/index.js";
25
+
26
+ /** A stub carrying only the two members the guard reads. */
27
+ function requestFrom(ip: string): FastifyRequest {
28
+ // @ts-expect-error: a full FastifyRequest cannot be built without a server.
29
+ return {
30
+ headers: { "user-agent": "Mozilla/5.0 (compatible; TestAgent/1.0)" },
31
+ ip,
32
+ };
33
+ }
34
+
35
+ const CAPPED_TOOLS = [
36
+ "capture_lead",
37
+ "start_purchase",
38
+ "create_booking",
39
+ "submit_flow_step",
40
+ ] as const;
41
+
42
+ describe("AbuseGuard — each daily-capped tool spends its own tracker (AC-4)", () => {
43
+ it.each(CAPPED_TOOLS)(
44
+ "exhausting %s leaves the other three callable from the same IP",
45
+ (spent) => {
46
+ const guard = new AbuseGuard({
47
+ captureLeadDailyCapPerIp: 1,
48
+ startPurchaseDailyCapPerIp: 1,
49
+ createBookingDailyCapPerIp: 1,
50
+ submitFlowStepDailyCapPerIp: 1,
51
+ blockedUserAgents: "",
52
+ });
53
+ const request = requestFrom("198.51.100.4");
54
+
55
+ expect(guard.validate(spent, {}, request).allowed).toBe(true);
56
+ const refused = guard.validate(spent, {}, request);
57
+ expect(refused).toMatchObject({
58
+ allowed: false,
59
+ code: AbuseReasonCode.DAILY_CAP_REACHED,
60
+ });
61
+
62
+ for (const other of CAPPED_TOOLS.filter((t) => t !== spent)) {
63
+ expect(guard.validate(other, {}, request).allowed).toBe(true);
64
+ }
65
+ },
66
+ );
67
+ });
68
+
69
+ describe("createAbuseGuardFromEnv — a blank or non-numeric cap falls back to the default (AC-5)", () => {
70
+ const CASES = [
71
+ {
72
+ toolName: "create_booking",
73
+ envVar: "MCP_ABUSE_CREATE_BOOKING_DAILY_CAP",
74
+ defaultCap: DEFAULT_CREATE_BOOKING_DAILY_CAP,
75
+ },
76
+ {
77
+ toolName: "submit_flow_step",
78
+ envVar: "MCP_ABUSE_SUBMIT_FLOW_STEP_DAILY_CAP",
79
+ defaultCap: DEFAULT_SUBMIT_FLOW_STEP_DAILY_CAP,
80
+ },
81
+ ] as const;
82
+
83
+ afterEach(() => {
84
+ for (const { envVar } of CASES) delete process.env[envVar];
85
+ });
86
+
87
+ it("the defaults are 20 bookings and 50 flow steps per IP per day", () => {
88
+ expect(DEFAULT_CREATE_BOOKING_DAILY_CAP).toBe(20);
89
+ expect(DEFAULT_SUBMIT_FLOW_STEP_DAILY_CAP).toBe(50);
90
+ });
91
+
92
+ describe.each(CASES)("$envVar", ({ toolName, envVar, defaultCap }) => {
93
+ it.each(["", "abc"])("set to %p refuses the call at default + 1", (raw) => {
94
+ process.env[envVar] = raw;
95
+ const guard = createAbuseGuardFromEnv();
96
+ const request = requestFrom("198.51.100.5");
97
+
98
+ for (let i = 0; i < defaultCap; i += 1) {
99
+ expect(guard.validate(toolName, {}, request).allowed).toBe(true);
100
+ }
101
+ expect(guard.validate(toolName, {}, request)).toMatchObject({
102
+ allowed: false,
103
+ code: AbuseReasonCode.DAILY_CAP_REACHED,
104
+ });
105
+ });
106
+
107
+ it("set to a positive integer uses that cap", () => {
108
+ process.env[envVar] = "3";
109
+ const guard = createAbuseGuardFromEnv();
110
+ const request = requestFrom("198.51.100.6");
111
+
112
+ for (let i = 0; i < 3; i += 1) {
113
+ expect(guard.validate(toolName, {}, request).allowed).toBe(true);
114
+ }
115
+ expect(guard.validate(toolName, {}, request).allowed).toBe(false);
116
+ });
117
+ });
118
+ });
119
+
120
+ describe("AbuseGuard — create_booking's attendee.email (D3)", () => {
121
+ const guard = () =>
122
+ new AbuseGuard({ captureLeadDailyCapPerIp: 50, blockedUserAgents: "" });
123
+
124
+ it("refuses a malformed attendee.email with INVALID_EMAIL", () => {
125
+ const result = guard().validate(
126
+ "create_booking",
127
+ { attendee: { name: "Ada", email: "not-an-email" } },
128
+ requestFrom("198.51.100.7"),
129
+ );
130
+
131
+ expect(result).toMatchObject({
132
+ allowed: false,
133
+ code: AbuseReasonCode.INVALID_EMAIL,
134
+ });
135
+ });
136
+
137
+ it("allows a well-formed attendee.email", () => {
138
+ const result = guard().validate(
139
+ "create_booking",
140
+ { attendee: { name: "Ada", email: "ada@example.com" } },
141
+ requestFrom("198.51.100.8"),
142
+ );
143
+
144
+ expect(result.allowed).toBe(true);
145
+ });
146
+
147
+ it("does not read a top-level email for create_booking", () => {
148
+ const result = guard().validate(
149
+ "create_booking",
150
+ { email: "not-an-email", attendee: { email: "ada@example.com" } },
151
+ requestFrom("198.51.100.9"),
152
+ );
153
+
154
+ expect(result.allowed).toBe(true);
155
+ });
156
+
157
+ it.each([undefined, null, "ada@example.com", ["not-an-email"], 7, {}])(
158
+ "treats attendee %p as a missing email — allowed, never a throw",
159
+ (attendee) => {
160
+ expect(() =>
161
+ guard().validate(
162
+ "create_booking",
163
+ { attendee },
164
+ requestFrom("198.51.100.10"),
165
+ ),
166
+ ).not.toThrow();
167
+ expect(
168
+ guard().validate(
169
+ "create_booking",
170
+ { attendee },
171
+ requestFrom("198.51.100.10"),
172
+ ).allowed,
173
+ ).toBe(true);
174
+ },
175
+ );
176
+
177
+ it("tolerates a non-object input", () => {
178
+ for (const input of [null, "x", 3]) {
179
+ expect(
180
+ guard().validate("create_booking", input, requestFrom("198.51.100.11"))
181
+ .allowed,
182
+ ).toBe(true);
183
+ }
184
+ });
185
+
186
+ it("still reads capture_lead's top-level email", () => {
187
+ const result = guard().validate(
188
+ "capture_lead",
189
+ { email: "not-an-email" },
190
+ requestFrom("198.51.100.12"),
191
+ );
192
+
193
+ expect(result).toMatchObject({
194
+ allowed: false,
195
+ code: AbuseReasonCode.INVALID_EMAIL,
196
+ });
197
+ });
198
+ });
@@ -231,10 +231,22 @@ describe("buildSinglePageAuditInput — sitewide identity (#6597 AC-7)", () => {
231
231
  expect(evidenceOf(report, "schema-coverage")?.pass).toBe(true);
232
232
  });
233
233
 
234
- it("the monorepo home finds the identity: Identity (organization) ✓", async () => {
234
+ // The content root's own _site.mdx names the site's identity kind: the monorepo's is
235
+ // Organization, a person-service scaffold's is Person. Read it, don't hard-code it (#8419).
236
+ const siteKind = (): "Organization" | "Person" => {
237
+ const site = readFileSync(join(CONTENT_ROOT, "_site.mdx"), "utf-8");
238
+ const kind = /^siteIdentity:\s*\n(?:[ \t]+.*\n)*?[ \t]+kind:\s*["']?(\w+)["']?\s*$/m.exec(site)?.[1];
239
+ if (kind !== "Organization" && kind !== "Person") {
240
+ throw new Error(`content/_site.mdx siteIdentity.kind is not Organization or Person: ${String(kind)}`);
241
+ }
242
+ return kind;
243
+ };
244
+
245
+ it("the content root's home finds the identity its _site.mdx names", async () => {
246
+ const kind = siteKind();
235
247
  const report = await auditOf({ slug: "home" }, CONTENT_ROOT);
236
248
  expect(evidenceOf(report, "entity-markup")?.evidence).toContain(
237
- "Identity (organization) ✓",
249
+ `Identity (${kind.toLowerCase()}) ✓`,
238
250
  );
239
251
  });
240
252
 
@@ -243,7 +255,7 @@ describe("buildSinglePageAuditInput — sitewide identity (#6597 AC-7)", () => {
243
255
  const nodes = input.jsonLdByPage["AboutPage/about"] as Array<Record<string, unknown>>;
244
256
  expect(nodes[0]!["@type"]).toBe("AboutPage");
245
257
  expect(nodes.map((n) => n["@type"])).toEqual(
246
- expect.arrayContaining(["Organization", "WebSite"]),
258
+ expect.arrayContaining([siteKind(), "WebSite"]),
247
259
  );
248
260
  });
249
261
 
@@ -10,13 +10,23 @@
10
10
  * - AC-3: a bad value throws naming the variable; a blank per-minute rate
11
11
  * falls back to its default rather than disabling the limit.
12
12
  *
13
+ * And from spec docs/specs/8071-mcp-write-guard-spec.md:
14
+ * - AC-5: a blank or non-numeric MCP_ABUSE_CREATE_BOOKING_DAILY_CAP /
15
+ * MCP_ABUSE_SUBMIT_FLOW_STEP_DAILY_CAP yields the default cap through the
16
+ * env reader both entries share, never an unlimited one.
17
+ *
13
18
  * Real sockets on port 0, the pattern of `http-transport.test.ts`.
14
19
  */
15
20
 
16
21
  import type { AddressInfo } from "node:net";
17
22
 
18
- import type { FastifyInstance } from "fastify";
23
+ import type { FastifyInstance, FastifyRequest } from "fastify";
19
24
 
25
+ import {
26
+ AbuseGuard,
27
+ DEFAULT_CREATE_BOOKING_DAILY_CAP,
28
+ DEFAULT_SUBMIT_FLOW_STEP_DAILY_CAP,
29
+ } from "../src/abuse/index.js";
20
30
  import { httpTransportConfigFromEnv, readStatelessEnv } from "../src/boot.js";
21
31
  import { buildHttpTransportApp } from "../src/http-transport.js";
22
32
 
@@ -29,6 +39,8 @@ const VARS = [
29
39
  "MCP_CORS_WRITE_ALLOWED_ORIGINS",
30
40
  "MCP_ABUSE_CAPTURE_LEAD_DAILY_CAP",
31
41
  "MCP_ABUSE_START_PURCHASE_DAILY_CAP",
42
+ "MCP_ABUSE_CREATE_BOOKING_DAILY_CAP",
43
+ "MCP_ABUSE_SUBMIT_FLOW_STEP_DAILY_CAP",
32
44
  "MCP_ABUSE_BLOCKED_USER_AGENTS",
33
45
  ] as const;
34
46
 
@@ -49,6 +61,15 @@ afterEach(() => {
49
61
  }
50
62
  });
51
63
 
64
+ /** A stub carrying only the two members the guard reads. */
65
+ function requestFrom(ip: string): FastifyRequest {
66
+ // @ts-expect-error: a full FastifyRequest cannot be built without a server.
67
+ return {
68
+ headers: { "user-agent": "Mozilla/5.0 (compatible; TestAgent/1.0)" },
69
+ ip,
70
+ };
71
+ }
72
+
52
73
  const TOOLS_LIST = { jsonrpc: "2.0", id: 1, method: "tools/list", params: {} };
53
74
 
54
75
  async function listen(app: FastifyInstance): Promise<number> {
@@ -172,6 +193,8 @@ describe("httpTransportConfigFromEnv — today's reading of every other variable
172
193
  abuse: {
173
194
  captureLeadDailyCapPerIp: 50,
174
195
  startPurchaseDailyCapPerIp: 20,
196
+ createBookingDailyCapPerIp: 20,
197
+ submitFlowStepDailyCapPerIp: 50,
175
198
  blockedUserAgents: "",
176
199
  },
177
200
  });
@@ -186,6 +209,8 @@ describe("httpTransportConfigFromEnv — today's reading of every other variable
186
209
  "https://a.example, https://b.example";
187
210
  process.env["MCP_ABUSE_CAPTURE_LEAD_DAILY_CAP"] = "3";
188
211
  process.env["MCP_ABUSE_START_PURCHASE_DAILY_CAP"] = "2";
212
+ process.env["MCP_ABUSE_CREATE_BOOKING_DAILY_CAP"] = "4";
213
+ process.env["MCP_ABUSE_SUBMIT_FLOW_STEP_DAILY_CAP"] = "5";
189
214
  process.env["MCP_ABUSE_BLOCKED_USER_AGENTS"] = "BadBot";
190
215
 
191
216
  expect(httpTransportConfigFromEnv()).toEqual({
@@ -196,8 +221,60 @@ describe("httpTransportConfigFromEnv — today's reading of every other variable
196
221
  abuse: {
197
222
  captureLeadDailyCapPerIp: 3,
198
223
  startPurchaseDailyCapPerIp: 2,
224
+ createBookingDailyCapPerIp: 4,
225
+ submitFlowStepDailyCapPerIp: 5,
199
226
  blockedUserAgents: "BadBot",
200
227
  },
201
228
  });
202
229
  });
203
230
  });
231
+
232
+ // #8071 AC-5, through the wiring both entries use: the env value reaches the
233
+ // AbuseGuard the HTTP transport builds from `config.abuse`
234
+ // (http-transport.ts). A guard built straight from AbuseConfig cannot show
235
+ // this — it defaults an absent cap on its own — so each case also pins the
236
+ // value httpTransportConfigFromEnv() returns.
237
+ describe("#8071 AC-5 — the booking and flow-step caps reach the guard both entries build", () => {
238
+ const CASES = [
239
+ {
240
+ toolName: "create_booking",
241
+ envVar: "MCP_ABUSE_CREATE_BOOKING_DAILY_CAP",
242
+ field: "createBookingDailyCapPerIp",
243
+ defaultCap: DEFAULT_CREATE_BOOKING_DAILY_CAP,
244
+ },
245
+ {
246
+ toolName: "submit_flow_step",
247
+ envVar: "MCP_ABUSE_SUBMIT_FLOW_STEP_DAILY_CAP",
248
+ field: "submitFlowStepDailyCapPerIp",
249
+ defaultCap: DEFAULT_SUBMIT_FLOW_STEP_DAILY_CAP,
250
+ },
251
+ ] as const;
252
+
253
+ /** Calls `toolName` until refused; returns how many calls were allowed. */
254
+ function allowedCalls(toolName: string, limit: number): number {
255
+ const guard = new AbuseGuard(httpTransportConfigFromEnv().abuse);
256
+ const request = requestFrom("198.51.100.7");
257
+ let allowed = 0;
258
+ while (allowed <= limit && guard.validate(toolName, {}, request).allowed) {
259
+ allowed += 1;
260
+ }
261
+ return allowed;
262
+ }
263
+
264
+ describe.each(CASES)("$envVar", ({ toolName, envVar, field, defaultCap }) => {
265
+ it.each(["", "abc"])(
266
+ "set to %p yields the default cap, refusing the call at default + 1",
267
+ (raw) => {
268
+ process.env[envVar] = raw;
269
+ expect(httpTransportConfigFromEnv().abuse[field]).toBe(defaultCap);
270
+ expect(allowedCalls(toolName, defaultCap)).toBe(defaultCap);
271
+ },
272
+ );
273
+
274
+ it("set to a positive integer, the guard refuses the call past that cap", () => {
275
+ process.env[envVar] = "3";
276
+ expect(httpTransportConfigFromEnv().abuse[field]).toBe(3);
277
+ expect(allowedCalls(toolName, defaultCap)).toBe(3);
278
+ });
279
+ });
280
+ });