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
@@ -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
- return { ...actual, checkout: { createSession: jest.fn() } };
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
- jest.mock("@working-theory/api-client", () => ({
15
- apiClient: {
16
- leads: {
17
- callback: jest.fn(),
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
- // Mock @working-theory/api-client before importing the handler
16
- jest.mock("@working-theory/api-client", () => ({
17
- apiClient: {
18
- leads: {
19
- submit: jest.fn(),
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
- jest.mock("@working-theory/api-client", () => ({
15
- apiClient: {
16
- leads: {
17
- subscribe: jest.fn(),
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
- // Mock server-only in Jest environments — the package throws by design
16
- // outside Next.js. The real guard runs in production Next.js builds.
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