create-ailk 0.11.2 → 0.11.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/component-catalog.json +12 -1
  2. package/component-catalog.md +6 -5
  3. package/dist/surfaces.d.ts +4 -5
  4. package/dist/surfaces.js +50 -8
  5. package/package.json +3 -3
  6. package/templates/.env.example +29 -0
  7. package/templates/apps/api/src/__tests__/leads-forwarded-rate-limit.test.ts +135 -0
  8. package/templates/apps/api/src/__tests__/vercel-handler.test.ts +233 -12
  9. package/templates/apps/api/src/lib/auth-email-budget.ts +3 -2
  10. package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +529 -1
  11. package/templates/apps/api/src/middleware/rate-limit.ts +341 -28
  12. package/templates/apps/api/src/routes/auth/__tests__/transactional-email.test.ts +105 -1
  13. package/templates/apps/api/src/routes/auth/transactional-email.ts +37 -9
  14. package/templates/apps/api/src/server.ts +28 -0
  15. package/templates/apps/api/src/services/__tests__/lead-accept-user.test.ts +251 -0
  16. package/templates/apps/api/src/services/lead-accept-user.ts +126 -0
  17. package/templates/apps/api/src/vercel-handler.ts +58 -12
  18. package/templates/apps/mcp/__tests__/submit_lead.test.ts +81 -0
  19. package/templates/apps/mcp/src/tools/capture_lead.ts +4 -0
  20. package/templates/apps/mcp/src/tools/subscribe_newsletter.ts +3 -1
  21. package/templates/apps/web/.env.example +4 -0
  22. package/templates/apps/web/app/.well-known/ai-plugin.json/route.ts +2 -1
  23. package/templates/apps/web/app/.well-known/mcp.json/build-manifest.ts +14 -9
  24. package/templates/apps/web/app/.well-known/mcp.json/route.ts +5 -4
  25. package/templates/apps/web/app/[locale]/(auth)/__tests__/password-reset.test.tsx +2 -0
  26. package/templates/apps/web/app/[locale]/(auth)/sign-in/__tests__/page.test.tsx +69 -2
  27. package/templates/apps/web/app/[locale]/(auth)/sign-in/__tests__/sign-in-card.test.tsx +156 -0
  28. package/templates/apps/web/app/[locale]/(auth)/sign-in/page.tsx +44 -26
  29. package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-card.tsx +104 -4
  30. package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-text.ts +57 -0
  31. package/templates/apps/web/app/[locale]/(authed)/__tests__/gate.test.tsx +306 -28
  32. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/item.test.tsx +143 -0
  33. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout.test.tsx +297 -0
  34. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +145 -234
  35. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/settings.test.tsx +397 -0
  36. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/support.test.tsx +203 -0
  37. package/templates/apps/web/app/[locale]/(authed)/account/account-data.ts +183 -0
  38. package/templates/apps/web/app/[locale]/(authed)/account/account-frame.tsx +143 -0
  39. package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +3 -1
  40. package/templates/apps/web/app/[locale]/(authed)/account/error-row.tsx +35 -0
  41. package/templates/apps/web/app/[locale]/(authed)/account/for-you-labels.ts +36 -0
  42. package/templates/apps/web/app/[locale]/(authed)/account/inbox/[kind]/[id]/page.tsx +96 -0
  43. package/templates/apps/web/app/[locale]/(authed)/account/layout.tsx +88 -0
  44. package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +120 -297
  45. package/templates/apps/web/app/[locale]/(authed)/account/retry-button.tsx +27 -0
  46. package/templates/apps/web/app/[locale]/(authed)/account/settings/color-mode.tsx +68 -0
  47. package/templates/apps/web/app/[locale]/(authed)/account/settings/deletion-request.tsx +55 -0
  48. package/templates/apps/web/app/[locale]/(authed)/account/settings/name-form.tsx +118 -0
  49. package/templates/apps/web/app/[locale]/(authed)/account/settings/page.tsx +298 -0
  50. package/templates/apps/web/app/[locale]/(authed)/account/settings/theme-modes.ts +32 -0
  51. package/templates/apps/web/app/[locale]/(authed)/account/support/page.tsx +60 -0
  52. package/templates/apps/web/app/[locale]/(authed)/account/support/support-form.tsx +98 -0
  53. package/templates/apps/web/app/[locale]/(authed)/account/support-actions.ts +98 -0
  54. package/templates/apps/web/app/[locale]/(authed)/layout.tsx +77 -3
  55. package/templates/apps/web/app/[locale]/(authed)/sign-in-gate.tsx +52 -9
  56. package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +63 -2
  57. package/templates/apps/web/app/[locale]/contact/contact.actions.ts +22 -2
  58. package/templates/apps/web/app/[locale]/layout.tsx +41 -28
  59. package/templates/apps/web/app/[locale]/newsletter/__tests__/newsletter.actions.test.ts +66 -2
  60. package/templates/apps/web/app/[locale]/newsletter/newsletter.actions.ts +22 -6
  61. package/templates/apps/web/app/openapi.json/__tests__/route.test.ts +176 -0
  62. package/templates/apps/web/app/openapi.json/route.ts +127 -0
  63. package/templates/apps/web/auth.ts +29 -5
  64. package/templates/apps/web/jest.config.cjs +5 -0
  65. package/templates/apps/web/lib/__tests__/account-config.test.ts +285 -0
  66. package/templates/apps/web/lib/__tests__/auth-email.test.ts +156 -1
  67. package/templates/apps/web/lib/__tests__/auth-routes.test.ts +41 -2
  68. package/templates/apps/web/lib/account-config.ts +219 -11
  69. package/templates/apps/web/lib/auth-email.ts +104 -2
  70. package/templates/apps/web/lib/auth-env.ts +37 -5
  71. package/templates/apps/web/lib/auth-routes.ts +33 -4
  72. package/templates/apps/web/messages/en.json +147 -21
  73. package/templates/database/CHANGELOG.md +7 -0
  74. package/templates/database/package.json +1 -1
  75. package/versions.json +33 -33
  76. package/templates/apps/web/app/[locale]/(authed)/account/sign-out-button.tsx +0 -39
