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.
- package/component-catalog.json +11939 -0
- package/component-catalog.md +176 -64
- package/dist/cli.js +48 -10
- package/dist/derive-architecture.js +5 -2
- package/dist/lib/extract-module-bundle.d.ts +88 -1
- package/dist/lib/extract-module-bundle.js +375 -80
- 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.d.ts +20 -0
- package/dist/lib/fetch-template.js +133 -0
- package/dist/lib/module-license-gate.d.ts +8 -0
- package/dist/lib/module-license-gate.js +12 -4
- package/dist/module-architecture.d.ts +116 -7
- package/dist/module-architecture.js +465 -94
- package/dist/parse-args.d.ts +8 -1
- package/dist/parse-args.js +49 -2
- package/dist/programmatic.d.ts +36 -1
- package/dist/programmatic.js +216 -5
- 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 +140 -23
- package/dist/template-manifest.d.ts +144 -0
- package/dist/template-manifest.js +337 -0
- package/dist/template-names.d.ts +41 -0
- package/dist/template-names.js +45 -0
- package/package.json +10 -2
- package/site-templates/_base/ailk-template.json +6 -0
- package/site-templates/_base/content/en/Pricing/pricing.mdx +169 -0
- package/site-templates/_base/content/en/PrivacyPage/privacy.mdx +79 -0
- package/site-templates/_base/content/en/TermsPage/terms.mdx +80 -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 +9 -0
- package/site-templates/marketing-site-free/provenance.json +4 -0
- 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 +173 -0
- package/templates/apps/api/src/config/modules.ts +1 -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 +122 -3
- 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/app/[locale]/pricing/page.tsx +40 -0
- package/templates/apps/web/app/[locale]/privacy/page.tsx +40 -0
- package/templates/apps/web/app/[locale]/terms/page.tsx +40 -0
- 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 +12 -15
- package/templates/apps/web/lib/__tests__/server-api.test.ts +61 -0
- 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 +29 -13
- 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 +86 -1
- 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 +21 -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/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: "
|
|
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
|
|
349
|
-
//
|
|
350
|
-
// slug
|
|
351
|
-
// (
|
|
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
|
|
1
|
+
// nav-config.ts — Nav item registry for apps/web (#1451, re-shaped by #5469).
|
|
2
2
|
//
|
|
3
|
-
// The
|
|
4
|
-
//
|
|
5
|
-
// Paths are locale-agnostic; Nav
|
|
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
|
-
{
|
|
11
|
-
|
|
12
|
-
|
|
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`
|
|
7
|
-
* auth preHandler resolves the SAME Better Auth
|
|
8
|
-
* both apps share the session store.
|
|
9
|
-
* is
|
|
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
|
-
*
|
|
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/
|
|
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
|
|
61
|
-
* `waitlistExperiments.*` call accepts.
|
|
62
|
-
*
|
|
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
|
-
|
|
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": "
|
|
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
|
});
|