create-ailk 0.4.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 (146) hide show
  1. package/component-catalog.json +11939 -0
  2. package/component-catalog.md +176 -64
  3. package/dist/cli.js +29 -1
  4. package/dist/derive-architecture.js +5 -2
  5. package/dist/lib/extract-module-bundle.d.ts +50 -2
  6. package/dist/lib/extract-module-bundle.js +223 -15
  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.js +4 -0
  11. package/dist/module-architecture.d.ts +59 -8
  12. package/dist/module-architecture.js +334 -115
  13. package/dist/parse-args.js +6 -2
  14. package/dist/programmatic.d.ts +10 -0
  15. package/dist/programmatic.js +134 -19
  16. package/dist/site-chrome.d.ts +80 -0
  17. package/dist/site-chrome.js +135 -0
  18. package/dist/surfaces.d.ts +21 -6
  19. package/dist/surfaces.js +107 -32
  20. package/dist/template-manifest.d.ts +55 -3
  21. package/dist/template-manifest.js +181 -12
  22. package/dist/template-names.d.ts +1 -1
  23. package/dist/template-names.js +1 -0
  24. package/package.json +9 -2
  25. package/site-templates/_base/ailk-template.json +6 -0
  26. package/site-templates/_base/content/en/Pricing/pricing.mdx +169 -0
  27. package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +331 -0
  28. package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +393 -0
  29. package/site-templates/marketing-site/ailk-template.json +11 -0
  30. package/site-templates/marketing-site/content/en/blog/05-this-vc-morning-routine-will-make-you-super-annoying.mdx +36 -0
  31. package/site-templates/marketing-site/provenance.json +4 -0
  32. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +288 -0
  33. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +285 -0
  34. package/site-templates/marketing-site-free/ailk-template.json +2 -1
  35. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.json +271 -0
  36. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +324 -0
  37. package/site-templates/minimalism-site/ailk-template.json +21 -0
  38. package/site-templates/minimalism-site/assets/avatar-placeholder-1.svg +7 -0
  39. package/site-templates/minimalism-site/assets/avatar-placeholder-2.svg +7 -0
  40. package/site-templates/minimalism-site/assets/avatar-placeholder-3.svg +7 -0
  41. package/site-templates/minimalism-site/assets/avatar-placeholder-4.svg +7 -0
  42. package/site-templates/minimalism-site/assets/avatar-placeholder-5.svg +7 -0
  43. package/site-templates/minimalism-site/assets/avatar-placeholder-6.svg +7 -0
  44. package/site-templates/minimalism-site/assets/hero-backdrop.svg +17 -0
  45. package/site-templates/minimalism-site/assets/logo-placeholder-1.svg +6 -0
  46. package/site-templates/minimalism-site/assets/logo-placeholder-2.svg +6 -0
  47. package/site-templates/minimalism-site/assets/logo-placeholder-3.svg +6 -0
  48. package/site-templates/minimalism-site/content/en/AboutPage/about.mdx +110 -0
  49. package/site-templates/minimalism-site/content/en/ContactPage/contact.mdx +68 -0
  50. package/site-templates/minimalism-site/content/en/FAQPage/faq.mdx +57 -0
  51. package/site-templates/minimalism-site/content/en/HomePage/home.mdx +214 -0
  52. package/site-templates/minimalism-site/content/en/LandingPage/referral.mdx +77 -0
  53. package/site-templates/minimalism-site/content/en/LandingPage/reports.mdx +73 -0
  54. package/site-templates/minimalism-site/content/en/ReportPage/annual-letter-2024.mdx +52 -0
  55. package/site-templates/minimalism-site/content/en/ReportPage/field-notes-2023.mdx +52 -0
  56. package/site-templates/minimalism-site/content/en/ReportPage/platform-shift-2025.mdx +52 -0
  57. package/site-templates/minimalism-site/content/en/ReportPage/state-of-the-market-2026.mdx +52 -0
  58. package/site-templates/minimalism-site/content/en/Service/first-engagement.mdx +38 -0
  59. package/site-templates/minimalism-site/content/en/Service/second-engagement.mdx +38 -0
  60. package/site-templates/minimalism-site/content/en/Service/third-engagement.mdx +38 -0
  61. package/site-templates/minimalism-site/content/en/ServicesList/services.mdx +73 -0
  62. package/site-templates/minimalism-site/flows/referral.flow.json +71 -0
  63. package/site-templates/minimalism-site/provenance.json +4 -0
  64. package/site-templates/saas-product/TEMPLATE-MANIFEST.md +61 -0
  65. package/site-templates/saas-product/ailk-template.json +16 -0
  66. package/site-templates/saas-product/provenance.json +10 -0
  67. package/templates/.claude/skills/README.md +16 -25
  68. package/templates/.env.example +32 -7
  69. package/templates/.github/workflows/ci.yml +48 -0
  70. package/templates/README.md +3 -1
  71. package/templates/apps/api/CLAUDE.md +4 -1
  72. package/templates/apps/api/eslint.config.mjs +69 -0
  73. package/templates/apps/api/package.json +6 -2
  74. package/templates/apps/api/src/__tests__/module-exclusion.test.ts +6 -2
  75. package/templates/apps/api/src/__tests__/review-surface-registration.test.ts +167 -0
  76. package/templates/apps/api/src/__tests__/workspace-module-registration.test.ts +8 -1
  77. package/templates/apps/api/src/lib/__mocks__/prisma.ts +3 -0
  78. package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +339 -0
  79. package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +108 -0
  80. package/templates/apps/api/src/middleware/default-tenant.ts +136 -0
  81. package/templates/apps/api/src/routes/leads/__tests__/list.test.ts +171 -0
  82. package/templates/apps/api/src/routes/leads/list.ts +104 -0
  83. package/templates/apps/api/src/server.ts +82 -6
  84. package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +408 -0
  85. package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +329 -0
  86. package/templates/apps/api/src/services/default-tenant.ts +261 -0
  87. package/templates/apps/api/src/services/lead-promotion.ts +161 -0
  88. package/templates/apps/api/src/services/leads-inbox.ts +263 -0
  89. package/templates/apps/api/src/services/owner-workspaces.ts +132 -0
  90. package/templates/apps/mcp/CLAUDE.md +1 -1
  91. package/templates/apps/mcp/__tests__/parity.test.ts +2 -2
  92. package/templates/apps/mcp/__tests__/tools/{scaffold.test.ts → create_project.test.ts} +23 -23
  93. package/templates/apps/mcp/eslint.config.mjs +69 -0
  94. package/templates/apps/mcp/package.json +6 -2
  95. package/templates/apps/mcp/src/server.ts +2 -2
  96. package/templates/apps/mcp/src/tools/{scaffold.ts → create_project.ts} +3 -3
  97. package/templates/apps/mcp/src/tools/index.ts +2 -2
  98. package/templates/apps/web/.env.example +3 -3
  99. package/templates/apps/web/CLAUDE.md +2 -0
  100. package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +4 -6
  101. package/templates/apps/web/app/[locale]/(authed)/layout.tsx +1 -1
  102. package/templates/apps/web/app/[locale]/(authed)/leads/__tests__/page.test.tsx +181 -0
  103. package/templates/apps/web/app/[locale]/(authed)/leads/components/LeadsTable.tsx +137 -0
  104. package/templates/apps/web/app/[locale]/(authed)/leads/page.tsx +111 -0
  105. package/templates/apps/web/app/[locale]/(authed)/sign-in-gate.tsx +1 -1
  106. package/templates/apps/web/app/[locale]/[slug]/__tests__/page.test.tsx +111 -0
  107. package/templates/apps/web/app/[locale]/[slug]/page.tsx +65 -0
  108. package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +1 -1
  109. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +1 -1
  110. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +6 -5
  111. package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +1 -1
  112. package/templates/apps/web/app/[locale]/layout.tsx +214 -17
  113. package/templates/apps/web/components/google-analytics.tsx +18 -1
  114. package/templates/apps/web/eslint.config.mjs +69 -0
  115. package/templates/apps/web/jest.config.cjs +7 -15
  116. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +32 -5
  117. package/templates/apps/web/lib/nav-config.ts +27 -7
  118. package/templates/apps/web/lib/server-api.ts +1 -1
  119. package/templates/apps/web/lib/site-chrome.ts +29 -0
  120. package/templates/apps/web/lib/telemetry-scrub.ts +46 -0
  121. package/templates/apps/web/messages/en.json +29 -2
  122. package/templates/apps/web/next.config.mjs +20 -0
  123. package/templates/apps/web/package.json +6 -2
  124. package/templates/apps/web/sentry.client.config.ts +131 -0
  125. package/templates/apps/web/sentry.edge.config.ts +72 -0
  126. package/templates/apps/web/sentry.server.config.ts +72 -0
  127. package/templates/apps/web/src/lib/routes.ts +13 -18
  128. package/templates/brand_assets/README.md +24 -0
  129. package/templates/content/en/AboutPage/about.mdx +160 -15
  130. package/templates/content/en/ContactPage/contact.mdx +90 -13
  131. package/templates/content/en/FAQPage/faq.mdx +62 -22
  132. package/templates/content/en/HomePage/home.mdx +244 -20
  133. package/templates/content/en/LandingPage/blog.mdx +83 -0
  134. package/templates/content/en/LandingPage/features.mdx +103 -0
  135. package/templates/content/en/LandingPage/how-it-works.mdx +143 -0
  136. package/templates/database/CHANGELOG.md +7 -0
  137. package/templates/database/__tests__/seed.test.ts +1 -1
  138. package/templates/database/eslint.config.mjs +69 -0
  139. package/templates/database/package.json +7 -3
  140. package/templates/database/seed.ts +1 -1
  141. package/templates/package.json +1 -14
  142. package/versions.json +52 -0
  143. package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +0 -103
  144. /package/site-templates/{marketing-site-free → _base}/content/en/PrivacyPage/privacy.mdx +0 -0
  145. /package/site-templates/{marketing-site-free → _base}/content/en/TermsPage/terms.mdx +0 -0
  146. /package/templates/apps/web/{src/components → components}/SiteNav.tsx +0 -0
