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
|
@@ -20,12 +20,20 @@
|
|
|
20
20
|
* - Blocked UA returns 403
|
|
21
21
|
* - #5971: a start_purchase burst past the per-IP daily cap is refused with
|
|
22
22
|
* the rate-limit error, and no Checkout Session is created
|
|
23
|
+
* - #8071: create_booking and submit_flow_step take the same write branch —
|
|
24
|
+
* the origin allow-list, each tool's own daily cap (no hub call for a
|
|
25
|
+
* refused one), one cap unit per batched call, and for create_booking an
|
|
26
|
+
* attendee.email format check
|
|
27
|
+
* - #8073: a capture_lead call forwards the socket address the transport
|
|
28
|
+
* trusted (never a caller-forged X-Forwarded-For) plus
|
|
29
|
+
* AILK_VISITOR_FORWARD_KEY as the two visitor-forwarding headers, and sends
|
|
30
|
+
* neither when the key is unset
|
|
23
31
|
*/
|
|
24
32
|
|
|
25
33
|
import type { AddressInfo } from "node:net";
|
|
26
34
|
|
|
27
35
|
import type { FastifyInstance, FastifyRequest } from "fastify";
|
|
28
|
-
import { checkout } from "@working-theory/api-client";
|
|
36
|
+
import { apiClient, checkout } from "@working-theory/api-client";
|
|
29
37
|
|
|
30
38
|
import {
|
|
31
39
|
__setMcpCommerceClaimForTest,
|
|
@@ -41,7 +49,17 @@ import { listRegisteredTools } from "../src/server.js";
|
|
|
41
49
|
|
|
42
50
|
jest.mock("@working-theory/api-client", () => {
|
|
43
51
|
const actual = jest.requireActual("@working-theory/api-client");
|
|
44
|
-
|
|
52
|
+
// The hub stubs for the two #8071 write tools: every other surface stays
|
|
53
|
+
// real, so a tool these tests do not name behaves exactly as before.
|
|
54
|
+
return {
|
|
55
|
+
...actual,
|
|
56
|
+
checkout: { createSession: jest.fn() },
|
|
57
|
+
apiClient: {
|
|
58
|
+
...actual.apiClient,
|
|
59
|
+
schedule: { ...actual.apiClient.schedule, book: jest.fn() },
|
|
60
|
+
flows: { ...actual.apiClient.flows, submitStep: jest.fn() },
|
|
61
|
+
},
|
|
62
|
+
};
|
|
45
63
|
});
|
|
46
64
|
|
|
47
65
|
/** Permissive JSON-RPC 2.0 envelope type for response body assertions. */
|
|
@@ -340,6 +358,70 @@ describe("Abuse rules", () => {
|
|
|
340
358
|
});
|
|
341
359
|
});
|
|
342
360
|
|
|
361
|
+
/**
|
|
362
|
+
* #8073 — the lead-write tools forward the address the transport TRUSTED
|
|
363
|
+
* (`request.ip`), not a caller-supplied `X-Forwarded-For` value. The
|
|
364
|
+
* long-running entry (`buildHttpTransportApp`, used throughout this file)
|
|
365
|
+
* sets no `trustProxy`, so `request.ip` is always the real socket address —
|
|
366
|
+
* 127.0.0.1 here — exactly the D5 guarantee a forged header cannot move.
|
|
367
|
+
*/
|
|
368
|
+
describe("capture_lead forwards the trusted caller address (#8073)", () => {
|
|
369
|
+
let app: FastifyInstance;
|
|
370
|
+
let port: number;
|
|
371
|
+
let submitSpy: jest.SpiedFunction<typeof apiClient.leads.submit>;
|
|
372
|
+
|
|
373
|
+
beforeEach(async () => {
|
|
374
|
+
submitSpy = jest
|
|
375
|
+
.spyOn(apiClient.leads, "submit")
|
|
376
|
+
.mockResolvedValue({ ok: true, body: { id: "lead_1", persisted: "db" } });
|
|
377
|
+
app = await buildHttpTransportApp(DEFAULT_CONFIG);
|
|
378
|
+
await app.listen({ host: "127.0.0.1", port: 0 });
|
|
379
|
+
port = (app.server.address() as AddressInfo).port;
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
afterEach(async () => {
|
|
383
|
+
await app.close();
|
|
384
|
+
submitSpy.mockRestore();
|
|
385
|
+
delete process.env.AILK_VISITOR_FORWARD_KEY;
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
function capturePayload(id: number, email: string) {
|
|
389
|
+
return {
|
|
390
|
+
jsonrpc: "2.0",
|
|
391
|
+
id,
|
|
392
|
+
method: "tools/call",
|
|
393
|
+
params: { name: "capture_lead", arguments: { name: "Jane", email } },
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
it("sends the socket address and the forward key as the two visitor headers", async () => {
|
|
398
|
+
process.env.AILK_VISITOR_FORWARD_KEY = "spoke-key";
|
|
399
|
+
|
|
400
|
+
const res = await mcpPost(port, capturePayload(40, "jane@example.com"), {
|
|
401
|
+
// A caller-forged hop: D5 says this must not move the key.
|
|
402
|
+
"x-forwarded-for": "203.0.113.7",
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
expect(res.status).toBe(200);
|
|
406
|
+
expect(submitSpy).toHaveBeenCalledTimes(1);
|
|
407
|
+
const init = submitSpy.mock.calls[0]?.[1];
|
|
408
|
+
expect(init?.headers).toEqual({
|
|
409
|
+
"x-ailk-visitor-ip": "127.0.0.1",
|
|
410
|
+
"x-ailk-visitor-forward-key": "spoke-key",
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it("sends no visitor headers (and no second argument) when the key is unset", async () => {
|
|
415
|
+
delete process.env.AILK_VISITOR_FORWARD_KEY;
|
|
416
|
+
|
|
417
|
+
const res = await mcpPost(port, capturePayload(41, "jane2@example.com"));
|
|
418
|
+
|
|
419
|
+
expect(res.status).toBe(200);
|
|
420
|
+
expect(submitSpy).toHaveBeenCalledTimes(1);
|
|
421
|
+
expect(submitSpy.mock.calls[0]).toHaveLength(1);
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
|
|
343
425
|
/**
|
|
344
426
|
* AC-3 — a `start_purchase` burst beyond the AbuseGuard per-IP cap is refused
|
|
345
427
|
* with the rate-limit error and creates no Checkout Session (#5971 D6).
|
|
@@ -860,3 +942,210 @@ describe("abuse refusal status is keyed on the typed reason code (#5994)", () =>
|
|
|
860
942
|
expect(ABUSE_REFUSAL_STATUS[AbuseReasonCode.INVALID_EMAIL]).toBe(422);
|
|
861
943
|
});
|
|
862
944
|
});
|
|
945
|
+
|
|
946
|
+
/**
|
|
947
|
+
* #8071 — `create_booking` and `submit_flow_step` send caller-chosen data to
|
|
948
|
+
* the hub, so they take the same write branch as `capture_lead`: the
|
|
949
|
+
* write-origin allow-list, then the AbuseGuard once per call — each on its OWN
|
|
950
|
+
* daily-cap tracker.
|
|
951
|
+
*
|
|
952
|
+
* Every assertion here is a refusal the preHandler makes, so the suite holds
|
|
953
|
+
* in a scaffold that does not register the tool (no `scheduling` or
|
|
954
|
+
* `signal-collectors` module): the hub stub is checked for "no request for the
|
|
955
|
+
* refused call", never for a count the handler would have to produce.
|
|
956
|
+
*/
|
|
957
|
+
const HUB_WRITE_TOOLS = [
|
|
958
|
+
{
|
|
959
|
+
toolName: "create_booking",
|
|
960
|
+
capField: "createBookingDailyCapPerIp",
|
|
961
|
+
hubStub: () => apiClient.schedule.book as jest.Mock,
|
|
962
|
+
args: () => ({
|
|
963
|
+
calLink: "acme/intro",
|
|
964
|
+
start: "2030-01-07T15:00:00.000Z",
|
|
965
|
+
attendee: { name: "Ada", email: "ada@example.com" },
|
|
966
|
+
}),
|
|
967
|
+
},
|
|
968
|
+
{
|
|
969
|
+
toolName: "submit_flow_step",
|
|
970
|
+
capField: "submitFlowStepDailyCapPerIp",
|
|
971
|
+
hubStub: () => apiClient.flows.submitStep as jest.Mock,
|
|
972
|
+
args: () => ({
|
|
973
|
+
slug: "intake",
|
|
974
|
+
stepKey: "contact",
|
|
975
|
+
values: { company: "Acme" },
|
|
976
|
+
}),
|
|
977
|
+
},
|
|
978
|
+
] as const;
|
|
979
|
+
|
|
980
|
+
describe.each(HUB_WRITE_TOOLS)(
|
|
981
|
+
"$toolName takes the write branch of the HTTP guard (#8071)",
|
|
982
|
+
({ toolName, capField, hubStub, args }) => {
|
|
983
|
+
const CAP = 2;
|
|
984
|
+
const config = {
|
|
985
|
+
...DEFAULT_CONFIG,
|
|
986
|
+
cors: { writeAllowedOrigins: ["https://allowed.example"] as string[] },
|
|
987
|
+
abuse: {
|
|
988
|
+
captureLeadDailyCapPerIp: 50,
|
|
989
|
+
startPurchaseDailyCapPerIp: 20,
|
|
990
|
+
[capField]: CAP,
|
|
991
|
+
blockedUserAgents: "",
|
|
992
|
+
},
|
|
993
|
+
};
|
|
994
|
+
|
|
995
|
+
let app: FastifyInstance;
|
|
996
|
+
let port: number;
|
|
997
|
+
|
|
998
|
+
function call(id: number, callArgs: object = args()) {
|
|
999
|
+
return {
|
|
1000
|
+
jsonrpc: "2.0",
|
|
1001
|
+
id,
|
|
1002
|
+
method: "tools/call",
|
|
1003
|
+
params: { name: toolName, arguments: callArgs },
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
beforeEach(async () => {
|
|
1008
|
+
hubStub().mockResolvedValue({ ok: false, status: 503, body: {} });
|
|
1009
|
+
app = await buildHttpTransportApp(config);
|
|
1010
|
+
await app.listen({ host: "127.0.0.1", port: 0 });
|
|
1011
|
+
port = (app.server.address() as AddressInfo).port;
|
|
1012
|
+
});
|
|
1013
|
+
|
|
1014
|
+
afterEach(async () => {
|
|
1015
|
+
await app.close();
|
|
1016
|
+
jest.clearAllMocks();
|
|
1017
|
+
});
|
|
1018
|
+
|
|
1019
|
+
it("is a member of WRITE_TOOLS", () => {
|
|
1020
|
+
expect(WRITE_TOOLS.has(toolName)).toBe(true);
|
|
1021
|
+
});
|
|
1022
|
+
|
|
1023
|
+
it("refuses a call from an origin not on writeAllowedOrigins with 403", async () => {
|
|
1024
|
+
const res = await mcpPost(port, call(80), {
|
|
1025
|
+
origin: "https://evil.example",
|
|
1026
|
+
});
|
|
1027
|
+
|
|
1028
|
+
expect(res.status).toBe(403);
|
|
1029
|
+
const body = (await res.json()) as { error?: string };
|
|
1030
|
+
expect(body.error).toContain("CORS");
|
|
1031
|
+
expect(hubStub()).not.toHaveBeenCalled();
|
|
1032
|
+
});
|
|
1033
|
+
|
|
1034
|
+
it("refuses the call past its daily cap with 403, and the hub gets no request for it", async () => {
|
|
1035
|
+
for (let i = 0; i < CAP; i += 1) {
|
|
1036
|
+
const res = await mcpPost(port, call(81 + i));
|
|
1037
|
+
expect(res.status).toBe(200);
|
|
1038
|
+
}
|
|
1039
|
+
const hubCallsBefore = hubStub().mock.calls.length;
|
|
1040
|
+
|
|
1041
|
+
const capped = await mcpPost(port, call(90));
|
|
1042
|
+
|
|
1043
|
+
expect(capped.status).toBe(403);
|
|
1044
|
+
const body = (await capped.json()) as { error?: string };
|
|
1045
|
+
expect(body.error).toContain(toolName);
|
|
1046
|
+
expect(hubStub().mock.calls.length).toBe(hubCallsBefore);
|
|
1047
|
+
});
|
|
1048
|
+
|
|
1049
|
+
it("a batch of N calls charges N cap units", async () => {
|
|
1050
|
+
const res = await mcpPost(port, [call(91), call(92), call(93)]);
|
|
1051
|
+
|
|
1052
|
+
expect(res.status).toBe(403);
|
|
1053
|
+
expect(hubStub()).not.toHaveBeenCalled();
|
|
1054
|
+
});
|
|
1055
|
+
|
|
1056
|
+
it("its cap is its own — spending it leaves capture_lead callable", async () => {
|
|
1057
|
+
for (let i = 0; i < CAP; i += 1) await mcpPost(port, call(94 + i));
|
|
1058
|
+
expect((await mcpPost(port, call(97))).status).toBe(403);
|
|
1059
|
+
|
|
1060
|
+
const lead = await mcpPost(port, {
|
|
1061
|
+
jsonrpc: "2.0",
|
|
1062
|
+
id: 98,
|
|
1063
|
+
method: "tools/call",
|
|
1064
|
+
params: {
|
|
1065
|
+
name: "capture_lead",
|
|
1066
|
+
arguments: { name: "Ada", email: "ada@example.com" },
|
|
1067
|
+
},
|
|
1068
|
+
});
|
|
1069
|
+
expect(lead.status).not.toBe(403);
|
|
1070
|
+
});
|
|
1071
|
+
|
|
1072
|
+
it("a refusal maps through the typed code: past the cap is DAILY_CAP_REACHED", () => {
|
|
1073
|
+
const guard = new AbuseGuard({
|
|
1074
|
+
captureLeadDailyCapPerIp: 50,
|
|
1075
|
+
[capField]: 1,
|
|
1076
|
+
blockedUserAgents: "",
|
|
1077
|
+
});
|
|
1078
|
+
// @ts-expect-error: a stub carrying only the two members the guard reads.
|
|
1079
|
+
const request: FastifyRequest = {
|
|
1080
|
+
headers: { "user-agent": "Mozilla/5.0 (compatible; TestAgent/1.0)" },
|
|
1081
|
+
ip: "203.0.113.9",
|
|
1082
|
+
};
|
|
1083
|
+
|
|
1084
|
+
expect(guard.validate(toolName, args(), request).allowed).toBe(true);
|
|
1085
|
+
const refused = guard.validate(toolName, args(), request);
|
|
1086
|
+
|
|
1087
|
+
expect(refused.allowed).toBe(false);
|
|
1088
|
+
if (refused.allowed) return;
|
|
1089
|
+
expect(refused.code).toBe(AbuseReasonCode.DAILY_CAP_REACHED);
|
|
1090
|
+
expect(ABUSE_REFUSAL_STATUS[refused.code]).toBe(403);
|
|
1091
|
+
});
|
|
1092
|
+
},
|
|
1093
|
+
);
|
|
1094
|
+
|
|
1095
|
+
describe("create_booking — attendee.email is format-checked before the hub (#8071)", () => {
|
|
1096
|
+
let app: FastifyInstance;
|
|
1097
|
+
let port: number;
|
|
1098
|
+
|
|
1099
|
+
const book = () => apiClient.schedule.book as jest.Mock;
|
|
1100
|
+
|
|
1101
|
+
function booking(id: number, attendee: unknown) {
|
|
1102
|
+
return {
|
|
1103
|
+
jsonrpc: "2.0",
|
|
1104
|
+
id,
|
|
1105
|
+
method: "tools/call",
|
|
1106
|
+
params: {
|
|
1107
|
+
name: "create_booking",
|
|
1108
|
+
arguments: {
|
|
1109
|
+
calLink: "acme/intro",
|
|
1110
|
+
start: "2030-01-07T15:00:00.000Z",
|
|
1111
|
+
attendee,
|
|
1112
|
+
},
|
|
1113
|
+
},
|
|
1114
|
+
};
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
beforeEach(async () => {
|
|
1118
|
+
book().mockResolvedValue({ ok: false, status: 503, body: {} });
|
|
1119
|
+
app = await buildHttpTransportApp(DEFAULT_CONFIG);
|
|
1120
|
+
await app.listen({ host: "127.0.0.1", port: 0 });
|
|
1121
|
+
port = (app.server.address() as AddressInfo).port;
|
|
1122
|
+
});
|
|
1123
|
+
|
|
1124
|
+
afterEach(async () => {
|
|
1125
|
+
await app.close();
|
|
1126
|
+
jest.clearAllMocks();
|
|
1127
|
+
});
|
|
1128
|
+
|
|
1129
|
+
it("refuses a malformed attendee.email with 422 and leaves the hub stub untouched", async () => {
|
|
1130
|
+
const res = await mcpPost(
|
|
1131
|
+
port,
|
|
1132
|
+
booking(100, { name: "Spammer", email: "not-an-email" }),
|
|
1133
|
+
);
|
|
1134
|
+
|
|
1135
|
+
expect(res.status).toBe(422);
|
|
1136
|
+
const body = (await res.json()) as { error?: string };
|
|
1137
|
+
expect(body.error).toContain("email");
|
|
1138
|
+
expect(book()).not.toHaveBeenCalled();
|
|
1139
|
+
});
|
|
1140
|
+
|
|
1141
|
+
it.each([null, "ada@example.com", ["ada@example.com"], 42])(
|
|
1142
|
+
"a non-object attendee (%p) is not a guard refusal and never a 500",
|
|
1143
|
+
async (attendee) => {
|
|
1144
|
+
const res = await mcpPost(port, booking(101, attendee));
|
|
1145
|
+
|
|
1146
|
+
// The handler's own Zod schema is the authority on the shape.
|
|
1147
|
+
expect(res.status).toBe(200);
|
|
1148
|
+
expect(book()).not.toHaveBeenCalled();
|
|
1149
|
+
},
|
|
1150
|
+
);
|
|
1151
|
+
});
|
|
@@ -7,17 +7,27 @@
|
|
|
7
7
|
* 3. API 400 → { ok: false, error: { kind: 'api_error', status: 400 } }
|
|
8
8
|
* 4. API 503 → { ok: false, error: { kind: 'api_error', status: 503 } }
|
|
9
9
|
* 5. API 429 → { ok: false, error: { kind: 'api_error', status: 429 } }
|
|
10
|
+
* 6. #8073 — forwards the trusted caller address + AILK_VISITOR_FORWARD_KEY
|
|
11
|
+
* as the two visitor-forwarding headers only when both are present
|
|
10
12
|
*/
|
|
11
13
|
|
|
12
14
|
import { ailkRequestCallback } from "../src/tools/request_callback.js";
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
import { trustedCallerAddress } from "../src/request-context.js";
|
|
16
|
+
|
|
17
|
+
// Everything but `apiClient.leads.callback` stays the ACTUAL
|
|
18
|
+
// implementation — in particular `visitorForwardHeaders`, which
|
|
19
|
+
// `request-context.ts` (imported above, for `trustedCallerAddress`) reads
|
|
20
|
+
// for real.
|
|
21
|
+
jest.mock("@working-theory/api-client", () => {
|
|
22
|
+
const actual = jest.requireActual("@working-theory/api-client");
|
|
23
|
+
return {
|
|
24
|
+
...actual,
|
|
25
|
+
apiClient: {
|
|
26
|
+
...actual.apiClient,
|
|
27
|
+
leads: { ...actual.apiClient.leads, callback: jest.fn() },
|
|
18
28
|
},
|
|
19
|
-
}
|
|
20
|
-
})
|
|
29
|
+
};
|
|
30
|
+
});
|
|
21
31
|
|
|
22
32
|
|
|
23
33
|
const { apiClient } = require("@working-theory/api-client") as {
|
|
@@ -161,3 +171,55 @@ describe("ailkRequestCallback — siteId override", () => {
|
|
|
161
171
|
expect(call.siteId).toBe("site-abc");
|
|
162
172
|
});
|
|
163
173
|
});
|
|
174
|
+
|
|
175
|
+
describe("ailkRequestCallback — visitor-forwarding headers (#8073)", () => {
|
|
176
|
+
afterEach(() => {
|
|
177
|
+
delete process.env.AILK_VISITOR_FORWARD_KEY;
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("calls api-client with exactly one argument when there is no trusted address", async () => {
|
|
181
|
+
apiClient.leads.callback.mockResolvedValueOnce({
|
|
182
|
+
ok: true,
|
|
183
|
+
body: { id: "fallback-1700000000000-e1", persisted: "file" },
|
|
184
|
+
});
|
|
185
|
+
process.env.AILK_VISITOR_FORWARD_KEY = "spoke-key";
|
|
186
|
+
|
|
187
|
+
await ailkRequestCallback(VALID_INPUT);
|
|
188
|
+
|
|
189
|
+
expect(apiClient.leads.callback.mock.calls[0]).toHaveLength(1);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("forwards the trusted address and the forward key as the two visitor headers", async () => {
|
|
193
|
+
apiClient.leads.callback.mockResolvedValueOnce({
|
|
194
|
+
ok: true,
|
|
195
|
+
body: { id: "fallback-1700000000000-e2", persisted: "file" },
|
|
196
|
+
});
|
|
197
|
+
process.env.AILK_VISITOR_FORWARD_KEY = "spoke-key";
|
|
198
|
+
|
|
199
|
+
await trustedCallerAddress.run("203.0.113.9", () =>
|
|
200
|
+
ailkRequestCallback(VALID_INPUT),
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
const init = apiClient.leads.callback.mock.calls[0][1] as
|
|
204
|
+
| { headers?: Record<string, string> }
|
|
205
|
+
| undefined;
|
|
206
|
+
expect(init?.headers).toEqual({
|
|
207
|
+
"x-ailk-visitor-ip": "203.0.113.9",
|
|
208
|
+
"x-ailk-visitor-forward-key": "spoke-key",
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("sends no headers when there is a trusted address but no forward key", async () => {
|
|
213
|
+
apiClient.leads.callback.mockResolvedValueOnce({
|
|
214
|
+
ok: true,
|
|
215
|
+
body: { id: "fallback-1700000000000-e3", persisted: "file" },
|
|
216
|
+
});
|
|
217
|
+
delete process.env.AILK_VISITOR_FORWARD_KEY;
|
|
218
|
+
|
|
219
|
+
await trustedCallerAddress.run("203.0.113.9", () =>
|
|
220
|
+
ailkRequestCallback(VALID_INPUT),
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
expect(apiClient.leads.callback.mock.calls[0]).toHaveLength(1);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
@@ -7,19 +7,28 @@
|
|
|
7
7
|
* 3. API 400 → { ok: false, error: { kind: 'api_error', status: 400 } }
|
|
8
8
|
* 4. API 503 → { ok: false, error: { kind: 'api_error', status: 503 } }
|
|
9
9
|
* 5. API 429 → { ok: false, error: { kind: 'api_error', status: 429 } }
|
|
10
|
+
* 6. #8073 — forwards the trusted caller address + AILK_VISITOR_FORWARD_KEY
|
|
11
|
+
* as the two visitor-forwarding headers only when both are present
|
|
10
12
|
*/
|
|
11
13
|
|
|
12
14
|
import { LEAD_SITE_MARKER_KEY, leadSubmitSchema } from "@working-theory/validation";
|
|
13
15
|
import { ailkCaptureLead } from "../src/tools/capture_lead.js";
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
import { trustedCallerAddress } from "../src/request-context.js";
|
|
17
|
+
|
|
18
|
+
// Mock @working-theory/api-client before importing the handler. Everything
|
|
19
|
+
// but `apiClient.leads.submit` stays the ACTUAL implementation — in
|
|
20
|
+
// particular `visitorForwardHeaders`, which `request-context.ts` (imported
|
|
21
|
+
// above, for `trustedCallerAddress`) reads for real.
|
|
22
|
+
jest.mock("@working-theory/api-client", () => {
|
|
23
|
+
const actual = jest.requireActual("@working-theory/api-client");
|
|
24
|
+
return {
|
|
25
|
+
...actual,
|
|
26
|
+
apiClient: {
|
|
27
|
+
...actual.apiClient,
|
|
28
|
+
leads: { ...actual.apiClient.leads, submit: jest.fn() },
|
|
20
29
|
},
|
|
21
|
-
}
|
|
22
|
-
})
|
|
30
|
+
};
|
|
31
|
+
});
|
|
23
32
|
|
|
24
33
|
|
|
25
34
|
const { apiClient } = require("@working-theory/api-client") as {
|
|
@@ -244,3 +253,55 @@ describe("ailkCaptureLead — payload site marker (#7397)", () => {
|
|
|
244
253
|
expect(call.payload["site"]).toBe("spoke-a.example");
|
|
245
254
|
});
|
|
246
255
|
});
|
|
256
|
+
|
|
257
|
+
describe("ailkCaptureLead — visitor-forwarding headers (#8073)", () => {
|
|
258
|
+
afterEach(() => {
|
|
259
|
+
delete process.env.AILK_VISITOR_FORWARD_KEY;
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it("calls api-client with exactly one argument when there is no trusted address", async () => {
|
|
263
|
+
apiClient.leads.submit.mockResolvedValueOnce({
|
|
264
|
+
ok: true,
|
|
265
|
+
body: { id: "lead_00000000-0000-7000-8000-0000000000e1", persisted: "db" },
|
|
266
|
+
});
|
|
267
|
+
process.env.AILK_VISITOR_FORWARD_KEY = "spoke-key";
|
|
268
|
+
|
|
269
|
+
await ailkCaptureLead(VALID_INPUT);
|
|
270
|
+
|
|
271
|
+
expect(apiClient.leads.submit.mock.calls[0]).toHaveLength(1);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("forwards the trusted address and the forward key as the two visitor headers", async () => {
|
|
275
|
+
apiClient.leads.submit.mockResolvedValueOnce({
|
|
276
|
+
ok: true,
|
|
277
|
+
body: { id: "lead_00000000-0000-7000-8000-0000000000e2", persisted: "db" },
|
|
278
|
+
});
|
|
279
|
+
process.env.AILK_VISITOR_FORWARD_KEY = "spoke-key";
|
|
280
|
+
|
|
281
|
+
await trustedCallerAddress.run("203.0.113.9", () =>
|
|
282
|
+
ailkCaptureLead(VALID_INPUT),
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
const init = apiClient.leads.submit.mock.calls[0][1] as
|
|
286
|
+
| { headers?: Record<string, string> }
|
|
287
|
+
| undefined;
|
|
288
|
+
expect(init?.headers).toEqual({
|
|
289
|
+
"x-ailk-visitor-ip": "203.0.113.9",
|
|
290
|
+
"x-ailk-visitor-forward-key": "spoke-key",
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it("sends no headers when there is a trusted address but no forward key", async () => {
|
|
295
|
+
apiClient.leads.submit.mockResolvedValueOnce({
|
|
296
|
+
ok: true,
|
|
297
|
+
body: { id: "lead_00000000-0000-7000-8000-0000000000e3", persisted: "db" },
|
|
298
|
+
});
|
|
299
|
+
delete process.env.AILK_VISITOR_FORWARD_KEY;
|
|
300
|
+
|
|
301
|
+
await trustedCallerAddress.run("203.0.113.9", () =>
|
|
302
|
+
ailkCaptureLead(VALID_INPUT),
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
expect(apiClient.leads.submit.mock.calls[0]).toHaveLength(1);
|
|
306
|
+
});
|
|
307
|
+
});
|
|
@@ -7,17 +7,27 @@
|
|
|
7
7
|
* 3. API 400 → { ok: false, error: { kind: 'api_error', status: 400 } }
|
|
8
8
|
* 4. API 503 → { ok: false, error: { kind: 'api_error', status: 503 } }
|
|
9
9
|
* 5. API 429 → { ok: false, error: { kind: 'api_error', status: 429 } }
|
|
10
|
+
* 6. #8073 — forwards the trusted caller address + AILK_VISITOR_FORWARD_KEY
|
|
11
|
+
* as the two visitor-forwarding headers only when both are present
|
|
10
12
|
*/
|
|
11
13
|
|
|
12
14
|
import { ailkSubscribeNewsletter } from "../src/tools/subscribe_newsletter.js";
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
import { trustedCallerAddress } from "../src/request-context.js";
|
|
16
|
+
|
|
17
|
+
// Everything but `apiClient.leads.subscribe` stays the ACTUAL
|
|
18
|
+
// implementation — in particular `visitorForwardHeaders`, which
|
|
19
|
+
// `request-context.ts` (imported above, for `trustedCallerAddress`) reads
|
|
20
|
+
// for real.
|
|
21
|
+
jest.mock("@working-theory/api-client", () => {
|
|
22
|
+
const actual = jest.requireActual("@working-theory/api-client");
|
|
23
|
+
return {
|
|
24
|
+
...actual,
|
|
25
|
+
apiClient: {
|
|
26
|
+
...actual.apiClient,
|
|
27
|
+
leads: { ...actual.apiClient.leads, subscribe: jest.fn() },
|
|
18
28
|
},
|
|
19
|
-
}
|
|
20
|
-
})
|
|
29
|
+
};
|
|
30
|
+
});
|
|
21
31
|
|
|
22
32
|
|
|
23
33
|
const { apiClient } = require("@working-theory/api-client") as {
|
|
@@ -159,3 +169,55 @@ describe("ailkSubscribeNewsletter — siteId override", () => {
|
|
|
159
169
|
expect(call.siteId).toBe("site-abc");
|
|
160
170
|
});
|
|
161
171
|
});
|
|
172
|
+
|
|
173
|
+
describe("ailkSubscribeNewsletter — visitor-forwarding headers (#8073)", () => {
|
|
174
|
+
afterEach(() => {
|
|
175
|
+
delete process.env.AILK_VISITOR_FORWARD_KEY;
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("calls api-client with exactly one argument when there is no trusted address", async () => {
|
|
179
|
+
apiClient.leads.subscribe.mockResolvedValueOnce({
|
|
180
|
+
ok: true,
|
|
181
|
+
body: { id: "subscr_00000000-0000-7000-8000-0000000000e1", persisted: "db" },
|
|
182
|
+
});
|
|
183
|
+
process.env.AILK_VISITOR_FORWARD_KEY = "spoke-key";
|
|
184
|
+
|
|
185
|
+
await ailkSubscribeNewsletter(VALID_INPUT);
|
|
186
|
+
|
|
187
|
+
expect(apiClient.leads.subscribe.mock.calls[0]).toHaveLength(1);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("forwards the trusted address and the forward key as the two visitor headers", async () => {
|
|
191
|
+
apiClient.leads.subscribe.mockResolvedValueOnce({
|
|
192
|
+
ok: true,
|
|
193
|
+
body: { id: "subscr_00000000-0000-7000-8000-0000000000e2", persisted: "db" },
|
|
194
|
+
});
|
|
195
|
+
process.env.AILK_VISITOR_FORWARD_KEY = "spoke-key";
|
|
196
|
+
|
|
197
|
+
await trustedCallerAddress.run("203.0.113.9", () =>
|
|
198
|
+
ailkSubscribeNewsletter(VALID_INPUT),
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
const init = apiClient.leads.subscribe.mock.calls[0][1] as
|
|
202
|
+
| { headers?: Record<string, string> }
|
|
203
|
+
| undefined;
|
|
204
|
+
expect(init?.headers).toEqual({
|
|
205
|
+
"x-ailk-visitor-ip": "203.0.113.9",
|
|
206
|
+
"x-ailk-visitor-forward-key": "spoke-key",
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("sends no headers when there is a trusted address but no forward key", async () => {
|
|
211
|
+
apiClient.leads.subscribe.mockResolvedValueOnce({
|
|
212
|
+
ok: true,
|
|
213
|
+
body: { id: "subscr_00000000-0000-7000-8000-0000000000e3", persisted: "db" },
|
|
214
|
+
});
|
|
215
|
+
delete process.env.AILK_VISITOR_FORWARD_KEY;
|
|
216
|
+
|
|
217
|
+
await trustedCallerAddress.run("203.0.113.9", () =>
|
|
218
|
+
ailkSubscribeNewsletter(VALID_INPUT),
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
expect(apiClient.leads.subscribe.mock.calls[0]).toHaveLength(1);
|
|
222
|
+
});
|
|
223
|
+
});
|
|
@@ -12,16 +12,8 @@ module.exports = {
|
|
|
12
12
|
testMatch: ['**/__tests__/**/*.test.ts'],
|
|
13
13
|
moduleFileExtensions: ['ts', 'js', 'json'],
|
|
14
14
|
moduleNameMapper: {
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
'^server-only$': '<rootDir>/../../__mocks__/server-only.ts',
|
|
18
|
-
// Map workspace packages to their TypeScript source so ts-jest compiles them
|
|
19
|
-
// directly (the published dist is ESM, which Jest's CJS preset can't load).
|
|
20
|
-
'^@working-theory/aeo$': '<rootDir>/../../packages/aeo/src/index.ts',
|
|
21
|
-
'^@working-theory/common$': '<rootDir>/../../packages/common/src/index.ts',
|
|
22
|
-
'^@working-theory/aeo/(.*)$': '<rootDir>/../../packages/aeo/src/$1',
|
|
23
|
-
'^@working-theory/content-adapters$': '<rootDir>/../../packages/content-adapters/src/index.ts',
|
|
24
|
-
'^@working-theory/content-adapters/(.*)$': '<rootDir>/../../packages/content-adapters/src/$1',
|
|
15
|
+
// @working-theory/* resolve from the installed packages; their ESM
|
|
16
|
+
// dist is converted by the transform below.
|
|
25
17
|
// The constants + one-workspace predicate subpath only (#5876 spec D11) —
|
|
26
18
|
// no @prisma/client import, so ts-jest compiles it directly like every
|
|
27
19
|
// other workspace mapping below. The database/dist/ build is empty in a
|
|
@@ -29,23 +21,11 @@ module.exports = {
|
|
|
29
21
|
// can't load. The CLIENT root (`@working-theory/database`) stays
|
|
30
22
|
// deliberately UNMAPPED — see apps/mcp/src/lib/prisma.ts.
|
|
31
23
|
'^@working-theory/database/tenancy$': '<rootDir>/../../database/tenancy.ts',
|
|
32
|
-
// tenancy.ts's isSentinel import (above) is a VALUE import, so every
|
|
33
|
-
// suite that loads the tenancy subpath transitively loads this one too
|
|
34
|
-
// — mirrors apps/api/jest.config.cjs's identical mapping.
|
|
35
|
-
'^@working-theory/ids$': '<rootDir>/../../packages/ids/src/index.ts',
|
|
36
|
-
'^@working-theory/license$': '<rootDir>/../../packages/license/src/index.ts',
|
|
37
|
-
'^@working-theory/metadata$': '<rootDir>/../../packages/metadata/src/index.ts',
|
|
38
|
-
'^@working-theory/schema$': '<rootDir>/../../packages/schema/src/index.ts',
|
|
39
|
-
'^@working-theory/schema/(.*)$': '<rootDir>/../../packages/schema/src/$1',
|
|
40
|
-
'^@working-theory/schema-validator$': '<rootDir>/../../packages/schema-validator/src/index.ts',
|
|
41
|
-
'^@working-theory/schema-pack-registry$': '<rootDir>/../../packages/schema-pack-registry/src/index.ts',
|
|
42
|
-
'^@working-theory/validation$': '<rootDir>/../../packages/validation/src/index.ts',
|
|
43
|
-
'^@working-theory/api-client$': '<rootDir>/../../packages/api-client/src/index.ts',
|
|
44
24
|
'^(\\.{1,2}/.*)\\.js$': '$1',
|
|
45
25
|
},
|
|
46
26
|
collectCoverageFrom: ['src/**/*.ts', '!src/**/*.d.ts'],
|
|
47
27
|
transformIgnorePatterns: [
|
|
48
|
-
'/node_modules/(?!(\\.pnpm/)?jose@?[^/]
|
|
28
|
+
'/node_modules/(?!(\\.pnpm/)?(jose@?[^/]*?|cookie@?[^/]*?|@vercel(?:\\+analytics@[^/]*?|/analytics)|@working-theory(?:\\+[^/]*?@?[^/]*?|/[^/]+))(/|$))',
|
|
49
29
|
],
|
|
50
30
|
transform: {
|
|
51
31
|
'^.+\\.(tsx?|jsx?|mjs)$': [
|
|
@@ -61,27 +41,6 @@ module.exports = {
|
|
|
61
41
|
skipLibCheck: true,
|
|
62
42
|
ignoreDeprecations: '6.0',
|
|
63
43
|
allowJs: true,
|
|
64
|
-
// paths must mirror moduleNameMapper above so ts-jest's TypeScript
|
|
65
|
-
// type-checker can resolve workspace package imports during compile.
|
|
66
|
-
// Jest's moduleNameMapper handles runtime resolution; TS uses paths.
|
|
67
|
-
baseUrl: '.',
|
|
68
|
-
paths: {
|
|
69
|
-
'@working-theory/aeo': ['../../packages/aeo/src/index.ts'],
|
|
70
|
-
'@working-theory/common': ['../../packages/common/src/index.ts'],
|
|
71
|
-
'@working-theory/aeo/*': ['../../packages/aeo/src/*'],
|
|
72
|
-
'@working-theory/api-client': ['../../packages/api-client/src/index.ts'],
|
|
73
|
-
'@working-theory/content-adapters': ['../../packages/content-adapters/src/index.ts'],
|
|
74
|
-
'@working-theory/content-adapters/*': ['../../packages/content-adapters/src/*'],
|
|
75
|
-
'@working-theory/database/tenancy': ['../../database/tenancy.ts'],
|
|
76
|
-
'@working-theory/ids': ['../../packages/ids/src/index.ts'],
|
|
77
|
-
'@working-theory/license': ['../../packages/license/src/index.ts'],
|
|
78
|
-
'@working-theory/metadata': ['../../packages/metadata/src/index.ts'],
|
|
79
|
-
'@working-theory/schema': ['../../packages/schema/src/index.ts'],
|
|
80
|
-
'@working-theory/schema/*': ['../../packages/schema/src/*'],
|
|
81
|
-
'@working-theory/schema-validator': ['../../packages/schema-validator/src/index.ts'],
|
|
82
|
-
'@working-theory/schema-pack-registry': ['../../packages/schema-pack-registry/src/index.ts'],
|
|
83
|
-
'@working-theory/validation': ['../../packages/validation/src/index.ts'],
|
|
84
|
-
},
|
|
85
44
|
},
|
|
86
45
|
// Suppress "import.meta only allowed in ESM modules" — source is ESM,
|
|
87
46
|
// but ts-jest compiles to CommonJS for Jest. The import.meta fallback
|