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.
- package/component-catalog.json +11939 -0
- package/component-catalog.md +176 -64
- package/dist/cli.js +29 -1
- package/dist/derive-architecture.js +5 -2
- package/dist/lib/extract-module-bundle.d.ts +50 -2
- package/dist/lib/extract-module-bundle.js +223 -15
- package/dist/lib/fetch-guard.d.ts +28 -0
- package/dist/lib/fetch-guard.js +101 -0
- package/dist/lib/fetch-module.js +4 -0
- package/dist/lib/fetch-template.js +4 -0
- package/dist/module-architecture.d.ts +59 -8
- package/dist/module-architecture.js +334 -115
- package/dist/parse-args.js +6 -2
- package/dist/programmatic.d.ts +10 -0
- package/dist/programmatic.js +134 -19
- package/dist/site-chrome.d.ts +80 -0
- package/dist/site-chrome.js +135 -0
- package/dist/surfaces.d.ts +21 -6
- package/dist/surfaces.js +107 -32
- package/dist/template-manifest.d.ts +55 -3
- package/dist/template-manifest.js +181 -12
- package/dist/template-names.d.ts +1 -1
- package/dist/template-names.js +1 -0
- package/package.json +9 -2
- package/site-templates/_base/ailk-template.json +6 -0
- package/site-templates/_base/content/en/Pricing/pricing.mdx +169 -0
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +331 -0
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +393 -0
- package/site-templates/marketing-site/ailk-template.json +11 -0
- package/site-templates/marketing-site/content/en/blog/05-this-vc-morning-routine-will-make-you-super-annoying.mdx +36 -0
- package/site-templates/marketing-site/provenance.json +4 -0
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +288 -0
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +285 -0
- package/site-templates/marketing-site-free/ailk-template.json +2 -1
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.json +271 -0
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +324 -0
- package/site-templates/minimalism-site/ailk-template.json +21 -0
- package/site-templates/minimalism-site/assets/avatar-placeholder-1.svg +7 -0
- package/site-templates/minimalism-site/assets/avatar-placeholder-2.svg +7 -0
- package/site-templates/minimalism-site/assets/avatar-placeholder-3.svg +7 -0
- package/site-templates/minimalism-site/assets/avatar-placeholder-4.svg +7 -0
- package/site-templates/minimalism-site/assets/avatar-placeholder-5.svg +7 -0
- package/site-templates/minimalism-site/assets/avatar-placeholder-6.svg +7 -0
- package/site-templates/minimalism-site/assets/hero-backdrop.svg +17 -0
- package/site-templates/minimalism-site/assets/logo-placeholder-1.svg +6 -0
- package/site-templates/minimalism-site/assets/logo-placeholder-2.svg +6 -0
- package/site-templates/minimalism-site/assets/logo-placeholder-3.svg +6 -0
- package/site-templates/minimalism-site/content/en/AboutPage/about.mdx +110 -0
- package/site-templates/minimalism-site/content/en/ContactPage/contact.mdx +68 -0
- package/site-templates/minimalism-site/content/en/FAQPage/faq.mdx +57 -0
- package/site-templates/minimalism-site/content/en/HomePage/home.mdx +214 -0
- package/site-templates/minimalism-site/content/en/LandingPage/referral.mdx +77 -0
- package/site-templates/minimalism-site/content/en/LandingPage/reports.mdx +73 -0
- package/site-templates/minimalism-site/content/en/ReportPage/annual-letter-2024.mdx +52 -0
- package/site-templates/minimalism-site/content/en/ReportPage/field-notes-2023.mdx +52 -0
- package/site-templates/minimalism-site/content/en/ReportPage/platform-shift-2025.mdx +52 -0
- package/site-templates/minimalism-site/content/en/ReportPage/state-of-the-market-2026.mdx +52 -0
- package/site-templates/minimalism-site/content/en/Service/first-engagement.mdx +38 -0
- package/site-templates/minimalism-site/content/en/Service/second-engagement.mdx +38 -0
- package/site-templates/minimalism-site/content/en/Service/third-engagement.mdx +38 -0
- package/site-templates/minimalism-site/content/en/ServicesList/services.mdx +73 -0
- package/site-templates/minimalism-site/flows/referral.flow.json +71 -0
- package/site-templates/minimalism-site/provenance.json +4 -0
- package/site-templates/saas-product/TEMPLATE-MANIFEST.md +61 -0
- package/site-templates/saas-product/ailk-template.json +16 -0
- package/site-templates/saas-product/provenance.json +10 -0
- package/templates/.claude/skills/README.md +16 -25
- package/templates/.env.example +32 -7
- package/templates/.github/workflows/ci.yml +48 -0
- package/templates/README.md +3 -1
- package/templates/apps/api/CLAUDE.md +4 -1
- package/templates/apps/api/eslint.config.mjs +69 -0
- package/templates/apps/api/package.json +6 -2
- package/templates/apps/api/src/__tests__/module-exclusion.test.ts +6 -2
- package/templates/apps/api/src/__tests__/review-surface-registration.test.ts +167 -0
- package/templates/apps/api/src/__tests__/workspace-module-registration.test.ts +8 -1
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +3 -0
- package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +339 -0
- package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +108 -0
- package/templates/apps/api/src/middleware/default-tenant.ts +136 -0
- package/templates/apps/api/src/routes/leads/__tests__/list.test.ts +171 -0
- package/templates/apps/api/src/routes/leads/list.ts +104 -0
- package/templates/apps/api/src/server.ts +82 -6
- package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +408 -0
- package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +329 -0
- package/templates/apps/api/src/services/default-tenant.ts +261 -0
- package/templates/apps/api/src/services/lead-promotion.ts +161 -0
- package/templates/apps/api/src/services/leads-inbox.ts +263 -0
- package/templates/apps/api/src/services/owner-workspaces.ts +132 -0
- package/templates/apps/mcp/CLAUDE.md +1 -1
- package/templates/apps/mcp/__tests__/parity.test.ts +2 -2
- package/templates/apps/mcp/__tests__/tools/{scaffold.test.ts → create_project.test.ts} +23 -23
- package/templates/apps/mcp/eslint.config.mjs +69 -0
- package/templates/apps/mcp/package.json +6 -2
- package/templates/apps/mcp/src/server.ts +2 -2
- package/templates/apps/mcp/src/tools/{scaffold.ts → create_project.ts} +3 -3
- package/templates/apps/mcp/src/tools/index.ts +2 -2
- package/templates/apps/web/.env.example +3 -3
- package/templates/apps/web/CLAUDE.md +2 -0
- package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +4 -6
- package/templates/apps/web/app/[locale]/(authed)/layout.tsx +1 -1
- package/templates/apps/web/app/[locale]/(authed)/leads/__tests__/page.test.tsx +181 -0
- package/templates/apps/web/app/[locale]/(authed)/leads/components/LeadsTable.tsx +137 -0
- package/templates/apps/web/app/[locale]/(authed)/leads/page.tsx +111 -0
- package/templates/apps/web/app/[locale]/(authed)/sign-in-gate.tsx +1 -1
- package/templates/apps/web/app/[locale]/[slug]/__tests__/page.test.tsx +111 -0
- package/templates/apps/web/app/[locale]/[slug]/page.tsx +65 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +1 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +6 -5
- package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +1 -1
- package/templates/apps/web/app/[locale]/layout.tsx +214 -17
- package/templates/apps/web/components/google-analytics.tsx +18 -1
- package/templates/apps/web/eslint.config.mjs +69 -0
- package/templates/apps/web/jest.config.cjs +7 -15
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +32 -5
- package/templates/apps/web/lib/nav-config.ts +27 -7
- package/templates/apps/web/lib/server-api.ts +1 -1
- package/templates/apps/web/lib/site-chrome.ts +29 -0
- package/templates/apps/web/lib/telemetry-scrub.ts +46 -0
- package/templates/apps/web/messages/en.json +29 -2
- package/templates/apps/web/next.config.mjs +20 -0
- package/templates/apps/web/package.json +6 -2
- package/templates/apps/web/sentry.client.config.ts +131 -0
- package/templates/apps/web/sentry.edge.config.ts +72 -0
- package/templates/apps/web/sentry.server.config.ts +72 -0
- package/templates/apps/web/src/lib/routes.ts +13 -18
- package/templates/brand_assets/README.md +24 -0
- package/templates/content/en/AboutPage/about.mdx +160 -15
- package/templates/content/en/ContactPage/contact.mdx +90 -13
- package/templates/content/en/FAQPage/faq.mdx +62 -22
- package/templates/content/en/HomePage/home.mdx +244 -20
- package/templates/content/en/LandingPage/blog.mdx +83 -0
- package/templates/content/en/LandingPage/features.mdx +103 -0
- package/templates/content/en/LandingPage/how-it-works.mdx +143 -0
- package/templates/database/CHANGELOG.md +7 -0
- package/templates/database/__tests__/seed.test.ts +1 -1
- package/templates/database/eslint.config.mjs +69 -0
- package/templates/database/package.json +7 -3
- package/templates/database/seed.ts +1 -1
- package/templates/package.json +1 -14
- package/versions.json +52 -0
- package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +0 -103
- /package/site-templates/{marketing-site-free → _base}/content/en/PrivacyPage/privacy.mdx +0 -0
- /package/site-templates/{marketing-site-free → _base}/content/en/TermsPage/terms.mdx +0 -0
- /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
|
-
"
|
|
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": "
|
|
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
|
|
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.
|