@@ -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,6 +177,23 @@
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."
@@ -408,7 +435,7 @@
408
435
  "emptyDescription": "Purchases you complete will appear here."
409
436
  }
410
437
  },
411
- "workspace": {
438
+ "admin": {
412
439
  "title": "Workspace settings",
413
440
  "description": "Your workspace name, site settings, and team.",
414
441
  "nav": {
@@ -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
  });
@@ -7,9 +7,81 @@
7
7
 
8
8
  import * as Sentry from "@sentry/nextjs";
9
9
 
10
+ import { scrubTelemetryUrl } from "./lib/telemetry-scrub";
11
+
12
+ // #5613 / security review PR #5639: the report-grant token (?g=<id> on
13
+ // /reports/<slug>, #5562) is a bearer credential for a private customer
14
+ // deliverable and must never leave this app in a URL. The RSC render of the
15
+ // grant page can run in this runtime too, so scrub the request URL, its
16
+ // Referer header, the transaction name, navigation breadcrumbs, and any URL
17
+ // captured as span/trace data (e.g. `url.full`,
18
+ // `http.request.header.referer`) here as well.
19
+ function scrubSentryEvent<
20
+ T extends {
21
+ request?: { url?: string; headers?: Record<string, string> };
22
+ transaction?: string;
23
+ breadcrumbs?: { category?: string; data?: Record<string, unknown> }[];
24
+ contexts?: { trace?: { data?: Record<string, unknown> } };
25
+ spans?: { data?: Record<string, unknown> }[];
26
+ },
27
+ >(event: T): T {
28
+ if (event.request?.url) {
29
+ event.request.url = scrubTelemetryUrl(event.request.url);
30
+ }
31
+ if (event.request?.headers) {
32
+ for (const key of ["Referer", "referer"]) {
33
+ const value = event.request.headers[key];
34
+ if (typeof value === "string") {
35
+ event.request.headers[key] = scrubTelemetryUrl(value);
36
+ }
37
+ }
38
+ }
39
+ if (event.transaction) {
40
+ event.transaction = scrubTelemetryUrl(event.transaction);
41
+ }
42
+ for (const crumb of event.breadcrumbs ?? []) {
43
+ if (!crumb.data) continue;
44
+ if (crumb.category === "navigation") {
45
+ for (const key of ["to", "from"]) {
46
+ const value = crumb.data[key];
47
+ if (typeof value === "string") {
48
+ crumb.data[key] = scrubTelemetryUrl(value);
49
+ }
50
+ }
51
+ }
52
+ // fetch/xhr breadcrumbs (and others) can carry `data.url` independent
53
+ // of category — scrub it regardless (security review, PR #5639).
54
+ const url = crumb.data["url"];
55
+ if (typeof url === "string") {
56
+ crumb.data["url"] = scrubTelemetryUrl(url);
57
+ }
58
+ }
59
+ const scrubSpanData = (data?: Record<string, unknown>): void => {
60
+ if (!data) return;
61
+ for (const key of [
62
+ "url.full",
63
+ "http.url",
64
+ "http.request.header.referer",
65
+ ]) {
66
+ const value = data[key];
67
+ if (typeof value === "string") {
68
+ data[key] = scrubTelemetryUrl(value);
69
+ }
70
+ }
71
+ };
72
+ scrubSpanData(event.contexts?.trace?.data);
73
+ for (const span of event.spans ?? []) {
74
+ scrubSpanData(span.data);
75
+ }
76
+ return event;
77
+ }
78
+
10
79
  Sentry.init({
11
80
  dsn: process.env.SENTRY_DSN,
12
81
  enabled: !!process.env.SENTRY_DSN,
13
82
 
14
83
  tracesSampleRate: process.env.NODE_ENV === "development" ? 1.0 : 0.1,
84
+
85
+ beforeSend: scrubSentryEvent,
86
+ beforeSendTransaction: scrubSentryEvent,
15
87
  });
@@ -12,9 +12,81 @@
12
12
 
13
13
  import * as Sentry from "@sentry/nextjs";
14
14
 
15
+ import { scrubTelemetryUrl } from "./lib/telemetry-scrub";
16
+
17
+ // #5613 / security review PR #5639: the report-grant token (?g=<id> on
18
+ // /reports/<slug>, #5562) is a bearer credential for a private customer
19
+ // deliverable and must never leave this app in a URL. The RSC render of the
20
+ // grant page runs in this runtime, so scrub the request URL, its Referer
21
+ // header, the transaction name, navigation breadcrumbs, and any URL
22
+ // captured as span/trace data (e.g. `url.full`,
23
+ // `http.request.header.referer`) here too.
24
+ function scrubSentryEvent<
25
+ T extends {
26
+ request?: { url?: string; headers?: Record<string, string> };
27
+ transaction?: string;
28
+ breadcrumbs?: { category?: string; data?: Record<string, unknown> }[];
29
+ contexts?: { trace?: { data?: Record<string, unknown> } };
30
+ spans?: { data?: Record<string, unknown> }[];
31
+ },
32
+ >(event: T): T {
33
+ if (event.request?.url) {
34
+ event.request.url = scrubTelemetryUrl(event.request.url);
35
+ }
36
+ if (event.request?.headers) {
37
+ for (const key of ["Referer", "referer"]) {
38
+ const value = event.request.headers[key];
39
+ if (typeof value === "string") {
40
+ event.request.headers[key] = scrubTelemetryUrl(value);
41
+ }
42
+ }
43
+ }
44
+ if (event.transaction) {
45
+ event.transaction = scrubTelemetryUrl(event.transaction);
46
+ }
47
+ for (const crumb of event.breadcrumbs ?? []) {
48
+ if (!crumb.data) continue;
49
+ if (crumb.category === "navigation") {
50
+ for (const key of ["to", "from"]) {
51
+ const value = crumb.data[key];
52
+ if (typeof value === "string") {
53
+ crumb.data[key] = scrubTelemetryUrl(value);
54
+ }
55
+ }
56
+ }
57
+ // fetch/xhr breadcrumbs (and others) can carry `data.url` independent
58
+ // of category — scrub it regardless (security review, PR #5639).
59
+ const url = crumb.data["url"];
60
+ if (typeof url === "string") {
61
+ crumb.data["url"] = scrubTelemetryUrl(url);
62
+ }
63
+ }
64
+ const scrubSpanData = (data?: Record<string, unknown>): void => {
65
+ if (!data) return;
66
+ for (const key of [
67
+ "url.full",
68
+ "http.url",
69
+ "http.request.header.referer",
70
+ ]) {
71
+ const value = data[key];
72
+ if (typeof value === "string") {
73
+ data[key] = scrubTelemetryUrl(value);
74
+ }
75
+ }
76
+ };
77
+ scrubSpanData(event.contexts?.trace?.data);
78
+ for (const span of event.spans ?? []) {
79
+ scrubSpanData(span.data);
80
+ }
81
+ return event;
82
+ }
83
+
15
84
  Sentry.init({
16
85
  dsn: process.env.SENTRY_DSN,
17
86
  enabled: !!process.env.SENTRY_DSN,
18
87
 
19
88
  tracesSampleRate: process.env.NODE_ENV === "development" ? 1.0 : 0.1,
89
+
90
+ beforeSend: scrubSentryEvent,
91
+ beforeSendTransaction: scrubSentryEvent,
20
92
  });
@@ -5,6 +5,8 @@
5
5
  * handlers to avoid copy-paste duplication.
6
6
  */
7
7
 
8
+ import { routeFor } from "@working-theory/templates/routes";
9
+
8
10
  import { SITE_BASE_URL } from "../../lib/site-brand";
9
11
 
10
12
  /**
@@ -29,22 +31,15 @@ export function resolveBaseUrl(): string {
29
31
  /**
30
32
  * Maps a content-adapter page type + slug to a site-relative path.
31
33
  * Drives both /llms.txt and /sitemap.xml URL generation.
34
+ *
35
+ * A lookup over the routes map (#5572 U2, spec § D4 row 1) — `routeFor` is
36
+ * byte-identical to the switch this replaced for its six `case` literals and
37
+ * the `` `/${slug}` `` default (proven by
38
+ * `packages/templates/src/__tests__/routes-map.test.ts`'s table test and
39
+ * `apps/web/__tests__/routes/routes-map-identity.test.ts`'s differential
40
+ * test over the live content tree). The signature is annotated explicitly so
41
+ * this stays the exact `(type: string, slug: string) => string` contract the
42
+ * three call sites (`sitemap.xml`, `llms.txt`, `docs-blog/llms-route.ts`)
43
+ * depend on, even though `routeFor`'s own `slug` parameter is optional.
32
44
  */
33
- export function slugToPath(type: string, slug: string): string {
34
- switch (type) {
35
- case "HomePage":
36
- return "/";
37
- case "BlogPosting":
38
- return `/blog/${slug}`;
39
- case "Product":
40
- return `/products/${slug}`;
41
- case "Service":
42
- return `/services/${slug}`;
43
- case "AboutPage":
44
- return "/about";
45
- case "ContactPage":
46
- return "/contact";
47
- default:
48
- return `/${slug}`;
49
- }
50
- }
45
+ export const slugToPath: (type: string, slug: string) => string = routeFor;
@@ -0,0 +1,24 @@
1
+ # brand_assets/
2
+
3
+ This folder is where you put your project's real brand material. It ships
4
+ empty — nothing here is required to run `pnpm dev` — but populating it is
5
+ what lets Claude Code (and anyone else building this site) use your actual
6
+ brand instead of inventing placeholder colors and copy.
7
+
8
+ `apps/web/CLAUDE.md` reads from this folder before designing a page. If a
9
+ file below is present, it's used; if it's absent, generic defaults are used
10
+ instead.
11
+
12
+ ## What goes here
13
+
14
+ | File / folder | Contents |
15
+ | ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
16
+ | `logo.svg` (or `.png`) | Your logo, ideally an SVG. Add light/dark variants if your mark needs them (e.g. `logo-dark.svg`). |
17
+ | `brand-guide.md` | The global theme — brand colors, type direction, tone of voice, do's and don'ts — used when choosing layout and components for a page. |
18
+ | `brand-brief.md` | Positioning, messaging, and the target citation phrases a page's content should work toward. |
19
+ | `favicon.ico` / `favicon-*.png` | Favicon assets for `apps/web/app/favicon.ico` and any additional sizes your deploy target wants. |
20
+ | `photos/` | Real photography — team, product, office — to use instead of stock or placeholder images. |
21
+ | `reference/` | Mockups, screenshots, or other visual references to match when building or reviewing a page's design. |
22
+
23
+ None of these are mandatory. Add what you have; the rest can stay
24
+ placeholder until you're ready.