create-ailk 0.3.0 → 0.5.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.
Files changed (155) hide show
  1. package/component-catalog.json +11939 -0
  2. package/component-catalog.md +176 -64
  3. package/dist/cli.js +48 -10
  4. package/dist/derive-architecture.js +5 -2
  5. package/dist/lib/extract-module-bundle.d.ts +88 -1
  6. package/dist/lib/extract-module-bundle.js +375 -80
  7. package/dist/lib/fetch-guard.d.ts +28 -0
  8. package/dist/lib/fetch-guard.js +101 -0
  9. package/dist/lib/fetch-module.js +4 -0
  10. package/dist/lib/fetch-template.d.ts +20 -0
  11. package/dist/lib/fetch-template.js +133 -0
  12. package/dist/lib/module-license-gate.d.ts +8 -0
  13. package/dist/lib/module-license-gate.js +12 -4
  14. package/dist/module-architecture.d.ts +116 -7
  15. package/dist/module-architecture.js +465 -94
  16. package/dist/parse-args.d.ts +8 -1
  17. package/dist/parse-args.js +49 -2
  18. package/dist/programmatic.d.ts +36 -1
  19. package/dist/programmatic.js +216 -5
  20. package/dist/site-chrome.d.ts +80 -0
  21. package/dist/site-chrome.js +135 -0
  22. package/dist/surfaces.d.ts +21 -6
  23. package/dist/surfaces.js +140 -23
  24. package/dist/template-manifest.d.ts +144 -0
  25. package/dist/template-manifest.js +337 -0
  26. package/dist/template-names.d.ts +41 -0
  27. package/dist/template-names.js +45 -0
  28. package/package.json +10 -2
  29. package/site-templates/_base/ailk-template.json +6 -0
  30. package/site-templates/_base/content/en/Pricing/pricing.mdx +169 -0
  31. package/site-templates/_base/content/en/PrivacyPage/privacy.mdx +79 -0
  32. package/site-templates/_base/content/en/TermsPage/terms.mdx +80 -0
  33. package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +331 -0
  34. package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +393 -0
  35. package/site-templates/marketing-site/ailk-template.json +11 -0
  36. package/site-templates/marketing-site/content/en/blog/05-this-vc-morning-routine-will-make-you-super-annoying.mdx +36 -0
  37. package/site-templates/marketing-site/provenance.json +4 -0
  38. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +288 -0
  39. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +285 -0
  40. package/site-templates/marketing-site-free/ailk-template.json +9 -0
  41. package/site-templates/marketing-site-free/provenance.json +4 -0
  42. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.json +271 -0
  43. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +324 -0
  44. package/site-templates/minimalism-site/ailk-template.json +21 -0
  45. package/site-templates/minimalism-site/assets/avatar-placeholder-1.svg +7 -0
  46. package/site-templates/minimalism-site/assets/avatar-placeholder-2.svg +7 -0
  47. package/site-templates/minimalism-site/assets/avatar-placeholder-3.svg +7 -0
  48. package/site-templates/minimalism-site/assets/avatar-placeholder-4.svg +7 -0
  49. package/site-templates/minimalism-site/assets/avatar-placeholder-5.svg +7 -0
  50. package/site-templates/minimalism-site/assets/avatar-placeholder-6.svg +7 -0
  51. package/site-templates/minimalism-site/assets/hero-backdrop.svg +17 -0
  52. package/site-templates/minimalism-site/assets/logo-placeholder-1.svg +6 -0
  53. package/site-templates/minimalism-site/assets/logo-placeholder-2.svg +6 -0
  54. package/site-templates/minimalism-site/assets/logo-placeholder-3.svg +6 -0
  55. package/site-templates/minimalism-site/content/en/AboutPage/about.mdx +110 -0
  56. package/site-templates/minimalism-site/content/en/ContactPage/contact.mdx +68 -0
  57. package/site-templates/minimalism-site/content/en/FAQPage/faq.mdx +57 -0
  58. package/site-templates/minimalism-site/content/en/HomePage/home.mdx +214 -0
  59. package/site-templates/minimalism-site/content/en/LandingPage/referral.mdx +77 -0
  60. package/site-templates/minimalism-site/content/en/LandingPage/reports.mdx +73 -0
  61. package/site-templates/minimalism-site/content/en/ReportPage/annual-letter-2024.mdx +52 -0
  62. package/site-templates/minimalism-site/content/en/ReportPage/field-notes-2023.mdx +52 -0
  63. package/site-templates/minimalism-site/content/en/ReportPage/platform-shift-2025.mdx +52 -0
  64. package/site-templates/minimalism-site/content/en/ReportPage/state-of-the-market-2026.mdx +52 -0
  65. package/site-templates/minimalism-site/content/en/Service/first-engagement.mdx +38 -0
  66. package/site-templates/minimalism-site/content/en/Service/second-engagement.mdx +38 -0
  67. package/site-templates/minimalism-site/content/en/Service/third-engagement.mdx +38 -0
  68. package/site-templates/minimalism-site/content/en/ServicesList/services.mdx +73 -0
  69. package/site-templates/minimalism-site/flows/referral.flow.json +71 -0
  70. package/site-templates/minimalism-site/provenance.json +4 -0
  71. package/site-templates/saas-product/TEMPLATE-MANIFEST.md +61 -0
  72. package/site-templates/saas-product/ailk-template.json +16 -0
  73. package/site-templates/saas-product/provenance.json +10 -0
  74. package/templates/.claude/skills/README.md +16 -25
  75. package/templates/.env.example +32 -7
  76. package/templates/.github/workflows/ci.yml +48 -0
  77. package/templates/README.md +3 -1
  78. package/templates/apps/api/CLAUDE.md +4 -1
  79. package/templates/apps/api/eslint.config.mjs +69 -0
  80. package/templates/apps/api/package.json +6 -2
  81. package/templates/apps/api/src/__tests__/module-exclusion.test.ts +6 -2
  82. package/templates/apps/api/src/__tests__/review-surface-registration.test.ts +167 -0
  83. package/templates/apps/api/src/__tests__/workspace-module-registration.test.ts +173 -0
  84. package/templates/apps/api/src/config/modules.ts +1 -1
  85. package/templates/apps/api/src/lib/__mocks__/prisma.ts +3 -0
  86. package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +339 -0
  87. package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +108 -0
  88. package/templates/apps/api/src/middleware/default-tenant.ts +136 -0
  89. package/templates/apps/api/src/routes/leads/__tests__/list.test.ts +171 -0
  90. package/templates/apps/api/src/routes/leads/list.ts +104 -0
  91. package/templates/apps/api/src/server.ts +122 -3
  92. package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +408 -0
  93. package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +329 -0
  94. package/templates/apps/api/src/services/default-tenant.ts +261 -0
  95. package/templates/apps/api/src/services/lead-promotion.ts +161 -0
  96. package/templates/apps/api/src/services/leads-inbox.ts +263 -0
  97. package/templates/apps/api/src/services/owner-workspaces.ts +132 -0
  98. package/templates/apps/mcp/CLAUDE.md +1 -1
  99. package/templates/apps/mcp/__tests__/parity.test.ts +2 -2
  100. package/templates/apps/mcp/__tests__/tools/{scaffold.test.ts → create_project.test.ts} +23 -23
  101. package/templates/apps/mcp/eslint.config.mjs +69 -0
  102. package/templates/apps/mcp/package.json +6 -2
  103. package/templates/apps/mcp/src/server.ts +2 -2
  104. package/templates/apps/mcp/src/tools/{scaffold.ts → create_project.ts} +3 -3
  105. package/templates/apps/mcp/src/tools/index.ts +2 -2
  106. package/templates/apps/web/.env.example +3 -3
  107. package/templates/apps/web/CLAUDE.md +2 -0
  108. package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +4 -6
  109. package/templates/apps/web/app/[locale]/(authed)/layout.tsx +1 -1
  110. package/templates/apps/web/app/[locale]/(authed)/leads/__tests__/page.test.tsx +181 -0
  111. package/templates/apps/web/app/[locale]/(authed)/leads/components/LeadsTable.tsx +137 -0
  112. package/templates/apps/web/app/[locale]/(authed)/leads/page.tsx +111 -0
  113. package/templates/apps/web/app/[locale]/(authed)/sign-in-gate.tsx +1 -1
  114. package/templates/apps/web/app/[locale]/[slug]/__tests__/page.test.tsx +111 -0
  115. package/templates/apps/web/app/[locale]/[slug]/page.tsx +65 -0
  116. package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +1 -1
  117. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +1 -1
  118. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +6 -5
  119. package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +1 -1
  120. package/templates/apps/web/app/[locale]/layout.tsx +214 -17
  121. package/templates/apps/web/app/[locale]/pricing/page.tsx +40 -0
  122. package/templates/apps/web/app/[locale]/privacy/page.tsx +40 -0
  123. package/templates/apps/web/app/[locale]/terms/page.tsx +40 -0
  124. package/templates/apps/web/components/google-analytics.tsx +18 -1
  125. package/templates/apps/web/eslint.config.mjs +69 -0
  126. package/templates/apps/web/jest.config.cjs +12 -15
  127. package/templates/apps/web/lib/__tests__/server-api.test.ts +61 -0
  128. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +32 -5
  129. package/templates/apps/web/lib/nav-config.ts +27 -7
  130. package/templates/apps/web/lib/server-api.ts +29 -13
  131. package/templates/apps/web/lib/site-chrome.ts +29 -0
  132. package/templates/apps/web/lib/telemetry-scrub.ts +46 -0
  133. package/templates/apps/web/messages/en.json +86 -1
  134. package/templates/apps/web/next.config.mjs +20 -0
  135. package/templates/apps/web/package.json +6 -2
  136. package/templates/apps/web/sentry.client.config.ts +131 -0
  137. package/templates/apps/web/sentry.edge.config.ts +72 -0
  138. package/templates/apps/web/sentry.server.config.ts +72 -0
  139. package/templates/apps/web/src/lib/routes.ts +13 -18
  140. package/templates/brand_assets/README.md +24 -0
  141. package/templates/content/en/AboutPage/about.mdx +160 -15
  142. package/templates/content/en/ContactPage/contact.mdx +90 -13
  143. package/templates/content/en/FAQPage/faq.mdx +62 -22
  144. package/templates/content/en/HomePage/home.mdx +244 -20
  145. package/templates/content/en/LandingPage/blog.mdx +83 -0
  146. package/templates/content/en/LandingPage/features.mdx +103 -0
  147. package/templates/content/en/LandingPage/how-it-works.mdx +143 -0
  148. package/templates/database/CHANGELOG.md +21 -0
  149. package/templates/database/__tests__/seed.test.ts +1 -1
  150. package/templates/database/eslint.config.mjs +69 -0
  151. package/templates/database/package.json +7 -3
  152. package/templates/database/seed.ts +1 -1
  153. package/templates/package.json +1 -14
  154. package/versions.json +52 -0
  155. /package/templates/apps/web/{src/components → components}/SiteNav.tsx +0 -0
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Tests for `forwardedCookieInit` — the RSC cookie/origin-forwarding helper
3
+ * (#5371 shepherd fix-up).
4
+ *
5
+ * Contract: the returned `init.headers` always carries the forwarded
6
+ * `cookie`, and carries `origin` ONLY when the incoming request's `Origin`
7
+ * header is present — never a fabricated value. This is what lets a Server
8
+ * Action `PUT`/`PATCH` through `@working-theory/api-client`'s `workspace.*`
9
+ * namespace pass `apps/api`'s same-origin CSRF check (`isOriginAllowed`),
10
+ * which Node's server-side `fetch` cannot satisfy on its own since it never
11
+ * sets `Origin`.
12
+ */
13
+ import { forwardedCookieInit } from "../server-api";
14
+
15
+ const mockCookies = jest.fn();
16
+ const mockHeaders = jest.fn();
17
+
18
+ jest.mock("next/headers", () => ({
19
+ __esModule: true,
20
+ cookies: (...args: unknown[]) => mockCookies(...args),
21
+ headers: (...args: unknown[]) => mockHeaders(...args),
22
+ }));
23
+
24
+ describe("forwardedCookieInit", () => {
25
+ beforeEach(() => {
26
+ mockCookies.mockReset();
27
+ mockHeaders.mockReset();
28
+ });
29
+
30
+ it("forwards cookie + origin when the incoming request carries an Origin header", async () => {
31
+ mockCookies.mockResolvedValue({
32
+ toString: () => "better-auth.session_token=abc",
33
+ });
34
+ mockHeaders.mockResolvedValue(
35
+ new Headers({ origin: "https://app.example.com" }),
36
+ );
37
+
38
+ const init = await forwardedCookieInit();
39
+
40
+ expect(init).toEqual({
41
+ headers: {
42
+ cookie: "better-auth.session_token=abc",
43
+ origin: "https://app.example.com",
44
+ },
45
+ });
46
+ });
47
+
48
+ it("forwards cookie alone — no origin key at all — when Origin is absent", async () => {
49
+ mockCookies.mockResolvedValue({
50
+ toString: () => "better-auth.session_token=abc",
51
+ });
52
+ mockHeaders.mockResolvedValue(new Headers());
53
+
54
+ const init = await forwardedCookieInit();
55
+
56
+ expect(init).toEqual({
57
+ headers: { cookie: "better-auth.session_token=abc" },
58
+ });
59
+ expect(init.headers).not.toHaveProperty("origin");
60
+ });
61
+ });
@@ -310,7 +310,7 @@ const NO_STRUCTURED_DATA_ROUTES: ReadonlyArray<{
310
310
  reason: "interactive flow engine, API-backed",
311
311
  subtree: true,
312
312
  },
313
- { route: "schedule", reason: "booking surface, API-backed", subtree: true },
313
+ { route: "meet", reason: "booking surface, API-backed", subtree: true },
314
314
  { route: "sign-in", reason: "auth surface, not a content page" },
315
315
  ];
