create-ailk 0.18.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +6 -5
- package/NOTICE +2 -0
- package/dist/add-module.js +4 -1
- package/dist/category-profile.d.ts +10 -0
- package/dist/category-profile.js +21 -0
- package/dist/derive-architecture.d.ts +5 -1
- package/dist/derive-architecture.js +8 -3
- package/dist/lib/license-key-files.d.ts +6 -3
- package/dist/lib/license-key-files.js +8 -2
- package/dist/lib/scaffold-render-sweep.d.ts +9 -0
- package/dist/lib/scaffold-render-sweep.js +24 -3
- package/dist/license-set.d.ts +1 -1
- package/dist/license-set.js +13 -8
- package/dist/module-architecture.js +119 -0
- package/dist/programmatic.js +3 -0
- package/dist/spoke-checkout.d.ts +11 -3
- package/dist/spoke-checkout.js +62 -12
- package/dist/starter-grid.d.ts +1 -1
- package/dist/surfaces.js +140 -1
- package/dist/template-manifest.d.ts +4 -3
- package/dist/topology.d.ts +7 -2
- package/dist/topology.js +60 -3
- package/package.json +5 -4
- package/templates/.env.example +3 -3
- package/templates/.github/workflows/ci.yml +5 -0
- package/templates/apps/api/CLAUDE.md +25 -7
- package/templates/apps/api/LICENSE +6 -5
- package/templates/apps/api/NOTICE +2 -0
- package/templates/apps/api/jest.config.cjs +4 -29
- package/templates/apps/api/package.json +1 -0
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +9 -0
- package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +38 -0
- package/templates/apps/api/src/lib/deliverable-asset-store.ts +5 -4
- package/templates/apps/api/src/lib/stripe.ts +81 -0
- package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +266 -39
- package/templates/apps/api/src/middleware/auth.ts +206 -46
- package/templates/apps/api/src/routes/leads/index.ts +4 -0
- package/templates/apps/api/src/server.ts +107 -0
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +58 -0
- package/templates/apps/api/src/services/lead-routing.ts +30 -2
- package/templates/apps/api/vercel.json +2 -2
- package/templates/apps/mcp/.env.example +10 -0
- package/templates/apps/mcp/CLAUDE.md +9 -4
- package/templates/apps/mcp/LICENSE +6 -5
- package/templates/apps/mcp/NOTICE +2 -0
- package/templates/apps/mcp/__tests__/abuse/guard.test.ts +198 -0
- package/templates/apps/mcp/__tests__/audit-single-page.test.ts +15 -3
- package/templates/apps/mcp/__tests__/boot.test.ts +78 -1
- package/templates/apps/mcp/__tests__/http-transport.test.ts +291 -2
- package/templates/apps/mcp/__tests__/request_callback.test.ts +69 -7
- package/templates/apps/mcp/__tests__/submit_lead.test.ts +69 -8
- package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +69 -7
- package/templates/apps/mcp/jest.config.cjs +3 -44
- package/templates/apps/mcp/src/abuse/index.ts +117 -21
- package/templates/apps/mcp/src/boot.ts +5 -23
- package/templates/apps/mcp/src/http-transport.ts +93 -73
- package/templates/apps/mcp/src/index.ts +2 -0
- package/templates/apps/mcp/src/request-context.ts +52 -0
- package/templates/apps/mcp/src/tools/capture_lead.ts +9 -1
- package/templates/apps/mcp/src/tools/request_callback.ts +9 -1
- package/templates/apps/mcp/src/tools/subscribe_newsletter.ts +9 -1
- package/templates/apps/web/LICENSE +6 -5
- package/templates/apps/web/NOTICE +2 -0
- package/templates/apps/web/app/.well-known/ai-plugin.json/route.ts +3 -3
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/billing.test.tsx +288 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/general.test.tsx +568 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/hub-mode.test.tsx +272 -42
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/item.test.tsx +11 -13
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout.test.tsx +115 -16
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/new-menu.test.tsx +305 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/new-popups.test.tsx +82 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/notifications.test.tsx +126 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +163 -72
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/popups.test.tsx +451 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/security.test.tsx +139 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/settings.test.tsx +20 -390
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/support.test.tsx +16 -11
- package/templates/apps/web/app/[locale]/(authed)/account/account-data.ts +1 -1
- package/templates/apps/web/app/[locale]/(authed)/account/account-frame.tsx +114 -50
- package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +24 -45
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/page.tsx +253 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/review-form.tsx +145 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/versions/[version]/file/route.ts +100 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/file-route.test.ts +179 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/page.test.tsx +514 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/review.test.tsx +288 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/artifact-actions.ts +123 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/artifact-data.ts +82 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/page.tsx +177 -0
- package/templates/apps/web/app/[locale]/(authed)/account/billing/page.tsx +172 -0
- package/templates/apps/web/app/[locale]/(authed)/account/billing-source.ts +147 -0
- package/templates/apps/web/app/[locale]/(authed)/account/components/request-quote.tsx +229 -0
- package/templates/apps/web/app/[locale]/(authed)/account/components/schedule-service.tsx +276 -0
- package/templates/apps/web/app/[locale]/(authed)/account/components/send-message.tsx +128 -0
- package/templates/apps/web/app/[locale]/(authed)/account/general/billing-anchor.tsx +27 -0
- package/templates/apps/web/app/[locale]/(authed)/account/general/deletion-request.tsx +132 -0
- package/templates/apps/web/app/[locale]/(authed)/account/general/page.tsx +131 -0
- package/templates/apps/web/app/[locale]/(authed)/account/general/profile-form.tsx +104 -0
- package/templates/apps/web/app/[locale]/(authed)/account/general/profile-source.ts +73 -0
- package/templates/apps/web/app/[locale]/(authed)/account/inbox/message/[id]/file/__tests__/route.test.ts +18 -8
- package/templates/apps/web/app/[locale]/(authed)/account/inbox/message/[id]/file/route.ts +4 -6
- package/templates/apps/web/app/[locale]/(authed)/account/layout.tsx +4 -1
- package/templates/apps/web/app/[locale]/(authed)/account/new-actions.ts +219 -0
- package/templates/apps/web/app/[locale]/(authed)/account/new-menu.tsx +340 -0
- package/templates/apps/web/app/[locale]/(authed)/account/new-popups.tsx +107 -0
- package/templates/apps/web/app/[locale]/(authed)/account/notifications/page.tsx +57 -0
- package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +85 -26
- package/templates/apps/web/app/[locale]/(authed)/account/profile-actions.ts +116 -0
- package/templates/apps/web/app/[locale]/(authed)/account/security/page.tsx +80 -0
- package/templates/apps/web/app/[locale]/(authed)/account/settings/page.tsx +13 -354
- package/templates/apps/web/app/[locale]/(authed)/account/settings-frame.tsx +108 -0
- package/templates/apps/web/app/[locale]/(authed)/account/sign-in-method.ts +37 -0
- package/templates/apps/web/app/[locale]/(authed)/account/support-actions.ts +9 -10
- package/templates/apps/web/app/[locale]/layout.tsx +23 -4
- package/templates/apps/web/app/api/auth/hub/__tests__/continuation.test.ts +143 -0
- package/templates/apps/web/app/api/auth/hub/authorize/__tests__/route.test.ts +62 -10
- package/templates/apps/web/app/api/auth/hub/authorize/route.ts +22 -7
- package/templates/apps/web/app/api/auth/hub/continuation.ts +73 -0
- package/templates/apps/web/app/api/auth/hub/continue/[ref]/__tests__/route.test.ts +188 -0
- package/templates/apps/web/app/api/auth/hub/continue/[ref]/route.ts +53 -0
- package/templates/apps/web/app/robots.txt/route.ts +10 -4
- package/templates/apps/web/auth.ts +5 -1
- package/templates/apps/web/lib/__tests__/account-config.test.ts +258 -46
- package/templates/apps/web/lib/account-config.ts +188 -52
- package/templates/apps/web/lib/hub-session/__tests__/hub-session.test.ts +143 -0
- package/templates/apps/web/lib/hub-session/index.ts +1 -0
- package/templates/apps/web/lib/hub-session/reseal.ts +90 -0
- package/templates/apps/web/lib/hub-session/seal.ts +13 -9
- package/templates/apps/web/lib/topology.generated.ts +24 -0
- package/templates/apps/web/messages/en.json +156 -35
- package/templates/database/CHANGELOG.md +21 -0
- package/templates/database/LICENSE +6 -5
- package/templates/database/NOTICE +2 -0
- package/templates/database/README.md +1 -1
- package/templates/database/__tests__/member-area.db.test.ts +15 -3
- package/templates/database/__tests__/member-contract-migration.test.ts +85 -0
- package/templates/database/__tests__/member-contract.db.test.ts +413 -0
- package/templates/database/__tests__/seed.test.ts +54 -2
- package/templates/database/inbox/schema.prisma +90 -15
- package/templates/database/migrations/20261012120000_member_contract/migration.sql +192 -0
- package/templates/database/migrations/20261013120000_lead_spoke_checkout/migration.sql +20 -0
- package/templates/database/package.json +3 -2
- package/templates/database/seed.ts +34 -2
- package/versions.json +36 -36
- package/templates/apps/api/src/__tests__/content-write-production-boot.test.ts +0 -104
- package/templates/apps/api/src/__tests__/cors.test.ts +0 -276
- package/templates/apps/api/src/__tests__/module-exclusion.test.ts +0 -279
- package/templates/apps/api/src/__tests__/openapi-keyless-hub.test.ts +0 -122
- package/templates/apps/api/src/__tests__/review-surface-registration.test.ts +0 -167
- package/templates/apps/api/src/__tests__/root-modules.test.ts +0 -175
- package/templates/apps/api/src/__tests__/schedule-forwarded-rate-limit.test.ts +0 -117
- package/templates/apps/api/src/__tests__/server.test.ts +0 -591
- package/templates/apps/api/src/__tests__/workspace-module-registration.test.ts +0 -175
- package/templates/apps/api/src/openapi/__tests__/surface-drift.test.ts +0 -293
- package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +0 -1620
- package/templates/apps/api/src/routes/project-listings/__tests__/directory.test.ts +0 -528
- package/templates/apps/api/src/routes/workspaces/__tests__/inbox-wire-schemas.test.ts +0 -198
- package/templates/apps/api/src/services/__tests__/aeo-score.test.ts +0 -652
- package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +0 -408
- package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +0 -476
- package/templates/apps/mcp/__tests__/audit.test.ts +0 -346
- package/templates/apps/mcp/__tests__/parity.test.ts +0 -325
- package/templates/apps/mcp/__tests__/workspace-surface.test.ts +0 -364
- package/templates/apps/web/app/[locale]/(authed)/account/settings/color-mode.tsx +0 -68
- package/templates/apps/web/app/[locale]/(authed)/account/settings/deletion-request.tsx +0 -55
- package/templates/apps/web/app/[locale]/(authed)/account/settings/name-form.tsx +0 -118
- package/templates/database/__tests__/build.test.ts +0 -131
|
@@ -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
|
|
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 — #
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,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
|
-
|
|
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
|
-
|
|
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([
|
|
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
|
+
});
|