@@ -12,11 +12,17 @@
12
12
  * `captureLeadAction` does: the API's 503 `lead_capture_not_configured` →
13
13
  * `unavailable`; any other non-2xx or thrown error → the generic `error`;
14
14
  * only a 2xx is `success`.
15
+ *
16
+ * Visitor forwarding (#7394): forwards the visitor exactly as
17
+ * `captureLeadAction` does, through `visitorForwardHeaders` and the
18
+ * server-only `AILK_VISITOR_FORWARD_KEY`; without them the call is made
19
+ * with one argument, as before.
15
20
  */
16
21
 
17
- import { leads } from "@working-theory/api-client";
22
+ import { leads, visitorForwardHeaders } from "@working-theory/api-client";
18
23
  import { LEAD_CAPTURE_HONEYPOT, type LeadCaptureFormState } from "@working-theory/ui";
19
24
  import { subscribeNewsletterInputSchema } from "@working-theory/validation";
25
+ import { headers } from "next/headers";
20
26
 
21
27
  const GENERIC_ERROR = "Something went wrong, please try again.";
22
28
 
@@ -40,12 +46,22 @@ export async function subscribeNewsletterAction(
40
46
  }
41
47
 
42
48
  const siteId = process.env.AILK_SITE_ID ?? "default";
49
+ const body = { siteId, source: "newsletter", email: parsed.data.email };
50
+ let init: { headers: Record<string, string> } | undefined;
51
+ try {
52
+ const forwarded = visitorForwardHeaders(
53
+ await headers(),
54
+ process.env.AILK_VISITOR_FORWARD_KEY,
55
+ );
56
+ if (forwarded) init = { headers: forwarded };
57
+ } catch {
58
+ // No request scope to read — subscribe without forwarding.
59
+ }
60
+
43
61
  try {
44
- const res = await leads.subscribe({
45
- siteId,
46
- source: "newsletter",
47
- email: parsed.data.email,
48
- });
62
+ const res = init
63
+ ? await leads.subscribe(body, init)
64
+ : await leads.subscribe(body);
49
65
  if (!res.ok) {
50
66
  const error =
51
67
  typeof res.body === "object" && res.body !== null
@@ -0,0 +1,176 @@
1
+ /**
2
+ * @jest-environment node
3
+ */
4
+
5
+ /**
6
+ * Route handler tests: GET /openapi.json (#7396)
7
+ *
8
+ * `/.well-known/ai-plugin.json` advertises `${baseUrl}/openapi.json` as its
9
+ * `api.url`. This route serves that document on the web host by proxying the
10
+ * API's own `/v1/openapi.json`; when the API cannot answer it, the route
11
+ * answers 502 — never the 404 an agent following the manifest used to get.
12
+ * A relative `servers[]` URL is resolved against the API origin, so the
13
+ * document never sends an agent to this (web) host for an API call.
14
+ */
15
+
16
+ import { GET } from "../route";
17
+
18
+ const SPEC = {
19
+ openapi: "3.1.0",
20
+ info: { title: "Example API", version: "1.0.0" },
21
+ paths: { "/v1/content/search": {} },
22
+ };
23
+
24
+ const API_ENV = ["AILK_API_URL", "NEXT_PUBLIC_AILK_API_URL"] as const;
25
+ const saved: Record<string, string | undefined> = {};
26
+ let fetchSpy: jest.SpyInstance;
27
+
28
+ beforeEach(() => {
29
+ for (const name of API_ENV) {
30
+ saved[name] = process.env[name];
31
+ delete process.env[name];
32
+ }
33
+ fetchSpy = jest.spyOn(globalThis, "fetch");
34
+ });
35
+
36
+ afterEach(() => {
37
+ for (const name of API_ENV) {
38
+ if (saved[name] === undefined) delete process.env[name];
39
+ else process.env[name] = saved[name];
40
+ }
41
+ fetchSpy.mockRestore();
42
+ });
43
+
44
+ function apiAnswers(status: number, body: string): void {
45
+ fetchSpy.mockResolvedValue(
46
+ new Response(body, {
47
+ status,
48
+ headers: { "content-type": "application/json" },
49
+ }),
50
+ );
51
+ }
52
+
53
+ /** The URL the route asked the API for. */
54
+ function requestedUrl(): string {
55
+ return String(fetchSpy.mock.calls[0]?.[0]);
56
+ }
57
+
58
+ describe("GET /openapi.json", () => {
59
+ it("answers 200 with the API's OpenAPI document, unchanged", async () => {
60
+ apiAnswers(200, JSON.stringify(SPEC));
61
+
62
+ const response = await GET();
63
+
64
+ expect(response.status).toBe(200);
65
+ expect(response.headers.get("content-type")).toMatch(/application\/json/);
66
+ expect(await response.json()).toEqual(SPEC);
67
+ });
68
+
69
+ it("reads the API's /v1/openapi.json at AILK_API_URL", async () => {
70
+ process.env["AILK_API_URL"] = "http://api.internal:4000";
71
+ apiAnswers(200, JSON.stringify(SPEC));
72
+
73
+ await GET();
74
+
75
+ expect(fetchSpy).toHaveBeenCalledTimes(1);
76
+ expect(requestedUrl()).toBe("http://api.internal:4000/v1/openapi.json");
77
+ });
78
+
79
+ it("falls back to NEXT_PUBLIC_AILK_API_URL, then the local-dev API origin", async () => {
80
+ apiAnswers(200, JSON.stringify(SPEC));
81
+ await GET();
82
+ expect(requestedUrl()).toBe("http://localhost:3001/v1/openapi.json");
83
+
84
+ fetchSpy.mockClear();
85
+ process.env["NEXT_PUBLIC_AILK_API_URL"] = "https://api.example.com/";
86
+ apiAnswers(200, JSON.stringify(SPEC));
87
+ await GET();
88
+ expect(requestedUrl()).toBe("https://api.example.com/v1/openapi.json");
89
+ });
90
+
91
+ it("answers 502, not 404, when the API cannot be reached", async () => {
92
+ fetchSpy.mockRejectedValue(new TypeError("fetch failed"));
93
+
94
+ const response = await GET();
95
+
96
+ expect(response.status).toBe(502);
97
+ expect(response.headers.get("content-type")).toMatch(/application\/json/);
98
+ expect(response.headers.get("cache-control")).toBe("no-store");
99
+ const body = (await response.json()) as { error?: { code?: string } };
100
+ expect(body.error?.code).toBe("upstream_unavailable");
101
+ });
102
+
103
+ it("answers 502 when the API answers anything but 200", async () => {
104
+ apiAnswers(404, JSON.stringify({ error: { code: "not_found" } }));
105
+
106
+ const response = await GET();
107
+
108
+ expect(response.status).toBe(502);
109
+ expect(response.headers.get("cache-control")).toBe("no-store");
110
+ });
111
+
112
+ it("resolves a relative servers[] URL against the public API origin", async () => {
113
+ process.env["AILK_API_URL"] = "http://api.internal:4000";
114
+ process.env["NEXT_PUBLIC_AILK_API_URL"] = "https://api.example.com/";
115
+ apiAnswers(
116
+ 200,
117
+ JSON.stringify({ ...SPEC, servers: [{ url: "/", description: "x" }] }),
118
+ );
119
+
120
+ const body = (await (await GET()).json()) as typeof SPEC & {
121
+ servers: Array<{ url: string }>;
122
+ };
123
+
124
+ expect(requestedUrl()).toBe("http://api.internal:4000/v1/openapi.json");
125
+ expect(body.servers).toEqual([
126
+ { url: "https://api.example.com", description: "This deployment's API" },
127
+ ]);
128
+ });
129
+
130
+ it("resolves a relative servers[] URL against AILK_API_URL when no public origin is set", async () => {
131
+ process.env["AILK_API_URL"] = "http://localhost:3700";
132
+ apiAnswers(200, JSON.stringify({ ...SPEC, servers: [{ url: "/" }] }));
133
+
134
+ const body = (await (await GET()).json()) as {
135
+ servers: Array<{ url: string }>;
136
+ };
137
+
138
+ expect(body.servers[0]?.url).toBe("http://localhost:3700");
139
+ });
140
+
141
+ it("leaves an absolute servers[] URL as the API declared it", async () => {
142
+ const servers = [
143
+ { url: "https://api.example.com", description: "This deployment" },
144
+ ];
145
+ apiAnswers(200, JSON.stringify({ ...SPEC, servers }));
146
+
147
+ const body = (await (await GET()).json()) as { servers: unknown };
148
+
149
+ expect(body.servers).toEqual(servers);
150
+ });
151
+
152
+ it("answers 502 when the API answers JSON that is not an OpenAPI document", async () => {
153
+ apiAnswers(200, JSON.stringify({ status: "ok" }));
154
+
155
+ const response = await GET();
156
+
157
+ expect(response.status).toBe(502);
158
+ });
159
+
160
+ it("answers 502 when the API's 200 body is not JSON", async () => {
161
+ apiAnswers(200, "<html>not a spec</html>");
162
+
163
+ const response = await GET();
164
+
165
+ expect(response.status).toBe(502);
166
+ });
167
+
168
+ it("never names the API's internal origin in its error body", async () => {
169
+ process.env["AILK_API_URL"] = "http://api.internal:4000";
170
+ fetchSpy.mockRejectedValue(new TypeError("fetch failed"));
171
+
172
+ const response = await GET();
173
+
174
+ expect(await response.text()).not.toContain("api.internal");
175
+ });
176
+ });
@@ -0,0 +1,127 @@
1
+ /**
2
+ * GET /openapi.json
3
+ *
4
+ * The OpenAPI document `/.well-known/ai-plugin.json` advertises as its
5
+ * `api.url` (#7396). apps/api renders it at `/v1/openapi.json`; this route
6
+ * serves that same document on the web host, so the URL the manifest names
7
+ * answers here instead of 404ing. It is read over HTTP — apps/web never
8
+ * imports apps/api.
9
+ *
10
+ * A `servers[]` URL that is relative (apps/api emits `/` when
11
+ * AILK_PUBLIC_API_URL is unset) is resolved against the API origin before the
12
+ * document is served. Left relative, it would resolve against THIS host,
13
+ * which serves none of the API's paths.
14
+ *
15
+ * When the API cannot be reached, or answers anything but an OpenAPI
16
+ * document with a 200, this route answers 502 with a JSON error that names no
17
+ * upstream origin, and is never cached.
18
+ */
19
+
20
+ // Read per request: the document is the running API's, and `next build` has
21
+ // no API to read it from.
22
+ export const dynamic = "force-dynamic";
23
+
24
+ const UPSTREAM_PATH = "/v1/openapi.json";
25
+ const UPSTREAM_TIMEOUT_MS = 10_000;
26
+
27
+ const LOCAL_API_ORIGIN = "http://localhost:3001";
28
+
29
+ /**
30
+ * The apps/api origin this server reads from — the server-side resolution
31
+ * order of `getApiBase()` in `@working-theory/api-client`, which the package
32
+ * root does not export. The deprecated `NEXT_PUBLIC_API_URL` is not read.
33
+ */
34
+ function apiBase(): string {
35
+ return stripSlash(
36
+ process.env["AILK_API_URL"] ||
37
+ process.env["NEXT_PUBLIC_AILK_API_URL"] ||
38
+ LOCAL_API_ORIGIN,
39
+ );
40
+ }
41
+
42
+ /**
43
+ * The apps/api origin an agent reading the document calls — the
44
+ * browser-facing name first, since AILK_API_URL may be a private address.
45
+ */
46
+ function publicApiBase(): string {
47
+ return stripSlash(
48
+ process.env["NEXT_PUBLIC_AILK_API_URL"] ||
49
+ process.env["AILK_API_URL"] ||
50
+ LOCAL_API_ORIGIN,
51
+ );
52
+ }
53
+
54
+ function stripSlash(url: string): string {
55
+ return url.replace(/\/+$/, "");
56
+ }
57
+
58
+ interface OpenApiDocument {
59
+ openapi: string;
60
+ servers?: Array<{ url?: unknown; description?: unknown }>;
61
+ }
62
+
63
+ function isOpenApiDocument(value: unknown): value is OpenApiDocument {
64
+ return (
65
+ typeof value === "object" &&
66
+ value !== null &&
67
+ typeof (value as { openapi?: unknown }).openapi === "string"
68
+ );
69
+ }
70
+
71
+ /** Resolve every relative `servers[].url` against the public API origin. */
72
+ function withAbsoluteServers(spec: OpenApiDocument): OpenApiDocument {
73
+ if (!Array.isArray(spec.servers)) return spec;
74
+ const origin = publicApiBase();
75
+ return {
76
+ ...spec,
77
+ servers: spec.servers.map((server) =>
78
+ typeof server.url === "string" && !/^[a-z][a-z\d+.-]*:/i.test(server.url)
79
+ ? {
80
+ ...server,
81
+ url: stripSlash(new URL(server.url, `${origin}/`).href),
82
+ description: "This deployment's API",
83
+ }
84
+ : server,
85
+ ),
86
+ };
87
+ }
88
+
89
+ function badGateway(message: string): Response {
90
+ return Response.json(
91
+ { error: { code: "upstream_unavailable", message } },
92
+ { status: 502, headers: { "cache-control": "no-store" } },
93
+ );
94
+ }
95
+
96
+ export async function GET(): Promise<Response> {
97
+ let upstream: Response;
98
+ try {
99
+ upstream = await fetch(`${apiBase()}${UPSTREAM_PATH}`, {
100
+ cache: "no-store",
101
+ signal: AbortSignal.timeout(UPSTREAM_TIMEOUT_MS),
102
+ });
103
+ } catch {
104
+ return badGateway("The API did not answer.");
105
+ }
106
+
107
+ if (upstream.status !== 200) {
108
+ await upstream.body?.cancel();
109
+ return badGateway(`The API answered ${upstream.status}.`);
110
+ }
111
+
112
+ let spec: unknown;
113
+ try {
114
+ spec = await upstream.json();
115
+ } catch {
116
+ return badGateway("The API answered a body that is not JSON.");
117
+ }
118
+
119
+ if (!isOpenApiDocument(spec)) {
120
+ return badGateway("The API answered JSON that is not an OpenAPI document.");
121
+ }
122
+
123
+ return Response.json(withAbsoluteServers(spec), {
124
+ status: 200,
125
+ headers: { "cache-control": "public, max-age=3600" },
126
+ });
127
+ }
@@ -29,9 +29,11 @@ import type { PrismaClient } from "@working-theory/database";
29
29
  import { betterAuth } from "better-auth";
30
30
  import { prismaAdapter } from "better-auth/adapters/prisma";
31
31
  import { nextCookies } from "better-auth/next-js";
32
+ import { magicLink } from "better-auth/plugins";
32
33
 
33
34
  import {
34
35
  authEmailTransport,
36
+ makeMagicLinkOptions,
35
37
  makeSendPasswordChangedNotice,
36
38
  makeSendResetPassword,
37
39
  makeSendVerificationEmail,
@@ -164,10 +166,9 @@ function buildSocialProviders(): SocialProviders {
164
166
  };
165
167
  }
166
168
 
167
- // "email" is reported by getActiveProviderIds when AUTH_EMAIL_SERVER +
168
- // AUTH_EMAIL_FROM are set, but Better Auth core does not ship a built-in
169
- // magic-link provider — that wiring is deferred to a follow-up
170
- // (see spec §7 "Out of Scope").
169
+ // "email" (the password method) and "magic-link" (the emailed sign-in
170
+ // link, #7435) are not social providers: `createAuth()` wires them as
171
+ // `emailAndPassword` and the `magicLink` plugin respectively.
171
172
 
172
173
  return providers;
173
174
  }
@@ -300,6 +301,12 @@ export function createAuth(deps: CreateAuthDeps = {}) {
300
301
  */
301
302
  const resetConfigured = resetSendConfigured();
302
303
 
304
+ /**
305
+ * The emailed sign-in link (#7435 § D1). The SAME predicate `/sign-in` and
306
+ * the `(authed)` gate read, so the form renders iff the endpoint exists.
307
+ */
308
+ const magicLinkActive = getActiveProviderIds().includes("magic-link");
309
+
303
310
  const trustedProxies = resolveTrustedProxies();
304
311
 
305
312
  /** Plain assignment, no cast — see `VerificationStore`. */
@@ -517,6 +524,11 @@ export function createAuth(deps: CreateAuthDeps = {}) {
517
524
  "/request-password-reset": { window: 900, max: 5 },
518
525
  "/reset-password": { window: 900, max: 10 },
519
526
  "/send-verification-email": { window: 900, max: 5 },
527
+ // #7435 § D6. Without a resolvable IP the plugin's own
528
+ // 5 / 60 s rule applies — for the shared-bucket reason above.
529
+ ...(magicLinkActive
530
+ ? { "/sign-in/magic-link": { window: 900, max: 5 } }
531
+ : {}),
520
532
  },
521
533
  }
522
534
  : {}),
@@ -586,7 +598,19 @@ export function createAuth(deps: CreateAuthDeps = {}) {
586
598
  expiresIn: 60 * 60 * 24 * 7, // 7 days
587
599
  updateAge: 60 * 60 * 24, // refresh in DB once per day
588
600
  },
589
- plugins: [nextCookies()],
601
+ /**
602
+ * `magicLink` only when its predicate holds (#7435 § D2): absent, its
603
+ * `/sign-in/magic-link` and `/magic-link/verify` endpoints do not exist.
604
+ * Sign-in only — the callback mails existing users and nobody else, and
605
+ * `disableSignUp` refuses to create one even for a live token
606
+ * (lib/auth-email.ts).
607
+ */
608
+ plugins: magicLinkActive
609
+ ? [
610
+ nextCookies(),
611
+ magicLink(makeMagicLinkOptions({ send: authEmailTransport })),
612
+ ]
613
+ : [nextCookies()],
590
614
  });
591
615
  }
592
616
 
@@ -60,6 +60,11 @@ module.exports = {
60
60
  "<rootDir>/node_modules/@working-theory/sections-registry/dist/index.js",
61
61
  "^@working-theory/templates/shells$":
62
62
  "<rootDir>/node_modules/@working-theory/templates/dist/shells/index.js",
63
+ // The account-area subpath (#7429, spec 7423 D7) — the contribution
64
+ // contract `lib/account-config.ts` types against, and the Inbox pieces
65
+ // the /account routes render.
66
+ "^@working-theory/templates/account$":
67
+ "<rootDir>/node_modules/@working-theory/templates/dist/account/index.js",
63
68
  // Pure-data routes-map subpath (#5572 U2) — mapped directly to source so
64
69
  // a consumer of the lightweight `./routes` export (routes.ts,
65
70
  // next.config.mjs, this suite) never pulls in the templates package's