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
|
@@ -57,6 +57,10 @@ import type { FastifyInstance } from "fastify";
|
|
|
57
57
|
import { SITE_LISTING_CONFIG_DEFAULTS } from "@working-theory/validation";
|
|
58
58
|
|
|
59
59
|
import { setupLeadFileEnv } from "../../../__tests__/factories/lead-fixture";
|
|
60
|
+
import {
|
|
61
|
+
mockNoWorkspace,
|
|
62
|
+
mockWorkspaceExists,
|
|
63
|
+
} from "../../../lib/__mocks__/prisma.js";
|
|
60
64
|
import { prisma } from "../../../lib/prisma.js";
|
|
61
65
|
import { listProjectListings } from "../../../services/project-listings.js";
|
|
62
66
|
import { patchProjectListingDraftRoute } from "../patch-draft.js";
|
|
@@ -137,6 +141,8 @@ const COLLECTED_WITH_OPT_IN = [
|
|
|
137
141
|
"logoMarkUrl",
|
|
138
142
|
"logoWordmarkUrl",
|
|
139
143
|
"requestedWindowDays",
|
|
144
|
+
// #5096 — the fourth. Same opt-in treatment for the same reason.
|
|
145
|
+
"solution",
|
|
140
146
|
];
|
|
141
147
|
|
|
142
148
|
const COLLECTED_BEFORE_5285 = [
|
|
@@ -198,6 +204,13 @@ function matches(row: Row, where: Record<string, unknown>): boolean {
|
|
|
198
204
|
}
|
|
199
205
|
|
|
200
206
|
function installStore(): void {
|
|
207
|
+
// #6335 — the shared prisma mock no longer resolves ANY requested
|
|
208
|
+
// workspace id. `startProjectListingDraft` stamps the row's `workspaceId`
|
|
209
|
+
// by resolving the site it was filed against (`resolveWorkspaceIdForSite`,
|
|
210
|
+
// services/project-listings.ts), so this suite states the ONE site it is
|
|
211
|
+
// built around. Keyed on the requested id — nothing else resolves.
|
|
212
|
+
mockWorkspaceExists(prisma, SITE_ID);
|
|
213
|
+
|
|
201
214
|
rows = [];
|
|
202
215
|
nextId = 1;
|
|
203
216
|
configData = null;
|
|
@@ -258,6 +271,7 @@ function installStore(): void {
|
|
|
258
271
|
headline: null,
|
|
259
272
|
logoMarkUrl: null,
|
|
260
273
|
logoWordmarkUrl: null,
|
|
274
|
+
solution: null,
|
|
261
275
|
featured: false,
|
|
262
276
|
decidedAt: null,
|
|
263
277
|
decidedBy: null,
|
|
@@ -552,9 +566,9 @@ describe("the site-answers store's anonymous-write bounds (#5285)", () => {
|
|
|
552
566
|
});
|
|
553
567
|
});
|
|
554
568
|
|
|
555
|
-
// ── The
|
|
569
|
+
// ── The four contract fields ──
|
|
556
570
|
|
|
557
|
-
describe("the
|
|
571
|
+
describe("the four contract fields that became writable", () => {
|
|
558
572
|
it("patch: the two logo urls round-trip onto the row, on a site that asked", async () => {
|
|
559
573
|
configData = { collectedFields: COLLECTED_WITH_OPT_IN };
|
|
560
574
|
const draft = await startDraft(app);
|
|
@@ -569,7 +583,7 @@ describe("the site-answers store's anonymous-write bounds (#5285)", () => {
|
|
|
569
583
|
expect(rows[0]!["logoWordmarkUrl"]).toBe("https://cdn.example.com/wordmark.svg");
|
|
570
584
|
});
|
|
571
585
|
|
|
572
|
-
it("patch: REFUSES all
|
|
586
|
+
it("patch: REFUSES all four on a site that never opted in, writing nothing", async () => {
|
|
573
587
|
// The defect the back-compat review found, at the route. `collectedFields`
|
|
574
588
|
// defaults to every field the payload defines, so adding these three
|
|
575
589
|
// silently made them writable on every site that had configured nothing —
|
|
@@ -583,12 +597,14 @@ describe("the site-answers store's anonymous-write bounds (#5285)", () => {
|
|
|
583
597
|
{ logoMarkUrl: "https://evil.example/a.png" },
|
|
584
598
|
{ logoWordmarkUrl: "https://evil.example/b.svg" },
|
|
585
599
|
{ requestedWindowDays: 9000 },
|
|
600
|
+
{ solution: "Text a site never asked for." },
|
|
586
601
|
]) {
|
|
587
602
|
expect((await patch(app, draft, body)).statusCode).toBe(400);
|
|
588
603
|
}
|
|
589
604
|
|
|
590
605
|
expect(rows[0]!["logoMarkUrl"]).toBeNull();
|
|
591
606
|
expect(rows[0]!["logoWordmarkUrl"]).toBeNull();
|
|
607
|
+
expect(rows[0]!["solution"]).toBeNull();
|
|
592
608
|
});
|
|
593
609
|
|
|
594
610
|
it("patch: refuses a logo url that is not http(s) — they render as image sources", async () => {
|
|
@@ -650,15 +666,20 @@ describe("the site-answers store's anonymous-write bounds (#5285)", () => {
|
|
|
650
666
|
|
|
651
667
|
// ── The floor the three new fields must not move ──
|
|
652
668
|
|
|
653
|
-
describe("submit requirements are unchanged by the
|
|
654
|
-
it("the
|
|
669
|
+
describe("submit requirements are unchanged by the four added fields", () => {
|
|
670
|
+
it("the four are OPT-IN, absent from the default collected set, and never required at submit", async () => {
|
|
655
671
|
// This assertion was inverted by the back-compat review. The default
|
|
656
672
|
// `collectedFields` derives from the application schema's own shape, so
|
|
657
673
|
// adding a field WIDENED what an anonymous caller could write on every
|
|
658
674
|
// site that never wrote an explicit mask — and the two logo fields ride
|
|
659
675
|
// the public read and render as image sources. They are excluded from
|
|
660
676
|
// the default now; a site that wants them names them.
|
|
661
|
-
for (const field of [
|
|
677
|
+
for (const field of [
|
|
678
|
+
"logoMarkUrl",
|
|
679
|
+
"logoWordmarkUrl",
|
|
680
|
+
"requestedWindowDays",
|
|
681
|
+
"solution",
|
|
682
|
+
]) {
|
|
662
683
|
expect(SITE_LISTING_CONFIG_DEFAULTS.collectedFields).not.toContain(field);
|
|
663
684
|
}
|
|
664
685
|
|
|
@@ -672,6 +693,7 @@ describe("the site-answers store's anonymous-write bounds (#5285)", () => {
|
|
|
672
693
|
expect(rows[0]!["status"]).toBe("requested");
|
|
673
694
|
expect(rows[0]!["logoMarkUrl"]).toBeNull();
|
|
674
695
|
expect(rows[0]!["logoWordmarkUrl"]).toBeNull();
|
|
696
|
+
expect(rows[0]!["solution"]).toBeNull();
|
|
675
697
|
});
|
|
676
698
|
|
|
677
699
|
it("a site whose stored collectedFields predates #5285 submits exactly as it did", async () => {
|
|
@@ -712,6 +734,85 @@ describe("the site-answers store's anonymous-write bounds (#5285)", () => {
|
|
|
712
734
|
expect(body.error.missingFields).not.toContain("logoMarkUrl");
|
|
713
735
|
expect(body.error.missingFields).not.toContain("logoWordmarkUrl");
|
|
714
736
|
expect(body.error.missingFields).not.toContain("requestedWindowDays");
|
|
737
|
+
expect(body.error.missingFields).not.toContain("solution");
|
|
738
|
+
});
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
// ── #5096 — `solution` round-trips for a site that asked ──
|
|
742
|
+
|
|
743
|
+
describe("the `solution` field (#5096)", () => {
|
|
744
|
+
it("patch: persists it onto the row, on a site that asked", async () => {
|
|
745
|
+
configData = { collectedFields: COLLECTED_WITH_OPT_IN };
|
|
746
|
+
const draft = await startDraft(app);
|
|
747
|
+
|
|
748
|
+
const res = await patch(app, draft, { solution: "We publish the answer, not the page." });
|
|
749
|
+
|
|
750
|
+
expect(res.statusCode).toBe(200);
|
|
751
|
+
expect(rows[0]!["solution"]).toBe("We publish the answer, not the page.");
|
|
715
752
|
});
|
|
753
|
+
|
|
754
|
+
it("patch: refuses an empty solution and one over 600 chars", async () => {
|
|
755
|
+
configData = { collectedFields: COLLECTED_WITH_OPT_IN };
|
|
756
|
+
const draft = await startDraft(app);
|
|
757
|
+
|
|
758
|
+
expect((await patch(app, draft, { solution: "" })).statusCode).toBe(400);
|
|
759
|
+
expect((await patch(app, draft, { solution: "x".repeat(601) })).statusCode).toBe(400);
|
|
760
|
+
expect(rows[0]!["solution"]).toBeNull();
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
// The half that would fail QUIETLY: `submit` validates an application
|
|
764
|
+
// reassembled from the ROW, and a stored value the reassembly drops is
|
|
765
|
+
// indistinguishable — from the founder's side — from one that saved.
|
|
766
|
+
it("submit: the stored solution survives the row-reassembly `submit` validates", async () => {
|
|
767
|
+
configData = { collectedFields: COLLECTED_WITH_OPT_IN };
|
|
768
|
+
const draft = await startDraft(app);
|
|
769
|
+
await patch(app, draft, COMPLETE_APPLICATION);
|
|
770
|
+
await patch(app, draft, { solution: "We publish the answer, not the page." });
|
|
771
|
+
|
|
772
|
+
const res = await submit(app, draft);
|
|
773
|
+
|
|
774
|
+
expect(res.statusCode).toBe(200);
|
|
775
|
+
expect(rows[0]!["status"]).toBe("requested");
|
|
776
|
+
expect(rows[0]!["solution"]).toBe("We publish the answer, not the page.");
|
|
777
|
+
});
|
|
778
|
+
});
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
// ── #6335 — the explicit workspace setup above is load-bearing ────────────
|
|
782
|
+
//
|
|
783
|
+
// Every case in this file states that this suite's site has a workspace.
|
|
784
|
+
// This one takes that away, and proves the statement was doing work rather
|
|
785
|
+
// than decorating the setup: `startProjectListingDraft` resolves the site it
|
|
786
|
+
// is filing against (`resolveWorkspaceIdForSite`, services/project-listings.ts)
|
|
787
|
+
// and refuses BEFORE any row is written. With the shared prisma mock's old
|
|
788
|
+
// permissive default in place this case could not be written at all — every
|
|
789
|
+
// id resolved, so the fail-closed path was unreachable from a route test.
|
|
790
|
+
describe("#6335 — start fails closed when the site names no workspace", () => {
|
|
791
|
+
let app: FastifyInstance;
|
|
792
|
+
|
|
793
|
+
beforeEach(async () => {
|
|
794
|
+
jest.clearAllMocks();
|
|
795
|
+
installStore();
|
|
796
|
+
mockNoWorkspace(prisma);
|
|
797
|
+
app = await buildApp();
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
afterEach(async () => {
|
|
801
|
+
await app.close();
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
it("answers 503 tenant_unresolved and creates no ProjectListing", async () => {
|
|
805
|
+
const res = await app.inject({
|
|
806
|
+
method: "POST",
|
|
807
|
+
url: "/v1/project-listings",
|
|
808
|
+
payload: START_PAYLOAD,
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
expect(res.statusCode).toBe(503);
|
|
812
|
+
expect((res.json() as { error: { code: string } }).error.code).toBe(
|
|
813
|
+
"tenant_unresolved",
|
|
814
|
+
);
|
|
815
|
+
expect(prisma.projectListing.create).not.toHaveBeenCalled();
|
|
816
|
+
expect(rows).toHaveLength(0);
|
|
716
817
|
});
|
|
717
818
|
});
|
|
@@ -50,10 +50,17 @@ jest.mock("@working-theory/license", () => ({
|
|
|
50
50
|
}),
|
|
51
51
|
}));
|
|
52
52
|
|
|
53
|
+
import { DEFAULT_WORKSPACE_ID } from "@working-theory/database/tenancy";
|
|
53
54
|
import Fastify from "fastify";
|
|
54
55
|
import type { FastifyInstance } from "fastify";
|
|
55
56
|
|
|
57
|
+
import {
|
|
58
|
+
mockNoWorkspace,
|
|
59
|
+
mockSentinelWorkspaceExists,
|
|
60
|
+
mockWorkspaceExists,
|
|
61
|
+
} from "../../../lib/__mocks__/prisma.js";
|
|
56
62
|
import { prisma } from "../../../lib/prisma.js";
|
|
63
|
+
import { __resetSiteWorkspaceCacheForTests } from "../../../lib/site.js";
|
|
57
64
|
import { publicProjectListingsRoute } from "../public.js";
|
|
58
65
|
import { startProjectListingRoute } from "../start.js";
|
|
59
66
|
|
|
@@ -106,6 +113,14 @@ function installStore(): void {
|
|
|
106
113
|
rows = [];
|
|
107
114
|
nextId = 1;
|
|
108
115
|
|
|
116
|
+
// #6335 — the shared prisma mock no longer resolves ANY requested
|
|
117
|
+
// workspace id, so this suite states the two sites it is built around.
|
|
118
|
+
// `resolveWorkspaceIdForSite` (services/project-listings.ts) keys on the
|
|
119
|
+
// requested id, and nothing else resolves: the env-alias cases below add
|
|
120
|
+
// the ONE further id each of them expects, and the stamp suite's refusal
|
|
121
|
+
// case takes them all away.
|
|
122
|
+
mockWorkspaceExists(prisma, SITE_A, SITE_B);
|
|
123
|
+
|
|
109
124
|
// One delegate, two lookups (#5048 D2): `where.publicKey` is the site-key
|
|
110
125
|
// resolution, `where.siteId` the per-site config read. No config row for the
|
|
111
126
|
// latter, so the platform defaults apply.
|
|
@@ -158,6 +173,7 @@ function installStore(): void {
|
|
|
158
173
|
headline: null,
|
|
159
174
|
logoMarkUrl: null,
|
|
160
175
|
logoWordmarkUrl: null,
|
|
176
|
+
solution: null,
|
|
161
177
|
featured: false,
|
|
162
178
|
decidedAt: null,
|
|
163
179
|
decidedBy: null,
|
|
@@ -214,6 +230,7 @@ function seedActiveListing(siteId: string, slug: string): void {
|
|
|
214
230
|
questions: null,
|
|
215
231
|
logoMarkUrl: null,
|
|
216
232
|
logoWordmarkUrl: null,
|
|
233
|
+
solution: null,
|
|
217
234
|
featured: false,
|
|
218
235
|
status: "active",
|
|
219
236
|
consentAt: new Date("2026-08-01T00:00:00.000Z"),
|
|
@@ -254,6 +271,12 @@ describe("the public site key (#5048 D2)", () => {
|
|
|
254
271
|
savedPublicKey = process.env["AILK_LISTING_PUBLIC_KEY"];
|
|
255
272
|
delete process.env["AILK_SITE_ID"];
|
|
256
273
|
delete process.env["AILK_LISTING_PUBLIC_KEY"];
|
|
274
|
+
// #6254 — `resolveSiteWorkspaceId()` memoizes its FIRST successful
|
|
275
|
+
// resolution for the process lifetime (by design, for the hot request
|
|
276
|
+
// path); several tests in this file change `AILK_SITE_ID` between runs,
|
|
277
|
+
// so the memo must be cleared per test or a later test would silently
|
|
278
|
+
// reuse an earlier test's resolved workspace id.
|
|
279
|
+
__resetSiteWorkspaceCacheForTests();
|
|
257
280
|
app = await buildApp();
|
|
258
281
|
});
|
|
259
282
|
|
|
@@ -331,22 +354,34 @@ describe("the public site key (#5048 D2)", () => {
|
|
|
331
354
|
expect(rows.some((r) => r["siteId"] === "the-deployments-own-site")).toBe(false);
|
|
332
355
|
});
|
|
333
356
|
|
|
334
|
-
it("start (single-tenant): AILK_LISTING_PUBLIC_KEY resolves to currentSiteId() with no config row", async () => {
|
|
357
|
+
it("start (single-tenant): AILK_LISTING_PUBLIC_KEY resolves to the RESOLVED currentSiteId() workspace, with no config row", async () => {
|
|
335
358
|
// The default `create-ailk` scaffold is one customer per deployment and
|
|
336
359
|
// has no `SiteListingConfig` row at all; requiring one would mean a site
|
|
337
360
|
// could not accept an application until an operator wrote a database row.
|
|
361
|
+
//
|
|
362
|
+
// #6254 AC-6/AC-10 — `siteIdForPublicKey`'s single-tenant fallback now
|
|
363
|
+
// resolves the alias (`resolveSiteWorkspaceId()`) rather than returning
|
|
364
|
+
// the raw `"default"` literal: `project_listings` rejects a `site_id`
|
|
365
|
+
// that disagrees with `workspace_id`
|
|
366
|
+
// (database/migrations/20260922120000_tenancy_fence), and the two must
|
|
367
|
+
// agree from create time.
|
|
338
368
|
process.env["AILK_LISTING_PUBLIC_KEY"] = "pk-single-tenant";
|
|
339
369
|
delete process.env["AILK_SITE_ID"]; // → currentSiteId() falls back to "default"
|
|
370
|
+
// #6335 — the "default" alias resolves through the SENTINEL workspace
|
|
371
|
+
// row, which this case (alone in the file) expects to exist.
|
|
372
|
+
mockSentinelWorkspaceExists(prisma, SITE_A, SITE_B);
|
|
340
373
|
|
|
341
374
|
const res = await start(app, { ...START_PAYLOAD, siteKey: "pk-single-tenant" });
|
|
342
375
|
|
|
343
376
|
expect(res.statusCode).toBe(201);
|
|
344
|
-
expect(rows[0]!["siteId"]).toBe(
|
|
377
|
+
expect(rows[0]!["siteId"]).toBe(DEFAULT_WORKSPACE_ID);
|
|
345
378
|
});
|
|
346
379
|
|
|
347
380
|
it("start (single-tenant): the env key resolves to AILK_SITE_ID where one is set", async () => {
|
|
348
381
|
process.env["AILK_LISTING_PUBLIC_KEY"] = "pk-single-tenant";
|
|
349
382
|
process.env["AILK_SITE_ID"] = "the-only-site";
|
|
383
|
+
// #6335 — this case's own deployment site, named explicitly.
|
|
384
|
+
mockWorkspaceExists(prisma, SITE_A, SITE_B, "the-only-site");
|
|
350
385
|
|
|
351
386
|
const res = await start(app, { ...START_PAYLOAD, siteKey: "pk-single-tenant" });
|
|
352
387
|
|
|
@@ -397,3 +432,53 @@ describe("the public site key (#5048 D2)", () => {
|
|
|
397
432
|
expect(prisma.projectListing.findMany).not.toHaveBeenCalled();
|
|
398
433
|
});
|
|
399
434
|
});
|
|
435
|
+
|
|
436
|
+
// ── #6203 AC-4 — the workspace stamp, and its own refusal ──────────────────
|
|
437
|
+
//
|
|
438
|
+
// The site-key resolution above (siteKey -> siteId) is UNCHANGED by #6203 —
|
|
439
|
+
// it was already server-side and already refused a body-supplied override,
|
|
440
|
+
// which the suite above already proves. What #6203 adds is a SECOND, NEW
|
|
441
|
+
// resolution on top of that already-resolved siteId: siteId -> workspaceId
|
|
442
|
+
// (`project-listings.ts`'s `resolveWorkspaceIdForSite`). There is no THIRD
|
|
443
|
+
// body field for an attacker to spoof here (`projectListingStartSchema`
|
|
444
|
+
// carries no workspaceId-shaped field at all, and the suite above already
|
|
445
|
+
// proves a body-supplied `siteId` is refused outright) — so the only new
|
|
446
|
+
// behaviour to assert is this second resolution's own failure mode.
|
|
447
|
+
describe("the workspace stamp (#6203) — start's second resolution, on top of siteKey", () => {
|
|
448
|
+
let app: FastifyInstance;
|
|
449
|
+
|
|
450
|
+
beforeEach(async () => {
|
|
451
|
+
jest.clearAllMocks();
|
|
452
|
+
installStore();
|
|
453
|
+
delete process.env["AILK_SITE_ID"];
|
|
454
|
+
delete process.env["AILK_LISTING_PUBLIC_KEY"];
|
|
455
|
+
__resetSiteWorkspaceCacheForTests();
|
|
456
|
+
app = await buildApp();
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
afterEach(async () => {
|
|
460
|
+
await app.close();
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
it("a siteId that resolves via siteKey but names no workspace is a 503 tenant_unresolved, and writes no row", async () => {
|
|
464
|
+
mockNoWorkspace(prisma);
|
|
465
|
+
|
|
466
|
+
const res = await start(app, { ...START_PAYLOAD, siteKey: KEY_A });
|
|
467
|
+
|
|
468
|
+
expect(res.statusCode).toBe(503);
|
|
469
|
+
expect((res.json() as { error: { code: string } }).error.code).toBe(
|
|
470
|
+
"tenant_unresolved",
|
|
471
|
+
);
|
|
472
|
+
expect(prisma.projectListing.create).not.toHaveBeenCalled();
|
|
473
|
+
expect(rows).toHaveLength(0);
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
it("on resolution, the created row carries workspaceId equal to the resolved siteId", async () => {
|
|
477
|
+
const res = await start(app, { ...START_PAYLOAD, siteKey: KEY_B });
|
|
478
|
+
|
|
479
|
+
expect(res.statusCode).toBe(201);
|
|
480
|
+
expect(rows).toHaveLength(1);
|
|
481
|
+
expect(rows[0]!["siteId"]).toBe(SITE_B);
|
|
482
|
+
expect(rows[0]!["workspaceId"]).toBe(SITE_B);
|
|
483
|
+
});
|
|
484
|
+
});
|
|
@@ -62,6 +62,10 @@ import Fastify from "fastify";
|
|
|
62
62
|
import type { FastifyInstance } from "fastify";
|
|
63
63
|
|
|
64
64
|
import { setupLeadFileEnv } from "../../../__tests__/factories/lead-fixture";
|
|
65
|
+
import {
|
|
66
|
+
mockNoWorkspace,
|
|
67
|
+
mockWorkspaceExists,
|
|
68
|
+
} from "../../../lib/__mocks__/prisma.js";
|
|
65
69
|
import { prisma } from "../../../lib/prisma.js";
|
|
66
70
|
import { patchProjectListingDraftRoute } from "../patch-draft.js";
|
|
67
71
|
import { startProjectListingRoute } from "../start.js";
|
|
@@ -178,6 +182,13 @@ function matches(row: Row, where: Record<string, unknown>): boolean {
|
|
|
178
182
|
}
|
|
179
183
|
|
|
180
184
|
function installStore(): void {
|
|
185
|
+
// #6335 — the shared prisma mock no longer resolves ANY requested
|
|
186
|
+
// workspace id. `startProjectListingDraft` stamps the row's `workspaceId`
|
|
187
|
+
// by resolving the site it was filed against (`resolveWorkspaceIdForSite`,
|
|
188
|
+
// services/project-listings.ts), so this suite states the ONE site it is
|
|
189
|
+
// built around. Keyed on the requested id — nothing else resolves.
|
|
190
|
+
mockWorkspaceExists(prisma, SITE_ID);
|
|
191
|
+
|
|
181
192
|
rows = [];
|
|
182
193
|
nextId = 1;
|
|
183
194
|
|
|
@@ -243,6 +254,7 @@ function installStore(): void {
|
|
|
243
254
|
headline: null,
|
|
244
255
|
logoMarkUrl: null,
|
|
245
256
|
logoWordmarkUrl: null,
|
|
257
|
+
solution: null,
|
|
246
258
|
featured: false,
|
|
247
259
|
decidedAt: null,
|
|
248
260
|
decidedBy: null,
|
|
@@ -482,3 +494,42 @@ describe("the structured founder address (#5279)", () => {
|
|
|
482
494
|
});
|
|
483
495
|
});
|
|
484
496
|
});
|
|
497
|
+
|
|
498
|
+
// ── #6335 — the explicit workspace setup above is load-bearing ────────────
|
|
499
|
+
//
|
|
500
|
+
// Every case in this file states that this suite's site has a workspace.
|
|
501
|
+
// This one takes that away, and proves the statement was doing work rather
|
|
502
|
+
// than decorating the setup: `startProjectListingDraft` resolves the site it
|
|
503
|
+
// is filing against (`resolveWorkspaceIdForSite`, services/project-listings.ts)
|
|
504
|
+
// and refuses BEFORE any row is written. With the shared prisma mock's old
|
|
505
|
+
// permissive default in place this case could not be written at all — every
|
|
506
|
+
// id resolved, so the fail-closed path was unreachable from a route test.
|
|
507
|
+
describe("#6335 — start fails closed when the site names no workspace", () => {
|
|
508
|
+
let app: FastifyInstance;
|
|
509
|
+
|
|
510
|
+
beforeEach(async () => {
|
|
511
|
+
jest.clearAllMocks();
|
|
512
|
+
installStore();
|
|
513
|
+
mockNoWorkspace(prisma);
|
|
514
|
+
app = await buildApp();
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
afterEach(async () => {
|
|
518
|
+
await app.close();
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
it("answers 503 tenant_unresolved and creates no ProjectListing", async () => {
|
|
522
|
+
const res = await app.inject({
|
|
523
|
+
method: "POST",
|
|
524
|
+
url: "/v1/project-listings",
|
|
525
|
+
payload: START_PAYLOAD,
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
expect(res.statusCode).toBe(503);
|
|
529
|
+
expect((res.json() as { error: { code: string } }).error.code).toBe(
|
|
530
|
+
"tenant_unresolved",
|
|
531
|
+
);
|
|
532
|
+
expect(prisma.projectListing.create).not.toHaveBeenCalled();
|
|
533
|
+
expect(rows).toHaveLength(0);
|
|
534
|
+
});
|
|
535
|
+
});
|
|
@@ -110,7 +110,11 @@ export const startProjectListingRoute: FastifyPluginAsync = async (fastify) => {
|
|
|
110
110
|
// #5275 — a `consents` map declining a REQUIRED purpose contradicts the
|
|
111
111
|
// body's own `consent: true`; no row was created.
|
|
112
112
|
if (!result.ok) {
|
|
113
|
-
|
|
113
|
+
// #6203 — `tenant_unresolved` is a server-side tenancy
|
|
114
|
+
// misconfiguration (the resolved `siteId` names no workspace this
|
|
115
|
+
// database holds), not a client input problem — 503, never 400.
|
|
116
|
+
const status = result.error.kind === "tenant_unresolved" ? 503 : 400;
|
|
117
|
+
return reply.status(status).send({
|
|
114
118
|
ok: false,
|
|
115
119
|
error: { code: result.error.kind, message: result.error.message },
|
|
116
120
|
});
|