316
316
 
@@ -345,14 +345,36 @@ function exemption(route: string): string | undefined {
345
345
  )?.reason;
346
346
  }
347
347
 
348
- // The page identity a route renders — the two `renderPage({ type, slug })`
349
- // shapes tools/lint-check-content-renders.ts recognises (a literal slug, or a
350
- // slug taken from params). A route that reads its identity another way
351
- // (e.g. a page-type manifest) is not recognised and must be declared.
348
+ // The page identity a route renders — the three shapes
349
+ // tools/lint-check-content-renders.ts recognises: `renderPage({ type, slug })`
350
+ // with a literal slug, the same with a slug taken from params, and a direct
351
+ // `getMdxSource("<Type>", slug, …)` for a route that composes its own body.
352
+ // This file previously mirrored only the first two and so drifted from the
353
+ // tool its comment names (whose own `GET_MDX_SOURCE_RE` has carried the third
354
+ // arm all along); #5562 restored the parity. A route that reads its identity
355
+ // some other way (e.g. a page-type manifest) is still unrecognised and must
356
+ // be declared.
352
357
  const RENDER_PAGE_EXACT_RE =
353
358
  /renderPage\s*\(\s*\{[^}]*type\s*:\s*["']([A-Za-z]+)["'][^}]*slug\s*:\s*["']([^"']+)["']/s;
354
359
  const RENDER_PAGE_DYNAMIC_RE =
355
360
  /renderPage\s*\(\s*\{[^}]*type\s*:\s*["']([A-Za-z]+)["'][^}]*\bslug\s*(?!:\s*["'])/s;
361
+ // A route that composes its own body instead of calling `renderPage` — it
362
+ // resolves content with `getMdxSource(<Type>, slug, locale)` and emits the
363
+ // graph itself via `pageToJsonLd` + `<JsonLdScript>`. That is not a route
364
+ // hiding from this check; it is the SAME declaration one level down, since
365
+ // `renderPage` resolves its own content with exactly this call. `/reports/
366
+ // [slug]` (#5562) is the first: it owns a full-bleed `<main>` and a header
367
+ // section that already renders the title, so `renderPage`'s own `<h1>`/`<p>`
368
+ // would duplicate it.
369
+ //
370
+ // Recognising the shape is deliberately better than exempting the route:
371
+ // an exemption declares "nothing is known to emit JSON-LD here" — false for
372
+ // these routes — and would silence the check for a page that DOES emit a
373
+ // graph. Recognised, the route is rendered and validated like any other.
374
+ // Matched only after both `renderPage` shapes above, so a route that uses
375
+ // `renderPage` is still read through that (richer, slug-bearing) form.
376
+ // Mirrors `GET_MDX_SOURCE_RE` in tools/lint-check-content-renders.ts.
377
+ const GET_MDX_SOURCE_RE = /getMdxSource\s*\(\s*["']([A-Za-z]+)["']/s;
356
378
 
357
379
  function routeIdentity(
358
380
  file: string,
@@ -362,6 +384,11 @@ function routeIdentity(
362
384
  if (exact) return { type: exact[1] as string, slug: exact[2] as string };
363
385
  const dynamic = RENDER_PAGE_DYNAMIC_RE.exec(src);
364
386
  if (dynamic) return { type: dynamic[1] as string, slug: null };
387
+ // Self-composing route (see GET_MDX_SOURCE_RE). Its slug always comes from
388
+ // the route params — a self-composing FIXED route would name its slug as a
389
+ // literal to `getMdxSource`, and none exists — so the dynamic path applies.
390
+ const ownBody = GET_MDX_SOURCE_RE.exec(src);
391
+ if (ownBody) return { type: ownBody[1] as string, slug: null };
365
392
  return null;
366
393
  }
367
394
 
@@ -1,13 +1,33 @@
1
- // nav-config.ts — Nav item registry for apps/web OSS template (#1451)
1
+ // nav-config.ts — Nav item registry for apps/web (#1451, re-shaped by #5469).
2
2
  //
3
- // The OSS template ships a focused navigation inventory — home, docs, and blog.
4
- // Marketing-site items (pricing, services, about) belong in the per-site fork.
5
- // Paths are locale-agnostic; Nav prefixes them with the active locale at render.
3
+ // The Free marketing-site template's nav (the approved chrome, #5469 D5):
4
+ // a Product flyout (How it works · Features · FAQ), then How it works,
5
+ // Pricing and About as top-level items. Paths are locale-agnostic; Nav
6
+ // prefixes them with the active locale at render.
7
+ //
8
+ // In a SCAFFOLD this file is regenerated by create-ailk from the declared
9
+ // route surfaces (`renderNavConfig` in packages/create-ailk/src/surfaces.ts)
10
+ // so the nav never links a page the scaffold does not carry — keep the two in
11
+ // step when the base item set changes.
6
12
 
7
13
  import type { NavItem } from "@working-theory/ui";
14
+ import { BookOpen, HelpCircle, Layers, Sparkles } from "lucide-react";
8
15
 
9
16
  export const navItems: NavItem[] = [
10
- { labelKey: "home", path: "/" },
11
- { labelKey: "docs", path: "/docs" },
12
- { labelKey: "blog", path: "/blog" },
17
+ {
18
+ labelKey: "product",
19
+ flyout: {
20
+ presentation: "list",
21
+ width: "trigger",
22
+ features: [
23
+ { labelKey: "howItWorks", descKey: "howItWorksDesc", icon: Layers, path: "/how-it-works" },
24
+ { labelKey: "features", descKey: "featuresDesc", icon: Sparkles, path: "/features" },
25
+ { labelKey: "faq", descKey: "faqDesc", icon: HelpCircle, path: "/faq" },
26
+ ],
27
+ actions: [{ labelKey: "docs", icon: BookOpen, path: "/how-it-works" }],
28
+ },
29
+ },
30
+ { labelKey: "howItWorks", path: "/how-it-works" },
31
+ { labelKey: "pricing", path: "/pricing" },
32
+ { labelKey: "about", path: "/about" },
13
33
  ];
@@ -3,22 +3,26 @@
3
3
  * (#4665 D9).
4
4
  *
5
5
  * Forwards the incoming request's cookies to `@working-theory/api-client`'s
6
- * `waitlistExperiments` namespace (B0, #4671) so `apps/api`'s cookie-session
7
- * auth preHandler resolves the SAME Better Auth session the browser holds —
8
- * both apps share the session store. GETs only, every page is an RSC; there
9
- * is no client fetch and no `NEXT_PUBLIC_` credential (D9).
6
+ * `waitlistExperiments` / `workspace` namespaces (B0, #4671; #5371) so
7
+ * `apps/api`'s cookie-session auth preHandler resolves the SAME Better Auth
8
+ * session the browser holds — both apps share the session store. Every page
9
+ * is an RSC and every write is a Server Action; there is no client fetch and
10
+ * no `NEXT_PUBLIC_` credential (D9). For a Server Action `PUT`/`PATCH`, the
11
+ * incoming request's `Origin` is forwarded alongside the cookie so
12
+ * `apps/api`'s same-origin CSRF check (`isOriginAllowed`) passes — Node's
13
+ * server-side `fetch` never sets `Origin` on its own.
10
14
  *
11
15
  * `apps/web` may not import `apps/api` directly (architecture.yaml) — every
12
- * read goes through `@working-theory/api-client`'s typed namespace, never a
16
+ * call goes through `@working-theory/api-client`'s typed namespace, never a
13
17
  * hand-rolled `fetch`. This module supplies exactly the two things that
14
- * namespace doesn't own: the cookie-forwarding `init` (B0's additive
18
+ * namespace doesn't own: the cookie/origin-forwarding `init` (B0's additive
15
19
  * `init?: { headers }` on `http.get`) and the envelope→`ApiError` unwrap
16
- * (mirrors `apps-paid/workspace/lib/api-error.ts`'s `unwrapEnvelope`, adapted to
20
+ * (mirrors `apps-paid/admin/lib/api-error.ts`'s `unwrapEnvelope`, adapted to
17
21
  * the `@working-theory/api-client` `ApiResult<T>` shape B0 already returns
18
22
  * rather than re-parsing the wire envelope by hand).
19
23
  */
20
24
 
21
- import { cookies } from "next/headers";
25
+ import { cookies, headers } from "next/headers";
22
26
 
23
27
  /**
24
28
  * Structural mirror of `@working-theory/api-client`'s `ApiResult<T>`
@@ -57,15 +61,27 @@ interface ErrorBody {
57
61
  }
58
62
 
59
63
  /**
60
- * Forward the incoming request's cookies as the `init` every
61
- * `waitlistExperiments.*` call accepts. Call once per request — `cookies()`
62
- * reads the current RSC request context.
64
+ * Forward the incoming request's cookies — and, when present, its `Origin` —
65
+ * as the `init` every `waitlistExperiments.*` / `workspace.*` call accepts.
66
+ * Call once per request — `cookies()` / `headers()` read the current RSC
67
+ * request context.
68
+ *
69
+ * The `Origin` forward matters for every non-GET call: `apps/api`'s
70
+ * `authPreHandler` (`isOriginAllowed`) requires a matching `Origin` header on
71
+ * a cookie-authenticated mutating request, and Node's server-side `fetch`
72
+ * (unlike a browser XHR/fetch) never sets one on its own. A Server Action's
73
+ * incoming request DOES carry the browser's real `Origin`, so re-forwarding
74
+ * it here — rather than fabricating one — lets a genuine same-origin write
75
+ * pass the check exactly as a browser-originated one would. RSC `GET`s are
76
+ * unaffected: `isOriginAllowed` short-circuits `true` for safe methods before
77
+ * it ever inspects the header.
63
78
  */
64
79
  export async function forwardedCookieInit(): Promise<{
65
- headers: { cookie: string };
80
+ headers: { cookie: string; origin?: string };
66
81
  }> {
67
82
  const cookieHeader = (await cookies()).toString();
68
- return { headers: { cookie: cookieHeader } };
83
+ const origin = (await headers()).get("origin");
84
+ return { headers: { cookie: cookieHeader, ...(origin ? { origin } : {}) } };
69
85
  }
70
86
 
71
87
  /**
@@ -0,0 +1,29 @@
1
+ // site-chrome.ts — resolved template layout knobs.
2
+ //
3
+ // GENERATED by create-ailk from the declared template's `layout` block
4
+ // (#5612). A scaffold with no template, or a template with no `layout`
5
+ // block, gets these defaults — byte-identical to every scaffold before this
6
+ // file existed. `apps/web/app/[locale]/layout.tsx` reads this to decide:
7
+ // - nav: "mark-only" → Nav renders the brand mark + right-hand controls
8
+ // only (no nav links).
9
+ // - footer: "centered" → Footer renders its existing centered variant.
10
+ // - announcementBanner: false → the home-only announcement Banner is not
11
+ // mounted (the cookie-consent Banner is unaffected — it is not an
12
+ // announcement).
13
+ // - home: "deck" → the locale-root route mounts MarketingShell with
14
+ // snap="mandatory" + deckHero (spec 5426 D7); every other route is
15
+ // unaffected.
16
+
17
+ export interface SiteChrome {
18
+ nav: "full" | "mark-only";
19
+ footer: "columns" | "centered";
20
+ announcementBanner: boolean;
21
+ home: "default" | "deck";
22
+ }
23
+
24
+ export const siteChrome: SiteChrome = {
25
+ nav: "full",
26
+ footer: "columns",
27
+ announcementBanner: true,
28
+ home: "default",
29
+ };
@@ -0,0 +1,46 @@
1
+ // Strips token-bearing query params from a URL before it's reported to
2
+ // third-party telemetry (Google Analytics, Sentry). #5562 found the report
3
+ // grant token (`/reports/<slug>?g=<grantId>`) — the bearer credential for a
4
+ // private customer deliverable — riding `document.location.href` into GA's
5
+ // `page_location` default and into Sentry events, transactions, and session
6
+ // replays. This is the single source of the stripped-parameter list.
7
+ //
8
+ // Mirrored (duplicated, not shared) in packages/analytics/src/scrub.ts,
9
+ // since apps/* must not import @working-theory/* packages — keep both lists
10
+ // in sync (#5613).
11
+ export const TOKEN_QUERY_PARAMS = ["g"];
12
+
13
+ // Sentry navigation breadcrumbs (`data.to`/`data.from`) are relative
14
+ // (`path?search`, no origin) rather than the absolute URLs GA and
15
+ // `request.url` deal in — resolve against a throwaway base so relative
16
+ // values get scrubbed too, then strip the base back off (security review,
17
+ // PR #5639).
18
+ const RELATIVE_URL_BASE = "https://scrub.invalid";
19
+
20
+ function hasScheme(url: string): boolean {
21
+ return /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(url) || url.startsWith("//");
22
+ }
23
+
24
+ export function scrubTelemetryUrl(url: string): string {
25
+ const relative = !hasScheme(url);
26
+ let parsed: URL;
27
+ try {
28
+ parsed = relative ? new URL(url, RELATIVE_URL_BASE) : new URL(url);
29
+ } catch {
30
+ return url;
31
+ }
32
+
33
+ let changed = false;
34
+ for (const param of TOKEN_QUERY_PARAMS) {
35
+ if (parsed.searchParams.has(param)) {
36
+ parsed.searchParams.delete(param);
37
+ changed = true;
38
+ }
39
+ }
40
+
41
+ if (!changed) {
42
+ return url;
43
+ }
44
+ const result = parsed.toString();
45
+ return relative ? result.slice(RELATIVE_URL_BASE.length) : result;
46
+ }
@@ -9,7 +9,17 @@
9
9
  "schedule": "Book a call",
10
10
  "signIn": "Sign in",
11
11
  "signOut": "Sign out",
12
- "account": "Account"
12
+ "account": "Account",
13
+ "product": "Product",
14
+ "howItWorks": "How it works",
15
+ "howItWorksDesc": "What happens from install to launch",
16
+ "features": "Features",
17
+ "featuresDesc": "Everything the kit ships with",
18
+ "faq": "FAQ",
19
+ "faqDesc": "Answers to the questions we get most",
20
+ "pricing": "Pricing",
21
+ "about": "About",
22
+ "getStarted": "Get started"
13
23
  },
14
24
  "consent": {
15
25
  "title": "Cookie Preferences",
@@ -167,10 +177,35 @@
167
177
  "heading": "Launch Service",
168
178
  "emptyState": "Launch Service page content coming soon."
169
179
  },
180
+ "leadsInbox": {
181
+ "title": "Leads",
182
+ "description": "Every lead your site has captured, newest first.",
183
+ "forbiddenHeadline": "You don't have access",
184
+ "forbiddenDescription": "Sign in with the account that owns this site to see the leads it captured.",
185
+ "table": {
186
+ "caption": "Leads captured by your site",
187
+ "columnName": "Name",
188
+ "columnEmail": "Email",
189
+ "columnSource": "Source",
190
+ "columnCreated": "Captured",
191
+ "unavailable": "—",
192
+ "next": "Next page",
193
+ "emptyHeadline": "No leads yet",
194
+ "emptyDescription": "Leads captured by your site's forms will appear here."
195
+ }
196
+ },
170
197
  "partners": {
171
198
  "heading": "Partners",
172
199
  "emptyState": "Partners listing coming soon."
173
200
  },
201
+ "pricing": {
202
+ "heading": "Pricing",
203
+ "emptyState": "Pricing page content coming soon."
204
+ },
205
+ "privacy": {
206
+ "heading": "Privacy Policy",
207
+ "emptyState": "Privacy policy content coming soon."
208
+ },
174
209
  "products": {
175
210
  "heading": "Products",
176
211
  "emptyState": "Products listing coming soon."
@@ -215,6 +250,10 @@
215
250
  "description": "Deep dives into the architecture and internals of this site.",
216
251
  "emptyState": "Technical articles coming soon."
217
252
  },
253
+ "terms": {
254
+ "heading": "Terms of Service",
255
+ "emptyState": "Terms of service content coming soon."
256
+ },
218
257
  "testimonials": {
219
258
  "heading": "Testimonials",
220
259
  "emptyState": "Testimonials coming soon."
@@ -395,5 +434,51 @@
395
434
  "emptyHeadline": "No purchases yet",
396
435
  "emptyDescription": "Purchases you complete will appear here."
397
436
  }
437
+ },
438
+ "admin": {
439
+ "title": "Workspace settings",
440
+ "description": "Your workspace name, site settings, and team.",
441
+ "nav": {
442
+ "account": "Account",
443
+ "general": "General",
444
+ "site": "Site",
445
+ "team": "Team"
446
+ },
447
+ "general": {
448
+ "heading": "General",
449
+ "nameLabel": "Workspace name",
450
+ "saveButton": "Save",
451
+ "saving": "Saving\u2026",
452
+ "saved": "Saved",
453
+ "saveError": "Could not save. Try again."
454
+ },
455
+ "site": {
456
+ "heading": "Site",
457
+ "identityHeading": "Identity",
458
+ "identityName": "Site name",
459
+ "identityUrl": "Site URL",
460
+ "identityDescription": "Description",
461
+ "seoHeading": "SEO",
462
+ "seoDescription": "Meta description",
463
+ "seoOgImage": "Social share image URL",
464
+ "themeHeading": "Theme",
465
+ "themeLightPrimary": "Primary colour (light)",
466
+ "themeLightAccent": "Accent colour (light)",
467
+ "themeDarkPrimary": "Primary colour (dark)",
468
+ "themeDarkAccent": "Accent colour (dark)",
469
+ "themeUnconfigured": "This site uses the default theme. Colours become editable once a theme is configured for the workspace.",
470
+ "saveButton": "Save",
471
+ "saving": "Saving\u2026",
472
+ "saved": "Saved",
473
+ "saveError": "Could not save. Try again."
474
+ },
475
+ "team": {
476
+ "heading": "Team",
477
+ "name": "Name",
478
+ "email": "Email",
479
+ "role": "Role",
480
+ "emptyHeadline": "No members yet",
481
+ "emptyDescription": "People with access to this workspace will appear here."
482
+ }
398
483
  }
399
484
  }
@@ -4,6 +4,7 @@ import { parse as parseYaml } from "yaml";
4
4
  import bundleAnalyzer from "@next/bundle-analyzer";
5
5
  import { withSentryConfig } from "@sentry/nextjs";
6
6
  import createNextIntlPlugin from "next-intl/plugin";
7
+ import { legacyRedirects } from "@working-theory/templates/routes";
7
8
 
8
9
  // @next/bundle-analyzer — enabled when ANALYZE=true (set by ci-bundle.yml).
9
10
  // analyzerMode 'json' writes .next/analyze/client.json (and nodejs.json,
@@ -118,6 +119,10 @@ const nextConfig = {
118
119
  "@working-theory/site-config",
119
120
  "@working-theory/theme",
120
121
  "@working-theory/ui",
122
+ "@working-theory/ui-commerce",
123
+ "@working-theory/ui-docs-blog",
124
+ "@working-theory/ui-scheduling",
125
+ "@working-theory/ui-signal-collectors",
121
126
  "@working-theory/validation",
122
127
  "@working-theory/web-i18n",
123
128
  ],
@@ -127,6 +132,14 @@ const nextConfig = {
127
132
  // Marking better-auth as a server external prevents webpack from statically
128
133
  // bundling it (and the unused kysely adapter) into the server chunk.
129
134
  serverExternalPackages: ["better-auth"],
135
+ // Disable Next 16's auto-generated AGENTS.md / CLAUDE.md agent-rules
136
+ // block. Default true: `next dev`, on detecting an AI coding agent in
137
+ // the environment, upserts a managed `<!-- BEGIN:nextjs-agent-rules -->`
138
+ // block into apps/web/CLAUDE.md on every dev-server start (verified at
139
+ // node_modules/next/dist/server/lib/generate-agent-files.js). That
140
+ // mutates a tracked file as a side effect of running the dev server,
141
+ // which this scaffold does not want.
142
+ agentRules: false,
130
143
  env: {
131
144
  // Comma-separated locale list baked at build time — consumed by middleware
132
145
  // and the sitemap route. NEXT_PUBLIC_ prefix makes it available in Edge
@@ -147,6 +160,13 @@ const nextConfig = {
147
160
  },
148
161
  ];
149
162
  },
163
+ // #5572 U2 (spec § D4 row 3) — a rename appends the old path to a row's
164
+ // `legacyRoutes` and this ships the 301 with no other edit here. Empty at
165
+ // HEAD: no row has moved yet. Proven against a FIXTURE, never the live map
166
+ // (apps/web/__tests__/routes/next-config-redirects.test.ts).
167
+ async redirects() {
168
+ return legacyRedirects();
169
+ },
150
170
  };
151
171
 
152
172
  // Sentry build-time plugin wrap (#894). When SENTRY_AUTH_TOKEN / SENTRY_ORG /
@@ -5,12 +5,12 @@
5
5
  "description": "AI Launch Kit web — Next.js App Router UX surface (placeholder; MDX content adapter wires in B1).",
6
6
  "license": "Apache-2.0",
7
7
  "scripts": {
8
- "dev": "next dev",
8
+ "dev": "next dev --webpack",
9
9
  "build": "next build --webpack",
10
10
  "postbuild": "tsx scripts/assert-content-bundled.ts",
11
11
  "start": "next start",
12
12
  "type-check": "tsc --noEmit",
13
- "lint": "next lint",
13
+ "lint": "eslint .",
14
14
  "test": "jest",
15
15
  "test:watch": "jest --watch"
16
16
  },
@@ -60,6 +60,10 @@
60
60
  "tailwindcss": "^4.3.2",
61
61
  "ts-jest": "^29.4.11",
62
62
  "typescript": "^6.0.3",
63
+ "eslint": "^10.6.0",
64
+ "@eslint/js": "^10.0.1",
65
+ "globals": "^17.7.0",
66
+ "typescript-eslint": "^8.62.1",
63
67
  "@working-theory/license": "workspace:*",
64
68
  "@working-theory/sections-registry": "workspace:*"
65
69
  }
@@ -13,6 +13,80 @@
13
13
 
14
14
  import * as Sentry from "@sentry/nextjs";
15
15
 
16
+ import { scrubTelemetryUrl } from "./lib/telemetry-scrub";
17
+
18
+ // #5613 / security review PR #5639: the report-grant token (?g=<id> on
19
+ // /reports/<slug>, #5562) is a bearer credential for a private customer
20
+ // deliverable and must never leave this app in a URL. Scrub it from every
21
+ // error event and transaction before it reaches Sentry — the request URL,
22
+ // its Referer header, the transaction name, navigation breadcrumbs, and any
23
+ // URL captured as span/trace data (e.g. `url.full`,
24
+ // `http.request.header.referer`).
25
+ function scrubRequestUrlAndHeaders(request?: {
26
+ url?: string;
27
+ headers?: Record<string, string>;
28
+ }): void {
29
+ if (!request) return;
30
+ if (request.url) {
31
+ request.url = scrubTelemetryUrl(request.url);
32
+ }
33
+ if (request.headers) {
34
+ for (const key of ["Referer", "referer"]) {
35
+ const value = request.headers[key];
36
+ if (typeof value === "string") {
37
+ request.headers[key] = scrubTelemetryUrl(value);
38
+ }
39
+ }
40
+ }
41
+ }
42
+
43
+ function scrubSpanData(data?: Record<string, unknown>): void {
44
+ if (!data) return;
45
+ for (const key of ["url.full", "http.url", "http.request.header.referer"]) {
46
+ const value = data[key];
47
+ if (typeof value === "string") {
48
+ data[key] = scrubTelemetryUrl(value);
49
+ }
50
+ }
51
+ }
52
+
53
+ function scrubSentryEvent<
54
+ T extends {
55
+ request?: { url?: string; headers?: Record<string, string> };
56
+ transaction?: string;
57
+ breadcrumbs?: { category?: string; data?: Record<string, unknown> }[];
58
+ contexts?: { trace?: { data?: Record<string, unknown> } };
59
+ spans?: { data?: Record<string, unknown> }[];
60
+ },
61
+ >(event: T): T {
62
+ scrubRequestUrlAndHeaders(event.request);
63
+ if (event.transaction) {
64
+ event.transaction = scrubTelemetryUrl(event.transaction);
65
+ }
66
+ for (const crumb of event.breadcrumbs ?? []) {
67
+ if (!crumb.data) continue;
68
+ if (crumb.category === "navigation") {
69
+ for (const key of ["to", "from"]) {
70
+ const value = crumb.data[key];
71
+ if (typeof value === "string") {
72
+ crumb.data[key] = scrubTelemetryUrl(value);
73
+ }
74
+ }
75
+ }
76
+ // fetch/xhr breadcrumbs (and others) can carry `data.url` independent
77
+ // of category — scrub it regardless (security review, PR #5639).
78
+ const url = crumb.data["url"];
79
+ if (typeof url === "string") {
80
+ crumb.data["url"] = scrubTelemetryUrl(url);
81
+ }
82
+ }
83
+ scrubSpanData(event.contexts?.trace?.data);
84
+ for (const span of event.spans ?? []) {
85
+ scrubSpanData(span.data);
86
+ }
87
+ return event;
88
+ }
89
+
16
90
  Sentry.init({
17
91
  dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
18
92
  enabled: !!process.env.NEXT_PUBLIC_SENTRY_DSN,
@@ -24,4 +98,61 @@ Sentry.init({
24
98
  // Session replays: 10% of all sessions, 100% of sessions that hit an error.
25
99
  replaysSessionSampleRate: 0.1,
26
100
  replaysOnErrorSampleRate: 1.0,
101
+
102
+ beforeSend: scrubSentryEvent,
103
+ beforeSendTransaction: scrubSentryEvent,
104
+
105
+ integrations: [
106
+ Sentry.replayIntegration({
107
+ // Custom recording events (navigation/history frames) can be scrubbed
108
+ // here. Events added by the underlying DOM recording library (rrweb) —
109
+ // notably the initial page-load snapshot's href — can NOT be modified
110
+ // via this or any other documented option in @sentry/replay 10.58.0:
111
+ // checked ReplayPluginOptions, BeforeAddRecordingEvent, and the core
112
+ // Options' beforeSend/beforeSendTransaction. That snapshot href is a
113
+ // residual, called out in the PR body.
114
+ beforeAddRecordingEvent(event) {
115
+ const { payload } = event.data;
116
+ if (
117
+ "description" in payload &&
118
+ typeof payload.description === "string"
119
+ ) {
120
+ payload.description = scrubTelemetryUrl(payload.description);
121
+ }
122
+ const data = "data" in payload ? payload.data : undefined;
123
+ if (data && typeof data === "object") {
124
+ const record = data as Record<string, unknown>;
125
+ // "url" covers click/slowClick/multiClick frames' `data.url`
126
+ // (security review, PR #5639), alongside history/navigation
127
+ // frames' `previous`/`to`/`from`.
128
+ for (const key of ["previous", "to", "from", "url"] as const) {
129
+ const value = record[key];
130
+ if (typeof value === "string") {
131
+ record[key] = scrubTelemetryUrl(value);
132
+ }
133
+ }
134
+ }
135
+ return event;
136
+ },
137
+ }),
138
+ ],
139
+ });
140
+
141
+ // beforeSend/beforeSendTransaction never see the replay envelope — @sentry/
142
+ // replay builds and transports it directly. It still passes through
143
+ // `client.getEventProcessors()` (verified in @sentry/core's prepareEvent),
144
+ // so this is the one hook that reaches it: the session's visited-URLs list
145
+ // and the request Referer it carries (security review, PR #5639).
146
+ Sentry.addEventProcessor((event) => {
147
+ const replayEvent = event as typeof event & { urls?: unknown };
148
+ if (replayEvent.type !== "replay_event") {
149
+ return event;
150
+ }
151
+ if (Array.isArray(replayEvent.urls)) {
152
+ replayEvent.urls = replayEvent.urls.map((url) =>
153
+ typeof url === "string" ? scrubTelemetryUrl(url) : url,
154
+ );
155
+ }
156
+ scrubRequestUrlAndHeaders(event.request);
157
+ return event;
27
158
  });