create-ailk 0.4.0 → 0.6.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/README.md +55 -8
- package/component-catalog.json +12248 -0
- package/component-catalog.md +193 -68
- package/dist/cli.js +29 -1
- package/dist/derive-architecture.js +5 -2
- package/dist/lib/api-boot-assertion.d.ts +46 -0
- package/dist/lib/api-boot-assertion.js +70 -0
- 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/lib/import-graph.d.ts +29 -0
- package/dist/lib/import-graph.js +140 -0
- package/dist/module-architecture.d.ts +60 -8
- package/dist/module-architecture.js +587 -242
- 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 +209 -36
- package/dist/template-manifest.d.ts +88 -3
- package/dist/template-manifest.js +281 -29
- package/dist/template-names.d.ts +1 -1
- package/dist/template-names.js +2 -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/marketing-site/TEMPLATE-MANIFEST.json +351 -0
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +400 -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 +286 -0
- package/site-templates/marketing-site-free/ailk-template.json +2 -1
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.json +279 -0
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +336 -0
- package/site-templates/minimalism-site/ailk-template.json +22 -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/assets/wordmark-placeholder.svg +28 -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 +244 -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/site-templates/simple-service/TEMPLATE-MANIFEST.json +240 -0
- package/site-templates/simple-service/TEMPLATE-MANIFEST.md +278 -0
- package/site-templates/simple-service/ailk-template.json +10 -0
- package/site-templates/simple-service/content/en/AboutPage/about.mdx +38 -0
- package/site-templates/simple-service/content/en/ContactPage/contact.mdx +67 -0
- package/site-templates/simple-service/content/en/FAQPage/faq.mdx +56 -0
- package/site-templates/simple-service/content/en/HomePage/home.mdx +85 -0
- package/site-templates/simple-service/content/en/Service/landscape-design.mdx +40 -0
- package/site-templates/simple-service/content/en/Service/lawn-maintenance.mdx +40 -0
- package/site-templates/simple-service/content/en/Service/seasonal-cleanup.mdx +40 -0
- package/site-templates/simple-service/content/en/ServicesList/services.mdx +72 -0
- package/site-templates/simple-service/provenance.json +4 -0
- package/templates/.claude/skills/README.md +16 -25
- package/templates/.env.example +102 -12
- package/templates/.github/workflows/ci.yml +48 -0
- package/templates/README.md +3 -1
- package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-31.552Z-fallback-1789916251590-037e9005.json +8 -0
- package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.840Z-fallback-1789916253842-b52659e9.json +12 -0
- package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.844Z-fallback-1789916253845-6b266f8c.json +12 -0
- package/templates/apps/api/.env.example +13 -0
- package/templates/apps/api/CLAUDE.md +29 -2
- package/templates/apps/api/eslint.config.mjs +69 -0
- package/templates/apps/api/jest.config.cjs +3 -0
- package/templates/apps/api/package.json +7 -2
- package/templates/apps/api/src/__tests__/commerce-claim-registration.test.ts +407 -0
- package/templates/apps/api/src/__tests__/module-exclusion.test.ts +89 -9
- package/templates/apps/api/src/__tests__/review-surface-registration.test.ts +167 -0
- package/templates/apps/api/src/__tests__/server.test.ts +471 -0
- package/templates/apps/api/src/__tests__/vercel-handler.test.ts +45 -0
- package/templates/apps/api/src/__tests__/workspace-module-registration.test.ts +10 -1
- package/templates/apps/api/src/config/index.ts +62 -0
- package/templates/apps/api/src/config/modules.ts +7 -3
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +241 -2
- package/templates/apps/api/src/lib/__tests__/auth-email-budget.test.ts +143 -0
- package/templates/apps/api/src/lib/__tests__/content-adapter.test.ts +23 -0
- package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +125 -0
- package/templates/apps/api/src/lib/__tests__/error-serializer.test.ts +20 -0
- package/templates/apps/api/src/lib/__tests__/require-commerce-claim.test.ts +317 -0
- package/templates/apps/api/src/lib/__tests__/site.test.ts +238 -0
- package/templates/apps/api/src/lib/__tests__/stripe.test.ts +75 -0
- package/templates/apps/api/src/lib/auth-email-budget.ts +160 -0
- package/templates/apps/api/src/lib/content-adapter.ts +57 -31
- package/templates/apps/api/src/lib/deliverable-asset-store.ts +76 -0
- package/templates/apps/api/src/lib/error-serializer.ts +23 -3
- package/templates/apps/api/src/lib/outbound-http.ts +7 -2
- package/templates/apps/api/src/lib/require-commerce-claim.ts +250 -0
- package/templates/apps/api/src/lib/site.ts +208 -9
- package/templates/apps/api/src/lib/stripe.ts +66 -0
- package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +521 -0
- package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +150 -0
- package/templates/apps/api/src/middleware/default-tenant.ts +136 -0
- package/templates/apps/api/src/middleware/rate-limit.ts +7 -1
- package/templates/apps/api/src/openapi/spec.ts +543 -2
- package/templates/apps/api/src/routes/auth/__tests__/auth-user.functional.test.ts +48 -0
- package/templates/apps/api/src/routes/auth/__tests__/transactional-email.test.ts +309 -0
- package/templates/apps/api/src/routes/auth/__tests__/user.test.ts +123 -0
- package/templates/apps/api/src/routes/auth/transactional-email.ts +273 -0
- package/templates/apps/api/src/routes/auth/user.ts +61 -0
- package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +434 -0
- package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +888 -0
- package/templates/apps/api/src/routes/checkout/index.ts +34 -0
- package/templates/apps/api/src/routes/checkout/session.ts +139 -0
- package/templates/apps/api/src/routes/checkout/sessions.ts +197 -0
- package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +554 -0
- package/templates/apps/api/src/routes/content/__tests__/list-pages.test.ts +108 -0
- package/templates/apps/api/src/routes/content/__tests__/search.test.ts +450 -0
- package/templates/apps/api/src/routes/content/get-page.ts +94 -12
- package/templates/apps/api/src/routes/content/index.ts +13 -73
- package/templates/apps/api/src/routes/content/list-pages.ts +15 -3
- package/templates/apps/api/src/routes/content/rate-limit.ts +15 -6
- package/templates/apps/api/src/routes/content/search.ts +129 -0
- package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +447 -31
- package/templates/apps/api/src/routes/leads/__tests__/list.test.ts +171 -0
- package/templates/apps/api/src/routes/leads/index.ts +100 -13
- package/templates/apps/api/src/routes/leads/list.ts +104 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +51 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +351 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +51 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +5 -2
- package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +4 -1
- package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +58 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +107 -6
- package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +87 -2
- package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +51 -0
- package/templates/apps/api/src/routes/project-listings/start.ts +5 -1
- package/templates/apps/api/src/routes/requests/__tests__/confirm.test.ts +686 -0
- package/templates/apps/api/src/routes/requests/confirm.ts +601 -0
- package/templates/apps/api/src/routes/requests/index.ts +54 -0
- package/templates/apps/api/src/routes/webhooks/README.md +265 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-dunning.test.ts +749 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +330 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +262 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +285 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +213 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-request-paid.test.ts +301 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +648 -0
- package/templates/apps/api/src/routes/webhooks/index.ts +89 -0
- package/templates/apps/api/src/routes/webhooks/stripe.ts +1399 -0
- package/templates/apps/api/src/server.ts +521 -55
- package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +39 -0
- package/templates/apps/api/src/services/__tests__/aeo-score.test.ts +217 -40
- package/templates/apps/api/src/services/__tests__/audit-log.test.ts +620 -0
- package/templates/apps/api/src/services/__tests__/consent.test.ts +2 -0
- package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +300 -5
- package/templates/apps/api/src/services/__tests__/deliverable-resend.test.ts +20 -0
- package/templates/apps/api/src/services/__tests__/dunning-policy.test.ts +368 -0
- package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +408 -0
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +160 -0
- package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +329 -0
- package/templates/apps/api/src/services/__tests__/listing-config.test.ts +166 -0
- package/templates/apps/api/src/services/__tests__/project-listings.fence.test.ts +154 -0
- package/templates/apps/api/src/services/__tests__/public-site-resolver.test.ts +137 -0
- package/templates/apps/api/src/services/__tests__/request-confirm.test.ts +444 -0
- package/templates/apps/api/src/services/__tests__/revalidation.test.ts +238 -0
- package/templates/apps/api/src/services/__tests__/verified-email.test.ts +78 -0
- package/templates/apps/api/src/services/abandon-email.ts +8 -2
- package/templates/apps/api/src/services/aeo-score.ts +49 -12
- package/templates/apps/api/src/services/audit-log.ts +668 -0
- package/templates/apps/api/src/services/consent.ts +18 -0
- package/templates/apps/api/src/services/default-tenant.ts +300 -0
- package/templates/apps/api/src/services/deliverable-fulfillment.ts +141 -13
- package/templates/apps/api/src/services/dunning-policy.ts +294 -0
- package/templates/apps/api/src/services/entitlements.ts +80 -0
- package/templates/apps/api/src/services/lead-promotion.ts +160 -0
- package/templates/apps/api/src/services/lead-routing.ts +161 -2
- package/templates/apps/api/src/services/leads-inbox.ts +284 -0
- package/templates/apps/api/src/services/listing-config.ts +27 -9
- package/templates/apps/api/src/services/owner-workspaces.ts +132 -0
- package/templates/apps/api/src/services/project-listings.ts +201 -6
- package/templates/apps/api/src/services/public-site-resolver.ts +106 -0
- package/templates/apps/api/src/services/request-confirm.ts +646 -0
- package/templates/apps/api/src/services/revalidation.ts +173 -0
- package/templates/apps/api/src/services/verified-email.ts +71 -0
- package/templates/apps/api/src/vercel-handler.ts +75 -35
- package/templates/apps/mcp/.env.example +17 -0
- package/templates/apps/mcp/CLAUDE.md +58 -25
- package/templates/apps/mcp/__tests__/aeo-cli-parity.test.ts +6 -0
- package/templates/apps/mcp/__tests__/audit.test.ts +137 -8
- package/templates/apps/mcp/__tests__/catalog-drift.test.ts +79 -36
- package/templates/apps/mcp/__tests__/get_offerings.test.ts +111 -2
- package/templates/apps/mcp/__tests__/get_pricing.test.ts +82 -2
- package/templates/apps/mcp/__tests__/http-transport.test.ts +541 -2
- package/templates/apps/mcp/__tests__/index.test.ts +39 -0
- package/templates/apps/mcp/__tests__/lib/audit-single-page-readiness-scan.test.ts +143 -0
- package/templates/apps/mcp/__tests__/lib/commerce-claim.test.ts +111 -0
- package/templates/apps/mcp/__tests__/lib/content-adapter-parity.test.ts +65 -0
- package/templates/apps/mcp/__tests__/lib/content-adapter.test.ts +188 -0
- package/templates/apps/mcp/__tests__/lib/fail-closed-tools.test.ts +165 -0
- package/templates/apps/mcp/__tests__/lib/public-workspace.test.ts +124 -0
- package/templates/apps/mcp/__tests__/list_pages.test.ts +116 -0
- package/templates/apps/mcp/__tests__/module-exclusion.test.ts +7 -1
- package/templates/apps/mcp/__tests__/parity.test.ts +12 -9
- package/templates/apps/mcp/__tests__/purchase-disabled.test.ts +89 -22
- package/templates/apps/mcp/__tests__/read_content.test.ts +322 -1
- package/templates/apps/mcp/__tests__/search_content.test.ts +105 -2
- package/templates/apps/mcp/__tests__/server.test.ts +67 -14
- package/templates/apps/mcp/__tests__/start_purchase.test.ts +252 -0
- package/templates/apps/mcp/__tests__/tools/{scaffold.test.ts → create_project.test.ts} +23 -23
- package/templates/apps/mcp/__tests__/tools/purchase.test.ts +300 -123
- package/templates/apps/mcp/eslint.config.mjs +69 -0
- package/templates/apps/mcp/jest.config.cjs +13 -0
- package/templates/apps/mcp/package.json +8 -2
- package/templates/apps/mcp/src/abuse/index.ts +159 -24
- package/templates/apps/mcp/src/config/modules.ts +10 -5
- package/templates/apps/mcp/src/http-transport.ts +102 -29
- package/templates/apps/mcp/src/index.ts +73 -11
- package/templates/apps/mcp/src/lib/audit-single-page.ts +90 -3
- package/templates/apps/mcp/src/lib/commerce-claim.ts +94 -0
- package/templates/apps/mcp/src/lib/content-adapter.ts +113 -0
- package/templates/apps/mcp/src/lib/empty-content-adapter.ts +139 -0
- package/templates/apps/mcp/src/lib/prisma.ts +39 -0
- package/templates/apps/mcp/src/lib/public-workspace.ts +133 -0
- package/templates/apps/mcp/src/lib/stripe-tax-signal.ts +59 -0
- package/templates/apps/mcp/src/server.ts +61 -37
- package/templates/apps/mcp/src/tools/audit.ts +26 -2
- package/templates/apps/mcp/src/tools/{scaffold.ts → create_project.ts} +3 -3
- package/templates/apps/mcp/src/tools/get_business_hours.ts +4 -3
- package/templates/apps/mcp/src/tools/get_faq.ts +4 -3
- package/templates/apps/mcp/src/tools/get_locations.ts +5 -3
- package/templates/apps/mcp/src/tools/get_offerings.ts +16 -4
- package/templates/apps/mcp/src/tools/get_pricing.ts +25 -6
- package/templates/apps/mcp/src/tools/get_site_info.ts +4 -3
- package/templates/apps/mcp/src/tools/index.ts +9 -7
- package/templates/apps/mcp/src/tools/list_pages.ts +4 -3
- package/templates/apps/mcp/src/tools/purchase.ts +128 -45
- package/templates/apps/mcp/src/tools/read_content.ts +20 -6
- package/templates/apps/mcp/src/tools/search_content.ts +11 -4
- package/templates/apps/web/.env.example +74 -8
- package/templates/apps/web/CLAUDE.md +46 -0
- package/templates/apps/web/app/.well-known/mcp.json/build-manifest.ts +18 -2
- package/templates/apps/web/app/.well-known/mcp.json/route.ts +11 -3
- package/templates/apps/web/app/[locale]/(auth)/__tests__/auth-pages.test.tsx +282 -0
- package/templates/apps/web/app/[locale]/(auth)/__tests__/password-reset.test.tsx +515 -0
- package/templates/apps/web/app/[locale]/(auth)/forgot-password/forgot-password-card.tsx +89 -0
- package/templates/apps/web/app/[locale]/(auth)/forgot-password/page.tsx +109 -0
- package/templates/apps/web/app/[locale]/(auth)/layout.tsx +92 -0
- package/templates/apps/web/app/[locale]/(auth)/reset-password/page.tsx +135 -0
- package/templates/apps/web/app/[locale]/(auth)/reset-password/reset-password-card.tsx +109 -0
- package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/__tests__/page.test.tsx +48 -2
- package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/page.tsx +20 -6
- package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-card.tsx +125 -0
- package/templates/apps/web/app/[locale]/(auth)/sign-up/page.tsx +96 -0
- package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +101 -0
- package/templates/apps/web/app/[locale]/(auth)/use-auth-card-flow.ts +102 -0
- package/templates/apps/web/app/[locale]/(auth)/verify-email/page.tsx +87 -0
- package/templates/apps/web/app/[locale]/(authed)/__tests__/gate.test.tsx +20 -10
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +84 -0
- package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +58 -0
- package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +67 -14
- package/templates/apps/web/app/[locale]/(authed)/layout.tsx +19 -2
- 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]/cancellation/page.tsx +44 -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 +308 -65
- package/templates/apps/web/app/[locale]/purchase/__tests__/cancel.test.tsx +162 -0
- package/templates/apps/web/app/[locale]/purchase/__tests__/module-scope.test.ts +68 -0
- package/templates/apps/web/app/[locale]/purchase/__tests__/success.test.tsx +361 -0
- package/templates/apps/web/app/[locale]/purchase/cancel/page.tsx +115 -0
- package/templates/apps/web/app/[locale]/purchase/success/page.tsx +307 -0
- package/templates/apps/web/app/[locale]/returns/page.tsx +44 -0
- package/templates/apps/web/app/[locale]/shipping/page.tsx +44 -0
- package/templates/apps/web/app/api/revalidate/__tests__/route.test.ts +670 -0
- package/templates/apps/web/app/api/revalidate/route.ts +194 -0
- package/templates/apps/web/auth.ts +407 -8
- 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 +15 -19
- package/templates/apps/web/lib/__tests__/auth-email.test.ts +214 -0
- package/templates/apps/web/lib/__tests__/auth-routes.test.ts +74 -0
- package/templates/apps/web/lib/__tests__/meeting-slugs.test.ts +72 -0
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +50 -6
- package/templates/apps/web/lib/auth-email.ts +318 -0
- package/templates/apps/web/lib/auth-env.ts +85 -0
- package/templates/apps/web/lib/auth-routes.ts +55 -0
- package/templates/apps/web/lib/meeting-slugs.ts +53 -0
- 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 +50 -0
- package/templates/apps/web/messages/en.json +216 -2
- package/templates/apps/web/next.config.mjs +20 -0
- package/templates/apps/web/package.json +8 -3
- 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/commerce-claim.ts +33 -0
- package/templates/apps/web/src/lib/commerce-sections.ts +61 -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/CancellationPolicyPage/cancellation.mdx +108 -0
- 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/content/en/ReturnsPolicyPage/returns.mdx +108 -0
- package/templates/content/en/ShippingPolicyPage/shipping.mdx +108 -0
- package/templates/database/CHANGELOG.md +149 -0
- package/templates/database/README.md +10 -3
- package/templates/database/__tests__/appointment.db.test.ts +106 -0
- package/templates/database/__tests__/artifact-content-store.db.test.ts +316 -0
- package/templates/database/__tests__/composite-fence.db.test.ts +206 -0
- package/templates/database/__tests__/fence-fan-out.db.test.ts +711 -0
- package/templates/database/__tests__/fixtures/appointment-create-probe.ts +79 -0
- package/templates/database/__tests__/fixtures/project-listing-consent-probe.ts +100 -0
- package/templates/database/__tests__/flow-workspace-key.db.test.ts +263 -0
- package/templates/database/__tests__/prefixed-ids.db.test.ts +85 -9
- package/templates/database/__tests__/project-listing-consent-at.db.test.ts +111 -0
- package/templates/database/__tests__/seed.test.ts +1 -1
- package/templates/database/__tests__/tenancy.test.ts +141 -0
- package/templates/database/config/schema.prisma +79 -2
- package/templates/database/content/schema.prisma +87 -11
- package/templates/database/eslint.config.mjs +69 -0
- package/templates/database/inbox/schema.prisma +452 -9
- package/templates/database/jest.config.cjs +5 -3
- package/templates/database/migrations/20260915120000_project_listing_solution/migration.sql +15 -0
- package/templates/database/migrations/20260916120000_artifact_content_store/migration.sql +158 -0
- package/templates/database/migrations/20260917120000_callback_request_confirm/migration.sql +40 -0
- package/templates/database/migrations/20260918120000_add_appointment/migration.sql +67 -0
- package/templates/database/migrations/20260918120000_asset_metadata_columns/migration.sql +70 -0
- package/templates/database/migrations/20260919120000_flow_workspace_key/migration.sql +181 -0
- package/templates/database/migrations/20260919130000_flow_definition_admin_columns/migration.sql +172 -0
- package/templates/database/migrations/20260919130000_workspace_meetings/migration.sql +41 -0
- package/templates/database/migrations/20260919140000_inbox_workspace_key/migration.sql +185 -0
- package/templates/database/migrations/20260920120000_lead_handled_at/migration.sql +70 -0
- package/templates/database/migrations/20260920130000_lead_workspace_source_index/migration.sql +57 -0
- package/templates/database/migrations/20260921120000_deliverable_grant_workspace_key/migration.sql +68 -0
- package/templates/database/migrations/20260922120000_tenancy_fence/migration.sql +1067 -0
- package/templates/database/migrations/20260923120000_listing_capturing_magnet/migration.sql +210 -0
- package/templates/database/package.json +7 -3
- package/templates/database/seed.ts +1 -1
- package/templates/database/tenancy.ts +86 -1
- package/templates/database/tsconfig.json +1 -0
- 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/templates/apps/api/src/routes/content/__tests__/create.test.ts +0 -253
- package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +0 -215
- package/templates/apps/api/src/routes/content/__tests__/update.test.ts +0 -266
- package/templates/apps/api/src/routes/content/create.ts +0 -115
- package/templates/apps/api/src/routes/content/delete.ts +0 -82
- package/templates/apps/api/src/routes/content/update.ts +0 -112
- package/templates/apps/mcp/__tests__/create_page.test.ts +0 -202
- package/templates/apps/mcp/__tests__/delete_page.test.ts +0 -177
- package/templates/apps/mcp/__tests__/update_page.test.ts +0 -196
- package/templates/apps/mcp/src/tools/create_page.ts +0 -96
- package/templates/apps/mcp/src/tools/delete_page.ts +0 -74
- package/templates/apps/mcp/src/tools/update_page.ts +0 -92
- package/templates/apps/web/app/[locale]/sign-in/sign-in-card.tsx +0 -123
- /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/app/[locale]/{sign-in → (auth)/sign-in}/__tests__/next-path.test.ts +0 -0
- /package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/next-path.ts +0 -0
- /package/templates/apps/web/{src/components → components}/SiteNav.tsx +0 -0
|
@@ -12,11 +12,19 @@
|
|
|
12
12
|
import Script from "next/script";
|
|
13
13
|
import type { ReactElement } from "react";
|
|
14
14
|
|
|
15
|
+
import { TOKEN_QUERY_PARAMS } from "../lib/telemetry-scrub";
|
|
16
|
+
|
|
15
17
|
export function GoogleAnalytics(): ReactElement | null {
|
|
16
18
|
const tagId = process.env.NEXT_PUBLIC_GOOGLE_TAG_ID;
|
|
17
19
|
if (!tagId) {
|
|
18
20
|
return null;
|
|
19
21
|
}
|
|
22
|
+
// The init script below runs in the browser as raw inline JS, outside any
|
|
23
|
+
// module graph, so it can't `import` telemetry-scrub.ts — it gets its own
|
|
24
|
+
// copy of scrubTelemetryUrl, driven by the shared param list, so the
|
|
25
|
+
// report-grant token (?g=<id>, #5562) never reaches GA's page_location
|
|
26
|
+
// default (document.location.href) (#5613).
|
|
27
|
+
const scrubParamsJson = JSON.stringify(TOKEN_QUERY_PARAMS);
|
|
20
28
|
return (
|
|
21
29
|
<>
|
|
22
30
|
<Script
|
|
@@ -27,7 +35,16 @@ export function GoogleAnalytics(): ReactElement | null {
|
|
|
27
35
|
{`window.dataLayer = window.dataLayer || [];
|
|
28
36
|
function gtag(){dataLayer.push(arguments);}
|
|
29
37
|
gtag('js', new Date());
|
|
30
|
-
|
|
38
|
+
function scrubTelemetryUrl(url) {
|
|
39
|
+
try {
|
|
40
|
+
var parsed = new URL(url);
|
|
41
|
+
${scrubParamsJson}.forEach(function (param) { parsed.searchParams.delete(param); });
|
|
42
|
+
return parsed.toString();
|
|
43
|
+
} catch (e) {
|
|
44
|
+
return url;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
gtag('config', '${tagId}', { page_location: scrubTelemetryUrl(window.location.href) });`}
|
|
31
48
|
</Script>
|
|
32
49
|
</>
|
|
33
50
|
);
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Self-contained ESLint flat config for this scaffolded package.
|
|
2
|
+
//
|
|
3
|
+
// It deliberately does NOT depend on @working-theory/eslint-config — that
|
|
4
|
+
// package is internal to the AI Launch Kit monorepo and is not published, so
|
|
5
|
+
// it cannot be a dependency of a scaffolded starter. Everything below comes
|
|
6
|
+
// from public npm packages already pinned as devDependencies (see
|
|
7
|
+
// package.json): @eslint/js, globals and typescript-eslint.
|
|
8
|
+
//
|
|
9
|
+
// Source file name (#5556): this file is checked in as `_eslint.config.mjs`,
|
|
10
|
+
// not `eslint.config.mjs` — build-templates.mjs renames it at snapshot time
|
|
11
|
+
// (renameScaffoldLintConfigs()). A file literally named `eslint.config.mjs`
|
|
12
|
+
// anywhere under this monorepo (even one this repo's own root eslint.config.ts
|
|
13
|
+
// lists in its `ignores`) is auto-discovered by ESLint's flat-config nested
|
|
14
|
+
// resolution as a competing config root, which breaks the monorepo's own
|
|
15
|
+
// lint runs (verified live: typescript-eslint's "multiple candidate
|
|
16
|
+
// TSConfigRootDirs" parser error across unrelated packages/create-ailk
|
|
17
|
+
// files). The underscore prefix keeps it invisible to that discovery while
|
|
18
|
+
// it lives in this checkout; only the snapshot in templates/ — which ships
|
|
19
|
+
// to the customer, not this repo — gets the real filename.
|
|
20
|
+
import js from "@eslint/js";
|
|
21
|
+
import globals from "globals";
|
|
22
|
+
import tseslint from "typescript-eslint";
|
|
23
|
+
|
|
24
|
+
export default tseslint.config(
|
|
25
|
+
{
|
|
26
|
+
ignores: [
|
|
27
|
+
"**/.next/**",
|
|
28
|
+
"**/dist/**",
|
|
29
|
+
"**/generated/**",
|
|
30
|
+
"**/node_modules/**",
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
js.configs.recommended,
|
|
34
|
+
...tseslint.configs.recommended,
|
|
35
|
+
{
|
|
36
|
+
languageOptions: {
|
|
37
|
+
globals: {
|
|
38
|
+
...globals.node,
|
|
39
|
+
...globals.browser,
|
|
40
|
+
...globals.jest,
|
|
41
|
+
},
|
|
42
|
+
// Explicit, self-contained root — this scaffold config is never meant
|
|
43
|
+
// to combine with any sibling scaffold package's config (each ships
|
|
44
|
+
// standalone), and leaving it unset makes typescript-eslint's
|
|
45
|
+
// candidate-root disambiguation ambiguous the moment more than one
|
|
46
|
+
// scaffold package's config resolves in the same ESLint process (e.g.
|
|
47
|
+
// this monorepo's own `templates/` build, where all four exist
|
|
48
|
+
// side by side; a real customer scaffold never has that problem,
|
|
49
|
+
// since it only ever ships and lints ONE of these four at a time).
|
|
50
|
+
parserOptions: {
|
|
51
|
+
tsconfigRootDir: import.meta.dirname,
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
rules: {
|
|
55
|
+
// Downgraded rather than off, so both stay visible without failing a
|
|
56
|
+
// fresh scaffold's lint:
|
|
57
|
+
// - unused-vars: most of the source follows the underscore-prefix
|
|
58
|
+
// convention for intentionally-unused bindings, which this rule's
|
|
59
|
+
// default options don't recognize; a handful of test fixtures also
|
|
60
|
+
// carry genuinely-unused locals this minimal config doesn't try to
|
|
61
|
+
// triage file-by-file.
|
|
62
|
+
// - require-imports: CommonJS require() is the correct form in Jest
|
|
63
|
+
// config/mock files and in a few test-only dynamic re-require
|
|
64
|
+
// patterns.
|
|
65
|
+
"@typescript-eslint/no-unused-vars": "warn",
|
|
66
|
+
"@typescript-eslint/no-require-imports": "warn",
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
);
|
|
@@ -58,9 +58,15 @@ module.exports = {
|
|
|
58
58
|
"<rootDir>/node_modules/@working-theory/schema-validator/dist/index.js",
|
|
59
59
|
"^@working-theory/sections-registry$":
|
|
60
60
|
"<rootDir>/node_modules/@working-theory/sections-registry/dist/index.js",
|
|
61
|
-
|
|
62
61
|
"^@working-theory/templates/shells$":
|
|
63
62
|
"<rootDir>/node_modules/@working-theory/templates/dist/shells/index.js",
|
|
63
|
+
// Pure-data routes-map subpath (#5572 U2) — mapped directly to source so
|
|
64
|
+
// a consumer of the lightweight `./routes` export (routes.ts,
|
|
65
|
+
// next.config.mjs, this suite) never pulls in the templates package's
|
|
66
|
+
// shells (React/lucide-react) chain, mirroring the same entry in the
|
|
67
|
+
// root and create-ailk jest configs.
|
|
68
|
+
"^@working-theory/templates/routes$":
|
|
69
|
+
"<rootDir>/node_modules/@working-theory/templates/dist/routes-map.js",
|
|
64
70
|
"^@working-theory/templates$": "<rootDir>/node_modules/@working-theory/templates/dist/index.js",
|
|
65
71
|
"^@working-theory/ui$": "<rootDir>/node_modules/@working-theory/ui/dist/index.js",
|
|
66
72
|
"^@working-theory/ui/blocks$": "<rootDir>/node_modules/@working-theory/ui/dist/blocks/index.js",
|
|
@@ -69,20 +75,6 @@ module.exports = {
|
|
|
69
75
|
"<rootDir>/node_modules/@working-theory/ui/dist/primitives/index.js",
|
|
70
76
|
"^@working-theory/ui/panels$": "<rootDir>/node_modules/@working-theory/ui/dist/panels/index.js",
|
|
71
77
|
"^@working-theory/ui/sections$": "<rootDir>/node_modules/@working-theory/ui/dist/sections/index.js",
|
|
72
|
-
// #5214: the scheduling slice's renderer-registration subpath. Mapped to
|
|
73
|
-
// source like every sibling above — packages/ui/dist isn't built in this
|
|
74
|
-
// workspace pre-`pnpm build`. The target strips with the slice on the public
|
|
75
|
-
// tree, where nothing imports it, so the unused mapping is inert there.
|
|
76
|
-
"^@working-theory/ui/slices/scheduling$":
|
|
77
|
-
"<rootDir>/node_modules/@working-theory/ui/dist/sections/slices/scheduling.js",
|
|
78
|
-
// #5366: the signal-collectors slice's re-export subpath. Same reasoning
|
|
79
|
-
// as the scheduling mapping above.
|
|
80
|
-
"^@working-theory/ui/slices/signal-collectors$":
|
|
81
|
-
"<rootDir>/node_modules/@working-theory/ui/dist/sections/slices/signal-collectors.js",
|
|
82
|
-
// #5364: the commerce slice's renderer-registration + CheckoutPriceCard
|
|
83
|
-
// re-export subpath. Same reasoning as the scheduling mapping above.
|
|
84
|
-
"^@working-theory/ui/slices/commerce$":
|
|
85
|
-
"<rootDir>/node_modules/@working-theory/ui/dist/sections/slices/commerce.js",
|
|
86
78
|
"^@working-theory/theme$": "<rootDir>/node_modules/@working-theory/theme/dist/index.js",
|
|
87
79
|
// site-config publishes ESM in dist/; map to source so ts-jest compiles it
|
|
88
80
|
// through the same transformer as every sibling above. Only the
|
|
@@ -92,11 +84,15 @@ module.exports = {
|
|
|
92
84
|
"<rootDir>/node_modules/@working-theory/site-config/dist/schema/index.js",
|
|
93
85
|
"^@working-theory/validation$": "<rootDir>/node_modules/@working-theory/validation/dist/index.js",
|
|
94
86
|
"^@working-theory/web-i18n$": "<rootDir>/node_modules/@working-theory/web-i18n/dist/index.js",
|
|
95
|
-
// The dependency-free tenancy-constants subpath (database/tenancy.ts
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
// pre-`pnpm build`.
|
|
87
|
+
// The dependency-free tenancy-constants subpath (database/tenancy.ts).
|
|
88
|
+
// Mapped to source like every sibling above; `database/dist` is not
|
|
89
|
+
// built in this workspace pre-`pnpm build`.
|
|
99
90
|
"^@working-theory/database/tenancy$": "<rootDir>/node_modules/@working-theory/database/dist/tenancy.js",
|
|
91
|
+
// #5876: database/tenancy.ts imports isSentinel as a VALUE (not just the
|
|
92
|
+
// Id type) from @working-theory/ids, so ts-jest needs it resolvable at
|
|
93
|
+
// runtime here too — mirrors the same mapping added to
|
|
94
|
+
// database/jest.config.cjs and apps/mcp/jest.config.cjs.
|
|
95
|
+
"^@working-theory/ids$": "<rootDir>/node_modules/@working-theory/ids/dist/index.js",
|
|
100
96
|
// #5025: pnpm keys next-intl's virtual-store path on its resolved peers,
|
|
101
97
|
// so apps/web and packages/page-renderer can link two PHYSICALLY DISTINCT
|
|
102
98
|
// copies of the same version — and a `jest.mock("next-intl/server")` in
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file auth-email.test.ts — Better Auth's three mail callbacks (#5962 § D3).
|
|
3
|
+
*
|
|
4
|
+
* Written before the implementation. The transport is INJECTED, so nothing
|
|
5
|
+
* here mocks an internal module and nothing here sends mail.
|
|
6
|
+
*
|
|
7
|
+
* What the suite pins, in the order the design depends on it:
|
|
8
|
+
* - ONE serialization: the bytes that are signed are the bytes that are
|
|
9
|
+
* sent. A key-order or whitespace drift between signer and sender is the
|
|
10
|
+
* whole failure mode the raw-string seam exists to make impossible.
|
|
11
|
+
* - the signature is HMAC-SHA256 over `<timestamp>.<body>`, so the
|
|
12
|
+
* timestamp cannot be swapped independently of the body it authenticates.
|
|
13
|
+
* - the credential-account check (§ D14): a user with no `credential`
|
|
14
|
+
* account gets NO reset mail, and the suppression is silent — same return,
|
|
15
|
+
* no throw, log-only. An OAuth-only user who completes a reset would
|
|
16
|
+
* otherwise silently gain a password path around their IdP.
|
|
17
|
+
* - the § D12 PII boundary on every log line: a userId, a kind, a reason and
|
|
18
|
+
* an error KIND. Never the address, never the token, never the link.
|
|
19
|
+
* - nothing throws. A failed notification must never fail the password
|
|
20
|
+
* change it reports.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type { AuthEmailLog } from "../auth-email";
|
|
24
|
+
import {
|
|
25
|
+
buildAuthEmailRequest,
|
|
26
|
+
makeSendResetPassword,
|
|
27
|
+
makeSendVerificationEmail,
|
|
28
|
+
makeSendPasswordChangedNotice,
|
|
29
|
+
} from "../auth-email";
|
|
30
|
+
|
|
31
|
+
const ORIGINAL_ENV = process.env;
|
|
32
|
+
|
|
33
|
+
type LogLine = { level: string; fields: Record<string, unknown> };
|
|
34
|
+
|
|
35
|
+
function makeLog(): { lines: LogLine[]; log: AuthEmailLog } {
|
|
36
|
+
const lines: LogLine[] = [];
|
|
37
|
+
const record =
|
|
38
|
+
(level: string) =>
|
|
39
|
+
(fields: Record<string, unknown>): void => {
|
|
40
|
+
lines.push({ level, fields });
|
|
41
|
+
};
|
|
42
|
+
return { lines, log: { warn: record("warn"), info: record("info") } };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** A prisma double exposing only what these callbacks touch. */
|
|
46
|
+
function makePrisma(hasCredential: boolean) {
|
|
47
|
+
return {
|
|
48
|
+
account: {
|
|
49
|
+
findFirst: jest.fn().mockResolvedValue(
|
|
50
|
+
hasCredential ? { id: "acct_1" } : null,
|
|
51
|
+
),
|
|
52
|
+
},
|
|
53
|
+
verification: { deleteMany: jest.fn().mockResolvedValue({ count: 2 }) },
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
beforeEach(() => {
|
|
58
|
+
process.env = { ...ORIGINAL_ENV };
|
|
59
|
+
process.env.AUTH_EMAIL_WEBHOOK_SECRET = "webhook-secret";
|
|
60
|
+
process.env.AUTH_EMAIL_FROM = "noreply@site.example";
|
|
61
|
+
process.env.BETTER_AUTH_URL = "https://site.example";
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
afterAll(() => {
|
|
65
|
+
process.env = ORIGINAL_ENV;
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe("buildAuthEmailRequest()", () => {
|
|
69
|
+
it("signs the EXACT string it returns as the body", () => {
|
|
70
|
+
const req = buildAuthEmailRequest(
|
|
71
|
+
{ kind: "password-reset", userId: "user_1", to: "a@b.test", url: "https://site.example/r" },
|
|
72
|
+
"webhook-secret",
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const { createHmac } = require("node:crypto") as typeof import("node:crypto");
|
|
76
|
+
const timestamp = req.headers["x-ailk-auth-email-timestamp"];
|
|
77
|
+
const expected = `sha256=${createHmac("sha256", "webhook-secret")
|
|
78
|
+
.update(`${timestamp}.${req.rawBody}`, "utf8")
|
|
79
|
+
.digest("hex")}`;
|
|
80
|
+
|
|
81
|
+
expect(req.headers["x-ailk-auth-email-signature"]).toBe(expected);
|
|
82
|
+
// And the body really is a single string, not an object re-serialized later.
|
|
83
|
+
expect(typeof req.rawBody).toBe("string");
|
|
84
|
+
expect(JSON.parse(req.rawBody)).toMatchObject({
|
|
85
|
+
kind: "password-reset",
|
|
86
|
+
userId: "user_1",
|
|
87
|
+
to: "a@b.test",
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("puts a current unix-seconds timestamp inside the signed material", () => {
|
|
92
|
+
const req = buildAuthEmailRequest(
|
|
93
|
+
{ kind: "password-changed", userId: "user_1", to: "a@b.test" },
|
|
94
|
+
"webhook-secret",
|
|
95
|
+
);
|
|
96
|
+
const ts = Number(req.headers["x-ailk-auth-email-timestamp"]);
|
|
97
|
+
expect(Math.abs(Math.floor(Date.now() / 1000) - ts)).toBeLessThan(5);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe("sendResetPassword (§ D14 — the credential-account check)", () => {
|
|
102
|
+
it("sends for a user who HAS a credential account", async () => {
|
|
103
|
+
const send = jest.fn().mockResolvedValue({ ok: true, body: { ok: true } });
|
|
104
|
+
const { lines, log } = makeLog();
|
|
105
|
+
const prisma = makePrisma(true);
|
|
106
|
+
|
|
107
|
+
const cb = makeSendResetPassword({ prisma, send, log });
|
|
108
|
+
await cb({ user: { id: "user_1", email: "person@example.com" }, url: "https://site.example/api/auth/reset-password/tok" });
|
|
109
|
+
|
|
110
|
+
expect(send).toHaveBeenCalledTimes(1);
|
|
111
|
+
expect(lines.some((l) => l.fields.reason === "sent")).toBe(true);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("sends NOTHING for a user with no credential account, and does not throw", async () => {
|
|
115
|
+
const send = jest.fn();
|
|
116
|
+
const { lines, log } = makeLog();
|
|
117
|
+
const prisma = makePrisma(false);
|
|
118
|
+
|
|
119
|
+
const cb = makeSendResetPassword({ prisma, send, log });
|
|
120
|
+
await expect(
|
|
121
|
+
cb({ user: { id: "user_1", email: "person@example.com" }, url: "https://site.example/x" }),
|
|
122
|
+
).resolves.toBeUndefined();
|
|
123
|
+
|
|
124
|
+
expect(send).not.toHaveBeenCalled();
|
|
125
|
+
expect(lines.some((l) => l.fields.reason === "no_credential_account")).toBe(
|
|
126
|
+
true,
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("never throws when the transport fails", async () => {
|
|
131
|
+
const send = jest.fn().mockRejectedValue(new Error("network"));
|
|
132
|
+
const { lines, log } = makeLog();
|
|
133
|
+
const cb = makeSendResetPassword({ prisma: makePrisma(true), send, log });
|
|
134
|
+
|
|
135
|
+
await expect(
|
|
136
|
+
cb({ user: { id: "user_1", email: "p@e.test" }, url: "https://site.example/x" }),
|
|
137
|
+
).resolves.toBeUndefined();
|
|
138
|
+
expect(lines.some((l) => l.fields.reason === "send_failed")).toBe(true);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("logs nothing when the relay secret is absent — it does not send blind", async () => {
|
|
142
|
+
delete process.env.AUTH_EMAIL_WEBHOOK_SECRET;
|
|
143
|
+
const send = jest.fn();
|
|
144
|
+
const { lines, log } = makeLog();
|
|
145
|
+
const cb = makeSendResetPassword({ prisma: makePrisma(true), send, log });
|
|
146
|
+
|
|
147
|
+
await cb({ user: { id: "user_1", email: "p@e.test" }, url: "https://site.example/x" });
|
|
148
|
+
expect(send).not.toHaveBeenCalled();
|
|
149
|
+
expect(lines.some((l) => l.fields.reason === "not_configured")).toBe(true);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe("the § D12 PII boundary", () => {
|
|
154
|
+
it("never logs the address, the token or the assembled link", async () => {
|
|
155
|
+
const send = jest.fn().mockResolvedValue({ ok: true, body: { ok: true } });
|
|
156
|
+
const { lines, log } = makeLog();
|
|
157
|
+
const url =
|
|
158
|
+
"https://site.example/api/auth/reset-password/SECRETTOKEN?callbackURL=%2Freset-password";
|
|
159
|
+
|
|
160
|
+
const reset = makeSendResetPassword({ prisma: makePrisma(true), send, log });
|
|
161
|
+
await reset({ user: { id: "user_1", email: "person@example.com" }, url });
|
|
162
|
+
|
|
163
|
+
const verify = makeSendVerificationEmail({ send, log });
|
|
164
|
+
await verify({ user: { id: "user_1", email: "person@example.com" }, url });
|
|
165
|
+
|
|
166
|
+
const notice = makeSendPasswordChangedNotice({ send, log });
|
|
167
|
+
await notice({ id: "user_1", email: "person@example.com" });
|
|
168
|
+
|
|
169
|
+
const serialized = JSON.stringify(lines);
|
|
170
|
+
expect(serialized).not.toContain("person@example.com");
|
|
171
|
+
expect(serialized).not.toContain("SECRETTOKEN");
|
|
172
|
+
expect(serialized).not.toContain("site.example");
|
|
173
|
+
// What it DOES carry.
|
|
174
|
+
expect(serialized).toContain("user_1");
|
|
175
|
+
expect(serialized).toContain("password-reset");
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("carries only the tagged error KIND, never a message that can quote an address", async () => {
|
|
179
|
+
const send = jest.fn().mockResolvedValue({
|
|
180
|
+
ok: false,
|
|
181
|
+
status: 400,
|
|
182
|
+
body: { error: "validation_failed for person@example.com" },
|
|
183
|
+
});
|
|
184
|
+
const { lines, log } = makeLog();
|
|
185
|
+
const cb = makeSendResetPassword({ prisma: makePrisma(true), send, log });
|
|
186
|
+
|
|
187
|
+
await cb({ user: { id: "user_1", email: "person@example.com" }, url: "https://site.example/x" });
|
|
188
|
+
expect(JSON.stringify(lines)).not.toContain("person@example.com");
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe("makeSendPasswordChangedNotice (§ D17)", () => {
|
|
193
|
+
it("sends a password-changed body carrying no url", async () => {
|
|
194
|
+
const send = jest.fn().mockResolvedValue({ ok: true, body: { ok: true } });
|
|
195
|
+
const { log } = makeLog();
|
|
196
|
+
const notice = makeSendPasswordChangedNotice({ send, log });
|
|
197
|
+
|
|
198
|
+
await notice({ id: "user_1", email: "person@example.com" });
|
|
199
|
+
|
|
200
|
+
const [rawBody] = send.mock.calls[0];
|
|
201
|
+
const parsed = JSON.parse(rawBody);
|
|
202
|
+
expect(parsed.kind).toBe("password-changed");
|
|
203
|
+
expect(parsed).not.toHaveProperty("url");
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("never throws — a failed notice must not fail the password change", async () => {
|
|
207
|
+
const send = jest.fn().mockRejectedValue(new Error("down"));
|
|
208
|
+
const { log } = makeLog();
|
|
209
|
+
const notice = makeSendPasswordChangedNotice({ send, log });
|
|
210
|
+
await expect(
|
|
211
|
+
notice({ id: "user_1", email: "person@example.com" }),
|
|
212
|
+
).resolves.toBeUndefined();
|
|
213
|
+
});
|
|
214
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the auth-route predicate (#5963).
|
|
3
|
+
*
|
|
4
|
+
* `isAuthPath()` is the ONE list the locale layout branches on and the five
|
|
5
|
+
* `(auth)` page segments are named from, so the layout and the routes cannot
|
|
6
|
+
* drift. Covers bare paths, locale-prefixed paths, near misses, nested paths,
|
|
7
|
+
* and the absent-header fail-safe.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { AUTH_SEGMENTS, isAuthPath } from "../auth-routes";
|
|
11
|
+
|
|
12
|
+
const LOCALES = ["en", "fr", "de"];
|
|
13
|
+
|
|
14
|
+
describe("AUTH_SEGMENTS", () => {
|
|
15
|
+
it("names exactly the five auth routes", () => {
|
|
16
|
+
expect([...AUTH_SEGMENTS]).toEqual([
|
|
17
|
+
"sign-in",
|
|
18
|
+
"sign-up",
|
|
19
|
+
"forgot-password",
|
|
20
|
+
"reset-password",
|
|
21
|
+
"verify-email",
|
|
22
|
+
]);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe("isAuthPath", () => {
|
|
27
|
+
it.each(AUTH_SEGMENTS)("matches the bare /%s path", (segment) => {
|
|
28
|
+
expect(isAuthPath(`/${segment}`, LOCALES)).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it.each(AUTH_SEGMENTS)("matches the locale-prefixed /fr/%s path", (segment) => {
|
|
32
|
+
expect(isAuthPath(`/fr/${segment}`, LOCALES)).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("matches a trailing-slash auth path", () => {
|
|
36
|
+
expect(isAuthPath("/sign-in/", LOCALES)).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("matches an auth path carrying a query string", () => {
|
|
40
|
+
expect(isAuthPath("/sign-in?next=/account", LOCALES)).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("matches a nested path under an auth segment", () => {
|
|
44
|
+
expect(isAuthPath("/reset-password/confirm", LOCALES)).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("does not match a near miss", () => {
|
|
48
|
+
expect(isAuthPath("/sign-in-x", LOCALES)).toBe(false);
|
|
49
|
+
expect(isAuthPath("/sign", LOCALES)).toBe(false);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("does not match a marketing path", () => {
|
|
53
|
+
expect(isAuthPath("/pricing", LOCALES)).toBe(false);
|
|
54
|
+
expect(isAuthPath("/fr/pricing", LOCALES)).toBe(false);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("does not match the locale root or the site root", () => {
|
|
58
|
+
expect(isAuthPath("/", LOCALES)).toBe(false);
|
|
59
|
+
expect(isAuthPath("/fr", LOCALES)).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("does not strip a prefix that is not an enabled locale", () => {
|
|
63
|
+
// `/es` is not enabled here, so `es` is the first segment, not a prefix.
|
|
64
|
+
expect(isAuthPath("/es/sign-in", LOCALES)).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("fails to NOT-an-auth-path for an empty or absent pathname", () => {
|
|
68
|
+
// The fail-safe direction (§ D1): an absent `x-pathname` must never strip
|
|
69
|
+
// a marketing page's chrome. The worst case is an auth page WITH chrome,
|
|
70
|
+
// which the (auth) group's own tests catch.
|
|
71
|
+
expect(isAuthPath("", LOCALES)).toBe(false);
|
|
72
|
+
expect(isAuthPath(undefined, LOCALES)).toBe(false);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// oss: false
|
|
2
|
+
/**
|
|
3
|
+
* Tests for enabledMeetingSlugs (#6183 S9b D5) — the D4-resolving read both
|
|
4
|
+
* /meet pages share.
|
|
5
|
+
*/
|
|
6
|
+
const mockMeetingTypes = jest.fn();
|
|
7
|
+
jest.mock("@working-theory/api-client", () => ({
|
|
8
|
+
__esModule: true,
|
|
9
|
+
schedule: { meetingTypes: (...args: unknown[]) => mockMeetingTypes(...args) },
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
import { enabledMeetingSlugs } from "../meeting-slugs";
|
|
13
|
+
|
|
14
|
+
describe("enabledMeetingSlugs", () => {
|
|
15
|
+
const originalEnv = process.env["SCHEDULING_CAL_LINK"];
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
mockMeetingTypes.mockReset();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
if (originalEnv === undefined) delete process.env["SCHEDULING_CAL_LINK"];
|
|
23
|
+
else process.env["SCHEDULING_CAL_LINK"] = originalEnv;
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("passes a real, populated array through unchanged", async () => {
|
|
27
|
+
mockMeetingTypes.mockResolvedValue({
|
|
28
|
+
ok: true,
|
|
29
|
+
body: { slugs: ["intro", "demo"] },
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
await expect(enabledMeetingSlugs()).resolves.toEqual(["intro", "demo"]);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("passes a real, intentionally empty array through unchanged (D4 row 2 — never the fallback)", async () => {
|
|
36
|
+
process.env["SCHEDULING_CAL_LINK"] = "acme/intro-call";
|
|
37
|
+
mockMeetingTypes.mockResolvedValue({ ok: true, body: { slugs: [] } });
|
|
38
|
+
|
|
39
|
+
await expect(enabledMeetingSlugs()).resolves.toEqual([]);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("null + SCHEDULING_CAL_LINK set → the derived singleton (D4 row 1)", async () => {
|
|
43
|
+
process.env["SCHEDULING_CAL_LINK"] = "acme/intro-call";
|
|
44
|
+
mockMeetingTypes.mockResolvedValue({ ok: true, body: { slugs: null } });
|
|
45
|
+
|
|
46
|
+
await expect(enabledMeetingSlugs()).resolves.toEqual(["intro-call"]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("null + SCHEDULING_CAL_LINK unset → [] (no shipped page to protect)", async () => {
|
|
50
|
+
delete process.env["SCHEDULING_CAL_LINK"];
|
|
51
|
+
mockMeetingTypes.mockResolvedValue({ ok: true, body: { slugs: null } });
|
|
52
|
+
|
|
53
|
+
await expect(enabledMeetingSlugs()).resolves.toEqual([]);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("a non-ok API result → [], fail closed — an outage must not open the gate", async () => {
|
|
57
|
+
process.env["SCHEDULING_CAL_LINK"] = "acme/intro-call";
|
|
58
|
+
mockMeetingTypes.mockResolvedValue({ ok: false, status: 500, body: null });
|
|
59
|
+
|
|
60
|
+
await expect(enabledMeetingSlugs()).resolves.toEqual([]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("null + a MALFORMED SCHEDULING_CAL_LINK slug → [], never a corrupt fallback (D14 parity, security review 2026-09-19)", async () => {
|
|
64
|
+
// A comma in the derived slug would corrupt the batched event-meta
|
|
65
|
+
// `calLinks` join downstream (it splits on comma), misattributing the
|
|
66
|
+
// rest of the list to a foreign owner — MeetingSlugSchema rejects it.
|
|
67
|
+
process.env["SCHEDULING_CAL_LINK"] = "acme/intro,evil";
|
|
68
|
+
mockMeetingTypes.mockResolvedValue({ ok: true, body: { slugs: null } });
|
|
69
|
+
|
|
70
|
+
await expect(enabledMeetingSlugs()).resolves.toEqual([]);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -310,8 +310,25 @@ const NO_STRUCTURED_DATA_ROUTES: ReadonlyArray<{
|
|
|
310
310
|
reason: "interactive flow engine, API-backed",
|
|
311
311
|
subtree: true,
|
|
312
312
|
},
|
|
313
|
-
{ route: "
|
|
314
|
-
|
|
313
|
+
{ route: "meet", reason: "booking surface, API-backed", subtree: true },
|
|
314
|
+
// #6084 — both purchase return pages read a Checkout Session (or read
|
|
315
|
+
// nothing, for cancel) and render via SectionRenderer directly; neither
|
|
316
|
+
// is a content-adapter page, and the paid render's own schema.org node
|
|
317
|
+
// (`PaymentComplete`, D6) is asserted directly, not through this walk.
|
|
318
|
+
{
|
|
319
|
+
route: "purchase",
|
|
320
|
+
reason: "checkout return pages, API-backed / read-nothing",
|
|
321
|
+
subtree: true,
|
|
322
|
+
},
|
|
323
|
+
// The whole (auth) route group (#5963). The walk keys on the DIRECTORY
|
|
324
|
+
// path, and a route group's parens are part of it, so this one subtree
|
|
325
|
+
// entry covers all five auth pages — including /sign-in, whose own entry it
|
|
326
|
+
// replaces now that the page sits inside the group.
|
|
327
|
+
{
|
|
328
|
+
route: "(auth)",
|
|
329
|
+
reason: "auth surfaces, not content pages",
|
|
330
|
+
subtree: true,
|
|
331
|
+
},
|
|
315
332
|
];
|
|
316
333
|
|
|
317
334
|
interface RouteEntry {
|
|
@@ -345,14 +362,36 @@ function exemption(route: string): string | undefined {
|
|
|
345
362
|
)?.reason;
|
|
346
363
|
}
|
|
347
364
|
|
|
348
|
-
// The page identity a route renders — the
|
|
349
|
-
//
|
|
350
|
-
// slug
|
|
351
|
-
// (
|
|
365
|
+
// The page identity a route renders — the three shapes
|
|
366
|
+
// tools/lint-check-content-renders.ts recognises: `renderPage({ type, slug })`
|
|
367
|
+
// with a literal slug, the same with a slug taken from params, and a direct
|
|
368
|
+
// `getMdxSource("<Type>", slug, …)` for a route that composes its own body.
|
|
369
|
+
// This file previously mirrored only the first two and so drifted from the
|
|
370
|
+
// tool its comment names (whose own `GET_MDX_SOURCE_RE` has carried the third
|
|
371
|
+
// arm all along); #5562 restored the parity. A route that reads its identity
|
|
372
|
+
// some other way (e.g. a page-type manifest) is still unrecognised and must
|
|
373
|
+
// be declared.
|
|
352
374
|
const RENDER_PAGE_EXACT_RE =
|
|
353
375
|
/renderPage\s*\(\s*\{[^}]*type\s*:\s*["']([A-Za-z]+)["'][^}]*slug\s*:\s*["']([^"']+)["']/s;
|
|
354
376
|
const RENDER_PAGE_DYNAMIC_RE =
|
|
355
377
|
/renderPage\s*\(\s*\{[^}]*type\s*:\s*["']([A-Za-z]+)["'][^}]*\bslug\s*(?!:\s*["'])/s;
|
|
378
|
+
// A route that composes its own body instead of calling `renderPage` — it
|
|
379
|
+
// resolves content with `getMdxSource(<Type>, slug, locale)` and emits the
|
|
380
|
+
// graph itself via `pageToJsonLd` + `<JsonLdScript>`. That is not a route
|
|
381
|
+
// hiding from this check; it is the SAME declaration one level down, since
|
|
382
|
+
// `renderPage` resolves its own content with exactly this call. `/reports/
|
|
383
|
+
// [slug]` (#5562) is the first: it owns a full-bleed `<main>` and a header
|
|
384
|
+
// section that already renders the title, so `renderPage`'s own `<h1>`/`<p>`
|
|
385
|
+
// would duplicate it.
|
|
386
|
+
//
|
|
387
|
+
// Recognising the shape is deliberately better than exempting the route:
|
|
388
|
+
// an exemption declares "nothing is known to emit JSON-LD here" — false for
|
|
389
|
+
// these routes — and would silence the check for a page that DOES emit a
|
|
390
|
+
// graph. Recognised, the route is rendered and validated like any other.
|
|
391
|
+
// Matched only after both `renderPage` shapes above, so a route that uses
|
|
392
|
+
// `renderPage` is still read through that (richer, slug-bearing) form.
|
|
393
|
+
// Mirrors `GET_MDX_SOURCE_RE` in tools/lint-check-content-renders.ts.
|
|
394
|
+
const GET_MDX_SOURCE_RE = /getMdxSource\s*\(\s*["']([A-Za-z]+)["']/s;
|
|
356
395
|
|
|
357
396
|
function routeIdentity(
|
|
358
397
|
file: string,
|
|
@@ -362,6 +401,11 @@ function routeIdentity(
|
|
|
362
401
|
if (exact) return { type: exact[1] as string, slug: exact[2] as string };
|
|
363
402
|
const dynamic = RENDER_PAGE_DYNAMIC_RE.exec(src);
|
|
364
403
|
if (dynamic) return { type: dynamic[1] as string, slug: null };
|
|
404
|
+
// Self-composing route (see GET_MDX_SOURCE_RE). Its slug always comes from
|
|
405
|
+
// the route params — a self-composing FIXED route would name its slug as a
|
|
406
|
+
// literal to `getMdxSource`, and none exists — so the dynamic path applies.
|
|
407
|
+
const ownBody = GET_MDX_SOURCE_RE.exec(src);
|
|
408
|
+
if (ownBody) return { type: ownBody[1] as string, slug: null };
|
|
365
409
|
return null;
|
|
366
410
|
}
|
|
367
411
|
|